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.
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.
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.
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 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.
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 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.
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.
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 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.
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.
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']
)
;
}
}