I have recently started looking at using a PHP microframework to build a simple REST API for an app I’m working on. I was looking for something well supported/documented which would be easy to integrate with Doctrine 2. I’d heard good things about Slim 2 so thought I would check out the new version and maybe write a bit about it.

TL;DR

To jump straight to my final code you can checkout the example here.

Installation

Akrabat has already created a Slim 3 Skeleton App so I decided to use that as my starting point. As a bonus it comes with Twig and Monolog setup and ready to go too. Once you have created a new project using the skeleton app you can install Doctrine 2.

composer require doctrine/orm

That will install doctrine 2 and its dependencies.

Configuration

Inside the app/settings.php file append your Doctrine 2 configuration and adjust the connection details to your own settings.

app/settings.php

<?php
'doctrine' => [
    'meta' => [
        'entity_path' => [
            'app/src/Entity'
        ],
        'auto_generate_proxies' => true,
        'proxy_dir' =>  __DIR__.'/../cache/proxies',
        'cache' => null,
    ],
    'connection' => [
        'driver'   => 'pdo_mysql',
        'host'     => 'localhost',
        'dbname'   => 'your-db',
        'user'     => 'your-user-name',
        'password' => 'your-password',
    ]
]

Command Line Tools

Doctrine 2 has comes with various command line tools that are quite helpful during development. To use these we create a cli-config.php file which will register the EntityManager with the console. This file should live in the root directory of our app but as of Doctrine 2.4 it can be in a sub directory called config. The content of my config file is below.

config/cli-config.php

<?php
use Doctrine\ORM\Tools\Console\ConsoleRunner;

require 'vendor/autoload.php';

$settings = include 'app/settings.php';
$settings = $settings['settings']['doctrine'];

$config = \Doctrine\ORM\Tools\Setup::createAnnotationMetadataConfiguration(
    $settings['meta']['entity_path'],
    $settings['meta']['auto_generate_proxies'],
    $settings['meta']['proxy_dir'],
    $settings['meta']['cache'],
    false
);

$em = \Doctrine\ORM\EntityManager::create($settings['connection'], $config);

return ConsoleRunner::createHelperSet($em);

You can now see what tools are available by running this command.

php vendor/bin/doctrine

Two commands I use a lot during development are

php vendor/bin/doctrine orm:schema-tool:create

and

php vendor/bin/doctrine orm:schema-tool:update

These tools are only really for development use though and should not be run on a live production server for obvious reasons. I ran the create command to generate the necessary tables based on my entities in the entity path. For the purpose of this demo I just have the one below.

app/src/Entity/Photo.php

<?php
namespace App\Entity;

use App\Entity;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="photos", uniqueConstraints={@ORM\UniqueConstraint(name="photo_slug", columns={"slug"})}))
 */
class Photo
{
    /**
     * @ORM\Id
     * @ORM\Column(name="id", type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="string", length=64)
     */
    protected $title;

    /**
     * @ORM\Column(type="string", length=150)
     */
    protected $image;

    /**
     * @ORM\Column(type="string", length=100)
     */
    protected $slug;

    /**
     * Get array copy of object
     *
     * @return array
     */
    public function getArrayCopy()
    {
        return get_object_vars($this);
    }

    /**
     * Get photo id
     *
     * @ORM\return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Get photo title
     *
     * @ORM\return string
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * Get photo slug
     *
     * @ORM\return string
     */
    public function getSlug()
    {
        return $this->slug;
    }

    /**
     * Get photo image
     *
     * @ORM\return string
     */
    public function getImage()
    {
        return $this->image;
    }
}

If you are wondering why I specified a character length on my columns it’s because I am using MySQL.

I’ll leave it to you to add some dummy records into the table :)

Integration

Now that we have Doctrine 2 configured and access to the command line tools we can start to integrate it to our app. The first step is to setup a service container for Doctrine 2 EntityManager in the app/dependencies.php file.

app/dependencies.php

<?php
...
// Doctrine
$container['em'] = function ($c) {
    $settings = $c->get('settings');
    $config = \Doctrine\ORM\Tools\Setup::createAnnotationMetadataConfiguration(
        $settings['doctrine']['meta']['entity_path'],
        $settings['doctrine']['meta']['auto_generate_proxies'],
        $settings['doctrine']['meta']['proxy_dir'],
        $settings['doctrine']['meta']['cache'],
        false
    );
    return \Doctrine\ORM\EntityManager::create($settings['doctrine']['connection'], $config);
};

Slim 3 ships by default with a DI container that extends Pimple which I think is fine for the basic needs on my project. However, I have read over the last few months Shameer C, Julian Gut and Akrabat have been eager to replace this with their own choice of DI container. You should read about why in the links above. Make your own choice :)

