Melody: One-file PHP scripts with Composer dependencies

Melody is a really handy open-source tool to create one-file PHP scripts with Composer dependencies.

Melody is a tool to execute one-line PHP scripts with dependencies

Melody is a tool to execute one-line PHP scripts with dependencies

Installation

As Melody is just a PHAR file, we only need to download it and include it in one of the directories included in our PATH environment variable, for example at /usr/local/bin/:

$ sudo sh -c "curl http://get.sensiolabs.org/melody.phar -o /usr/local/bin/melody && chmod a+x /usr/local/bin/melody"

Then, if we execute “melody”, we should see something like this:

$ melody
Melody version 1.0 by SensioLabs

Usage:
 [options] command [arguments]
Options:
 --help (-h)           Display this help message.
 --quiet (-q)          Do not output any message.
 --verbose (-v|vv|vvv) Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug.
 --version (-V)        Display this application version.
 --ansi                Force ANSI output.
 --no-ansi             Disable ANSI output.
 --no-interaction (-n) Do not ask any interactive question.

Available commands:
 help          Displays help for a command
 list          Lists commands
 run           execute a script
 self-update   Update melody.phar to the latest version.

Simple example

Let’s create a simple example to understand how Melody works. The following script makes use of the symfony/finder component to look for PNG files in the current directory (and subdirectories):

<?php
<<<CONFIG
packages:
    - "symfony/finder: ~2.6"
CONFIG;

$finder = Symfony\Component\Finder\Finder::create()
    ->in(__DIR__)
    ->name('*.png')
;

foreach ($finder as $file) {
    echo $file, "\n";
}

As you can see, it’s a regular PHP script with a heredoc string at the beginning, which defines the packages the script depends on, using the YAML format in a similar way as we do it in composer.json files.

Melody reads the heredoc string called CONFIG and downloads the packages behind the scenes using Composer. To run the script:

$ melody run example.php
/Users/raulfraile/logo.png
/Users/raulfraile/photo.png

Extra arguments

It is possible to send additional parameters to the PHP script appending them after the name of the script so they can be read using the $argc and $argv variables. For example, to configure the directory to look for PNG images:

$finder = Symfony\Component\Finder\Finder::create()
    ->in($argv[1])
    ->name('*.png')
;

And then:

$ melody run example.php /tmp
/tmp/image.png

PHP options

What if you need to pass additional options to the PHP binary? For example, you may want to start the built-in server or change some settings. This can be done using the “php-options” setting.

In the following example, the memory_limit setting has been set to 256M and the built-in server will be launched:

<<<CONFIG
packages:
    - ...
php-options:
    - "-d"
    - "memory_limit=256M"
    - "-S"
    - "localhost:8000"
CONFIG;

Running gists

Melody has another nice feature, it can run GitHub gists directly! This can be useful for scripts that we want to execute in different computers. For example, I created a simple script to check your ServerGrove’s mail settings sending a test email based on servergrove/email-test. To execute it, you need to include 3 parameters: $from, $to and $password:

$ melody run https://gist.github.com/raulfraile/8b87792f1ecbca520191 from@test.com to@test.com 123456
OK

This script sends a test email from from@test.com to to@test.com using the ServerGrove’s mail server. Obviously, it will only work if you are using our mail server, but setting up a different one is trivial.

Conclusion

Melody can be really handy. At ServerGrove, we are thinking about creating a repository of small tools to help our clients to check and manage their servers. What do you think?

Photo: “Music box 2″, by grfx_guru.

April 15 / 2015
Author Raul Fraile
Category PHP, Tutorials
Comments No Comments

Upcoming Conferences

WeCamp

PHP New Zealand

PHP Summer Camp Croatia

PHPNE

MadisonPHP

brnoPHP

SymfonyLiveLondon

ZgPHP

PHPSouthAfrica

PHPNWUK

SymfonyLiveNYC

PHPForumParis

PHPARG

PHPWorld

TechMeetupUY

SymfonyConMadrid

Symfony2 components overview: OptionsResolver

