Working with TableGateway from Zend/Db

Integrating a peristence package for your project is probably the most common task we all need to figure out. There are a lot of different options to choose from, some light, some heavy, maybe you choose to use none at all and just use PDO. I want to show you one package I have recently fallen in love with. Zend Frameworks DB project and specifically the Table Gateway class.

Table Gateway

Let's take a look at a simple slim app.

<?php

require '../vendor/autoload.php';

$app = new Slim\App();

$container = $app->getContainer();

$container['settings']['displayErrorDetails'] = true;
// Setup Adapter
$container['settings']['db'] = [
    'driver' => 'Pdo_Mysql',
    'database' => 'test',
    'username' => 'root',
    'password' => '',
    'hostname' => '127.0.0.1'
];

$container['adapter'] = function ($c) {
    return new \Zend\Db\Adapter\Adapter($c['settings']['db']);
};

// Setup Gateways
$container['my_table'] = function ($c) {
    return new \Zend\Db\TableGateway\TableGateway('my_table', $c['adapter']);
};

//SQL SCHEMA
/**
CREATE TABLE `my_table` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`hi` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1
 */

// GET
/*
 * TEST
> curl -X GET http://localhost/tablegatewayapi/public/my_table/1
{"id":"1","hi":"hello"}
 */
$app->get('/my_table/{id}', function (\Slim\Http\Request $req,  \Slim\Http\Response $res, $args) {
    /** @var $gateway \Zend\Db\TableGateway\TableGateway */
    $gateway = $this->get('my_table');

    $result = $gateway->select(['id' => $args['id']]);
    return $res->withJson($result->current()->getArrayCopy());
});

// POST
/*
 * TEST
> curl -X POST http://localhost/tablegatewayapi/public/my_table -d "hi=hello"
{"id":"1"}
*/
$app->post('/my_table', function (\Slim\Http\Request $req,  \Slim\Http\Response $res, $args) {
    /** @var $gateway \Zend\Db\TableGateway\TableGateway */
    $gateway = $this->get('my_table');
    $gateway->insert($req->getParsedBody());
    $id = $gateway->lastInsertValue;

    return $res->withJson(["id" => $id]);
});

// PUT
/*
> curl -X PUT http://localhost/tablegatewayapi/public/my_table/1 -d "hi=ho"
{"hi":"ho"}
 */
$app->put('/my_table/{id}', function (\Slim\Http\Request $req,  \Slim\Http\Response $res, $args) {
    /** @var $gateway \Zend\Db\TableGateway\TableGateway */
    $gateway = $this->get('my_table');
    $gateway->update($req->getParsedBody(), ["id" => 1]);

    return $res->withJson($req->getParsedBody());
});

// DELETE
/*
> curl -X DELETE http://localhost/tablegatewayapi/public/my_table/1
{"message":1}
 */
$app->delete('/my_table/{id}', function (\Slim\Http\Request $req,  \Slim\Http\Response $res, $args) {
    /** @var $gateway \Zend\Db\TableGateway\TableGateway */
    $gateway = $this->get('my_table');
    $result = $gateway->delete(["id" => 1]);

    return $res->withJson(["message" => $result]);
});

$app->run();

/*
COMPOSER
  "require": {
    "slim/slim": "^3.3",
    "zendframework/zend-db": "^2.8"
  }
*/

In this app we implement a CRUD API for a single resource.

The TableGateway requires a SQL Table target, and an adapter. In our container we alias it to my_table.

TableGateway comes with all of the methods required for a simple CRUD operations.

  • $gateway->insert(array) - array of attributes that matches the columns. if the key-value doesn't match the table a SQL error is thrown
  • $gateway->update(array, array) - key-value array like above. the second array is a where clause, in our case it's just a id array.
  • $gateway->select(array) - array is a where clause
  • $gateway->delete(array) - array is a where clause

This code shows that you can easily build APIs with persistence, with further abstracting you can create small code-bases that have a lot of meaningful functionality. Further abstraction might lead you towards a more ActiveRecord type design and you could end up with a few different types of controllers in your application.

Entity Controller

abstract class BaseEntityController
{
    /** @var Gateway */
    private $model;

    /** @var array */
    protected $corsHeaders = [
        'Access-Control-Allow-Origin' => '*'
    ];

    /**
     * BaseEntityController constructor.
     * @param Gateway $model
     */
    public function __construct(Gateway $model)
    {
        $this->model = $model;
    }

    /**
     * @return array
     */
    public function getCorsHeaders()
    {
        return $this->corsHeaders;
    }

