/ModelManager

Model Manager for the Pomm database framework.

Primary LanguagePHPMIT LicenseMIT

Pomm ModelManager

Scrutinizer Code Quality Build Status Monthly Downloads License

Pomm's ModelManager package is the common model layer built upon the Foundation package. It makes developers able to create models against the database and get object oriented entities back. It is not an ORM, it grants developers with the ability to perform native queries using native real SQL and use almost all types of Postgresql. This makes model layer to meet with performances while staying lean.

This package is still is beta, this means code can change without notice but should never be broken. Use it to test Pomm or bootstrap a small project. .If you want to use Pomm in production now, look at Pomm 1.x.

Installation

Pomm components are available on packagist using composer. To install and use Pomm's model manager, add a require line to "pomm-project/model-manager" in your composer.json file. It is advised to install the CLI package as well.

Introduction

The model manager links classes with database relation through structure (relation physical ) and projection (fields returned to the application) to hydrate entity instances manipulated by web applications.

Diffrent classes involved

FlexibleEntity, Structure and Model classes can be generated by the Cli package tool by inspecting the database. It is also possible to map to virtual objects like VALUES sets or functions results by creating custom classes.

Model and queries

Model classes are the main point of Pomm model manager. They are bound with a flexible entity class thus responsible of issuing queries and returning hydrated instances of this type:

class DocumentModel extends Model
{
    use WriteQueries;

    protected function __construct()
    {
        $this->structure = (new RowStructure())
            ->setRelation('document')
            ->setPrimaryKey(['document_id', 'version_id'])
            ->addField('document_id', 'uuid')
            ->addField('version_id', 'int4')
            ...
            ;
        $this->flexible_entity_class = "\Model\PommTest\PyloneSchema\Document";
    }
}

The class shown above defines a structure using the RowStructure class for the example's sake. It is a good practice to define a dedicated structure class for each Model as the CLI package does. Since the document relation is a table, the class imports the WriteQueries trait that contains read and write predefined queries like findWhere(), createAndSave() and many more methods. As soon as this file is available in the project, it is possible to use the model layer from a controller using the Model client pooler (see the Foundation package):

function touchDocumentController($document_id)
{
    $document = $this->container->getService('pomm')['db_name']
        ->getModel('\Database\Schema\DocumentModel')
        ->updateByPK([$document_id], ['updated_at' => new \DateTime()])
        ;

    if (!$document) {
        throw new NotFound404Exception(
            sprintf("Could not find document '%d'.", $document_id)
            );
    }

    return json_encode($document->extract());
}

Here are the different methods loaded by the ReadQueries and WriteQueries traits:

  • findAll()

  • findWhere(Where)

  • countWhere(Where)

  • findByPK([primary_key])

  • insertOne(FlexibleEntity)

  • updateOne(FlexibleEntity, [changes])

  • deleteOne(FlexibleEntity)

  • createAndSave([fields])

  • updateByPK([primary_key], [changes])

  • deleteByPK([primary_key], [changes])

All the read and write queries either return an instance of the updated (or deleted) entity or update the entity by reference with values returned from the database.

Composing conditions

Some of the methods shown above can take a Where arguments. This is a condition builder allowing composition of conditions.

class DocumentModel extends Model
{
    use WriteQueries;

    protected function __construct()
    {
        $this->structure = new DocumentStructure;
        $this->flexible_entity_class = "\Model\Database\Schema\Document";
    }

    public function findSecret($secret_level, $where = null)
    {
        Where::create('secret_level > $*', [$secret_level])
            ->andWhere($where)
            ;

        return $this->findWhere($where);
    }

    public function findPersonnalSecretFile(SecretAgent $agent, Where $where = null)
    {
        Where::create('agent_id = $*', [$agent['agent_id']])
            ->andWhere($where)
            ;

        return $this->findSecret($agent['secret_level'], $where);
    }

Projection

Projection is the big difference between Pomm and ORM. ORM define the relation through a static class definition whereas Pomm defines a projection (ie the fields list of a select or returning) between a database relation and a flexible instance.

By default, a Model instance takes all the fields of its relation hence $model->findByPK(['document_id' => 2]) is equivalent to select d.* from myschema.document d where d.document_id = 2. But it is possible to tune this projection by overloading the createProjection() method:

function createProjection()
{
    return parent::createProjection()
        ->unsetField('unnecessary_field')
        ->setField('quality_score', '%page / (%modification + 1)', 'float4')
        ;
}

Now, calling findByPK will issue a query like select …, d.page / (d.modification + 1) as "quality_score", … from document d where ….

It is important to note that all queries are using the default projection so modifying it will change the values the entities are hydrated with. The method's third argument is the type associated with the added field. It makes the converter system to know how to convert the new value from its database representation to a usable PHP value.

Custom queries

Even though the queries coming with the traits are covering a broad range of what can be done on a relation, Pomm incites developers to write custom queries using the rich Postgres's SQL language. Since Pomm is not an ORM, it does not generate queries to fetch foreign information. Let's see a simple custom query:

class DocumentModel extends Model
{
    use WriteQueries;

    protected function __construct()
    {
        $this->structure = new DocumentStructure;
        $this->flexible_entity_class = "\Model\Database\Schema\Document";
    }