In the 13th post of the Symfony2 components series we will be talking about one little but extremely useful component: OptionsResolver. This component helps us to reduce the boilerplate code required to create an options system with default parameters. As stated in the official docs, is array_replace on steroids.

9468151425_1ca4b21cb0_z

Different ways to set options? The OptionsResolver component can help you

The problem

I am pretty sure this code sounds familiar to you:

$options = [
    'page' => isset($input['page']) ? $input['page'] : 1,
    'items' => isset($input['items']) ? $input['items'] : 10
];

Here, we want to have an array with options that come from the user (for example, from a RESTful API). And if any of the required options is not present, we use default options. For this, we need to check if it’s present and use the default value otherwise. All this boilerplate is difficult to read and very repetitive.

An alternative would be to use the array_replace function:

$options = array_replace([
    'page'  => 1,
    'items' => 10
], $input);

The array_replace function replaces elements from the second argument into the first one, so if $input contains a different value for any of the options, it would be overwritten.

While this solution is good enough, the OptionsResolver component provides a few interesting features. In addition to dealing with default values, it can validate and normalize the data.

Simple example

The previous example using OptionsResolver would look like this:

use Symfony\Component\OptionsResolver\OptionsResolver;

$resolver = new OptionsResolver();

$resolver->setDefaults([
    'page' => 1,
    'items' => 10
]);

$options = $resolver->resolve($input);

Once executed, $options will contain a plain array with two elements: ‘page’ and ‘items’. If $input contains values for any of these values, they will be overwritten. Let’s see a few examples:

$input = []; // 'page' => 1, 'items' => 10
$input = ['page' => 2]; // 'page' => 2, 'items' => 10
$input = ['page' => 2, 'items' => 20]; // 'page' => 2, 'items' => 20
$input = ['other' => 5]; // UndefinedOptionsException

That’s right! If we try to use a value other than the ones defined, it throws an exception. This would be the first advantage of using the component over array_replace, but there are more.

It is also possible to use a closure to set default parameters in case they depend on something else. We can even set default parameters based on other parameters. For example, let’s add a third parameter, ‘order’, and define the order randomly. You know… improving UX :)

$resolver->setDefault('order', function (Options $options) {
    $orders = ['asc', 'desc'];

    return $orders[rand(0, 1)];
});

As you can see, the function receives an Options object, so we can use it to generate default values based on other parameters. Imagine that we have also a ‘order_by’ parameter, so we could set the order based on this:

$resolver->setDefault('order', function (Options $options) {
    if ('creation_date' === $options['order_by']) {
        return 'desc';
    }

    return 'asc';
});

Validation

We may want to validate input data, something more than necessary when dealing with user-provided data. The component provides some ways to validate data by type or by value using the methods setAllowedTypes() and setAllowedValues().

With setAllowedTypes(), it is possible to restrict the value of the option to the given types. For example, to restrict ‘page’ values to integer values:

$resolver->setAllowedTypes('page', 'int');

It is also possible to allow more than one type:

$resolver->setAllowedTypes('page', ['int', 'float']);

What if we want to constrain the range of values that can be used? Using the method setAllowedValues() we can define a set of allowed values. For example, to limit the possible values of ‘items’ to 10, 20 or 40:

$resolver->setAllowedValues('items', [10, 20, 40]);

But setAllowedValues() is much more powerful, as it can receive a Closure. In the following example, we can validate that ‘page’ is between 1 and 10:

$resolver->setAllowedValues('page', function($value) {
    return $value >= 1 && $value <=10;
});

If you read the post about the Validator component, you know that there is already a constraint to define ranges. Integrating the validator component is really easy:

use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Constraints\Range;

$resolver->setAllowedValues('items', function($value) {
    $validator = Validation::createValidator();
    $constraint = new Range([
        'min' => 1,
        'max' => 10
    ]);

    $violations = $validator->validate($value, $constraint);

    return 0 === $violations->count();
});

Normalization

Finally, we can also normalize data before using the options with the setNormalizer() method. It accepts a Closure that receives the value and the rest of options. For example, we could normalize the ‘page’ option in case that the value provided by the user is over the maximum number of pages:

$resolver->setNormalizer('page', function ($options, $value) use ($maxPage) {
    if ($value > $maxPage) {
        $value = $maxPage;
    }

    return $value;
});

Who is using it

More info

Photo: You Choose Your Path, by James Wheeler

April 13 / 2015

Upcoming Conferences

WeCamp

PHP New Zealand

PHP Summer Camp Croatia

PHPNE

MadisonPHP

brnoPHP

SymfonyLiveLondon

ZgPHP

PHPSouthAfrica

PHPNWUK

SymfonyLiveNYC

PHPForumParis

PHPARG

PHPWorld

TechMeetupUY

SymfonyConMadrid

New Symfony installer: the fastest way to start your Symfony project

Yesterday, the Symfony team introduced the new Symfony installer. Its main goal is to help developers to create Symfony projects faster.

Until now, installing Symfony to start a new project required a few steps:

  1. Download the code (zip file, git clone, etc.)
  2. Install vendors with “composer install”
  3. Tweak configuration settings

The installer tries to do this in one step. It downloads a compressed file with all the code, including the vendors directory, so you don’t need anything else to run Symfony for the first time.

Installing the installer

The Symfony installer is a PHAR file that runs on PHP 5.4+ (if you are still using PHP 5.3, upgrade!). To install it in your system:

Unix-based systems (Linux, Mac OS)

$ sudo curl -LsS http://symfony.com/installer -o /usr/local/bin/symfony
$ sudo chmod a+x /usr/local/bin/symfony

Windows

c:\> php -r "readfile('http://symfony.com/installer');" > symfony

Create a new project

To create a new project, just execute “symfony new [directory]”:

New project

This will create a new Symfony project using the latest stable version. To use a different version, the command accepts another parameter to use a different version. This parameter can be a branch (it will install the latest version for the branch), a specific version or even “lts” to use the most recent LTS (long term support) version.

Generating a demo project

The tool includes a command to generate a demo project following the official best practices, so you can learn from it.

To generate the project, just run “symfony demo”:

Generate demo project

Then, execute “php app/console server:run” to test the demo project using the PHP built-in webserver. The demo project should look like this:

Demo project

And this is the demo backend:

Demo backend

What about Composer?

Composer is not being replaced. At all. In fact, Composer is being used in the server to download all dependencies before generating the compressed file that you will use. Also, you will still need Composer to add new dependencies or upgrading Symfony.

Future work

They have been working hard during the last few months to create the installer. It works great but it needs some extra features that I am sure will be added in the future, as the first goal was to have a tool easy to use and working in multiple platforms.

  • HTTPS support: currently, the installer downloads compressed files using HTTP, instead of HTTPS. Using HTTPS is highly recommended to avoid man-in-the-middle attacks, as someone might be able to change the contents of the compressed file to inject malicious code.
  • Caching: if you install the same version of Symfony twice, it will be downloaded twice. Adding a cache layer would be interesting, as it would also allow to have some sort of “offline mode”. Have you tried to start a new Symfony project while you are in a train? :)
  • Reduce file size: depending on the developer machine, the installer chooses between two formats: zip and gzip (tar.gz). Much better compression ratios can be achieved with bzip2 and xz. It would make the installer even faster!

Under the hood

The installer uses the Symfony console component to create a command-line application. Then, to create a new project, it chooses the format of the compressed file that will be downloaded using Distill. As the GZIP file is smaller than the ZIP one, it checks if your machine can decompress GZIP files. Files are downloaded with Guzzle and decompressed using again Distill. Finally, it removes some files that are not useful for your project, like LICENSE, UPGRADE*.md and CHANGELOG*.md files, generates a proper random secret value using openssl_random_pseudo_bytes() if available and creates an appropiate .gitignore file.

Photo: “Speed…”, by Rami.

March 27 / 2015
Author Raul Fraile
Category PHP, Symfony, Tutorials
Comments 5 Comments

Upcoming Conferences

WeCamp

PHP New Zealand

PHP Summer Camp Croatia

PHPNE

MadisonPHP

brnoPHP

SymfonyLiveLondon

ZgPHP