    /**
     * @param array $corsHeaders
     */
    public function setCorsHeaders($corsHeaders)
    {
        $this->corsHeaders = $corsHeaders;
    }


    /**
     * @return Gateway
     */
    public function getModel()
    {
        return $this->model;
    }

    /**
     * @param Request $request
     * @param Response $response
     * @param $args
     *
     * @return ResponseInterface
     */
    public function fetch(Request $request, Response $response, $args) {
        $result = $this->model->fetch($args['id']);

        return $response->withJson($result->toArray());
    }

    /**
     * @param Request $request
     * @param Response $response
     * @param $args
     *
     * @return ResponseInterface
     */
    public function create(Request $request, Response $response, $args) {
        try {
            $id = $this->model->create($request->getParsedBody());
            return $response->withJson(["result" => $id]);
        } catch (\Exception $e) {
            return $response->withStatus(400);
        }
    }

    /**
     * @param Request $request
     * @param Response $response
     * @param $args
     *
     * @return ResponseInterface
     */
    public function update(Request $request, Response $response, $args) {
        $result = $this->model->update($args['id'], $request->getParsedBody());

        return $response->withJson(["message" => $result]);
    }

    /**
     * @param Request $request
     * @param Response $response
     * @param $args
     *
     * @return ResponseInterface
     */
    public function remove(Request $request, Response $response, $args) {
        $result = $this->model->delete($args['id']);

        return $response->withJson(["message" => $result]);
    }

    /**
     * @param Request $request
     * @param Response $response
     * @param $args
     *
     * @return ResponseInterface
     */
    public function enableCorsRUD(Request $request, Response $response, $args) {
        return $response->withStatus(200)
                ->withHeader('Access-Control-Allow-Origin', $this->corsHeaders['Access-Control-Allow-Origin'])
                ->withHeader('Access-Control-Allow-Methods', 'GET,PUT,DELETE');
    }

    /**
     * @param Request $request
     * @param Response $response
     * @param $args
     *
     * @return ResponseInterface
     */
    public function enableCorsC(Request $request, Response $response, $args) {
        return $response->withStatus(200)
            ->withHeader('Access-Control-Allow-Origin', $this->corsHeaders['Access-Control-Allow-Origin'])
            ->withHeader('Access-Control-Allow-Methods', 'POST');
    }
}

So this controller class has all of the scaffolding for persisting our database objects. With this being abstracted we have a bit more scaffolding to do in the container and then set up routes to the appropriate CRUD methods on the controller. Note that this class is abstract meaning that you need to extend it in order to use it. The reason for this is that we want a differentiated controller for each database table.

UserEntityController Class

class UserEntityController extends BaseEntityController
{
    public function __construct(Gateway $model)
    {
        parent::__construct($model);
    }
}

Then in the container we need to load it up with the proper Gateway Class

$container[UserGateway::class] = function ($c) {
    return new UserGateway(
        new TableGateway('users', $c['adapter']),
        $c[Emitter::class]); //Ignore this :)
};

$container[UserEntityController::class] = function ($c) {
    return new UserEntityController($c[UserGateway::class]);
};

UserGateway Class

class UserGateway extends Gateway
{
    /**
     * @param $id
     * @return User
     */
    public function fetchAsObject($id)
    {
        return new User($this->fetchAsArray($id));
    }
}

Gateway Class

<?php
/**
 * Created by PhpStorm.
 * User: Glenn
 * Date: 2016-04-01
 * Time: 1:28 PM
 */

namespace Space\Data;

use League\Event\Emitter;
use Space\Data\Object\Model;
use Zend\Db\TableGateway\TableGateway;

/**
 * Class Model
 *
 * @package Space\Data
 *
 * Events
 *  - model.before.create ($attributes)
 *  - model.after.create ($id, $attributes)
 *  - model.before.update ($id, $attributes)
 *  - model.after.update ($id, $attributes)
 *  - model.before.delete ($id)
 *  - model.after.delete ($id, $result)
 */
abstract class Gateway
{
    /** @var TableGateway */
    protected $gateway;

    /** @var string */
    protected $primary_key = 'id';

    /** @var Emitter */
    protected $emitter;

    const MODEL_BEFORE_CREATE = 'model.before.create';
    const MODEL_AFTER_CREATE = 'model.after.create';
    const MODEL_BEFORE_UPDATE = 'model.before.update';
    const MODEL_AFTER_UPDATE = 'model.after.update';
    const MODEL_BEFORE_DELETE = 'model.before.delete';
    const MODEL_AFTER_DELETE = 'model.after.delete';

