Category: PHP

Simple way to add a filter to Zend-InputFilter

Using Zend-InputFilter is remarkably easy to use:

How do you add your filter to it though?

This is the world's most simple filter that does absolutely nothing: We'll call it MyFilter and store it in App\Filter\MyFilter.php:

Now you have a couple of choices:

Extend Zend\InputFilter\Factory

I needed to add my own filter in the least invasive way that I could and so I created App\InputFilter\Factory which extends Zend\InputFilter\Factory:

This class extends the standard Factory class and registers our filter into the filter chain's plugin manager. Note that we have to register the factory for the fully qualified filter classname and also we register an alias for the short form ('MyFilter') as that's much nicer to use in the specification.

To use our new factory, we change the use statement to use our new factory:

Now we can use 'MyFilter' in our specification:

Update your container's factory

If you're already injecting the InputFilter's Factory to the class that's specifying the InputFilter, then it's easier to update that factory. For Pimple, this looks something like:

We don't need to change anything else and we can use 'MyFilter' in our specification:

Inserting binary data into SQL Server with ZF1 & PHP 7

If you want to insert binary data into SQL Server in Zend Framework 1 then you probably used the trick of setting an array as the parameter's value with the info required by the sqlsrv driver as noted in Some notes on SQL Server blobs with sqlsrv.

Essentially you do this;

Where $db is an instance of Zend_Db_Adapter_Sqlsrv.

If you use SQL Server with ZF1 and happen to have updated to PHP 7, then you may have found that you get this error:

(At least, that's what happened to me!)

Working through the problem, I discovered that this is due to Zend_Db_Statement_Sqlsrv converting the $params array to references with this code:

The Sqlsrv driver (v4) for PHP 7 does not like this!

As Zend Framework 1 is EOL, we can't get a fix into upstream and update the new release, so we have to write our solution.

We want to override Zend_Db_Statement_Sqlsrv::_execute() with our own code. To do this we firstly need to override Zend_Db_Adapter_Sqlsrv. (Also, let's assume we already have a App directory registered with the autoloader)

Firstly our adapter:

App/Db/Adapter/Sqlsrv.php:

This class simply changes the default statement class to our new one. Now, we can write our Statement class:

App/Db/Statement/Sqlsrv.php:

This class, takes the _execute() method from Zend_Db_Statement_Sqlsrv and makes the necessary changes the section that creates parameter references. Specifically, we only create a reference if the parameter has a direction of SQLSRV_PARAM_OUT or SQLSRV_PARAM_INOUT:

Finally, we need to register our new adapter with Zend_Application's Database resource. This is done in the config file:

application/configs/application.ini:

That's it.

We can now insert binary data into our SQL Server database from PHP 7 using the latest sqlsrv drivers.

Autocomplete Composer script names on the command line

As I add more and more of my own script targets to my composer.json files, I find that it would be helpful to have tab autocomplete in bash. I asked on Twitter and didn't get an immediate solution and as I had already done something similar for Phing, I rolled up my sleeves and wrote my own.

Start by creating a new bash completion file called composer 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/ (assuming you have already installed with brew install bash-completion).

This is the file:

(Note that __ltrim_colon_completions is only in recent versions of bash-completion, so you may need to remove this line.)

Reading from the bottom, to get the list of commands to composer, we create a list of words for the -W option to compgen by running composer --no-ansi and then manipulating the output to remove everything that isn't a command using awk. We also create a separate list of flag arguments when the user types a hyphen and then presses tab.

Finally, we also autocomplete flags for any subcommand by running composer {cmd} -h --no-ansi and using tr and grep to limit the list to just words starting with a hyphen.

That's it. Now composer {tab} will autocomplete both built-in composer commands and also custom scripts!

Composer autocomplete

As you can see, in this example, in addition to the built-in commands like dump-autoload and show, you can also see my custom scripts, including apiary-fetch and .

This is very helpful for when my memory fails me!

Using CircleCI for a PHP project

For a new client project, I've decided to use CircleCI to run my tests every time I push to GitHub.

This turned out to be quite easy; this is how I did it.

I started by creating a config file, .circleci/config.yml containing the following:

The documentation is really good, but the file's organisation is pretty self-expanatory.

The config file has a list of jobs. The build job is run on a push to GitHub, so that's the one I've created. Inside the docker section contains a list of docker images that are required – in my case, I just need a single container running PHP 7.1. The other section in the job is the list of steps to run. Each step has a name and a command which is a bash command.

For my job, I need to install git and the PDO PHP extension. I then run the "magic" step called checkout which as per its name, checks out my source code. I then install composer, including verifying that it's valid and display the PHP version number and composer version number in case I ever need them to work out why a build failed.

I then turn my attention to the project itself and run composer install and then the tests themselves: phpcs and phpunit.

Running the build

To actually get builds to run, you log into CircleCI – usefully you authenticate via GitHub – and select the project to build. From now on, pushing to GitHub or a branch will result in the build running. On success, you get something like this:

Circle ci

Hooking up to Slack

Hooking up to Slack is done by going to Slack's Apps & Integrations section and searching for CircleCI. Add the configuration and follow the wizard. Once done, you get a URL that you add to the project's Chat Notifications settings in CircleCI. This is found by clicking on the "cog" next to the project's name in the builds list. There's also a setting callled "Fixed/Failed only" which I check.

Once that's done, you get Slack notifications on failures and then on the first success after a failure and you are now secure in the knowledge that your tests are being run reliably.

Stand-alone usage of Zend-InputFilter

Any data that you receive needs to be checked and validated. There are number of ways to do this including PHP's filter_var, but I prefer Zend-InputFilter. This is how to use it as a stand-alone component.

Installation

Firstly, we install it using Composer:

$ composer require zendframework/zend-inputfilter
$ composer require zendframework/zend-servicemanager

You don't have to have ServiceManager, but it makes working with InputFilter much easier, so it's worth installing.

Create the InputFilter

The easiest way to create an InputFilter is to use the provided Factory class. Let's consider an Author entity that has the properties: author_id, name, biography & date_of_birth. We can create an input filter like this:

use Zend\InputFilter\Factory as InputFilterFactory;

class Author
{
    protected $author_id;
    protected $name;
    protected $biography;
    protected $date_of_birth;

    // ...

    protected function createInputFilter()
    {
        $factory = new InputFilterFactory();
        $inputFilter = $factory->createInputFilter([
            'author_id' => [
                'required' => true,
                'validators' => [
                    ['name' => 'Uuid'],
                ],
            ],
            'name' => [
                'required' => true,
                'filters' => [
                    ['name' => 'StringTrim'],
                    ['name' => 'StripTags'],
                ],
            ],
            'biography' => [
                'required' => false,
                'filters' => [
                    ['name' => 'StringTrim'],
                    ['name' => 'StripTags'],
                ],
            ],
            'date_of_birth' => [
                'required' => false,
                'validators' => [
                    ['name' => 'Date'],
                    [
                        'name' => 'LessThan',
                        'options' => [
                            'max' => date('Y-m-d'),
                            'inclusive' => true,
                        ],
                    ],
                ],
            ],
        ]);

        return $inputFilter;
    }
}

The createInputFilter() method takes an associative array where the key is the name of the input and then the value is a specification. There are a number of elements in the specification, but we usually just specify required, filters and validators.

required This can be either true or false. If false, then the validators do not execute, but the filters do.
filters An optional array of Zend-Filters. A filter modifies the supplied data before it is passed to the validators (if any). The filtered data is used by the rest of the application. In this example, we have added two filters: StringTrim & StripTags.
validators An optional array of Zend-Validators. A validator will test the filtered value for the input and fail if the data is not valid. If any validator fails, then the entire InputFilter is invalid.

This particular input filter requires that author_id and name are present, but that biography and date_of_birth are optional. The author_id must be a UUID, the name & biography must not have leading or trailing whitespace or no HTML tags and the date_of_birth, if present, must be a valid date in the past.

Using the InputFilter

To use the InputFilter, we set the data and then call isValid(). This can be done in a validate() method that looks like this:

Use Crell\ApiProblem\ApiProblem;
use Error\Exception\ProblemException;

Class Author
{
    // ...

    /**
     * Create an author
     *
     * @param  array $data
     * @return Author
     * @throws ProblemException
     */
    public static function createAuthor($data)
    {
        $inputFilter = $this->createInputFilter();
        $inputFilter->setData($data);

        if ($inputFilter->isValid()) {
            return new Author($inputFilter->getValues());
        }

        $problem = new ApiProblem('Validation failed');
        $problem->setStatus(400);
        $problem['errors'] = $inputFilter->getMessages();

        throw new ProblemException($problem);
    }
}

In this case, it's an API, so the data has come from a PUT or POST request. We call setData() to pass the array of data into the InputFilter and then call isValid(). If the data is valid, we can return a newly instantiated Author object that is constructed with the filter data. If the validation fails, then we throw a ProblemException which needs an ApiProblem instance, so we create one for it.

To find out which validators failed, getMessages() provides a nested array which is very useful for passing back to the API client.

As an example, this is what failure looks like:

$ curl -i -X "POST" "http://localhost:8888/authors" \
     -H "Accept: application/json" \
     -H "Content-Type: application/json" \
     -d $'{ "name": "", "author_id": "1234" }'

HTTP/1.1 400 Bad Request
Host: localhost:8888
Connection: close
X-Powered-By: PHP/7.0.14
Content-type: application/problem+json

{
    "errors": {
        "author_id": {
            "valueNotUuid": "Invalid UUID format"
        },
        "name": {
            "isEmpty": "Value is required and can't be empty"
        }
    },
    "title": "Validation failed",
    "type": "about:blank",
    "status": 400
}

Fin

That's all there is to it. Zend-InputFilter is a very flexible data filter and validator and works really well for APIs, such as those written in Slim.

Rendering problem details in Slim

As I've already noted, in the project I'm currently building, I'm rendering errors in my Slim Framework API using RFC 7807: Problem Details for HTTP APIs via Larry Garfield's ApiProblem component and rka-content-type-renderer.

One place where we need to integrate this approach into Slim is in the error handlers. Let's look at NotFound. To ensure that we return a response in the right format, we need to implement our own NotFound handler:

src/App/Handler/NotFound.php:

<?php
namespace App\Handler;

use Crell\ApiProblem\ApiProblem;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class NotFound
{
    /**
     * Invoke not found handler
     *
     * @param  ServerRequestInterface $request  The most recent Request object
     * @param  ResponseInterface      $response The most recent Response object
     *
     * @return ResponseInterface
     */
    public function __invoke(ServerRequestInterface $request, ResponseInterface $response)
    {
        $problem = new ApiProblem(
            'Not Found',
            'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html'
        );
        $problem->setStatus(404);

        $renderer = new RKA\ContentTypeRenderer\ApiProblemRenderer();
        return $renderer->render($request, $response, $problem);
    }
}

The NotFound handler must return a Response. To do this, we create a new ApiProblem object with the title "Not Found" and set the type to HTML page that defines the 404 status code. We then instantiate the ApiProblemRenderer and call its render method. The renderer will then return a Response object in either XML or JSON based on the Accept header, with the correct Content-Type header. We then set the status code and return it.

To register our new handler, we use the container. If you're using the skeleton application as your base, then this goes in dependencies.php:

src/dependencies.php:

// Error handlers
$container['notFoundHandler'] = function () {
    return new App\Handler\NotFound();
};

Slim will now use our handler whenever a NotFoundException is raised.

This is it in action:

$ curl -i -H "Accept: application/xml" http://localhost:8888/foo
HTTP/1.1 404 Not Found
Host: localhost:8888
Connection: close
X-Powered-By: PHP/7.0.14
Content-type: application/problem+xml

<?xml version="1.0"?>
<problem>
  <title>Not Found</title>
  <type>http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html</type>
  <status>404</status>
</problem>

We can of course apply this to the other error handers: NotAllowed, Error & PhpError which all follow the same pattern as we have done for NotFound.

Rendering ApiProblem with PSR-7

In the API I'm currently building, I'm rendering errors using RFC 7807: Problem Details for HTTP APIs. As this is a Slim Framework project, it uses PSR-7, so I updated rka-content-type-renderer to support problem.

RFC 7807 defines a standard for sending details of an error in an HTTP response message. It supports both XML and JSON formats. From the RFC, an example response is:

HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: en

{
    "type": "https://example.com/probs/out-of-credit",
    "title": "You do not have enough credit.",
    "detail": "Your current balance is 30, but that costs 50.",
    "instance": "/account/12345/msgs/abc",
    "balance": 30,
    "accounts": [
        "/account/12345",
        "/account/67890"
    ]
}

Only title and type are required, though status should also be set. Full information is in the RFC, which is one of the easier ones to read.

In PHP, Larry Garfield has created the crell/ApiProblem component. The code to implement the above message is:

Use Crell\ApiProblem\ApiProblem;

$problem = new ApiProblem("You do not have enough credit.", "http://example.com/probs/out-of-credit");
$problem
  ->setDetail("Your current balance is 30, but that costs 50.")
  ->setInstance("http://example.net/account/12345/msgs/abc");

$problem['balance'] = 30;
$problem['accounts'] = array(
  "http://example.net/account/12345",
  "http://example.net/account/67890"
);

$jsonString = $problem->asJson();
$xmlString = problem->asXml();

ApiProblemRenderer

The only tricky bit is working out if we need to send back JSON or XML. This is called content negotiation and we read the Accept header to find out what the client wants. However, the Accept header has a complicated format with quality levels as you can see from RFC 7321, section 5.3.2. Fortunately, we can use Negotiation by Will Durand to deal with this, which is how rka-content-type-renderer works.

rka-content-type-render now has an ApiProblemRenderer which will read the Accept header and work out the if the client would prefer JSON or XML. If it can't determine, it will default to JSON.

In Expressive or Slim, it's used like this:

use Crell\ApiProblem\ApiProblem;
use RKA\ContentTypeRenderer\ApiProblemRenderer;
use Zend\Diactoros\Response;

$app->get('/', function ($request, $response, $next) {
    $problem = new ApiProblem(
        'Unauthorised',
        'http://www.example.com/api/docs/authentication'
    );
    $problem->setStatus(403);

    $renderer = new ApiProblemRenderer();
    return $renderer->render($request, new Response(), $problem);
});

This is it in action, with an XML accept header:

$ curl -i -H "Accept: application/vnd.akrabat.api+xml" http://localhost:8888/
HTTP/1.1 403 Forbidden
Host: localhost:8888
Connection: close
X-Powered-By: PHP/7.0.14
Content-type: application/problem+xml

<?xml version="1.0"?>
<problem>
  <title>Unauthorised</title>
  <type>http://www.example.com/api/docs/authentication</type>
  <status>403</status>
</problem>

And with a JSON one:

$ curl -i -H "Accept: application/vnd.akrabat.api+json" http://localhost:8888/
HTTP/1.1 403 Forbidden
Host: localhost:8888
Connection: close
X-Powered-By: PHP/7.0.14
Content-type: application/problem+json

{
    "title": "Unauthorised",
    "type": "http://www.example.com/api/docs/authentication",
    "status": 403
}

Ideally, the client should specify application/problem+json in their accept header, but in practice, I've never seen that happen, which is why rka-content-type-renderer works out the preferred format based on the media types specified.

A note on framework performance

A question came up recently wondering why Slim Framework was around 5 times slower than plain PHP.

All frameworks are by definition slower than no-code as there's more going on.

i.e. an index.php of:

    <?php
    header('Content-Type: application/json');
    echo json_encode(['result' => 1]);

is going to be much faster than a Slim application with this code:

    use \Slim\App;
    include 'vendor/autoload.php';
    
    $config = include 'Config/Container.php';
    $app = new App($config);
    $app->get('/do-it', function($request, $response){
        return $response->withJson(['result' => 1]);
    });
    
    $app->run();

This is not an apples-to-apples comparison of course as the Slim application is doing a lot more than the plain PHP one. It supports routing so that it can respond to different URLs (even ones with dynamic parameters in that URL) and HTTP methods, along with the ability to handle errors. It also a set of separate classes so that you can replace specific parts of our code with your own if you want it to do something different while still using the rest of our code.

To actually compare the performance of Slim to plain PHP, you need to make the plain PHP version have the same functionality as the Slim version and you'll discover that the performance difference is much smaller. Of course, custom written, application specific code will always be faster than generic code though. Slim is a micro framework, so already is pretty tight (more to do though and something I'm looking at for 4.0), but by definition, it supports more routing features than you may need.

However, you have to write it all that code yourself and, to be honest, parsing requests, sending responses, dealing with URL routing, formatting errors based on the Accept header, and so on are solved problems that don't add any value to your app if you write them yourself. You're much better off concentrating your efforts on the parts of your app that are unique to your app as that's where it brings value.

The definitive resource on this is Paul Jones' articles. Start with A Siege On Benchmarks. This was back in 2009 and somehow he managed to not test Slim! However, even if you take the fastest framework he measured (ZF 0.2!), you can see that at 180.56 requests per second, that's around 4.5 times slower than the baseline PHP of 829.82 requests/second.

Finally, the speed of this code will pale in comparison to the rest of your application's code, especially if you access a database or a web s service!

I'm interested in making Slim the fastest it can be, but the benefits of having a reliable framework that provides good routing middleware functionality out of the box cannot be overstated.

Homestead per-project crib sheet

I wanted a drop-dead simple way to try and replicate a problem someone was having on the Slim forums. I couldn't reproduce with php -S which is my go-to for this sort of thing, so I thought I'd try Homestead.

I had recently listend to a Voices of the Elephpant episode with Taylor Otwell & Joe Ferguson where Joe mentioned that Homestead worked on a per-project basis too. I didn't know this, so tried it out. The docs are fine, but there's a lot there that covers the global installation option when I just want to get up and running on a per-project basis.This is my crib sheet:

1. Create project

We just need a project that uses Composer. You probably have one already. If not, Slim Framework is a good choice!

2. Add Homestead to the project

The make command creates VagrantFile and a Homestead.yaml for configuration.

3. Deal with IP address and hostname

By default, the Homestead vagrant box is set up on 192.168.10.10 with the hostname homestead.app. You can change this in Homestead.yaml.

Add the IP address to /etc/hosts. This only needs to be done once if you don't change the defaults.

All done

We're all done, so we can use vagrant up to run our new website Go to homestead.app in a browser to see it. To shut down, use vagrant halt or vagrant destroy.

Slim home page

Using CharlesProxy's root SSL with home-brew curl

Once I installed Homebrew's curl for HTTP/2 usage, I discovered that I couldn't automatically proxy SSL through Charles Proxy any more.

$ export HTTPS_PROXY=https://localhost:8888
$ curl https://api.joind.in/v2.1/
curl: (60) SSL certificate problem: self signed certificate in certificate chain
More details here: https://curl.haxx.se/docs/sslcerts.html

curl performs SSL certificate verification by default, using a "bundle"
 of Certificate Authority (CA) public keys (CA certs). If the default
 bundle file isn't adequate, you can specify an alternate file
 using the --cacert option.
If this HTTPS server uses a certificate signed by a CA represented in
 the bundle, the certificate verification probably failed due to a
 problem with the certificate (it might be expired, or the name might
 not match the domain name in the URL).
If you'd like to turn off curl's verification of the certificate, use
 the -k (or --insecure) option.

This is a nuisance.

As I've noted previously, you need to install Charles' root certificate to use it with SSL. On OS X, you do Help -> SSL Proxying -> Install Charles Root Certificate which installs it into the system keychain.

However, this doesn't work with the Homebrew curl or with the curl functions inside PHP. To fix this, we need to add the Charles root certificate to OpenSSL's default_cert_file.

I've talked about this file before. The quickest way to find it is to run:

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

on the command line. The output should be similar to:

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

As you can see, the file I need is /usr/local/etc/openssl/cert.pem.

Grab the root certificate from the Charles app. On Mac, that's Help -> SSL Proxying -> Save Charles Root Certificate menu item.

You can then append the root certificate to the default cert file:

$ cat charles_root.crt >> /usr/local/etc/openssl/cert.pem

Now, everything works:

$ curl https://api.joind.in/v2.1/
{"events":"https:\/\/api.joind.in\/v2.1\/events","hot-events":"https:\/\/api.joind.in\/v2.1\/events?filter=hot","upcoming-events":"https:\/\/api.joind.in\/v2.1\/events?filter=upcoming","past-events":"https:\/\/api.joind.in\/v2.1\/events?filter=past","open-cfps":"https:\/\/api.joind.in\/v2.1\/events?filter=cfp","docs":"http:\/\/joindin.github.io\/joindin-api\/"}

(It also works in PHP as that's linked against the same curl if you followed my post on enabling HTTP/2 in PHP.)