Getting to Know Aura PHP – Part II

getting to know aura php - part ii

Introduction

Last week we took an introductory tour through AuraPHP and built a simple application using the Aura 2.x framework. This week, we're going to build on that foundation, by refactoring the code we wrote, so that it's more manageable and maintainable, as well as uses some of the other features on offer; such as logging, the response object, and the dependency injection container.

If you've not read the introductory post, please do, as you'll need that one to following along with this one. Assuming you have, let's get on with the code. To keep it clear and straight-forward, here's what we're going to do:

  • Retrieve and use the configured logger service
  • Redefine the Logger service configuration for the dev environment
  • Configure a custom service, the ExtendedPdo connection object
  • Refactor the data-view-sales dispatchable into an invokable class
  • Access some query variables from the request object

That's quite a decent list of things to cover, but it helps give a more fleshed out understanding of the features on offer.

Retrieve and Use the Configured Logger Service

Projects, based on the AuraPHP 2.x framework come with a number of services configured, including logging, request, response, sessions,validation, and forms. These services are configured in a series of config files.

Let's refactor modifyWebRouter to retrieve and make use of the logger service, which uses the excellent Monolog library. If you're not familiar with Monolog, it's written by Jordi Boggiano, creator and maintainer of Composer.

Monolog's a PSR-3 style logging library, which can send logs to a range of locations and services, including files, sockets, inboxes, databases, as well as various web services, such as Loggly and ElasticSearch.

I've used it on numerous occasions and definitely recommend it.

public function modifyWebRouter(Container $di)
{
    $router = $di->get('aura/web-kernel:router');

    /** @var MonologLogger $logger */
    $logger = $di->get('aura/project-kernel:logger');

Firstly, we get access to the service, via its service name, from the DI container.

$router->add('hello', '/')
           ->setValues(array('action' => 'hello'));

    $logger->addDebug('Added default route');

    // Add an about page
    $router->add('about', '/about')
        ->setValues(array('action' => 'about'));

    $logger->addDebug('Added about page route');

    // Add a sales data display page
    $router->add('data-view-sales', '/data/view/sales')
        ->setValues(array('action' => 'data-view-sales'));

    $logger->addDebug('Added sales data page route');
}

Then, after each route is initialised, we add a call to the logger'saddDebug() method, logging the route which was just added.

Log Output

By default, the logger writes to a file on the filesystem, located under/tmp/log, who's name is that of the currently executing environment, appended with .log. As the environment's currently set to 'dev', then the file will be called /tmp/log/dev.log. If it already exists, it will be appended to. If not, it will be created and written to.

Here's a sample of the logged messages:

[2016-09-08 16:46:34] NULL.DEBUG: Added default route [] []
[2016-09-08 16:46:34] NULL.DEBUG: Added about page route [] []
[2016-09-08 16:46:34] NULL.DEBUG: Added sales data page route [] []
[2016-09-08 16:46:34] NULL.DEBUG: AuraWeb_KernelWebKernelRouter GET / [] []
[2016-09-08 16:46:34] NULL.DEBUG: AuraWeb_KernelWebKernelDispatcher::logControllerValue to hello [] []

Redefine the Logger Service Configuration for the Dev Environment

Now it's great that we can use an existing configuration, but what about being able to change one if it doesn't suit our needs? After all, how often do pre-packaged solutions and configurations always work for us, just as we would like or need them to?

So let's see how to redefine the logging service configuration, just for the development environment. To do that, open config/Dev.php. By default, it will look like the code below.

<?php
namespace AuraFramework_Project_Config;

use AuraDiConfig;
use AuraDiContainer;
use MonologLogger;

class Dev extends Config
{
    public function define(Container $di)
    {
        ini_set('error_reporting', E_ALL);
        ini_set('display_errors', true);
    }

    public function modify(Container $di)
    {
    }
}

Let's now redefine the service, similar to how it's defined in Common.php, but this time also making use of the ChromePHPHandler, in the code below.