    public function findWithAttachmentCountWhere(Where $where)
    {
        // 1.define SQL query using placeholders
        $sql = <<<SQL
select
    :projection
from
    :document_table d
        join :attachment_table a using (document_id)
where :condition
SQL;
        // 2.define the projection
        $projection = $this
            ->createProjection()
            ->setField('attachment_count', 'count(a.*)', 'int4')
            ;

        // 3.replace placeholders
        $sql = strtr($sql,
            [
                ':projection'       => $projection ->formatFieldsWithFieldAlias('d'),
                ':document_table'   => $this->getStructure()->getRelation(),
                ':attachment_table' => $this
                                        ->getSession()
                                        ->getModel('\Model\Database\Schema\AttachementModel')
                                        ->getStructure()
                                        ->getRelation(),
                ':condition'        => $where
            ]
        );

        // 4.issue the query
        return $this->query($sql, $where->getValues(), $projection)
    }
}

This way to write queries allow developers to focus on what the query actually does instead of managing list of fields and aliases. All the goodness of Postgres'SQL like window functions, CTE etc. are usable.

Collection & flexible entities

findAll() and findWhere() methods return more than one entity, they return an iterator on result: a CollectionIterator. The easiest way to fetch entities from this iterator is to traverse it:

$iterator = $pomm['my_database']
    ->getModel('\Model\Database\Schema\Document')
    ->findSecret(Where::create("content ~* $*", ['alien']))
    ;

if ($iterator->isEmpty()) {
    printf("No document found.\n");
} else {
    foreach ($iterator as $document) {
        printf("Document '%s' is matching.\n", $document['title']);
    }
}

Even though flexible entities may appear as stupid data containers they are in fact complex typed schemaless containers. It is possible to specify special getters and setters:

class Document extends FlexibleEntity
{
    public function getTitle()
    {
        return ucword($this->get('title'));
    }
}

With example above, printf("Document '%s' is matching.\n", $document['title']); will display Document 'Super secret document' is matching.. Different kind of accessors can be created / overloaded:

  • get
  • has
  • set
  • add

add is a special accessors to add values to an existing array.

Structure file

Structure class is a very simple component, it binds fields with types. They are used by write queries. Here is a code example of a simple Structure class code:

class DocumentStructure extends RowStructure
{
    public function __construct()
    {
        $this
            ->setRelation('document')
            ->setPrimaryKey(['document_id', 'version_id'])
            ->addField('document_id', 'uuid')
            ->addField('version_id', 'int4')
            ->addField('title', 'varchar')
            ->addField('updated_at', 'timestamp')
            ->addField('creator_id', 'uuid')
            ;
    }
}

Most of the time, these structure files are generated using Pomm's CLI package so there is no need to really care about this. The interesting point is how these structures handle Postgresql inheritance:

class DocumentModel extends Model
{
    use WriteQueries;

    protected function __construct()
    {
        $this->structure = new DocumentStructure;
        $this->structure
            ->inherits(new AuthenticableStructure)
            ->inherits(new PostableStructure)
            ;
        $this->flexible_entity_class = "\Model\Database\Schema\Document";
    }

In the example above, the native document structure is augmented with a AuthenticableStructure and a PostableStructure exactly the way it is done in Postgresql.

Model layer

Even though the Model service is enough for small / medium applications, it becomes insufficient for applications with a need for complex business operations. Modern frameworks offer a code layer often referred as 'service layer' or 'transaction layer'. Pomm crafts a 'model layer' for this purpose. Here is an example:

class DocumentModelLayer extends ModelLayer
{
    public function archiveDocument($document_id)
    {
        $this->startTransaction();
        try {
            $document = $this
                ->getModel('\Model\Database\Schema\Document')
                ->deleteByPK(['document_id' => $document_id])
                ;

            if (!$document) {
                $this->rollbackTransaction();

                throw new \InvalidArgumentException(
                    sprintf("Could not find document '%d'.", $document_id)
                );
            }

            $archived_document = $this
                ->getModel('\Model\Database\Schema\ArchivedDocument')
                ->archiveDocument($document)
                ;
            $this->sendNotify(
                    'document',
                    json_encode($archived_document->extract())
                    );
            $this->commitTransaction();
        } catch(PommException $e) {
            $this->rollbackTransaction();

            throw new \RunTimeException(
                "A technical error occured.",
                1,
                $e
            );
        }

        return $archived_document;
    }
}

Thew example above shows how to embed model calls in a transaction and how to convert technical exceptions into business exceptions.

Watchful readers may have noticed a call to a method sendNotify(). This is a use of Postgresql's asynchronous messaging system.

Session configuration and PgEntity converter

The ModelManager package comes with its own SessionBuilder (see Foundation) that adds Model and ModelLayer session poolers to the session. Extend this class to benefit from these features. Use this class to declare custom converters. The ModelManager package ships an extra converter to transform composite types (types, tables and views structure) into a FlexibleEntity instance: the PgEntity converter.

<?php

namespace MyApplication\Model;

use PommProject\ModelManager\SessionBuilder;
use PommProject\ModelManager\Converter\PgEntity;

use PommProject\Foundation\Session\Session;

class MyApplicationSessionBuilder extends SessionBuilder
{
    protected function postConfigure(Session $session)
    {
        parent::postConfigure($session);
        $converter_holder = $session
            ->getPoolerForType('converter')
            ->getConverterHolder()
            ;

        $converter_holder
            ->registerConverter(
                'MyCompositeType',
                new PgEntity(
                    '\Model\MyApplication\MySchema\Type\MyCompositeFlexibleEntity',
                    new MyCompositeStructure()
                ),
                ['my_schema.composite_type', '\Model\MyApplication\MySchema\Type\MyCompositeFlexibleEntity']
            )
            ;
    }
}