Establish some routes

My basic API needed to do two things intially - return a list of photo resources and a single photo resource based on a slug.

app/routes.php

<?php
// Routes
$app->get('/api/photos', 'App\Action\PhotoAction:fetch');
$app->get('/api/photos/{slug}', 'App\Action\PhotoAction:fetchOne');

Am I done yet?

Well very nearly. At this point I could just have injected the EntityManager direct into my controller similar to how Twig and Monolog are in the skeleton app examples…

app/dependencies.php

<?php
...
$container['App\Action\PhotoAction'] = function ($c) {
    return new App\Action\PhotoAction($c->get('em'));
};

…and then requested photos directly from inside the controller.

app/src/Action/PhotoAction.php

<?php
namespace App\Action;

use Doctrine\ORM\EntityManager;

final class PhotoAction
{
    private $em;

    public function __construct(EntityManager $em)
    {
        $this->em = $em;
    }

    public function fetch($request, $response, $args)
    {
        $photos = $this->em->getRepository('App\Entity\Photo')->findAll();
        $photos = array_map(
            function ($photo) {
                return $photo->getArrayCopy();
            },
            $photos
        );
        return $response->withJSON($photos);
    }

    public function fetchOne($request, $response, $args)
    {
        $photo = $this->em->getRepository('App\Entity\Photo')->findBy(array('slug' => $args['slug']));
        if ($photo) {
            return $response->withJSON($photo->getArrayCopy());
        }
        return $response->withStatus(404, 'No photo found with slug '.$args['slug']);
    }
}

Although this works fine, I and many others would generally advise against this approach.

Resource Orientated

It is not good practice to fetch data through accessing the EntityManager directly in a controller. Controllers should be skinny and that type of application logic should be called through services. Since I am developing a REST API I abstracted this code out into separate resource classes. After all the nature of a REST architecture is that everything is a resource.

I took inspiration from A.Sharif who follows this approach in his post on integrating Doctrine 2 and Slim version 2. After having already written this post I then came across the Slim REST API (Slim 2) which goes one step further and abstracts out the relevant code to interact with EntityManager into a related service class which removes the dependency on EntityManager from the resource class.

To save time injecting the EntityManager into each resource I make I made an abstract resource class app/AbstractResource.php with the following contents. Each resource I make from now on can extend this class.

app/src/AbstractResource.php

<?php
namespace App;

use Doctrine\ORM\EntityManager;

abstract class AbstractResource
{
    /**
     * @var \Doctrine\ORM\EntityManager
     */
    protected $entityManager = null;

    public function __construct(EntityManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }
}

Below is my resource class for photo which extends the abstract resource.

app/src/Resource/PhotoResource.php

<?php

namespace App\Resource;

use App\AbstractResource;

/**
 * Class Resource
 * @package App
 */
class PhotoResource extends AbstractResource
{
    /**
     * @param string|null $slug
     *
     * @return array
     */
    public function get($slug = null)
    {
        if ($slug === null) {
            $photos = $this->entityManager->getRepository('App\Entity\Photo')->findAll();
            $photos = array_map(
                function ($photo) {
                    return $photo->getArrayCopy();
                },
                $photos
            );

            return $photos;
        } else {
            $photo = $this->entityManager->getRepository('App\Entity\Photo')->findOneBy(
                array('slug' => $slug)
            );
            if ($photo) {
                return $photo->getArrayCopy();
            }
        }

        return false;
    }
}

Skinny controller

Below is the controller I have to access photos. All it does it interact with the resource to get data and return a JSON response.

app/src/Action/PhotoAction.php

<?php
namespace App\Action;

use App\Resource\PhotoResource;

final class PhotoAction
{
    private $photoResource;

    public function __construct(PhotoResource $photoResource)
    {
        $this->photoResource = $photoResource;
    }

    public function fetch($request, $response, $args)
    {
        $photos = $this->photoResource->get();
        return $response->withJSON($photos);
    }

    public function fetchOne($request, $response, $args)
    {
        $photo = $this->photoResource->get($args['slug']);
        if ($photo) {
            return $response->withJSON($photo);
        }
        return $response->withStatus(404, 'No photo found with that slug.');
    }
}

Given these changes I now update my action factory for the Photo controller in the dependencies like so.

app/dependencies.php

<?php
...
$container['App\Action\PhotoAction'] = function ($c) {
    $photoResource = new \App\Resource\PhotoResource($c->get('em'));
    return new App\Action\PhotoAction($photoResource);
};

Now I’m done

Well like most good developer blog posts I’ve finished before actually getting started. You can view my example here. It’s up to you now to go off and carry on building your lightweight Slim 3 app using Doctrine 2. I’m off to pick some fresh tea leaves.

Comments

Sources