protected function modifyLogger(Container $di)
{
    $project = $di->get('project');
    $mode = $project->getMode();
    $file = $project->getPath("tmp/log/{$mode}.log");

    /** @var MonologLogger $logger */
    $logger = $di->get('aura/project-kernel:logger');
    $logger->pushHandler($di->newInstance(
        'MonologHandlerStreamHandler',
        array(
            'stream' => $file,
        )
    ));
    $logger->pushHandler($di->newInstance(
        'MonologHandlerChromePHPHandler'
    ), Logger::DEBUG);
}

As you can see, it's just about the same code, but with three extra lines at the bottom, which adds a lazy-loaded initialisation of the ChromePHPHandler, at the level of debug.

public function modify(Container $di)
{
    $this->modifyLogger($di);
}

After that, we add a call to the modifyLogger() method, to themodify() function, which will override the setup when the code's running in the dev environment.

Configure a Custom Service

Now that's how to retrieve an existing service, but what about configuring and using a service which doesn't come with the project? To do that, we're going to refactor modifyWebDispatcher() so that theExtendedPdo object is no longer instantiated directly, but lazy-loaded instead. To do that, we first refactor the code into the define() method as below.

public function define(Container $di)
{
    $di->set('aura/project-kernel:logger', $di->lazyNew('MonologLogger'));
    // add the database connection as a service
    $di->set(
        'db-connection',
        $di->lazyNew(
            'AuraSqlExtendedPdo', [
                'dsn' => 'sqlite:./../db/database.sqlite'
            ]
        )
    );
}

What this does is configure a new service called db-connection, using the same configuration as before. But this time, the service will only be instantiated the first time it's requested, instead of straight-away, making the application much more performant.

Next, in modifyWebDispatcher(), we then replace the instantiation of ExtendedPdo, with a reference to the service, like this: $this->di->get('db-connection');

Refactor a Dispatchable into an Invokable Class

Now I want to take the process a step further, as I don't like how everything's initialised directly in the modifyWebDispatcher()method. Instead, I want to make it more maintainable, by refactoring the code in to a callable.

If you're not familiar with callables, here's a great explanation from Lorna Jane.

The refactored code's a bit long, so I've broken it down in to annotated chunks. Let's step through them and see how it all fits together.

<?php

namespace AuraSupplementalRouteDispatchablesSales;

use AuraDiContainer;

class DataView
{
    const TEMPLATE_DIR = './../src/templates/';

    /**
     * @var string The SQL statement to run
     */
    protected $statement = '
        SELECT t.TrackId, sum(il.unitprice) as "TotalSales", t.Name as "TrackName", g.Name as Genre, a.Title as "AlbumTitle", at.Name as "ArtistName"
        from InvoiceLine il
        inner join track t on (t.TrackId = il.TrackId)
        INNER JOIN genre g on (g.GenreId = t.GenreId)
        inner join album a on (a.AlbumId = t.AlbumId)
        INNER JOIN artist at on (at.ArtistId = a.ArtistId)
        WHERE g.Name like :genre
        group by t.TrackId
        HAVING at.Name = :artist_name
        order by sum(il.UnitPrice) desc, t.Name asc
    ';

There's no base class to extend, nor interface to implement, so we just create a standard PHP class. I've then added a class constant, which provides the base path to the template directory, mainly for tidiness' sake, and created a protected class member variable, $statement, to store the SQL statement.

protected $view;
    protected $di;

    /**
     * @param AuraDiContainer $di
     */
    public function __construct(Container $di)
    {
        $this->di = $di;
        $this->buildView();
    }

I've then setup two further protected class member variables to store the view and DI container objects, and a class constructor. The constructor takes one argument, $di, an instance of the DI container, as the closure previously did.

So far the same functionality exists. Next, $di is initialised with the passed in $di object, and a call is made to the buildView() method, which we'll look at next.

protected function buildView()
    {
        $view_factory = new AuraViewViewFactory;
        $this->view = $view_factory->newInstance();

        $layout_registry = $this->view->getLayoutRegistry();
        $layout_registry->set('default', self::TEMPLATE_DIR . 'sales.layout.php');

        $view_registry = $this->view->getViewRegistry();
        $view_registry->set('sales-data', self::TEMPLATE_DIR . 'data/sales/view.php');
        $this->view->setView('sales-data');
        $this->view->setLayout('default');

        // the "sub" template
        $view_registry->set('_result', self::TEMPLATE_DIR . 'data/sales/result.php');
    }
}

