PHP SDK
PHP

The Blackfire PHP SDK can be added as a requirement to your project via Composer:

1
composer require blackfire/php-sdk

The PHP SDK works with PHP 5.3+.

The Blackfire PHP SDK uses the same underlying mechanisms as the Blackfire CLI or the Blackfire Browser Extensions; so you need to install the Blackfire PHP extension and configure the Blackfire agent properly.

The Blackfire probe exposes a class, which is part of the C extension, in the root namespace: \BlackfireProbe.

This class gives you the possibility to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
final class BlackfireProbe
{
    /**
     * Returns a global singleton of the probe.
     *
     * @return \BlackfireProbe
     */
    public static function getMainInstance() {}

    /**
     * Tells whether the probe is currently profiling or not.
     *
     * @return bool
     */
    public static function isEnabled() {}

    /**
     * Adds a marker for the Timeline View.
     * Production safe. Operates a no-op if no profile is requested.
     *
     * @param string $markerName
     */
    public static function addMarker($markerName = '') {}

    /**
     * Tells if the probe is cryptographically verified,
     * i.e. if the signature in X-Blackfire-Query HTTP header or
     * BLACKFIRE_QUERY environment variable is valid.
     *
     * @return bool
     */
    public function isVerified() {}

    /**
     * Enables manual instrumentation. Starts collecting profiling data.
     * Production safe. Operates a no-op if no profile is requested.
     *
     * @return bool False if enabling failed.
     */
    public function enable() {}

    /**
     * Discards collected data and disables instrumentation.
     *
     * Does not close the profile payload, allowing to re-enable the probe and aggregate data in the same profile.
     *
     * @return bool False if the probe was not enabled.
     */
    public function discard() {}

    /**
     * Disables instrumentation.
     * Production safe. Operates a no-op if no profile is requested.
     *
     * Does not close the profile payload, allowing to re-enable the probe
     * and to aggregate data in the same profile.
     *
     * @return bool False if the probe was not enabled.
     */
    public function disable() {}

    /**
     * Stops the profiling and forces the collected data to be sent to Blackfire.
     *
     * @return bool False if the probe was not enabled.
     */
    public function close() {}

    /**
     * Creates a sub-query string to create a new profile linked to the current one.
     * Generated query must be set in the X-Blackire-Query HTTP header or in the BLACKFIRE_QUERY environment variable.
     *
     * @return string|null The sub-query or null if the current profile is not the first sample or profiling is disabled.
     */
    public function createSubProfileQuery() {}

    /**
     * Sets a custom transaction name for Blackfire Monitoring.
     */
    public function setTransactionName() {}

    /**
     * Disables Blackfire Monitoring instrumentation for a transaction.
     */
    public function ignoreTransaction() {}       
}

The Blackfire PHP SDK allows you to generate profiles from your code and it eases the integration with third-party libraries (like PHPUnit). It can also be used to profile HTTP requests from PHP.

The main entry point of the SDK is the Blackfire\Client class:

1
$blackfire = new \Blackfire\Client();

The client object allows you to profile any parts of your code:

1
2
3
4
5
$probe = $blackfire->createProbe();

// some PHP code you want to profile

$profile = $blackfire->endProbe($probe);

The createProbe() method takes an optional Blackfire\Profile\Configuration object that allows you to configure the Blackfire Profile more finely:

1
2
$config = new \Blackfire\Profile\Configuration();
$probe = $blackfire->createProbe($config);

When calling endProbe(), the profile is generated and sent to Blackfire.io servers. The $profile variable is an instance of Blackfire\Profile that gives you access to the generated profile information.

You can store the profile UUID to get it back later:

1
2
3
4
5
// store the profile UUID
$uuid = $probe->getRequest()->getUuid();

// retrieve the profile later on
$profile = $blackfire->getProfile($uuid);

The $probe instance can also be used to instrument precisely only part of your code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$blackfire = new \Blackfire\Client();

// false here means that we want to instrument the code ourself
$probe = $blackfire->createProbe(null, false);

// some code that won't be instrumented

// start instrumentation and profiling
$probe->enable();

// some code that is going to be profiled

