Category Archives: PHP

Turn warnings into exceptions

As I keep looking this up in other projects that I've written, I'm putting it here so I can find it more easily.

There are a number of built-in PHP functions that generate a notice or warning that you can't turn off when something goes wrong, such as parse_ini_file and file_get_contents.

One common solution to this is to suppress using the @ operator:

$result = @file_get_contents($url);
if (false === $result) {
    // inspect error_get_last() to find out what went wrong
}

This doesn't work when using Xdebug with the xdebug.scream set to 1 and its also inelegant and inefficient..

A better way is to use set_error_handler:

set_error_handler(function ($severity, $message, $file, $line) {
    throw new \ErrorException($message, $severity, $severity, $file, $line);
});

$result = file_get_contents($url);

restore_error_handler();

In this case, we register our own error handler that converts every notice, warning and error into an ErrorException that can then be caught elsewhere. We then call the PHP function of interest and immediately call restore_error_handler to put back the one we had earlier.

Interestingly, in PHP7, we can expect to see exceptions in the engine itself which should allow us to solve this problem like this:

try {
    $result = file_get_contents($url);
} catch (EngineException $e) {
    // do something with $e
}

Building and testing the upcoming PHP7

The GoPHP7-ext project aims to ensure that all the known PHP extensions out there work with the upcoming PHP 7. This is non-trivial as some significant changes have occurred in the core PHP engine (related to performance) that mean that extensions need to be updated.

In order to help out (and prepare my own PHP code for PHP 7!), I needed the latest version of PHP7 working in a vagrant VM.

Fortunately Rasmus has created a such a VM called php7dev, so let's start there.

Firstly we make a new directory to work in:

$ mkdir php7dev
$ cd php7dev

Within this directory, we can set up the vagrant vm:

$ vagrant box add rasmus/php7dev
$ vagrant init rasmus/php7dev
$ vagrant up

If you are asked to enter the vagrant@127.0.0.1's password, then it's "vagrant".

We can now work within the VM to Update to the latest PHP 7 and work with extensions:

$ vagrant ssh

PHP versions within the VM

Rasmus' box comes with PHP versions 5.3, 5.4, 5.5, 5.6 and 7. For each of these versions, it provides four variants: release, debug, zts-release and zts-debug. A script, called newphp is provided that allows us to change between them like this:

$ newphp {version number} {type}

Where {version number} is one of: 53, 54, 55, 56, or 7 and {type} is one of: debug, zts or debugzts.

The ones I use are:

$ newphp 7
$ newphp 7 debug

The newphp script sets up PHP in both the CLI and nginx and rather usefully, sets up the correct phpize, so that when you build an extension, it will set it up for the current PHP.

Update PHP 7 to the latest version

PHP 7 is actively in development, so we're going to have to update it regularly to pick up the new changes. Rasmus has helpfully provided a script that, makephp, that does this for us:

$ makephp 70

This will grab the latest source code for PHP 7 and then compile and install both the release and debug versions. The makephp script can also compile zts and other PHP versions – run it without arguments to find out how.

Activate your new PHP build:

  • For PHP 7 release: $ newphp 7
  • For PHP 7 debug: $ newphp 7 debug

Check that the "built" date is correct by viewing the output of php -v

In my case, I see:

PHP 7.0.0-dev (cli) (built: Mar 29 2015 11:33:44) 
Copyright (c) 1997-2015 The PHP Group
Zend Engine v3.0.0-dev, Copyright (c) 1998-2015 Zend Technologies
    with Zend OPcache v7.0.4-dev, Copyright (c) 1999-2015, by Zend Technologies

Building an extension

Building an extension is easy enough. Let's walk through the apfd extension that's in PECL:

$ cd ~/src
$ git clone https://git.php.net/repository/pecl/http/apfd.git
$ cd apfd
$ make distclean; phpize && ./configure && make
$ make test
$ sudo make install

To install for any other PHP versions that you are using, change the current PHP installation via newphp and then repeating these steps.

