A Slim3 Skeleton

Warning Slim 3 is currently under active development. Expect BC breaks!

I'm creating a number of projects in Slim 3 (which is currently under development) at the moment in order test things out and found that I was setting them up in essentially the same way. To save me having to do this each time, I've now created a skeleton project that is set up with the directory structure that I like and also includes Twig and Monolog.

To create a new Slim 3 project, simply do this:

$ composer create-project -n -s dev akrabat/slim3-skeleton my-app

This will create a new folder called my-app with the project files in it and then install the composer dependencies. You now have a working skeleton application, so test it using the built-in PHP web server:

$ cd my-app
$ php -S 0.0.0.0:8888 -t public public/index.php

Browse to http://localhost:8888 and you should see this:

Slim3 skeleton screenshot

What's included?

When exploring the application, these are the key files to look at.

  • index.php instantiates the Slim application object, sets the application up using files in app/ and runs it.
  • app/dependencies.php contains all the dependencies that are registered with Pimple.
  • app/middleware.php is where you should register any application middleware that you want to use. It's empty in the skeleton.
  • app/routes.php contains all the routes that the application responds to. I like having them all in one place and to keep the file sensible, I use the DIC to grab the action for dispatch.
  • app/src/Action/HomeAction.php is the class that is loaded by the DIC and then the dispatch method is executed. The nice thing about having a class like this is that I can load the dependencies via the constructor. Look in dependencies.php for the factory for this class.
  • app/templates/home.twig is the Twig view script that is rendered.

That's it

From this point on, you should delete what you don't want and add what you do in order to write your application!

Role models

Wikipedia defines a role model like this:

A role model is a person whose behavior, example, or success is or can be emulated by others

All my life, I have identified people that I perceive to be a little better than I am in some facet of life that I want to get better at and I emulate them. I've been doing this all my life. They have been my role models.

My first recollection of this was when I was around 9 or 10 – my last two years of primary school – when I discovered that by emulating and learning how my friend Claire approached maths problems, I got better at it. Similarly at secondary school, I found the academically best people I could to inspire me.

At University there were three people that had characteristics that I wanted in myself. This was when I discovered that a role model could inspire me to change how I view and live my life as well as improve my abilities. My role models helped me get a first class degree, but one of them unknowingly helped me become a more empathetic person.

This trend that has continued all the way through my life. I've found people who are better than me and have emulated them. I pay attention to how they behave & what they do and try to change appropriately. My role models have (and continue to) inspired me to be better.

I cannot thank them enough.

I hadn't thought about the gender mix of my inspirations before reading Zaron Burnett's Can A Man Have Female Role Models? I found this article eye-opening and I highly recommend that you take the time to read. He points out that the way our society portrays and treats women has done us all (yet another) disservice by hindering our ability to select the best role-models we can:

This hidden hierarchy of gender in language and culture runs rampant as blackberry thicket and is just as thorny and difficult to remove. It’s that bias that makes men reluctant to be like women, or identify with women, or imagine life from a woman’s eyes, or conceive of what a day in her experience might be like that. That’s a problem.

A smart man selects from both men and women for his role models. He learns from both and draws lessons from the whole body of humanity’s experience, rather than only taking counsel from the half he might meet in a public restroom.

I will look up to whomever I choose in order to better myself. I suggest you do too.

Styling rst2pdf tables

I currently use rst2pdf to create presentations slide decks from reStructured Text files. I like rST a lot as it's more expressive than Markdown and allows for extension.

Tables in rST are marked up like this:

+-----------+-----------+-----------+
| Heading 1 | Heading 2 | Heading 3 |
+===========+===========+===========+
| a         | b         | c         |
|           |           |           |
| aa        |           |           |
+-----------+-----------+-----------+
| d         | e         | f         |
+-----------+-----------+-----------+
| g         | h         | i         |
+-----------+-----------+-----------+
| j         | k         | l         |
+-----------+-----------+-----------+