// stop instrumentation
$probe->disable();

// code here won't be profiled

// do it as many times as you want
// all profiled sections will be aggregated in one graph
$probe->enable();
$probe->disable();

// end the profiling session
$profile = $blackfire->endProbe($probe);

Note that having many enable()/disable() sections might make your call graph very difficult to interpret. You might want to create several profiles instead.

If you want to profile the same code more than once, use the samples feature.

Enabling the instrumentation via the Probe object instead of the Client one is very useful when you integrate Blackfire deeply in your code. For instance, you might create the Blackfire Client in one object and do the profiling in another portion of your code.

Blackfire\Profile instances (as returned by Client::endProbe()) give you access to generated profiles data:

1
$profile = $blackfire->endProbe($probe);

You can also get any profile by UUID:

1
$profile = $blackfire->getProfile($uuid);

Here is a quick summary of available methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// the profile URL
$profile->getUrl();

// get the main costs
$cost = $profile->getMainCost();

// get SQL queries (returns an array of Cost instances)
$queries = $profile->getSqls();

// get HTTP requests (returns an array of Cost instances)
$requests = $profile->getHttpRequests();

// Cost methods
$cost->getWallTime();
$cost->getCpu();
$cost->getIo();
$cost->getNetwork();
$cost->getPeakMemoryUsage();
$cost->getMemoryUsage();

You can also access the Blackfire test results:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// tests were successful (all assertions pass)
$profile->isSuccessful();

// an error occurred when running the tests (different from assertion failures)
// an error can be a syntax error in an assertion for instance
$profile->isErrored();

// get test results (returns an array of Blackfire\Profile\Test instances)
$tests = $profile->getTests();

// display all failures
if (!$profile->isErrored()) {
    foreach ($tests as $test) {
        if ($test->isSuccessful()) {
            continue;
        }

        printf("    %s: %s\n", $test->getState(), $test->getName());

        foreach ($test->getFailures() as $assertion) {
            printf("      - %s\n", $assertion);
        }
    }
}

The Client::createProbe() method takes a Blackfire\Profile\Configuration instance that allows to configure profile generation:

1
2
3
4
5
6
7
8
9
10
11
12
13
$config = new \Blackfire\Profile\Configuration();
$probe = $blackfire->createProbe($config);

// set the profile title
$config->setTitle('Homepage');

// attach some metadata to the profile
$config->setMetadata('pull-request', 42);

// By default, profiles created with the SDK won't appear in the profiles list.
// Disable "skip_timeline" if you want your profiles to be listed anyway.
// "false" must be passed as a string. "0" is valid as well.
//$config->setMetadata('skip_timeline', 'false');

The Configuration class implements a fluent interface:

1
$config = (new \Blackfire\Profile\Configuration())->setTitle('Homepage')->setMetadata('pull-request', 42);

Besides basic profile configuration, you can also configure assertions:

1
2
3
// set some assertions
// first argument is the assertion, second one is the assertion/test name
$config->assert('metrics.sql.queries.count > 50', 'I want many SQL requests');

To keep the API simple and unlike tests defined in .blackfire.yaml, tests defined via assert() only ever contains one assertion.

If you need to, you can create some custom metrics as well:

1
2
3
4
5
6
7
8
use Blackfire\Profile\Metric;

// define a custom metric
$metric = new Metric('cache.write_calls', '=Cache::write');

// add it to the profile configuration
// to  be able to use it in assertions
$config->defineMetric($metric);

When defining a Metric, the first constructor argument is the metric name (cache.write_calls here) which can used in an assertion by prefixing it with metrics. and appending a dimension (like in metrics.cache.write_calls.count).

The second constructor argument is a method selector or an array of method selectors:

1
$metric = new Metric('cache.write_calls', array('=Cache::write', '=Cache::store'));

When profiling from the CLI or a browser, you can set the number of samples you want for a profile (to get more accurate results). You can do the same with the Client, via the setSamples() configuration method:

1
$config->setSamples(10);

Be warned, you need to generate the samples manually in your code as Blackfire has no way to do it automatically:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// define the number of samples
$samples = 10;

$blackfire = new \Blackfire\Client();