PHPSouthAfrica

PHPNWUK

SymfonyLiveNYC

PHPForumParis

PHPARG

PHPWorld

TechMeetupUY

SymfonyConMadrid

Security tools for PHP projects

Security is getting more and more important, and the PHP community has been doing great improvements in this topic during the last few years. From better configuration settings to provide some level of security by default to frameworks providing functionality to avoid common attacks such as XSS, CSRF or SQL injection.

If you don't know that a bug exists yet, is it still a bug?

If you don’t know that a bug exists yet, is it still a bug?

There are dozens of measures we can take in order to prevent and minimize security issues, both in the server and in the project’s code:

  • Disable PHP modules that are not in use.
  • Restrict information leakage (don’t display PHP errors or extra information like PHP version)
  • Turn off remote execution (allow_url_fopen and allow_url_include should be Off)
  • Control the size of POST requests and uploaded files.
  • Control the maximum time and memory PHP can take to minimize DoS attacks.
  • Disable dangerous functions such as eval() or exec() if they are not going to be used.
  • Disable session IDs in URLs to prevent session fixation.
  • Include a token in your forms to avoid CSRF.
  • Properly escape the output to avoid XSS.

Recommended article: “Linux: 25 PHP Security Best Practices For Sys Admins”

So, our server and our code is ready. But, what about code that we don’t have full control of? Composer has dramatically changed the way we organize our projects, with many small packages and libraries, and this adds a new challenges for keeping our projects secure. The question is: is it safe to rely on code that others have written? are these package maintainers taking the same care as I do in my projects?

Well, any piece of software can have bugs, and obviously open source projects are not an exception. The good point is that security researchers, once they find a vulnerability, it is reported and added to a database of known vulnerabilities. We basically need to find a way to avoid using code with known vulnerabilities, and there are some interesting tools out there to help us.

Tools

The main goal of automatic tools is to let you know of security issues as soon as possible. Here, I present four tools to check PHP packages, configuration settings and PHP versions.

Security Advisories Checker

This service, created by SensioLabs (the company behind Symfony) checks your composer.lock file to find packages you depend on that have known security issues. These security issues are included in a database called Security Advisories Database, which is public and updated frequently.

There are three ways to use the service:

* Online checker: uploading the composer.lock file.
* CLI Checker: to check files from the command line.
* API: it can be integrated in your own projects.

For example, if we execute the CLI tool to check a project using Symfony 2.1 and an older version of Swiftmailer, we get a few vulnerabilities:

$ security-checker security:check composer.lock

Security Check Report
~~~~~~~~~~~~~~~~~~~~~

Checked file: /tmp/composer.lock

[CRITICAL]
2 packages have known vulnerabilities

swiftmailer/swiftmailer (v4.2.2)
--------------------------------

* Sendmail transport arbitrary shell execution
http://blog.swiftmailer.org/post/88660759928/security-fix-swiftmailer-5-2-1-released

symfony/symfony (2.1.x-dev)
---------------------------

* CVE-2014-4931: Code injection in the way Symfony implements translation caching in FrameworkBundle
http://symfony.com/blog/security-releases-cve-2014-4931-symfony-2-3-18-2-4-8-and-2-5-2-released

* CVE-2014-6072: CSRF vulnerability in the Web Profiler
http://symfony.com/blog/cve-2014-6072-csrf-vulnerability-in-the-web-profiler

* CVE-2014-6061: Security issue when parsing the Authorization header
http://symfony.com/blog/cve-2014-6061-security-issue-when-parsing-the-authorization-header

* CVE-2014-5244: Denial of service with a malicious HTTP Host header
http://symfony.com/blog/cve-2014-5244-denial-of-service-with-a-malicious-http-host-header

* CVE-2014-5245: Direct access of ESI URLs behind a trusted proxy
http://symfony.com/blog/cve-2014-5245-direct-access-of-esi-urls-behind-a-trusted-proxy