    /**
     * Model constructor.
     *
     * @param TableGateway $gateway
     */
    public function __construct(TableGateway $gateway, Emitter $emitter)
    {
        $this->gateway = $gateway;
        $this->emitter = $emitter;
    }

    /**
     * @param $id
     *
     * @return \Zend\Db\ResultSet\ResultSet
     */
    public function fetch($id) {
        return $this->gateway->select([$this->primary_key => $id]);
    }

    /**
     * @param $id
     * @return array|\ArrayObject|null
     */
    public function fetchAsArray($id) {
        return $this->fetch($id)->current();
    }

    /**
     * @param $id
     * @param array $attributes
     *
     * @return int
     */
    public function update($id, array $attributes) {
        $this->emitter->emit(self::MODEL_BEFORE_UPDATE, $id, $attributes);
        $result = $this->gateway->update($attributes, [$this->primary_key => $id]);
        $this->emitter->emit(self::MODEL_AFTER_UPDATE, $id, $attributes, $result);

        return $result;
    }

    /**
     * @param array $attributes
     *
     * @return int
     */
    public function create(array $attributes) {
        $this->emitter->emit(self::MODEL_BEFORE_CREATE, $attributes);
        $id = $this->gateway->insert($attributes);
        $this->emitter->emit(self::MODEL_AFTER_CREATE, $id, $attributes);

        return $this->gateway->lastInsertValue;
    }

    /**
     * @param $id
     *
     * @return int
     */
    public function delete($id) {
        $this->emitter->emit(self::MODEL_BEFORE_DELETE, $id);
        $result = $this->gateway->delete([$this->primary_key => $id]);
        $this->emitter->emit(self::MODEL_AFTER_DELETE, $id, $result);

        return $result;
    }

    /**
     * @param array $where
     *
     * @return \Zend\Db\ResultSet\ResultSet
     */
    public function all(array $where) {
        return $this->gateway->select($where);
    }

    /**
     * @return TableGateway
     */
    public function getGateway()
    {
        return $this->gateway;
    }

    /**
     * @return string
     */
    public function getPrimaryKey()
    {
        return $this->primary_key;
    }

    /**
     * @param string $primary_key
     */
    public function setPrimaryKey($primary_key)
    {
        $this->primary_key = (string)$primary_key;
    }

    /**
     * @return Emitter
     */
    public function getEmitter()
    {
        return $this->emitter;
    }

    /**
     * @param string $event
     * @param string $class
     * @param string $method
     */
    public function addListener($event, $class, $method) {
        $this->emitter->addListener((string)$event, [(string)$class, (string)$method]);
    }

    /**
     * @param Model $model
     */
    public function saveModel(Model $model) {
        $this->update($model->{$this->getPrimaryKey()}, $model->toArray());
    }

    abstract public function fetchAsObject($id);
}

Model

abstract class Model
{

    /**
     * Model constructor.
     * @param array $attributes
     */
    public function __construct($attributes = array())
    {
        foreach ($attributes as $k => $val) {
            $this->$k = $val;
        }
    }

    abstract public function toArray();
}

User Model

class User extends Model
{
    public $id;
    public $username;
    public $password;
    public $display_name;
    public $email;

    public function toArray()
    {
        return [
            'id' => $this->id,
            'username' => $this->username,
            'password' => $this->password,
            'display_name' => $this->display_name,
            'email' => $this->email
        ];
    }
}

Putting it all together

There is a lot of code up above so I will go through and explain it.

  1. Abstract Gateway Class
  2. Abstract Entity Controller
  3. UserGateway Class
  4. UserEntityController Class
  5. Model
  6. UserModel

Abstract Gateway Class

This is a base class that contains the bulk of the logic across the persistence layer. Your models should extend from this. It handles saving and loading the object from the database.

Abstract Entity Controller

The controller handles the request and uses the Gateway to persist the model

UserGateway/UserEntityController

Both of these are just concrete classes which act as a container for other methods that should operate on that specific domain object

Model

Model class acts as a Data Transer Object. Just a base constructor to set all of the object's properties

User Model

Model class that has properties.

Summary

The AbstractEntityController is really just an abstraction of the very first code I put above. If you want an easy CRUD solution it does it for you. The rest is my tinkering around with object oriented design and the possibility of a new Slim/Tablegateway framework. All of this code is prototyped for a project that I am working on, the naming could be better. I hope this code dump shows you how you can still arichtect elegent solutions in Slim. The Container is crazy useful and a great addition to Slim.

Written by Glenn Eggleton on Tuesday April 19, 2016
Permalink - Chapter: php