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 LoggerProvider
{
//...
}
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.