This checker can only detect vulnerabilities that are referenced
Disclaimer in the SensioLabs security advisories database. Execute this
command regularly to check the newly discovered vulnerabilities.
```

If you check your dependencies and get any vulnerability, please upgrade your packages as soon as possible.

Roave Security Advisories

The Roave team has created a Composer package called roave/security-advisories that prevents you from installing packages with known security issues by using an smart trick: it adds all packages with vulnerabilities to the `conflict` section, making them unavailable for your project.

    "conflict": {
        "contao/core": ">=2.11.0,<2.11.16|>=3.0.0,<3.1.0|>=3.1.0,<3.2.0|>=3.2.0,<3.2.7|>=2.11.0,<2.11.17|>=3.0.0,<3.1.0|>=3.1.0,<3.2.0|>=3.2.0,<3.2.9|>=2.11.0,<3.0.0|>=3.0.0,<3.1.0|>=3.1.0,<3.2.0|>=3.2.0,<3.2.19|>=3.3.0,<3.4.0|>=3.4.0,<3.4.4",
        "doctrine/dbal": ">=2.0.0,<2.0.8|>=2.1.0,<2.1.2",
        "doctrine/doctrine-module": "<=0.7.1|<=0.7.1",
        "doctrine/orm": ">=2.0.0,<2.0.3",
         ...
    }

So you only have to add this line to your `composer.json` file and say goodbye to those packages!

"require":{
    ...
    "roave/security-advisories": "dev-master"
}

Keep in mind that due to the nature of the problem, there are no releases or stable versions of the package, you need to rely on the dev-master branch, which is updated periodically. So the package is only suited for installation in the root of your deployable project.

To generate the list of packages, the same list as in the previous tool is used.

iniscan

iniscan is a tool created by Chris Cornutt that scans your php.ini file to check for possible security issues from your configuration settings. The list of tests that iniscan performs can be seen by executing “iniscan list-tests”. Currently, there are around 40 tests, some of them are listed below:

  • allow_url_fopen: (must be Off) It is recommended to not allow opening remote files.
  • allow_url_include: (must be Off) It is also highly recommended to not be able to include remote files.
  • display_errors: (must be Off) You should never display PHP errors in production.
  • expose_php: (must be Off) If is set to On, it adds a HTTP header with information about the PHP version. It is not recommended to expose additional information.
  • session.use_only_cookies: (must be On) It is not a good idea to allow passing session ids in URLs. If someone else intercepts the session id (it can also be predicted or guessed using brute-force) can use it to enter into the session.
  • session.cookie_httponly: (must be On) It makes cookies only accessible by the HTTP protocol, not by JavaScript. It helps to minimize the effects of a XSS attack.

After executing the command, you will get an output like this:

$ iniscan scan --fail-only
== Executing INI Scan [03.11.2015 08:11:40] ==

Status | Severity | PHP Version | Key | Description
----------------------------------------------------------------------
FAIL | WARNING | | session.cookie_domain | It is recommended that you set the default domain for cookies.
FAIL | ERROR | 5.2.0 | session.cookie_httponly | Setting session cookies to 'http only' makes them only readable by the browser
FAIL | WARNING | | session.hash_function | Weak hashing algorithms in use. Rather use one of these: sha224, sha256, sha384, sha512, ripemd128, ripemd160, ripemd256, ripemd320, whirlpool, tiger128,3, tiger160,3, tiger192,3, tiger128,4, tiger160,4, tiger192,4, snefru256, adler32, crc32, crc32b, fnv132, fnv164, joaat, haval128,3, haval160,3, haval192,3, haval224,3, haval256,3, haval128,4, haval160,4, haval192,4, haval224,4, haval256,4, haval128,5, haval160,5, haval192,5, haval224,5, haval256,5
FAIL | ERROR | 4.0.4 | session.cookie_secure | Cookie secure specifies whether cookies should only be sent over secure connections.
FAIL | WARNING | 5.5.2 | session.use_strict_mode | Strict mode prevents uninitialized session IDs in the built-in session handling.
FAIL | ERROR | 4.0.3 | allow_url_fopen | Do not allow the opening of remote file resources ('Off' recommended)
FAIL | WARNING | | display_errors | Don't show errors in production ('Off' recommended)
FAIL | WARNING | | expose_php | Showing the PHP signature exposes additional information
FAIL | WARNING | | post_max_size | Unless necessary, a maximum post size of 200M is too large
FAIL | WARNING | | display_startup_errors | Showing startup errors could provide extra information to potential attackers
FAIL | WARNING | | open_basedir | Restricting PHP's access to the file system to a certain directory prevents file-based attacks in unauthorized areas.
FAIL | WARNING | | upload_max_filesize | The max upload size should not be too high, to prevent server overload from large requests
FAIL | WARNING | | post_max_size | The max upload size should not be too high, to prevent server overload from large requests
FAIL | WARNING | | memory_limit | The standard memory limit should not be too high, if you need more memory for a single script you can adjust that during runtime using ini_set()
FAIL | WARNING | | xdebug.default_enable | Xdebug should be disabled in production
FAIL | WARNING | | disable_functions | Methods still enabled - exec, passthru, shell_exec, system, proc_open, popen, curl_exec, curl_multi_exec

23 passing
3 failure(s) and 13 warnings

pcc is a similar tool which also checks for PHP configuration settings.

versionscan

Unlike previous tools, versionscan does not check your dependencies or php.ini settings, it checks PHP itself to detect if known vulnerabilities are fixed or not. It can be really useful to execute in production environments. It was also created by Chris Cornutt.

It contains a local database of checks defined in a JSON file, so you will need to update the tool frequently to get updated lists of checks.

For example, if we execute the tool while using PHP 5.5.19 we get the following warnings:

$ versionscan scan
Executing against version: 5.5.19
+--------+---------------+------+------------------------------------------------------------------------------------------------------+
| Status | CVE ID | Risk | Summary |
+--------+---------------+------+------------------------------------------------------------------------------------------------------+
| FAIL | CVE-2015-0273 | 0 | Use after free vulnerability in unserialize() with DateTimeZone |
| FAIL | CVE-2014-9425 | 7.5 | Double free vulnerability in the zend_ts_hash_graceful_destroy function in zend_ts_hash.c in the ... |
| FAIL | CVE-2015-0232 | 6.8 | The exif_process_unicode function in ext/exif/exif.c in PHP before 5.4.37, 5.5.x before 5.5.21, a... |
| FAIL | CVE-2014-9427 | 6.4 | sapi/cgi/cgi_main.c in the CGI component in PHP through 5.4.36, 5.5.x through 5.5.20, and 5.6.x t... |
| FAIL | CVE-2014-9426 | 7.5 | The apprentice_load function in libmagic/apprentice.c in the Fileinfo component in PHP through 5.... |
| FAIL | CVE-2015-0231 | 7.5 | Use-after-free vulnerability in the process_nested_data function in ext/standard/var_unserializer... |
| FAIL | CVE-2014-8142 | 6.4 | Use-after-free vulnerability in the process_nested_data function in ext/standard/var_unserializer... |
+--------+---------------+------+------------------------------------------------------------------------------------------------------+

Scan complete
--------------------
Total checks: 315

Each line contains the CVE (Common Vulnerabilities and Exposures) ID associated to the vulnerability, the risk (a number between 0 and 10) and a quick summary of the issue. If you need more information about specific CVEs, you can look for them in the National Vulnerability Database.

By default, the tool checks vulnerabilities for the version contained in the `PHP_VERSION` constant, but it can also check other versions by using the “–php-version” argument:

$ versionscan scan --php-version=5.6.0
Executing against version: 5.6.0
+--------+---------------+------+------------------------------------------------------------------------------------------------------+
| Status | CVE ID | Risk | Summary |
+--------+---------------+------+------------------------------------------------------------------------------------------------------+
| FAIL | CVE-2014-9427 | 6.4 | sapi/cgi/cgi_main.c in the CGI component in PHP through 5.4.36, 5.5.x through 5.5.20, and 5.6.x t... |
| FAIL | CVE-2015-0232 | 6.8 | The exif_process_unicode function in ext/exif/exif.c in PHP before 5.4.37, 5.5.x before 5.5.21, a... |
| FAIL | CVE-2015-0273 | 0 | Use after free vulnerability in unserialize() with DateTimeZone |
| ...
+--------+---------------+------+------------------------------------------------------------------------------------------------------+

Scan complete
--------------------
Total checks: 315
Failures: 11

Conclusion

We have seen for different tools to help you to minimize security risks in your PHP projects. They are meant to detect known vulnerabilities as soon as possible, so you can take the appropriate measures.

Photo: “Just a boring traffic light”, by Tilemahos Efthimiadis

March 23 / 2015
Author Raul Fraile
Category PHP
Comments No Comments

Upcoming Conferences

WeCamp

PHP New Zealand

PHP Summer Camp Croatia

PHPNE

MadisonPHP

brnoPHP

SymfonyLiveLondon

ZgPHP

PHPSouthAfrica

PHPNWUK

SymfonyLiveNYC

PHPForumParis

PHPARG

PHPWorld

TechMeetupUY

SymfonyConMadrid

Symfony2 components overview: Stopwatch

It’s been a long wait, but we are back again with the Symfony2 components series. In the 12th post of the series, we cover the Stopwatch component. Even though is one of the smallest ones, that does not mean is not important, as plays a crucial role when we want to profile our code.

The Stopwatch component provides methods to measure execution time

The Stopwatch component provides methods to measure execution time

Installation

The recommended way of installing the component is through Composer:

{
    "require": {
        "symfony/stopwatch": "2.4.*"
    }
}

If you have never used Composer before check out our Composer 101 post.

The component

The Stopwatch component provides useful methods to measure the execution time of pieces of PHP code. In addition to parsing the output of the microtime() function for us, it allows tagging events, measure events in periods and get the max memory usage.

The component was added in Symfony 2.2, extracted from the HttpKernel component, so it was not written from scratch.

Simple example

Let’s see the component in action! In the following example, we try to measure the time spend by PHP to run the code sleep(1), which is obviously 1 second, but there is also some internal processing time such as looking for the function in the functions table:


use Symfony\Component\Stopwatch\Stopwatch;

$stopwatch = new Stopwatch();
$event = $stopwatch->start('test');
sleep(1);
$event->stop();
var_dump($event->getDuration()); // int(1030)
var_dump($event->getMemory()); // int(524288)

Here, we create a Stopwatch object, which returns a StopwatchEvent object when calling the start() method. Each of the events we want to measure have a unique name, in this example, ‘test’ is the event name. Then, after waiting a second, we stop the event and display both the duration in milliseconds and the maximum memory usage.

A much more useful example would be the following one. In this example, we create a function to calculate the nth number in the Fibonacci sequence. As you may know, the nth number in the Fibonacci sequence is the sum of the previous two numbers of the sequence, except the first two, which are 1. So, the complexity of calculating the nth number using a naive approach increases as the number gets bigger. If we want to measure the time spend to calculate the first 35 numbers of the series, would be as simple as this:

use Symfony\Component\Stopwatch\Stopwatch;

function fibonacci($n)
{
    if ($n <= 2) {
        return 1;
    } else {
        return fibonacci($n - 1) + fibonacci($n - 2);
    }
}

$stopwatch = new Stopwatch();
for ($i=1; $i<35;$i++) {
    $eventName = sprintf('Fibonacci %d', $i);
    $event = $stopwatch->start($eventName, 'fibonacci');
    fibonacci($i);
    $event->stop();

    printf("- %s: %dms\n", $eventName, $event->getDuration());
}

We get the expected results, being exponentially slower in every iteration.

- Fibonacci 1: 0ms
- Fibonacci 2: 0ms
...
- Fibonacci 15: 1ms
...
- Fibonacci 20: 16ms
...
- Fibonacci 25: 180ms
...
- Fibonacci 30: 2076ms
- Fibonacci 31: 3806ms
- Fibonacci 32: 5733ms
- Fibonacci 33: 8874ms
- Fibonacci 34: 15672ms
- Fibonacci 35: 24304ms

Periods

The lap() method allows us to measure the time for the same event in periods, which can be useful if what we want to measure is done is several steps. For example, we could measure the total time for reading, parsing and writing the results in another file, but divide it in different periods.

use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\Yaml\Yaml;

$stopwatch = new Stopwatch();

$event = $stopwatch->start('files');

// read file
$contents = file_get_contents('data.yml');
$event->lap();

// parse
$parsed = Yaml::parse($contents);
$event->lap();

// generate output
$output = '';
foreach ($parsed as $user) {
    $output .= $user['name'] . "\n";
}
$event->lap();

// write
file_put_contents('output.txt', $output);
$event->stop();

var_dump($event->getDuration());
foreach ($event->getPeriods() as $period) {
    var_dump($period->getDuration());
}

We get as an output:

int(5)
int(0)
int(4)
int(0)
int(1)

The first result is the total time, while the other four correspond to the four different periods. We can see that the most expensive ones are parsing the YAML file and writing the output to disk.

Sections

Sections are a nice way to group different events, even if they take place in separate parts of our code. For example, we may want to measure all the time spent in IO operations, so we can create a group named “io” and use openSection() and stopSection() to have a logical separation.

In the following example, we have two sections: “io” and “parsing”. Instead of using the actual functions, we call sleep() to make the example clearer.

use Symfony\Component\Stopwatch\Stopwatch;

$stopwatch = new Stopwatch();

// section io (1)
$stopwatch->openSection();
$stopwatch->start('read_file', 'read');
sleep(1);
$stopwatch->stop('read_file');
$stopwatch->stopSection('io');

// section parsing (1)
$stopwatch->openSection();
$stopwatch->start('parse_file', 'yaml');
sleep(1);
$stopwatch->stop('parse_file');
$stopwatch->stopSection('parsing');

// section io (2)
$stopwatch->openSection('io');
$stopwatch->start('write_file', 'write');
sleep(1);
$stopwatch->stop('write_file');
$stopwatch->stopSection('io');

echo "Category 'io'\n";
foreach ($stopwatch->getSectionEvents('io') as $event) {
    printf(" - %s: %d\n", $event->getCategory(), $event->getDuration());
}

echo "Category 'parsing'\n";
foreach ($stopwatch->getSectionEvents('parsing') as $event) {
    printf(" - %s: %d\n", $event->getCategory(), $event->getDuration());
}

Here, in the first two blocks, we open both sections, while in the third block, we open the previously used section “io”. Notice that we only provide the section name when closing it or if it already exists. Finally, we get each of the events for both sections and display their duration times:

Category 'io'
 - default: 2003
 - read: 1001
 - write: 1001
Category 'parsing'
 - default: 1000
 - yaml: 1000

As you see, there is an extra event, named “default”, which holds the sum of the times of all the events of the section.

Extending it

Unlike most Symfony2 components, the Stopwatch component it is really simple with only 3 classes (actually 4, but one of them is internal), and does not provide easy ways to extend it using interfaces and dependency injection, so the only way would be to use inheritance. Even with inheritance, the use of private properties makes it really hard and forces you to rewrite the component almost entirely.

It is nice that the component remains small and works most of the times, but having the option to extend it somehow would be cool for some use-cases, like having information of the number of opened files, cache status or database congestion. However, it is interesting to see how changing properties and methods to private in other components led to cleaner and better solutions.

Internals

Due to its simplicity, there is not much to say about how it works internally. To measure time uses the microtime() function, passing TRUE as an argument to get the result as a float representing the current time in seconds since the Unix epoch accurate to the nearest microsecond. To get the memory usage, the memory_get_usage() function is used, again passing TRUE as parameter to get the real size of memory allocated from system instead of just the one used by emalloc().

Who’s using it?

More info

Photo: Stopwatch, by Julian Lim

March 16 / 2015

Upcoming Conferences

WeCamp

PHP New Zealand

PHP Summer Camp Croatia

PHPNE

MadisonPHP

brnoPHP

SymfonyLiveLondon

ZgPHP

PHPSouthAfrica

PHPNWUK

SymfonyLiveNYC

PHPForumParis

PHPARG

PHPWorld

TechMeetupUY

SymfonyConMadrid