Last year I started developing my small project using CakePHP 2.4 framework. I store all my files in root Lib CakePHP directory. When this directory grew up the problems started with managing classes dependencies. I decided to configure Service Container and do some refactoring with my existing class files. I found that was quite easy thing to be done.

So here I am going to show you how to do this step by step...

Composer

Composer is dependency manager for PHP. It is very helpful and easy to install and use. We will need it to install Service Container component.

To install composer we need to simply execute command below in root directory of our project:

$ curl -sS https://getcomposer.org/installer | php

You might not have curl so there is another way to install composer:

$ php -r "readfile('https://getcomposer.org/installer');" | php
After that there should be composer.phar file placed. Do not commit this file to repository.

Namespaces in CakePHP 2

Unfortunately CakePHP 2.* does not support namespaces (these will be introduced in CakePHP 3). It would be much easier to keep our classes structure tidy with namespaces (besides using App::uses(...) is tiring to me). I suggest to also use autoloader component from Symfony 2. Let's install it via composer.

First we need to create composer.json file (it is configuration file with all dependencies) in root project directory and put there requirements for autoloader as follows:

{
    "require": {
        "symfony/class-loader": "2.3.*@stable"
    }
}

Then we need to run installation command with composer (in project root directory):

$ php composer.phar install
If everything is fine we should see newly created file: composer.lock. This file keeps data about versions of all dependencies and it should be placed in project repostiory. If we look into vendor directory we will see that is not empty. Composer installs all dependencies there. I think that is all we should care about for now.

Autoloader configuration in CakePHP

Let's move to the app/Config directory of our project and create there a new file named: autoload.php. We need to put into a configuration of UniversalClassLoader. Simply paste to this file content as follows:

<?php
require_once __DIR__. '/../../vendor/autoload.php';

use Symfony\Component\ClassLoader\UniversalClassLoader;

$loader = new UniversalClassLoader();
$loader->registerNamespace('Lib', '../../app');
$loader->register();

And then include this file at the beggining of core.php file:

/**
 * Universal class loader configuration.
 */
include_once __DIR__ . '/autoload.php';

Great! Now we can use namespaces in files placed in Lib project directory and we do not have to use App::uses() method anywhere (even in controllers). All file names and directory structures must follow PSR-0 standard.

Here is the small example of app/Lib/Logger/DefaultLoggerProvider.php file:

<?php
namespace Lib\Logger;

use Lib\Logger\File\FileLogger;

class DefaultLoggerProvider implements LoggerProvidable
{
    //...
}

Service Container configuration

First we need to install Service Container component via Composer. Let's modify composer.json to get it look like:

{
    "require": {
        "symfony/proxy-manager-bridge":"2.3.*",
        "symfony/yaml": "2.3.*@dev",
        "symfony/config": "2.5.*@dev",
        "symfony/class-loader": "2.3.*@stable",
        "symfony/dependency-injection": "2.5.*@dev"
    }
}

Then type command:

$ php composer.phar update

After successful installation let's create file app/Config/container.php and put there configuration as follows:

<?php
use Lib\Environment;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;

$container = new ContainerBuilder();
$loader = new XmlFileLoader($container, new FileLocator(__DIR__));
$loader->load('services.xml');

Environment::getInstance()->setContainer($container);

As you can see there is placed use statement for Lib\Environment class. This class has only one instance (singleton) per request and it contains whole configured container. Body of this class is shown below:

<?php
namespace Lib;

use Symfony\Component\DependencyInjection\ContainerBuilder;

class Environment
{
    /** Singleton instance of Environment class. */
    private static $instance;
    
    /** @var ContainerBuilder */
    private $container;
    
    /** @return Environment */
    public static function getInstance()
    {
        if (self::$instance == null) {
            self::$instance = new Environment();
        }
        return self::$instance;
    }

    /** We have to hide constructor. */
    private function __constructor() {}

    public function setContainer(ContainerBuilder $container) { $this->container = $container; }

    public function getContainer() { return $this->container; }
    
    public function getServiceById($id) { return $this->container->get($id); }
}

This class follows singleton design pattern. It is set up in container.php file (last line).

The last step to enable the service container is to include container.php in core.php configuration file. This should be placed right after include_once __DIR__ . '/autoload.php'; line like this...

/**
 * Universal class loader configuration.
 */
include_once __DIR__ . '/autoload.php';

/**
 * Dependency injection container configuration.
 */
include_once __DIR__ . '/container.php';

Using service container

Now when we have configured service container we can simply use it by calling getServiceById($id) method from Environment class. You can invoke that in any place in project:

use Lib\Environment;

Environment::getInstance()->getServiceById('my_project.example_service');

But first we need put services configuration to services.xml (in app/Config directory):

<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd"
>
    <services>
        <service id="my_project.example_service" class="Lib\Service\ExampleService" />
    </services>
</container>

CakePHP core classes in container

Below are shown examples of configurations for core CakePHP classes like Session or CakeRequest and other...

<!-- Cake PHP Core classes as services...  -->
<service id="cake.component_collection" class="ComponentCollection" />
<service id="cake.request" class="CakeRequest" 
         factory-class="Router"
         factory-method="getRequest" >
    <argument>true</argument>
</service>   

<service id="cake.session" class="Session"
         factory-service="cake.component_collection"
         factory-method="load">
    <argument>Session</argument>
</service>

<!-- Model class as service -->
<service id="my_project.model_name" class="ModelName"
        factory-class="ClassRegistry"
        factory-method="init">
   <argument>ModelName</argument>
</service>
            

These definitions are very useful for example when you want to create some abstraction layer over Model classes (you want, trust me). Objects injection is very easy then.

Conclusions

Autoloader & Service Container components are a great way to organize you services with its dependencies. Developing becomes much easier with these tools. When you work on the project written in CakePHP 2.* and you found annoying using App:uses(...) then these components will work perfectly for you.

References


Post comment