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;
protectedfunction__construct()
{
$this->structure = (newRowStructure())
->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):
functiontouchDocumentController($document_id)
{
$document = $this->container->getService('pomm')['db_name']
->getModel('\Database\Schema\DocumentModel')
->updateByPK([$document_id], ['updated_at' => new \DateTime()])
;
if (!$document) {
thrownewNotFound404Exception(
sprintf("Could not find document '%d'.", $document_id)
);
}
returnjson_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;
protectedfunction__construct()
{
$this->structure = newDocumentStructure;
$this->flexible_entity_class = "\Model\Database\Schema\Document";
}
publicfunctionfindSecret($secret_level, $where = null)
{
Where::create('secret_level > $*', [$secret_level])
->andWhere($where)
;
return$this->findWhere($where);
}
publicfunctionfindPersonnalSecretFile(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:
functioncreateProjection()
{
returnparent::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;
protectedfunction__construct()
{
$this->structure = newDocumentStructure;
$this->flexible_entity_class = "\Model\Database\Schema\Document";
}
publicfunctionfindWithAttachmentCountWhere(Where$where)
{
// 1.define SQL query using placeholders$sql = <<<SQLselect :projectionfrom :document_table d join :attachment_table a using (document_id)where :conditionSQL;
// 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 queryreturn$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 ($iteratoras$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
{
publicfunctiongetTitle()
{
returnucword($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
{
publicfunction__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;
protectedfunction__construct()
{
$this->structure = newDocumentStructure;
$this->structure
->inherits(newAuthenticableStructure)
->inherits(newPostableStructure)
;
$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
{
publicfunctionarchiveDocument($document_id)
{
$this->startTransaction();
try {
$document = $this
->getModel('\Model\Database\Schema\Document')
->deleteByPK(['document_id' => $document_id])
;
if (!$document) {
$this->rollbackTransaction();
thrownew \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();
thrownew \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.
<?phpnamespaceMyApplication\Model;
usePommProject\ModelManager\SessionBuilder;
usePommProject\ModelManager\Converter\PgEntity;
usePommProject\Foundation\Session\Session;
class MyApplicationSessionBuilder extends SessionBuilder
{
protectedfunctionpostConfigure(Session$session)
{
parent::postConfigure($session);
$converter_holder = $session
->getPoolerForType('converter')
->getConverterHolder()
;
$converter_holder
->registerConverter(
'MyCompositeType',
newPgEntity(
'\Model\MyApplication\MySchema\Type\MyCompositeFlexibleEntity',
newMyCompositeStructure()
),
['my_schema.composite_type', '\Model\MyApplication\MySchema\Type\MyCompositeFlexibleEntity']
)
;
}
}