Now we refactor the view initialisation code into a distinct, utility method, so that it's kept separate from the rest of the code. The only real changes are that we use the TEMPLATE_DIR class constant instead of the base path, making the path that bit more flexible and adaptable, and initialise the protected member variable $view, with the result of calling$view_factory->newInstance(); instead of initialising a standalone variable.

We now have a two-step view ready to go.

/** Make the class an invokable/callable */
    public function __invoke($bindOptions = array())
    {
        $this->view->setData([
            'results' => $this->di->get('db-connection')
                ->fetchObjects($this->statement, $bindOptions, 'AuraSupplementalDatabaseObjectsEntitySalesData')
        ]);

        $response = $this->di->get('aura/web-kernel:response');
        $response->content->set($this->view);

        return $response;
    }

Finally, we define the magic __invoke() method, which will be called, when the object is invoked as though it were a function.

It receives one argument, an array, which contains the bind values which will be bound to the SQL statement, generating the SQL query, which will be passed to the database to retrieve the results, as before. I've not added any special error or exception handling for when errors occur or no the array is empty.

Now we call the fetchObjects() method on our lazy-loaded database connection service, passing in the statement, bind options, and the class to hydrate per result and store the result of the call as a view template variable 'results'.

Then, as before, we retrieve the response service, and pass the view member variable to the set() method on the response's content object, finishing up by returning the $response object. We need to do this as a callable doesn't automatically make this available, as a closure would.

That was a bit to go through, but except for a different structure, it's doing the same as before.

The Modified modifyWebDispatcher Method

With all of the code refactored, we now only need update thesetObject() method, where we define the dispatchable, as I have below. Firstly, the use statement was added to reference the class, then a new DataView object, $dataViewDispatchable, was instantiated, passing to the constructor the DI instance.

Following that, the request object service is retrieved, to make the code more concise.Finally, the setObject() method is updated, using the instantiated DataView object as a callable, passing in an associative array, where the two bind parameters are retrieved from the request query string.

use AuraSupplementalRouteDispatchablesSalesDataView;

//...rest of the existing code

$dataViewDispatchable = new DataView($di);

$request = $di->get('aura/web-kernel:request');

$dispatcher->setObject('data-view-sales',
    $dataViewDispatchable([
        'genre' => $request->query->get('genre') . '%',
        'artist_name' => $request->query->get('artist')
    ])
);

With all this done, a range of different bind parameters can be supplied, through a more flexible route, such as
https://localhost:8080/data/view/sales?genre=TV&artist=Lost.

Please note that I've done no sanitation or validation whatsoever on the query parameters, and in a real world application that's just insane. But for the purposes of a simple example, it's enough to demonstrate a series of features.

Suggestion: How about using the validation package to implement validation and filtering to make the code more safe?

Wrapping Up

And that's how to refactor the code so that it's more manageable and maintainable, as well as use some of the other features on offer, especially the dependency injection container.

If you have any issues, or just have questions to help flesh out your understanding, make sure to use the mailing list or the IRC channel (#auraphp).

Finally, Aura's definitely one of the more well designed and written frameworks for PHP, as well as one of the more elegant. I strongly encourage you to check it out.

I hope you've enjoyed this two-part introductory series to AuraPHP and the AuraPHP framework, as much as I've enjoyed researching and writing it. I'd love to know your thoughts in the comments.

Back to the Blog

avatar of matthew setter

Matthew Setter

  • Conetix
  • Conetix

Let's Get Started

  • This field is for validation purposes and should be left unchanged.