To install the extension:

  • $ echo "extension=apfd.so" | sudo tee /etc/php7/conf.d/mysql.ini > /dev/null

    (Change php7 to the appropriate directory that's in /etc/ for other PHP versions)
  • $ php -m to check that the module is loaded.

Writing tests and upgrading an extension

As the internal C API has changed significantly, code changes are required to make an extension work on PHP7.

The process for tackling this is to "adopt" an extension on the GoPHP7-ext Extensions catalogue and then read the Compiling and testing extensions article on the GoPHP7-ext site, followed by the Testing Extensions page.

If you want to tackle fixing the C code, then the key changes that need to be made can be found on the Upgrading PHP extensions from PHP5 to NG wiki page.

Testing that your PHP code works on PHP7

To test my PHP code, I share it into the VM. This is done in the Vagrantfile using the config.vm.synced_folder directive.

I want to share my Zend Framework 1 source, so I edit Vagrantfile and after the config.vm.box line, I add this line:

config.vm.synced_folder "/www/zendframework/zf1", "/www/zf1"

This maps my local source code which is at /www/zendframework/zf1 into the VM at the /www/zf1 directory.

Run vagrant reload after changing the Vagrantfile in order to effect the change.

I can now vagrant ssh into the VM, cd /www/zf1 and run my unit tests (after installing PHPUnit, of course). If you want to run a website, then you need to set up a vhost as appropriate within the VM.

Fin

Rasmus has provided a PHP 7 VM that's very easy to keep up to date, so none of us have any excuse and need to be testing our PHP sites with it, reporting regressions and fixing our code!

Convert PHP Warnings and notices into fatal errors

Xdebug version 2.3 was released last week and includes a feature improvement that I requested back in 2013! Issue 1004 asked for the ability to halt on warnings and notices and I'm delighted that Derick implemented the feature and that it's now in the general release version.

It works really simply too.

Turn on the feature by setting xdebug.halt_level either in your php.ini or via ini_set():

<?php
ini_set('xdebug.halt_level', E_WARNING|E_NOTICE|E_USER_WARNING|E_USER_NOTICE);

Now cause a warning:

<?php
echo "Before";
imagecreatefromstring(null); // Don't pass null into imagecreatefromstring()!
echo "After";

The result is that "Before" is displayed and then we get the standard Xdebug notice, but "After" is not displayed as the script is halted on the due to the warning.

Xdebug halt-level example

I used to have a error handler in place solely to do this, but now I don't need it!

The most common use-case I have for it is a warning that occurs during a POSTed request that redirects to another page. Other people are dedicated log-checkers, but I'm not and vastly prefer seeing the big orange box telling what's gone wrong.

As I said, I'm really happy to see this feature; it's worth considering turning on your development set up too!

Sending test emails from PHP

I've written before about redirecting email while developing using a trap mail script on my development system. Other people also like MailCatcher.

I've recently switched to using a simple PHP script that creates a elm file on disk that can be opened by Apple's Mail client. This solution is much faster as there is no SMTP processing involved.

Adam Royle came up with this solution in his article Setup a testing mail server using PHP on Mac OS X, so thank you Adam!

In summary:

  • Create ~/smtp_out
  • Create ~/smtp_out/smtp_catcher.php with these contents:
    #!/usr/bin/php
    *lt;?php
    
    // create a filename for the emlx file
    list($ms, $time) = explode(' ', microtime());
    $filename = __DIR__ . '/' . date('Y-m-d h.i.s,', $time) . substr($ms,2,3) . '.eml';
    
    // write the email contents to the file
    $email_contents = fopen('php://stdin', 'r');
    $fstat = fstat($email_contents);
    file_put_contents($filename, $fstat['size'] . "\n");
    file_put_contents($filename, $email_contents, FILE_APPEND);
    
    // open up the eml file (using Apple Mail)
    exec('open ' . escapeshellarg($filename));
    
  • Make ~/smtp_out/smtp_catcher.php executable
  • Update php.ini and set:
    sendmail_path = sudo -u rob /Users/rob/smtp_out/smtp_catcher.php

    (Change rob to your username!)

  • Restart Apache
  • Give Apache permission to open Mail, by updating sudoers:
    • sudo visudo
    • Add this to the end:
      %www    ALL=(ALL)   NOPASSWD: /Users/rob/smtp_out/smtp_catcher.php

      (Again, change rob to your username!)

Read Adam's post for the full details on what's going on.

It's a nice solution and I'm liking it!

Setup Doctrine Migrations to update MySQL timestamp on update

One project I'm working on uses MySQL exclusively and is also using Doctrine Migrations.

I wanted to set up a column called updated that was automatically set to the timestamp of the last time the row was changed.

This is done in SQL like this:

CREATE TABLE foo (
    id INT AUTO_INCREMENT NOT NULL,
    bar VARCHAR(100) NOT NULL,
    updated timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    PRIMARY KEY(id)
);

It's not quite obvious how to do this in Migrations as it is designed to be portable across database engines and clearly this is a MySQL-ism.

To do it, you use the columnDefinition option to addColumn() within your up() method, like this:

public function up(Schema $schema)
{
  $myTable = $schema->createTable('foo');
  $myTable->addColumn('id', 'integer', ['autoincrement'=>true]);
  $myTable->addColumn('bar', 'string', ['length' => 100]);
  $myTable->addColumn(
    'updated',
    'datetime',
    ['columnDefinition' => 'timestamp default current_timestamp on update current_timestamp']
  );

  $myTable->setPrimaryKey(['id']);
}

The columnDefinition option replaces the type (which you must still set it to a valid portable option) and so you can easily put in database-specific definitions when you need to.

Recursively deleting elements from an array

I had a need recently to delete items from a nested associative array and also any empty sub-arrays. My initial thought was to use array_walk_recursive, but this doesn't work as you can't unset nested elements and you only have access to the leaves. Clearly I needed a recursive function.

I'm sure that this has been done many times before, but this is my solution:

/**
 * Remove any elements where the callback returns true
 *
 * @param  array    $array    the array to walk
 * @param  callable $callback callback takes ($value, $key, $userdata)
 * @param  mixed    $userdata additional data passed to the callback.
 * @return array
 */
function array_walk_recursive_delete(array &$array, callable $callback, $userdata = null)
{
    foreach ($array as $key => &$value) {
        if (is_array($value)) {
            $value = array_walk_recursive_delete($value, $callback, $userdata);
        }
        if ($callback($value, $key, $userdata)) {
            unset($array[$key]);
        }
    }

    return $array;
}

with this test:

class FunctionsTest extends \PHPUnit_Framework_TestCase
{
    public function testArrayWalkRecursiveDelete()
    {
        $array = [
            'a'=> 'a',
            'b'=> null,
            'c' => [
                'a' => null,
                'b' => 'b',
            ],
            'd' => [
                'a' => null
            ]
        ];

        $result = array_walk_recursive_delete($array, function ($value, $key) {
            if (is_array($value)) {
                return empty($value);
            }
            return ($value === null);
        });

        $expected = [
            'a'=> 'a',
            'c' => [
                'b' => 'b',
            ],
        ];

        $this->assertSame($expected, $result);
    }
}

This is very similar to how array_walk_recursive works except that I return the altered array rather than a boolean as it's a recursive function.

The test shows how I use it:

        $result = array_walk_recursive_delete($array, function ($value, $key) {
            if (is_array($value)) {
                return empty($value);
            }
            return ($value === null);
        });

If the callback returns true, then the element is deleted from the array, so for my case, I return true if the value is an empty array or null.

SSL certificate verification on PHP 5.6

I recently updated my local OS X Zend Server installation to PHP 5.6 and when I ran composer self-update, I got this error message:

[Composer\Downloader\TransportException]                                                                                       
The "https://getcomposer.org/version" file could not be downloaded: SSL operation failed with code 1. OpenSSL Error messages:  
error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed                                              
Failed to enable crypto                                                                                                        
failed to open stream: operation failed 

Googling around, I finally worked out that there have been various SSL improvements in PHP 5.6 and that the problem was that it couldn't find any OpenSSL certificates on my system. This isn't a total surprise as OS X has been moving away from using OpenSSL internally in favour of its own libraries.

There's a new PHP function openssl_get_cert_locations that helps with this and so I ran:

$ php -r "print_r(openssl_get_cert_locations());"

on the command line to find out where PHP was looking. On my system, I got this:

Array
(
    [default_cert_file] => /usr/local/openssl-0.9.8zb/ssl/cert.pem
    [default_cert_file_env] => SSL_CERT_FILE
    [default_cert_dir] => /usr/local/openssl-0.9.8zb/ssl/certs
    [default_cert_dir_env] => SSL_CERT_DIR
    [default_private_dir] => /usr/local/openssl-0.9.8zb/ssl/private
    [default_default_cert_area] => /usr/local/openssl-0.9.8zb/ssl
    [ini_cafile] => 
    [ini_capath] => 
)

There is no directory /usr/local/openssl-0.9.8zb on my system and SSL_CERT_FILE and SSL_CERT_DIR are not defined, so it's no surprise that PHP was struggling.

To fix it, I install openssl via homebrew:

brew install openssl

This installs the openssl certificates to /usr/local/etc/openssl/cert.pem, so we can now use the new PHP 5.6 INI setting openssl.cafile to tell PHP where to find the certificates:

Adding

openssl.cafile=/usr/local/etc/openssl/cert.pem

to Zend Server's php.ini solved the problem and I can now use composer once again!

Overriding the built-in Twig date filter

In one project that I'm working on, I'm using Twig and needed to format a date received from an API. The date string received is of the style "YYYYMMDD", however date produced an unexpected output.

Consider this:

{{ "20141216"|date('jS F Y') }}

creates the output:

20th May 1976

This surprised me. Then I thought about it some more and realised that the date filter is treating my date string as a unix timestamp. I investigated and discovered the problem in twig_date_converter:

    $asString = (string) $date;
    if (ctype_digit($asString) 
        || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) {
        $date = '@'.$date;
    }

    $date = new DateTime($date, $defaultTimezone);

This code tests to see if the dates string provided is a number (positive or negative) and then prepends an '@' symbol to the front. This has the effect of informing DateTime's constructor to treat $date as a unix timestamp.

Unfortunately, twig_date_converter is a function and so I couldn't override it, so I wrote my own extension that registers new date, date_modify filters and a new date function in order to solve my problem:

<?php
/*
 * Extension to provide updated date & date_modify filters along with an
 * updated date function which do not auto-convert strings of numbers to
 * a unix timestamp.
 *
 * Code within dateFilter(), modifyFilter() and dateFromString() extracted
 * from Twig/lib/Twig/Extension/Core.php which is (c) 2009 Fabien Potencier
 * and licensed as per https://github.com/twigphp/Twig/blob/master/LICENSE
 */
namespace My\Twig\Extension;

use Twig_Extension;
use Twig_SimpleFilter;
use Twig_SimpleFunction;
use DateTime;
use DateTimeInterface;
use DateTimeImmutable;

class DateExtension extends Twig_Extension
{
    public function getName()
    {
        return 'my_date';
    }

    public function getFilters()
    {
        return array(
            new Twig_SimpleFilter('date', [$this, 'dateFilter'],
                    ['needs_environment' => true]),
            new Twig_SimpleFilter('date_modify', [$this, 'modifyFilter'],
                    ['needs_environment' => true]),
        );
    }

    public function getFunctions()
    {
        return array(
            new Twig_SimpleFunction('date', [$this, 'dateFromString'],
                    ['needs_environment' => true]),
        );
    }

    public function dateFilter($env, $date, $format = null, $timezone = null)
    {
        if (null === $format) {
            $formats = $env->getExtension('core')->getDateFormat();
            $format = $date instanceof DateInterval ? $formats[1] : $formats[0];
        }

        if ($date instanceof DateInterval) {
            return $date->format($format);
        }

        return $this->dateFromString($env, $date, $timezone)->format($format);
    }

    public function modifyFilter($env, $date, $format = null, $timezone = null)
    {
        $date = $this->dateFromString($env, $date, false);
        $date->modify($modifier);

        return $date;
    }

    public function dateFromString($env, $date, $timezone)
    {
        // determine the timezone
        if (!$timezone) {
            $defaultTimezone = $env->getExtension('core')->getTimezone();
        } elseif (!$timezone instanceof DateTimeZone) {
            $defaultTimezone = new DateTimeZone($timezone);
        } else {
            $defaultTimezone = $timezone;
        }

        // immutable dates
        if ($date instanceof DateTimeImmutable) {
            return false !== $timezone ? $date->setTimezone($defaultTimezone) : $date;
        }

        if ($date instanceof DateTime || $date instanceof DateTimeInterface) {
            $date = clone $date;
            if (false !== $timezone) {
                $date->setTimezone($defaultTimezone);
            }

            return $date;
        }

        $date = new DateTime($date, $defaultTimezone);
        if (false !== $timezone) {
            $date->setTimezone($defaultTimezone);
        }

        return $date;
    }
}

This class simply registers new date, date_modify filters and a new date function to replace the ones in Twig core and then is a direct copy of the functions twig_date_format_filter, twig_date_modify_filter and twig_date_converter with the functionality above removed.

I also needed to register this extension with the Twig_Environment using: $env->addExtension(new \My\Twig\Extension\DateExtension()); and I'm done.

{{ "20141216"|date('jS F Y') }}

now correctly outputs:

16th December 2014

While, it's a shame I can't just override twig_date_converter, I'm glad that I can re-register the relevant Twig filters and function.

Autocomplete Phing targets on the command line

Shortly after writing my last post, it crossed my mind that it would be nice if I could autocomplete the targets in my Phing build file on the command line.

In order to do this, I read Buddy Lindsey's Quick and Dirty Write Your Own Bash Autocomplete and made it do what I needed!

Start by creating a new bash completion file in the bash_completion.d directory. This file needs executable permission. This directory can usually be found at /etc/bash_completion.d, but on OS X using Homebrew, it's at /usr/local/etc/bash_completion.d.

This is the file:

# Store this file in /etc/bash_completion.d/phing
 
_phing () {
    local cur prev
 
    COMPREPLY=()
    buildfile=build.xml
    _get_comp_words_by_ref cur prev
 
    [ ! -f $buildfile ] && return 0

    COMPREPLY=( $( compgen -W "$( phing -l | tr -s '\-' | sed s/^-/\|/ | tr -d '\|' \
        | sed s/\ \ .*\// \
        | sed s/Buildfile.*// | sed s/Default\ target:// | sed s/Subtargets:// \
        | sed s/Main\ targets:// \
        | tr -s ' ' \
        | sed 's/[^[:print:]]//g' | sed s/\\[.*// | tr '\n' ' ' | tr -s '\n' 2>/dev/null )" \
        -- "$cur" ) )
}
 
complete -F _phing phing

It's a bit messy because my knowledge of sed/tr isn't too hot. What we do is create a list of words for the -W option to compgen by running phing -l and then manipulating the output to remove everyting that isn't a target name using various sed and tr commands.

Once done, compgen creates the wordlist used by complete when you press tab after the word phing on the command line.

A few Phing tips

Following on from my last post, here's a few other Phing things that I've found helps me.

Hiding targets from Phing -l

I have a number of dependent targets in my build.xml that I don't want listed when I run phing -l. The easiest way to do this is to add the hidden="true" property to the targets I want to hide:

<?xml version="1.0"?>
<project name="buildfile" default="foo">

  <target name="foo" depends="bar,baz" />

  <target name="bar" hidden="true">
    <!-- tasks -->
  </target>

  <target name="baz" hidden="true">
    <!-- tasks -->
  </target>

</project>

The output of phing -l now shows just the foo target as I want:

$ phing -l
Buildfile: /Users/rob/tmp/build.xml
Default target:
-------------------------------------------------------------------------------
 foo

Subtargets:
-------------------------------------------------------------------------------
 foo

Main target vs subtarget

If you set a description on a target, then it becomes a Main target:

<?xml version="1.0"?>
<project name="buildfile" default="foo">

  <target name="foo" depends="bar,baz" description="Run foo to do stuff" />

  <!-- other hidden targets -->

</project>

The output is now:

$ phing -l
Buildfile: /Users/rob/tmp/build.xml
Default target:
-------------------------------------------------------------------------------
 foo

Main targets:
-------------------------------------------------------------------------------
 foo      Run foo to do stuff

It just looks better and provides additional information as well.

List available targets by default

If you run phing without a target, then the default target runs. If you've forgotten what that is, then this may not be the safest thing to happen. Ideally, the default target should do no harm and I've found it helpful to make the default target display a list of the available targets within the build file.

This is done by adding a hidden "list_targets" target and making it the default:

<?xml version="1.0"?>
<project name="buildfile" default="list_targets">

  <target name="list_targets" hidden="true">
    <exec command="phing -q -f ${phing.file} -l" passthru="true"/>
  </target>

  <target name="foo" depends="bar,baz" description="Run foo to do stuff" />

  <!-- other hidden targets -->

</project>

Essentially, we run phing -l against our current build file and set the passthru property so that the output is displayed. This gives us:

$ phing
Buildfile: /Users/rob/tmp/build.xml

buildfile > list:

Default target:
-------------------------------------------------------------------------------
 list

Main targets:
-------------------------------------------------------------------------------
 foo  Run foo to do stuff


BUILD FINISHED

Total time: 0.2176 seconds