We create a PDF file with the command rst2pdf test.rst which produces a table that looks like this:

Rst table standard

To style, this we create styles within a style file and then compile using rst2pdf test.rst -s my.style.

Let's start with the table element:

styles:
    table:
      commands: []
         [VALIGN, [ 0, 0 ], [ -1, -1 ], TOP ]
         [INNERGRID, [ 0, 0 ], [ -1, -1 ], 0.25, black ]
         [ROWBACKGROUNDS, [0, 0], [-1, -1], [white,#E0E0E0]]
         [BOX, [ 0, 0 ], [ -1, -1 ], 0.25, black ]

Behind the scenes, rst2pdf uses ReportLab to create the PDF. The commands style maps directly to ReportLab's TableStyle commands (section 7.4 of the current documentation)

Each command contains an identifier, the start and stop cell definition to which it applies and then the style to apply. The cell definition is defined as [X,Y] where [0,0] is the top left cell, [2,3] would be the cell with e in it in the definition above. Negative numbers count from the bottom right, so [-1,-1] is the bottom-right corner.

Key options:

VALIGN Vertical text alignment. Options: TOP, BOTTOM, MIDDLE.
INNERGRID, BOX, LINEBELOW, LINEABOVE, LINEBEFORE, LINEAFTER Style of the borders. First parameter is line thickness and second is colour.
BACKGROUND, ROWBACKGROUNDS, COLBACKGROUNDS Background colour. Parameter is an array of colours, used cyclically.
TOPPADDING, BOTTOMPADDING, LEFTPADDING, RIGHTPADDING Padding with cells. Parameter is a number.

The style for the table heading is called table-heading:

    table-heading:
        parent : heading
        backColor : beige
        alignment : TA_CENTER
        valign : BOTTOM
        borderPadding : 0

These settings should be obvious. I always override backColor!

The style for the table elements is table-body:

table-body:
      parent : normal

By default, it isn't styled, but if you want to change the textColor, this is where to do it.

Overriding on a per table basis

To override for a specific table, then set a class before the table:

.. class:: mytable

+-----------+-----------+-----------+
| Heading 1 | Heading 2 | Heading 3 |
+===========+===========+===========+
| a         | b         | c         |
|           |           |           |
| aa        |           |           |
+-----------+-----------+-----------+
| d         | e         | f         |
+-----------+-----------+-----------+

The most common reason to do this is to set up specific column widths:

    mytable:
        parent: table
        colWidths: [3cm, 6cm, 3cm]

This is mostly useful when the auto-sizing routine causes odd line breaks in heading text.

If you are targeting rst2pdf, then you can also set widths using the .. widths:: directive like this:

.. widths:: 20 20 60

+-----------+-----------+-----------+
| Heading 1 | Heading 2 | Heading 3 |
+===========+===========+===========+
| a         | b         | c         |
+-----------+-----------+-----------+
| d         | e         | f         |
+-----------+-----------+-----------+

The width numbers are percentages and it only works if you pass the command line option -e preprocess when compiling.

My defaults

My defaults currently are:

styles:
    table:
        commands: []
            [VALIGN, [ 0, 0 ], [ -1, -1 ], MIDDLE ]
            [ROWBACKGROUNDS, [0, 0], [-1, -1], [white]]
            [LINEBELOW, [0, 0], [-1, 0], 0.5, black]
            [BOTTOMPADDING, [0, 1], [-1, -1], 5]
            [TOPPADDING, [0, 1], [-1, -1], 5]
            [ALIGN, [ 0, 0 ], [ -1, -1 ], LEFT ]


    table-heading:
        parent : heading
        fontName: stdBold
        backColor : white
        alignment : TA_LEFT

This results in the very simple table style of:

Rst default

This suits me as a starting point.

ZF2 validator message keys

In Zend Framework 2, if you define your input filter via configuration, then you can override validation messages using a format along the lines of:

<validators>
    <notEmpty>
        <messages>
            <isEmpty>Please provide your telephone number</isEmpty>
        </messages>
    </notEmpty>
</validators>

Setting the message is easy enough, once you have the correct key name. This is a list of all the keys for the standard validators:

Validator Key name Default message
Alnum alnumInvalid Invalid type given. String, integer or float expected
Alnum notAlnum The input contains characters which are non alphabetic and no digits
Alnum alnumStringEmpty The input is an empty string
Alpha alphaInvalid Invalid type given. String expected
Alpha notAlpha The input contains non alphabetic characters
Alpha alphaStringEmpty The input is an empty string
Barcode barcodeInvalid Invalid type given. String expected
Barcode barcodeFailed The input failed checksum validation
Barcode barcodeInvalidChars The input contains invalid characters
Barcode barcodeInvalidLength The input should have a length of %length% characters
Between notBetween The input is not between '%min%' and '%max%', inclusively
Between notBetweenStrict The input is not strictly between '%min%' and '%max%'
Bitwise notAnd The input has no common bit set with '%control%'
Bitwise notAndStrict The input doesn't have the same bits set as '%control%'
Bitwise notXor The input has common bit set with '%control%'
Callback callbackInvalid An exception has been raised within the callback
Callback callbackValue The input is not valid
CreditCard creditcardChecksum The input seems to contain an invalid checksum
CreditCard creditcardContent The input must contain only digits
CreditCard creditcardInvalid Invalid type given. String expected
CreditCard creditcardLength The input contains an invalid amount of digits
CreditCard creditcardPrefix The input is not from an allowed institute
CreditCard creditcardService The input seems to be an invalid credit card number
CreditCard creditcardServiceFailure An exception has been raised while validating the input
Csrf notSame The form submitted did not originate from the expected site
Date dateInvalid Invalid type given. String, integer, array or DateTime expected
Date dateInvalidDate The input does not appear to be a valid date
Date dateFalseFormat The input does not fit the date format '%format%'
DateStep dateInvalid Invalid type given. String, integer, array or DateTime expected
DateStep dateInvalidDate The input does not appear to be a valid date
DateStep dateFalseFormat The input does not fit the date format '%format%'
DateStep dateStepNotStep The input is not a valid step
DateTime datetimeInvalid Invalid type given. String expected
DateTime datetimeInvalidDateTime The input does not appear to be a valid datetime
Digits notDigits The input must contain only digits
Digits digitsStringEmpty The input is an empty string
Digits digitsInvalid Invalid type given. String, integer or float expected
EmailAddress emailAddressInvalid Invalid type given. String expected
EmailAddress emailAddressInvalidFormat The input is not a valid email address. Use the basic format local-part@hostname
EmailAddress emailAddressInvalidHostname '%hostname%' is not a valid hostname for the email address
EmailAddress emailAddressInvalidMxRecord '%hostname%' does not appear to have any valid MX or A records for the email address
EmailAddress emailAddressInvalidSegment '%hostname%' is not in a routable network segment. The email address should not be resolved from public network
EmailAddress emailAddressDotAtom '%localPart%' can not be matched against dot-atom format
EmailAddress emailAddressQuotedString '%localPart%' can not be matched against quoted-string format
EmailAddress emailAddressInvalidLocalPart '%localPart%' is not a valid local part for the email address
EmailAddress emailAddressLengthExceeded The input exceeds the allowed length
Explode explodeInvalid Invalid type given
Float floatInvalid Invalid type given. String, integer or float expected
(Deprecated from ZF 2.4)
Float notFloat The input does not appear to be a float
(Deprecated from ZF 2.4)
GreaterThan notGreaterThan The input is not greater than '%min%'
GreaterThan notGreaterThanInclusive The input is not greater or equal than '%min%'
Hex hexInvalid Invalid type given. String expected
Hex notHex The input contains non-hexadecimal characters
Hostname hostnameCannotDecodePunycode The input appears to be a DNS hostname but the given punycode notation cannot be decoded
Hostname hostnameInvalid Invalid type given. String expected
Hostname hostnameDashCharacter The input appears to be a DNS hostname but contains a dash in an invalid position
Hostname hostnameInvalidHostname The input does not match the expected structure for a DNS hostname
Hostname hostnameInvalidHostnameSchema The input appears to be a DNS hostname but cannot match against hostname schema for TLD '%tld%'
Hostname hostnameInvalidLocalName The input does not appear to be a valid local network name
Hostname hostnameInvalidUri The input does not appear to be a valid URI hostname
Hostname hostnameIpAddressNotAllowed The input appears to be an IP address, but IP addresses are not allowed
Hostname hostnameLocalNameNotAllowed The input appears to be a local network name but local network names are not allowed
Hostname hostnameUndecipherableTld The input appears to be a DNS hostname but cannot extract TLD part
Hostname hostnameUnknownTld The input appears to be a DNS hostname but cannot match TLD against known list
Iban ibanNotSupported Unknown country within the IBAN
Iban ibanSepaNotSupported Countries outside the Single Euro Payments Area (SEPA) are not supported
Iban ibanFalseFormat The input has a false IBAN format
Iban ibanCheckFailed The input has failed the IBAN check
Identical notSame The two given tokens do not match
Identical missingToken No token was provided to match against
InArray notInArray The input was not found in the haystack
Int intInvalid Invalid type given. String or integer expected
(Deprecated from ZF 2.4)
Int notInt The input does not appear to be an integer
(Deprecated from ZF 2.4)
Ip ipInvalid Invalid type given. String expected
Ip notIpAddress The input does not appear to be a valid IP address
Isbn isbnInvalid Invalid type given. String or integer expected
Isbn isbnNoIsbn The input is not a valid ISBN number
IsInstanceOf notInstanceOf The input is not an instance of '%className%'
IsFloat floatInvalid Invalid type given. String, integer or float expected
(From ZF 2.4)
IsFloat notFloat The input does not appear to be a float
(From ZF 2.4)
IsInt intInvalid Invalid type given. String or integer expected
(From ZF 2.4)
IsInt notInt The input does not appear to be an integer
(From ZF 2.4)
LessThan notLessThan The input is not less than '%max%'
LessThan notLessThanInclusive The input is not less or equal than '%max%'
NotEmpty notEmptyInvalid Value is required and can't be empty
NotEmpty isEmpty Invalid type given. String, integer, float, boolean or array expected
PhoneNumber phoneNumberNoMatch The input does not match a phone number format
PhoneNumber phoneNumberUnsupported The country provided is currently unsupported
PhoneNumber phoneNumberInvalid Invalid type given. String expected
PostCode postcodeInvalid Invalid type given. String or integer expected
PostCode postcodeNoMatch The input does not appear to be a postal code
PostCode postcodeService The input does not appear to be a postal code
PostCode postcodeServiceFailure An exception has been raised while validating the input
Regex regexInvalid Invalid type given. String, integer or float expected
Regex regexNotMatch The input does not match against pattern '%pattern%'
Regex regexErrorous There was an internal error while using the pattern '%pattern%'
Step typeInvalid Invalid value given. Scalar expected
Step stepInvalid The input is not a valid step
StringLength stringLengthInvalid Invalid type given. String expected
StringLength stringLengthTooShort The input is less than %min% characters long
StringLength stringLengthTooLong The input is more than %max% characters long
Uri uriInvalid Invalid type given. String expected
Uri notUri The input does not appear to be a valid Uri

Using Zend\Config with a Slim app

Sometimes you need more configuration flexibility for your application than a single array. In these situations, I use the Zend\Config component which I install via composer:

composer require "zendframework/zend-config"

This will install the Zend\Config component, along with its dependency Zend\Stdlib.

Let's look at a couple of common situations.

Multiple files

It can be useful to to split your settings files out for administrative or environment-specific reasons. To set up within a Slim application, you do something like this.

The key class that we're interesting is in Zend\Config's Factory which takes a list of config files, loads each one and merges into a single array. If all your configuration files live in the same directory, then you can quite easily use a glob pattern:

$files = glob('../config/{global,local}*.php', GLOB_BRACE);
$settings = Zend\Config\Factory::fromFiles($files);
$app = new \Slim\Slim($settings);

This pattern will load all files starting with global before those starting with local. Hence, for the files: local.php, global.php, local.foo.php and global.foo.php, the order will be:

  1. global.foo.php
  2. global.php
  3. local.foo.php
  4. local.php

So, the local files will override the global ones. Each settings file needs to simply return an array.

Other formats

You may want to use a format other than PHP arrays. Zend\Config supports Ini, XML, JSON, Yaml and JavaProperties in addition to PHP arrays. You can mix and match too. Note that you'll need Zend\ServiceManager, so install it using:

composer require "zendframework/zend-servicemanager"

If you use JSON, you also need:

composer require "zendframework/zend-json"

For example, given:

global.ini:

debug = 0

local.json:

{
    "debug": 1
}

Then you can load these configuration files using:

$files = glob('../config/{global,local}*.{json,ini}', GLOB_BRACE);
$settings = Zend\Config\Factory::fromFiles($files);
$app = new \Slim\Slim($settings);

This will load files in this order: global*.json, global*.ini, local*.json and then local*.ini. Again, you end up with a single array in $settings, the var_dump shows that it contains:

array (size=1)
  'debug' => int 1

To sum up

That's all there is to it really. Zend\Config's Factory in conjunction with glob is a very flexible solution that allows you to put in place the exact configuration strategy that you want to, using the configuration format that you are most comfortable with.

installing XHGui via Ansible

I'm still using Ansible to provision Vagrant VMs. This is how I added the XHGui profiler to my standard setup.

Theres a number steps we need to do:

  • Install Composer
  • Install the uprofiler PHP extension
  • Install XHGui
  • Set up for profiling
  • Set up host for XHGui website

Install Composer

Installing Composer requires these tasks:

- name: Install Composer
  shell: curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin creates=/usr/local/bin/composer

- name: Rename composer.phar to composer
  shell: mv /usr/local/bin/composer.phar /usr/local/bin/composer creates=/usr/local/bin/composer

- name: Make composer executable
  file: path=/usr/local/bin/composer mode=a+x state=file

- name: Create global composer directory
  file: path=/usr/local/composer state=directory mode=0775

Firstly we download the Composer installer and run it to create composer.phar. We then rename to composer, make executable and then create a global directory for storing the packages that we download.

Install the uprofiler PHP extension

We install uprofiler via composer:

- name: Install uprofiler
  shell: export COMPOSER_HOME=/usr/local/composer && composer global require 'friendsofphp/uprofiler=dev-master' creates=/usr/local/composer/vendor/friendsofphp/uprofiler/composer.json

- name: Compile uprofiler
  shell: cd /usr/local/composer/vendor/friendsofphp/uprofiler/extension && phpize && ./configure && make && make install creates=/usr/lib/php5/20121212/uprofiler.so

- name: Configure PHP (cli)
  copy: src=uprofiler.ini dest=/etc/php5/cli/conf.d/21-uprofiler.ini mode=644

- name: Configure PHP (apache2)
  copy: src=uprofiler.ini dest=/etc/php5/apache2/conf.d/21-uprofiler.ini mode=644

The last two tasks copy uprofiler.ini to the relevant configuration directories. uprofiler.ini file is really simple:

[uprofiler]
extension=uprofiler.so

Install XHGui

Similarly, we install XHGui using composer:

- name: Install MongoDB
  apt: pkg={{ item }} state=latest
  with_items:
    - mongodb
    - php5-mongo

- name: Install XHGui
  shell: export COMPOSER_HOME=/usr/local/composer && composer global require --ignore-platform-reqs 'perftools/xhgui=dev-master' creates=/usr/local/composer/vendor/perftools/xhgui/composer.json

- name: Set XHGui permisssions
  file: path=/usr/local/composer/vendor/perftools/xhgui/cache group=www-data mode=775

- name: Configure XHGui
  template: src=xhgui_config.php dest=/usr/local/composer/vendor/perftools/xhgui/config/config.php owner=vagrant group=www-data mode=644

- name: Index mongo for XHGui
  script: xhgui_indexes.sh --some-arguments 1234 creates=/root/indexed_xhgui.txt

XHGi uses MongoDB for storage, so we install that install that first and then install XHGui via composer which pulls in all the dependencies. Note that XHGui has a extension dependency on xhprof, but we're using uprofiler, so we use the --ignore-platform-reqs flag to ignore.

XHGui requires a configuration file in it's config directory. I copied the default one and then changed it to profile every run. The minimum xhgui_config.php that you need is:

<?php
return [
    // Profile every request
    'profiler.enable' => function() {
        return true;
    },
]

This is the place where you could put in additional checks to decide whether to profile or not, such as checking for a GET variable of "profile", for instance.

Lastly, the XHGui README recommends that you add some indexes to MongoDB. I also wanted to automatically delete old records, which is also done via a MongoDB directive. This is done via the xhgui_indexes.sh shell script:

#!/bin/bash

# auto-remove records older than 2592000 seconds (30 days)
mongo xhprof --eval 'db.collection.ensureIndex( { "meta.request_ts" : 1 }, { expireAfterSeconds : 2592000 } )'

# indexes
mongo xhprof --eval  "db.collection.ensureIndex( { 'meta.SERVER.REQUEST_TIME' : -1 } )"
mongo xhprof --eval  "db.collection.ensureIndex( { 'profile.main().wt' : -1 } )"
mongo xhprof --eval  "db.collection.ensureIndex( { 'profile.main().mu' : -1 } )"
mongo xhprof --eval  "db.collection.ensureIndex( { 'profile.main().cpu' : -1 } )"
mongo xhprof --eval  "db.collection.ensureIndex( { 'meta.url' : 1 } )"

touch /root/indexed_xhgui.txt

Note that we create an empty file that is tested in the task as we only need to run this task once.

Set up for profiling

To profile a website, we just need to include /usr/local/composer/vendor/perftools/xhgui/external/header.php. This can be done by setting the auto_prepend_file PHP ini setting. As I use Apache, I can just add:

php_admin_value auto_prepend_file "/usr/local/composer/vendor/perftools/xhgui/external/header.php"

To my VirtualHost configuration.

Set up host for XHGui website

Finally, we need a VirtualHost for the XHGui website where we can view our profiles. I decided to use a separate subdomain, "profile", so my vhost looks like this:

<VirtualHost *:80>
  ServerName profiler.{{ server_name }}
  DocumentRoot /usr/local/composer/vendor/perftools/xhgui/webroot

  <Directory /usr/local/composer/vendor/perftools/xhgui/webroot>
      Options Indexes FollowSymLinks MultiViews
      AllowOverride All
      Order allow,deny
      Allow from all
      Require all granted
  </Directory>
</VirtualHost>

Where {{server_name}} is an Ansible variable that is the domain name of the site.

All done

That's it. Once I had worked out which pieces were required, putting them into Ansible tasks was remarkably obvious and now I can profile my website in development.

Logging errors in Slim 3

Slim Framework 3 is being actively developed at the moment and has a number of changes in it, including the use of the Pimple DI container and an overhaul of pretty much everything else! In this post, I'm going to look at error handling.

The default error handler in Slim 3 is Slim\Handlers\Error. It's fairly simple and renders the error quite nicely, setting the HTTP status to 500.

I want to log these errors via monolog.

Firstly, we set up a logger in the DIC:

$app['Logger'] = function($container) {
    $logger = new Monolog\Logger('logger');
    $filename = _DIR__ . '/../log/error.log';
    $stream = new Monolog\Handler\StreamHandler($filename, Monolog\Logger::DEBUG);
    $fingersCrossed = new Monolog\Handler\FingersCrossedHandler(
        $stream, Monolog\Logger::ERROR);
    $logger->pushHandler($fingersCrossed);

    return $logger;
};

Now, we can create our own error handler which extends the standard Slim one as all we want to do is add logging.

<?php

namespace App\Handlers;

use Psr\Http\Message\RequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Monolog\Logger;

final class Error extends \Slim\Handlers\Error
{
    protected $logger;

    public function __construct(Logger $logger)
    {
        $this->logger = $logger;
    }

    public function __invoke(Request $request, Response $response, \Exception $exception)
    {
        // Log the message
        $this->logger->critical($exception->getMessage());

        return parent::__invoke($request, $response, $exception);
    }
}

The error handler implements __invoke(), so our new class overrides this function, extracts the message and logs it as a critical. To get the Logger into the error handler, we use standard Dependency Injection techniques and write a constructor that takes the configured logger as a parameter.

All we need to do now is register our new error handler which we can do in index.php:

$app['errorHandler'] = function ($c) {
    return new App\Handlers\Error($c['Logger']);
};

Again, this is standard Pimple, so the 'errorHandler' key takes a closure which receives an instance of the container, $c. We instantiate a new App\Handlers\Error object and then retrieve the Logger from the container as we have already registered that with Pimple, so it knows how to create one for us.

With this done, we now have a new error handler in place. From the user's point of view, there's no difference, but we now get a message in our log file when something goes wrong.

Other error handlers

Obviously, we can use this technique to replace the entire error handler for situations when we don't want to display a comprehensive developer-friendly error to the user. Another case would be if we are writing an API, we may not want to respond with an HTML error page.

In these cases, we do exactly the same thing. For example, if we're writing a JSON API, then a suitable error handler looks like this:

<?php

namespace App\Handlers;

use Psr\Http\Message\RequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Monolog\Logger;

final class ApiError extends \Slim\Handlers\Error
{
    protected $logger;

    public function __construct(Logger $logger)
    {
        $this->logger = $logger;
    }

    public function __invoke(Request $request, Response $response, \Exception $exception)
    {
        // Log the message
        $this->logger->critical($exception->getMessage());

        // create a JSON error string for the Response body
        $body = json_encode([
            'error' => $exception->getMessage(),
            'code' => $exception->getCode(),
        ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    
        return $response
                ->withStatus(500)
                ->withHeader('Content-type', 'application/json')
                ->withBody(new Body(fopen('php://temp', 'r+')))
                ->write($body);
    }
}

This time we construct a JSON string for our response and then use Slim's PSR-7-compatible Response object to create a new one with the correct information in it, which we then return to the client.

Fin

As you can see, it's really easy to manipulate and control error handling in Slim 3. Compared to Slim 2, the best bit is that the PrettyExceptions middleware is not automatically added which had always annoyed me when writing APIs.

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!

WordCamp London, 2015

One of my recent goals has been to attend different conferences from the PHP-community-centric ones that I usually attend. I want to expose myself to different ideas, mindsets and communities. To this end, I attended WordCamp London last weekend and had a blast.

Everyone I spoke to was enthusiastic, friendly and welcoming which made for a very pleasant weekend and the selection of talks meant that I managed to learn about WordPress too!

The first day started with a talk by Laura Kalbag and on the potential pitfalls of using free products which harvest user data. I then followed this up by listening to Jack Lenonx talk about how to build themes with the new REST API that's coming to WordPress. This was a very interesting talk that showed how to use React in the browser to load data from the WordPress backend and display it as separate "pages" on the website without having to do a round-trip. Front-end development isn't one of my core skills, so I found this fascinating, though given that my clients sill need IE7 support, I wondered how practical it was…

Discussion of the new REST API was a consistent topic over the conference. The community is clearly very excited by this feature that's coming to WordPress core "soon". I think that being able to access data in a WordPress install via an API that gives back JSON is very useful and could potentially extend the uses of WordPress into the bespoke application world where I am. We'll have to see.

In the afternoon, Bruce Lawson spoke about how do to HTML responsive images with <picture> and the changes to <img> which I understood! As I've noted, front-end isn't really my bag, so Bruce's ability to put across these ideas in such a way that I thought that I could actually implement them was a god-send. We had more API stuff from Joe Hoyle who talked about how to implement your own endpoints in WordPress so that they were accessible to the new REST API and we finished the day with Simon Wheatley discussing how to write URL handlers. These two talks were quite WordPress specific; I found them interesting as background-knowledge about what's going on in a WordPress site.

The London WordCamp is a two day talk, so we did it all again on Sunday. You could certainly tell that it was an early start on the day after a late-night party! First up for me was Kathryn Reeve talking about JavaScript. I really liked this talk as it was easily digestible with a "this is the problem; this is the solution" format which worked really well. I then listened to Lorna Mitchell talk about more modern versions of PHP and what has changed. I've seen the slide before, but the performance improvements from PHP 5.2 to 5.6 are still very impressive!

After lunch, which was excellent both days, there were lightning talks in all three tracks. I went to the dev ones in the big room and we have 5 interesting short talks along with a few questions. I liked these a lot and liked that none ran over their allowed 5 minutes. It ran very smoothly and I learnt about the Codebug OS X client for Xdebug!

The final talk that I attended was the Q&A with three core developers. John, Helen & Mark answered questions from the audience intelligently and honestly. It gave us a good insight into the way the project "thinks" and if you want to help out, they would really appreciate some help with the Trac system!

At this point, I went to catch my train home. My thanks to Jenny for her excitement and enthusiasm which persuaded me to buy a ticket and attend. Hopefully, I'll get to attend more events like this.

Also, I got a new scarf!

RKA 2015 03 23 18 25 22

Run Slim 2 from the command line

If you need to run a Slim Framework 2 application from the command line then you need a separate script from your web-facing index.php. Let's call it bin/run.php:

bin/run.php:

#!/usr/bin/env php
<php

chdir(dirname(__DIR__)); // set directory to root
require 'vendor/autoload.php'; // composer autoload


// convert all the command line arguments into a URL
$argv = $GLOBALS['argv'];
array_shift($GLOBALS['argv']);
$pathInfo = '/' . implode('/', $argv);


// Create our app instance
$app = new Slim\Slim([
    'debug' => false,  // Turn off Slim's own PrettyExceptions
]);

// Set up the environment so that Slim can route
$app->environment = Slim\Environment::mock([
    'PATH_INFO'   => $pathInfo
]);


// CLI-compatible not found error handler
$app->notFound(function () use ($app) {
    $url = $app->environment['PATH_INFO'];
    echo "Error: Cannot route to $url";
    $app->stop();
});

// Format errors for CLI
$app->error(function (\Exception $e) use ($app) {
    echo $e;
    $app->stop();
});

// routes - as per normal - no HTML though!
$app->get('/hello/:name', function ($name) {
    echo "Hello, $name\n";
});

// run!
$app->run();

We set the script to be excutable and then we can then run it like this:

$ bin/run.php hello world

and the output is, as you would expect:

Hello, world

This works by converting the command line parameters into the URL path for Slim to route by imploding $argv with a '/' separator. Slim needs an environment that looks vaguely web-like. This is quite easy to do via the Slim\Environment::mock() method which will set up all the array keys that the framework expects to have access to. It's used for unit test, but also works really well here. All we need to do is set PATH_INFO to our previously created $pathInfo and Slim can now route.

We also need to stop Slim creating HTML errors, so we set our own closures for notFound and error and we're done.

The rest of the file is simply setting up the routes we need and then calling run().