$config = (new \Blackfire\Profile\Configuration())->setSamples($samples);

// explicitly pass the configuration
// and disable auto-enable
$probe = $blackfire->createProbe($config, false);

for ($i = 1; $i <= $samples; $i++) {
    // enable instrumentation
    $probe->enable();

    foo();

    // finish the profile
    // so that another one will be taken on the next loop iteration
    $probe->close();
}

// send the results back to Blackfire
$profile = $blackfire->endProbe($probe);

If you do not call $probe->close() the same number of times as the number of configured samples, you will get a 404 (not found) result when calling $blackfire->endProbe().

Using the Blackfire Client, you can create HTTP scenarios directly from PHP. Enabling code instrumentation for HTTP requests is done via a specific HTTP header (X-Blackfire-Query); the value containing the profile configuration and a signature used for authorization.

1
2
3
4
5
$blackfire = new \Blackfire\Client();

// generate the HTTP header to enable Blackfire
$request = $blackfire->createRequest();
$header = 'X-Blackfire-Query: '.$request->getToken();

Specify the profile title via the first argument:

1
$request = $blackfire->createRequest('Homepage');

Or pass a full Blackfire\Profile\Configuration instance:

1
2
$config = (new \Blackfire\Profile\Configuration())->setTitle('Homepage');
$request = $blackfire->createRequest($config);

Get the generated profile from the profile request:

1
$profile = $blackfire->getProfile($request->getUuid());

Learn more about how to use this feature with Guzzle or Goutte.

When an error occurs because of Blackfire, a \Blackfire\Exception\ExceptionInterface exception instance is thrown:

  • \Blackfire\Exception\NotAvailableException: The Blackfire PHP extension is not installed or not enabled.
  • \Blackfire\Exception\OfflineException: You are offline or the Blackfire servers are not reachable (check your proxy configuration).
  • \Blackfire\Exception\ApiException: An error occurred when communicating with Blackfire.
  • \Blackfire\Exception\ConfigNotFoundException: The Blackfire configuration file cannot be found.
  • \Blackfire\Exception\EnvNotFoundException: The configured environment does not exist.

To avoid breaking your code when such errors occur, wrap your code and catch the interface:

1
2
3
4
5
6
7
8
9
10
try {
    $probe = $blackfire->createProbe($config);

    // do something

    $profile = $blackfire->endProbe($probe);
} catch (\Blackfire\Exception\ExceptionInterface $e) {
    // Blackfire error occurred during profiling
    // do something
}

When creating a Client, Blackfire uses your local Blackfire configuration by default (stored in ~/.blackfire.ini). But you can also set the configuration explicitly:

1
2
3
4
5
6
7
8
// all arguments are optional
$config = new \Blackfire\ClientConfiguration($clientId, $clientToken, $defaultEnv);

// or read it from a file
$config = \Blackfire\ClientConfiguration::createFromFile('some-blackfire.ini');

// use the configuration
$blackfire = new \Blackfire\Client($config);

By default, profiles are sent to your personal profiles, but you can change the environment via the setEnv() method:

1
$config->setEnv('mywebsite');

Instead of creating Scenarios and Builds programmatically like described below, you may consider one of the following integrations provided by the PHP SDK:

When generating more than one profile, like when you profile complex user interactions with your application, you might want to aggregate them in a scenario. Scenarios can be grouped in a build.

Using builds and scenarios has the following benefits:

  • A scenario written in PHP is a powerful alternative to the scenarios defined in a .blackfire.yaml file.
  • A scenario consolidates tests results and generates a Report
  • A build consolidates scenarios results and sends notifications (Github, Slack, ...);
  • A build contains all profiles from a profiling session (individual profiles are not displayed in the dashboard anymore);

Creating a scenario is not that different from profiling individual pages as described in the previous paragraphs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// create a build (optional)
$build = $blackfire->startBuild('Symfony Prod', array(
    'title' => 'Build from PHP',
    'trigger_name' => 'PHP',
));

// create a scenario (if the $build argument is null, a new build will be created)
$scenario = $blackfire->startScenario($build, array(
    'title' => 'Test documentation'
));

