-
Notifications
You must be signed in to change notification settings - Fork 0
Model
The service layer is responsible for the data. That includes the database, a thin layer to access and augment the data and some transporter object to make life easier.
There is nothing complicated going on here. Simple MySQL database stores the data. No SQL Views or trigger are used. Only tables and relations between them.
On top of the database sits a layer of objects responsible for reading a writing to the database. This layer hides the fact that there is RDBM system that stores the data. Object in this layer are under the \Althingi\Service
namespace. Here is where all our SQL statements live.
Not all service object use the database for storage. Some service classes accept binary blobs that are images and are stored on the disk. Other may use external services accessed via TCP sockets. But the service classes that do communicate with the database usually implement the \Althingi\Lib\DatabaseAwareInterface
interface. That interface is responsible for making sure that service classes have access to a database driver.
namespace Althingi\Lib;
use \PDO;
interface DatabaseAwareInterface
{
public function setDriver(PDO $pdo);
public function getDriver();
}
When you then register an class to the ServiceManager that implements this interface, the SM will inject a valid, configured PDO driver to the class.
Let's first look at a Service class implementation. This is the Assembly service class:
namespace Althingi\Service;
use Althingi\Lib\DatabaseAwareInterface;
use PDO;
class Assembly implements DatabaseAwareInterface
{
use DatabaseService;
/**
* @var \PDO
*/
private $pdo;
/**
* Get one Assembly.
*
* @param $id
* @return null|object
*/
public function get($id)
{
$statement = $this->getDriver()->prepare("
select * from `Assembly` where assembly_id = :id
");
$statement->execute(['id' => $id]);
return $statement->fetchObject();
}
/**
* @param \PDO $pdo
*/
public function setDriver(PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* @return \PDO
*/
public function getDriver()
{
return $this->pdo;
}
}
And now let's look at how that service is registered in service.config.php
. It is made invokable by adding it to the invokables
array. When it is initialized, the ServiceManager will check if it implements the DatabaseAwareInterface
and if so, inject a Database Driver into it.
[
'initializers' => [
'Althingi\Lib\DatabaseAwareInterface' => function ($instance, $sm) {
if ($instance instanceof \Althingi\Lib\DatabaseAwareInterface) {
$instance->setDriver($sm->get('PDO'));
}
},
],
'invokables' => [
'Althingi\Service\Assembly' => 'Althingi\Service\Assembly',
],
]
There is one thing to notice about this Service object. It uses a trait called DatabaseService
. That trait contains two helpful methods for creating create and update statements and one data-convertion method called convert
.
insertString takes in a table name and a stdClass
of values that will be converted into a INSERT INTO [table-name] SET [ob-prop-name1], [ob-prop-name2] VALUES [ob-prop-value1], [ob-prop-value2];
string.
updateString does similar thing. It also requires a where
condition as a string.
convert is the final method in the trait. It takes in an stdClass
and convert it into an array
. If this stdClass
contains DateTime
object, that will be converted into a string
that the database understands.
###Decorate. When data is fetched from the database, it will be retuned by the PDO driver in is't simples form. Usually that is OK. RESTfull client is in no mood to accept data that is not telling the truth about what it is. Numeric values retuned from the database are returned as string. This is OK for PHP but you could imagine other clients that are written in static typed languages, they will not be so forgiven. To fix this we usually write decorator functions that are run on a database results and return the object in a cleaned up and presentable state.
The Service layer does not enforce this and there are no interfaces to implement. By conversion however there usually is a private method in the server class, sometimes called decorate
that will do this job. We will take a look at Assembly
again:
class Assembly implements DatabaseAwareInterface
{
public function get($id)
{
$statement = $this->getDriver()->prepare("
select * from `Assembly` where assembly_id = :id
");
$statement->execute(['id' => $id]);
return $this->decorate($statement->fetchObject());
}
public function fetchAll($from, $to, $order = 'desc')
{
$order = in_array($order, ['asc', 'desc']) ? $order : 'desc';
$statement = $this->getDriver()->prepare("
select * from `Assembly` A order by A.`from` {$order}
limit {$from}, {$to}
");
$statement->execute();
return array_map([$this, 'decorate'], $statement->fetchAll());
}
private function decorate($object)
{
if (!$object) {
return null;
}
$object->assembly_id = (int) $object->assembly_id;
return $object;
}
}
See how decorate($object)
is called on one object in the get($id)
method. Then on a collection in fetchAll($from, $to, $order = 'desc')
.
##Forms
When we think of Forms in ZF2 we usually thing of direct correlation between them and HTML forms. It is true that ZF2 has some view-helpers to convert an instance of \Zend\Form\Form
into an HTML form, but Forms are so much more than that.
In a RESTfull API application there are no HTML forms, there is no HTML, but we still need forms. Forms extract, formalise and validate date coming into out application. They are a halfway-house between a client and the service layer.
Fist let's take a look at when we create new resource through the Service Layer using Forms. Data comes in via POST request, its gets pushed into the Form and out comes clean and valid data:
In traditional HTML application, when we want to update something, we extract the whole resource, put it into a form, present it to the user is the form of inputs and textareas. When the user clicks the submit button the whole resource is updated since all the data is being returned from the form submission.
In a RESTfull application, often we will have a partial update via PATCH method. This could happen at any time. To make this possible, we have to, on update time, extract the resource from the Service Layer our self, merge it with the incoming data and then update.
Say we want to update the end-time of an assembly. We would be tempted to just inject that into the database. Maybe check that it is a valid date. But we really have to do more than that. We have to check if that date is later than begin-time and for that we need the begin-time. That value is not coming in from the client since this is a partial update (PATCH) action. So often, even though we allow only a small part of the data coming in, we need the whole resource to be able to validate it.
Often that data coming from the Service Layer doesn't line up with the Form.
Let's take Session as an example. Session is a period of time then a Congressman sits on an Assembly. In the database, this is represented like this:
Session (
session_id: int
congressman_id: int foreign-key
constituency: int foreign-key
party_id: int foreign-key
from: date
to: date
)
Now this make much sense in the database, but when presented to the end user (RESFfull API consumer) all these foreign keys make no sense. The end user wants real object instead of Integers pointing to some other data in the system. This is no problem; the Service Layer just swaps out the Integers for real objects. This is done in the decorator
methods that we looked at earlier.
class Session implements DatabaseAwareInterface
{
public function get($id)
{
$statement = $this->getDriver()->prepare("
select * from `Session` where session_id = :session_id
");
$statement->execute(['session_id' => $id]);
return $this->decorate($statement->fetchObject());
}
/**
* @param $object
* @return null|object
*/
private function decorate($object)
{
if (!$object) {
return null;
}
$constituencyStatement = $this->getDriver()->prepare("
select `constituency_id` as id, `name`, `abbr_short` as abbr
from `Constituency` where constituency_id = :id
");
$constituencyStatement->execute(['id' => $object->constituency_id]);
$partyStatement = $this->getDriver()->prepare("
select `party_id` as id, `name`, `abbr_short` as abbr
from `Party` where party_id = :id
");
$partyStatement->execute(['id' => $object->party_id]);
$object->session_id = (int) $object->session_id;
$object->congressman_id = (int) $object->congressman_id;
$object->constituency = $constituencyStatement->fetchObject();
$object->party = $partyStatement->fetchObject();
unset($object->constituency_id);
unset($object->party_id);
return $object;
}
}
See how the decorator
just queries the database for missing objects and populates the result object.
Now the consumer will see something like this:
{
"session_id": 56,
"congressman_id": 678,
"from": "2014-12-17",
"to": "1970-01-01",
"type": "þingmaður",
"abbr": null,
"constituency": {
"id": "52",
"name": "Suðvesturkjördæmi",
"abbr": "SV"
},
"party": {
"id": "38",
"name": "Samfylkingin",
"abbr": "Sf"
}
}
Now let's take a look at what is required to update a Session. From the API documentation we can see that a PACH action can contain those fields
from date When the session starts (optional)
to date When the session ends (optional)
type string Type, like 'Varamadur' (optional)
party_id int ID of of party (optional)
constituency_id int ID of constituency (optional)
So these is an inconsistency in data coming from the service layer and data coming from the client. Service Layer has constituency
but incoming data has constituency_id
, Service Layer has party
but incoming data has party_id
. It's up to the form to fix this.
Let's have a look at how the PATCH action would look like:
public function patch($id, $data)
{
$sessionService = $this->getServiceLocator()
->get('Althingi\Service\Session');
$session = $sessionService->get($id);
if (!$session) {
return $this->notFoundAction();
}
$form = (new Session())
->bind($session)
->setData($data);
if ($form->isValid()) {
$sessionService->update($form->getObject());
return (new EmptyModel())->setStatus(204);
}
return (new ErrorModel($form))->setStatus(400);
}
There is a lot going on here, so let's dive in. First we get the Service Layer object and we query it for the resource we want to update. If not found we return a 404 error. Next we create a Session Form object and we bind data from the Service Layer to it. Next we take the data coming in and inject that into the Form. If valid we update the resource else we return a 400 error telling the client that is was his fault.
Wait a minute, didn't you just say that there is an inconsistency in data coming in from the Service Layer and data coming in from the client. In deed I did. And here is how the Form can resolve this.
Inside the form there is a Hydrators, an object who's sole propose is to resolve this issue. Let's take a look at how the Session Form is constructed.
namespace Althingi\Form;
use Zend\InputFilter\InputFilterProviderInterface;
class Session extends Form implements InputFilterProviderInterface
{
public function __construct()
{
parent::__construct(get_class($this));
$this
->setHydrator(new \Althingi\Hydrator\Session())
->setObject((object)[]);
$this->add(array(
'name' => 'session_id',
'type' => 'Zend\Form\Element\Number',
));
// more elements added
}
public function getInputFilterSpecification()
{
return [
'session_id' => [
'required' => false,
'allow_empty' => true,
],
// more fields validated
];
}
}
See the line where I set the Hydrator ->setHydrator(new \Althingi\Hydrator\Session())
. I guess we have to take a look at the Hydrator.
namespace Althingi\Hydrator;
use Zend\Stdlib\Hydrator\HydratorInterface;
class Session implements HydratorInterface
{
/**
* Hydrate $object with the provided $data.
*
* @param array $data
* @param object $object
* @return object
*/
public function hydrate(array $data, $object)
{
return (object) $data;
}
/**
* Extract values from an object
*
* @param object $object
* @return array
*/
public function extract($object)
{
if (isset($object->constituency) && $object->constituency != null) {
$object->constituency_id = $object->constituency->id;
}
unset($object->constituency);
if (isset($object->party) && $object->party != null) {
$object->party_id = $object->party->id;
}
unset($object->party);
return (array)$object;
}
}
Have a good look at the extract($object)
method, It extracts the Ids from the nested objects and converts the result object into a flat, single level array before returning it.
When we are creating a resource, there is nothing in the database, so these is no object to bind to in the form. This is the reason for the setObject((object) [])
in the Form constructor
$this
->setHydrator(new \Althingi\Hydrator\Session())
->setObject((object)[]);
This is so that the form has some object to bind to and for the Hydrator to have some object to work on.
One thing to remember when creating forms is that there is a bug in \Zend\Form\Form. It will happily bind object to the Form and accept incoming data. But it will only validate the data coming in and not the data being bound by the object. To fix this I extended the \Zend\Form\Form
class. Every time you create a Form use Althingi\Form\Form
and NOT \Zend\Form\Form
.
One last thing about creating and updating resources. Let's again look at how Session is created. Required fields are:
from date When the session starts (optional)
to date When the session ends (optional)
type string Type, like 'Varamadur' (optional)
party_id int ID of of party (optional)
constituency_id int ID of constituency (optional)
And the location we want to sore it is /thingmenn/:id/thingseta/:session_id
. The database has both that data coming in from the client and the URI data stored in the same table:
session_id int
assembly_id int
from date
to date
type string
party_id int
constituency_id int
So when creating (not updating) you will see something like this happening, POST/PUT data is being merged with data extracted from the URI:
public function create($data)
{
$sessionService = $this->getServiceLocator()
->get('Althingi\Service\Session');
$congressmanId = $this->params('id');
$form = new Session();
$form->setData(array_merge($data, ['congressman_id' => $congressmanId]));
if ($form->isValid()) {
$sessionId = $sessionService->create($form->getObject());
return (new EmptyModel())
->setLocation(
$this->url()->fromRoute(
'home/thingseta/fundur',
['id' => $congressmanId, 'session_id' => $sessionId]
)
)
->setStatus(201);
}
return (new ErrorModel($form))->setStatus(400);
}
Where $form->setData(array_merge($data, ['congressman_id' => $congressmanId]));
is where the magic happens.
##Summary
What I am saying here is that Service
, Form
and Hydrator
form the Model. They should really be under the same namespace, but they are not (maybe a refactor could be in order). The Controller ties them together but does not act on them, the controller never changes the data coming in or going out. Service objects (or database result records) can begin their live in the database or in the Form
. They are modified in the Form
through the Hydrator
and returned to the Service
for storage.
Remember to extend Althingi\Form\Form
when creating forms. Remember to implement Althingi\Lib\DatabaseAwareInterface
if you want your service to persist data to the database. Use Althingi\Service\DatabaseService
trait to make your life easer.
So in the case of a read: The object is created by the service object and sent to the client. All aggregation like; calculating length of speech in minutes, who made the speech etc... should already been done in the service. No added properties or modifications are done on the day out.
In the case of create. The object is created in the form, with some help from incoming POST | PUT data.
The objects starts it's live in the service, it is passed to the Form where is is merged, hydrated and validated. Then it is passed back to the service.
Delete instructions are passed to the service where the object is deleted.