// create a configuration
$config = new \Blackfire\Profile\Configuration();
$config->setScenario($scenario);

// create as many profiles as you need
$probe = $blackfire->createProbe($config);

// some PHP code you want to profile

$blackfire->endProbe($probe);

// end the scenario and fetch the report
$report = $blackfire->closeScenario($scenario);

// end the build
$blackfire->closeBuild($build);
// or if the build was created automatically: $blackfire->closeBuild($scenario->getBuild());

To create a build, call startBuild() and pass it the environment name (or UUID). Optionally pass an array of options:

  • title: A build title;
  • trigger_name: A trigger name (displayed in the dashboard);
  • external_id: A unique identifier for the build; commonly, a unique reference from a third party service like the Git commit sha1 related to the build. It is used by some notifications (like Github);
  • external_parent_id: The unique identifier of the parent build.

The external_id and external_parent_id options can be used to integrate Blackfire with GitHub pull request statuses

To create a scenario, call startScenario() and pass it a build, or null to create a new build). Optionally pass an array of options:

  • title: A scenario title;
  • metadata: An array of metadata to associated with the scenario;
  • external_id: A unique identifier for the scenario;
  • external_parent_id: The unique identifier of the parent scenario. It is used to compare scenarios;

To store a profile in the scenario, call createProbe() and pass it a Configuration object that has been tied to the scenario ($config->setScenario($scenario)).

Stop the scenario by calling closeScenario() and the build by calling closeBuild(). The closeScenario() call returns the Report when ready.

You can store the scenario UUID to get report back later:

1
2
3
4
5
// store the scenario UUID
$uuid = $scenario->getUuid();

// retrieve the report later on
$report = $blackfire->getReport($uuid);

HTTP scenarios can be created with Goutte or Guzzle.

Blackfire\\Report instances (as returned by Client::closeScenario()) give you access to builds data:

1
$report = $blackfire->closeScenario($scenario);

You can also get any report by UUID:

1
$report = $blackfire->getReport($uuid);

Here is a summary of available methods:

1
2
3
4
5
6
7
8
9
// the report URL
$report->getUrl();

// tests were successful (all assertions pass)
$report->isSuccessful();

// an error occurred when running the tests (different from assertion failures)
// an error can be a syntax error in an assertion for instance
$report->isErrored();

When using the Blackfire CLI or a browser, Blackfire automatically instruments your code. If you want to control which part of the code is instrumented, you need to enable and disable instrumentation manually.

You can manually instrument some code by using the \BlackfireProbeclass that comes bundled with the Blackfire's probe:

1
2
// Get the probe main instance
$probe = BlackfireProbe::getMainInstance();

Starts gathering profiling data by calling enable():

1
2
// start profiling the code
$probe->enable();

Stops the profiling by calling disable():

1
2
// stop the profiling
$probe->disable();

You can call enable() and disable() as many times as needed in your code. You can also discard any collected data by calling discard().

Calling close() instead of disable() stops the profiling and forces the collected data to be sent to Blackfire:

1
2
3
// stop the profiling
// send the result to Blackfire
$probe->close();

As with auto-instrumentation, profiling is only active when the code is run through the browser extension or the blackfire CLI utility. If not, all calls are converted to no-ops.

The PHP SDK provides a way to profile consumers and daemons via the LoopClient class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
require_once __DIR__.'/vendor/autoload.php';

use Blackfire\LoopClient;
use Blackfire\Client;
use Blackfire\Profile\Configuration as ProfileConfiguration;

function consume()
{
    echo "Message consumed!\n";
}

$blackfire = new LoopClient(new Client(), 10);
$profileConfig = new ProfileConfiguration();
$profileConfig->setTitle('Consumer');

while (true) {
    $blackfire->startLoop($profileConfig);

    consume();

    if ($profile = $blackfire->endLoop()) {
        print $profile->getUrl()."\n";
    }

    usleep(400000);
}

The LoopClient constructor takes a Blackfire Client instance and the number of iterations for each profile. Call startLoop() when entering a new loop iteration (before consuming a message for instance), and call endLoop() at the end of the message processing.

Calling the generateBuilds() method automatically configures LoopClient to generate a build for each profile:

1
$blackfire->generateBuilds('ENV_NAME_OR_UUID');

If you need more flexibility, like setting a different title for each build, pass a Build factory as the second argument:

1
2
3
4
5
use Blackfire\Client;

$blackfire->generateBuilds('ENV_NAME_OR_UUID', function (Client $blackfire, $env) {
    return $blackfire->startBuild($env, array('title' => 'Some generated title'));
});

Profiling a consumer continuously is never a good idea. Instead, you should profile your consumers on demand or regularly.

LoopClient supports this use case via signals. Register the signal you want to use for profiling and the code will only profile when signaled:

1
2
$blackfire = new LoopClient(new Client(), 10);
$blackfire->setSignal(SIGUSR1);

With this code in place, generating a profile of your consumer can be done by sending the SIGUSR1 signal to the process:

1
pkill -SIGUSR1 -f consumer.php

Signal the consumer with SIGUSR1 to generate a regular profile.

Triggering profiles on a pre-defined schedule can be done by adding an entry in the crontab that signals the consumer process.

When using the SDK to automatically run test scenarios on git branches, for instance with a GitHub integration or GitLab integration, you will need to update the commit status to ease the validation and merge decision.

Commit statuses are associated to builds via the corresponding Git commit sha1. After enabling the notification channel (GitHub, Gitlab, or webhooks) for an environment, pass the Git sha1 to the startBuild() method options:

1
2
3
4
$build = $blackfire->startBuild('env-name-or-uuid', array(
    'title' => 'Build from PHP',
    'external_id' => '178f05003a095eb6b3a838d403544962262b7ed4',
));

You must pass the full 40-character sha1 or GitHub won't accept the commit statuses.

To activate the comparison assertions on scenarios, pass a external_parent_id to the scenario's options. This is typically the sha1 of the base branch of the pull request, concatenated with the name of the scenario:

1
2
3
4
5
$scenario = $blackfire->startScenario($build, array(
    'title' => 'My Scenario',
    'external_id' => '178f05003a095eb6b3a838d403544962262b7ed4:my-scenario',
    'external_parent_id' => 'f72aa774dd33cfc5eac43e1bfaf67e4028acca8b:my-scenario',
));

Thanks to the Distributed Profiling feature, you can embed sub-profiles in the main profile.

For instance, when profiling an application that calls HTTP services and/or sub-processes, you might want to trigger profiles for them as well. In PHP, the process can be either automatic or manual, depending on what you need to do.

Blackfire PHP probe supports automatic sub-profiles generation when doing HTTP requests with:

  • cURL;
  • HTTP streams (fopen/file_get_contents).

These 2 methods cover most of the HTTP client libraries available for PHP, including Symfony HttpClient, Guzzle, Goutte, etc.

If you use a different way to send HTTP requests, you then have to generate a sub-profile query manually. This query must be added as an X-Blackfire-Query HTTP header.

This is for instance the case when using the SOAP extension for PHP:

1
2
3
4
5
6
7
8
9
10
11
12
13
$soapOpts = [];
$probe = \BlackfireProbe::getMainInstance();
if (\BlackfireProbe::isEnabled()) {
    $soapOpts += [
        'stream_context' => stream_context_create([
            'http' => [
                'header' => 'X-Blackfire-Query: '.$probe->createSubProfileQuery(),
            ],
        ]),
    ];
}

$client = new \SoapClient('some.wsdl', $soapOpts);

It is also possible to trigger sub-profiles when spawning CLI processes from PHP scripts. The process is the same as for HTTP, except that the sub-profile query must be assigned to the BLACKFIRE_QUERY environment variable, which must be exposed to the CLI process.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Spawning to sub.php.
// Works with any language supported by Blackfire.

$processEnv = $_ENV;
if (\BlackfireProbe::isEnabled()) {
    $probe = \BlackfireProbe::getMainInstance();
    $processEnv['BLACKFIRE_QUERY'] = $probe->createSubProfileQuery();
}

$proc = proc_open(
    'php sub.php',
    [STDIN, STDOUT, STDOUT],
    $pipes, null,
    $processEnv
);
proc_close($proc);