Welcome to the real future of Propel2.
This pull request is rather large and is going to introduce a heavy change in the way Propel 2 works internally and externally.
We announced in several blog posts some years ago a more strictly decoupling of domain model logic and the actual persistent logic, so we support having POPO’s as model classes. The whole architecture is now based on no static methods anymore. The only static method used is to provide active-record facility.
This implements now such a decoupling with the extraction of the logic, we had injected in the object classes directly before, into Repository and Persister class through small peaces called Components. Generated code that is related to a actual persisting method has been moved to the Builders at Platform classes which can modify all builded classes. The glue together is through the UnitOfWork class.
With this decoupling I also decoupled SQL and Propel, so in the future it’s easier to support noSQL databases and even PHPCR.
How this works
Using POPO’s means using a instance of UnitOfWork which knows everything about living objects. It’s kinda the same as in doctrine, but more compatible to Hibernate’s way of doing it.
You have a $configuration object which knows anything about the connection and known DatabasesMaps. It creates also the necessary unitOfWork (called Session) to be used in your user land code.
We have now more builder classes:
- ActiveRecordTraitBuilder
This is new. It creates the trait for each model which can be used to activate
active-record on a entity.
- EntityMapBuilder
Old TablMapBuilder. Creates the class with all mapping information and
highly optimized methods used in persister and formatter classes.
- ObjectBuilder
The actual entity class builder, which is reduced to the absolut basics.
- QueryBuilder
No big change to old QueryBuilder except the splitting of those extremely
big classes in several handy peaces (Components).
- RepositoryBuilder
This is new. It implements basically the repository pattern also known
in Doctrine, which provides some basic methods to retrieve objects and handle
objects. It also contains the methods which were before in the object model itself behavior methods.
The ObjectBuilder generates the really basic entity class with injected trait of the ActiveRecordTraitBuilder if active-record is enabled. RepositoryBuilder generates a new base class <EntityName>
Repository which should be mainly used in controllers instead of static stuff of query::create(), means we have now $repository->createQuery()
.
What changed?
Schema
Through the decoupling of SQL it’s necessary to remove all workflow based on RDBMS, which means:
- Renamed
<table>
to <entity>
- entity:name refers now to the PHP Class name instead of the SQL table name.
- Renamed
<column>
to <field>
- field:name refers now to the PHP property name instead of the SQL column name.
- Renamed
<foreign-key>
to <relation>
foreign-key > reference
is now optional. If not defined but is necessary
(in case of RDBMS) it is automatically mapped to the Primary Key of the
foreign entity.
<relation>
has now a field
attribute instead of a phpName.
Configuration
platformClass moved to since we support now several different database vendors at the same time. It makes no sense to provide a —-platform option to build all models with the same platform, because it would generate the wrong code for entities using different platforms. (imagine a database for mysql and a database for mongodb).
Entity class
The actual entity class does not have any parent class anymore. At wish a active-record trait is injected or can be used to provide active-record methods like save(), delete() etc. This is completely optional. Lazy loading is achieved through a Proxy class, which will be returned in case the entity instance is created through database queries. The entity class contains basically only the properties and getter and setter methods.
Implementation details are hidden now per default. Means if you have a relation from Car to Brand then Car doesn’t have a property brand_id
nor its setter and getter method, but only brand
.
Code generation
I’ve basically deleted all those extremely big builder classes and started from scratch, copying peace for peace if needed into separat component classes. I also
created several traits which help you as component author with some handy methods which were in the AbstractBuilder classes before (which was too big as well).
To achieve this I’ve also introduced a OOP way of generating PHP classes, methods, properties and so on. It looks like:
$this->addMethod('isNew')
->addSimpleParameter('entity', $entityClassName)
->setType('boolean')
->setDescription("Returns true if this is a new (not yet saved/committed) instance.")
->setBody($body);
It generates PHPdoc blocks as well automatically. Double definition of methods are the past with this, although it’s possible to overwrite it acidentely through using incompatible behaviors that generate the same method. However, it’s now possible to detect easier if a method has been declared already or not.
Also the builder classes itself are now incredible slim:
class EntityMapBuilder extends AbstractBuilder
{
/**
* @param string $injectNamespace
*
* @return string
*/
public function getFullClassName($injectNamespace = '', $classPrefix = '')
{
$injectNamespace = 'Map';
if ($this->getGeneratorConfig() &&
$customNameSpace = $this->getBuildProperty('generator.objectModel.namespaceMap')) {
$injectNamespace = $customNameSpace;
}
return parent::getFullClassName($injectNamespace) . 'EntityMap';
}
public function buildClass()
{
$this->getDefinition()->declareUses(
'\Propel\Runtime\Propel',
'\Propel\Runtime\EntityMap'
);
$this->getDefinition()->setParentClassName('\Propel\Runtime\Map\EntityMap');
$this->applyComponent('EntityMap\\Constants');
$this->applyComponent('EntityMap\\ColConstants');
$this->applyComponent('EntityMap\\GetRepositoryClassMethod');
$this->applyComponent('EntityMap\\InitializeMethod');
$this->applyComponent('EntityMap\\BuildRelationsMethod');
$this->applyComponent('EntityMap\\BuildFieldsMethod');
$this->applyComponent('EntityMap\\BuildSqlBulkInsertPartMethod');
$this->applyComponent('EntityMap\\HasAutoIncrementMethod');
$this->applyComponent('EntityMap\\GetAutoIncrementFieldNamesMethod');
$this->applyComponent('EntityMap\\PopulateAutoIncrementFieldsMethod');
$this->applyComponent('EntityMap\\PopulateDependencyGraphMethod');
$this->applyComponent('EntityMap\\PersistDependenciesMethod');
$this->applyComponent('EntityMap\\GetPropReaderMethod');
$this->applyComponent('EntityMap\\GetPropWriterMethod');
$this->applyComponent('EntityMap\\PopulateObjectMethod');
$this->applyComponent('EntityMap\\IsValidRowMethod');
$this->applyComponent('EntityMap\\AddSelectFieldsMethod');
}
}
Unit of Work
The unit of work pattern is living in Runtime\Session\Session class and provides us now finally the session-per-request pattern. Active-record is still session-per-operation since each save() call creates a new session and database transaction.
Compared to doctrine it’s similar, except the fact that we highly utilize the unit of work pattern to gain extremely more performance. For example we use not a simple topological sort based on mapping information (like doctrine) but a grouped topological sort based on the actual living entity instances. This gives us the ability to fire bulk INSERTs and in general more optimized queries. Compared to doctrine we gained with our unit of work 5 times faster execution times (for 10k inserting rows with simple relation). See benchmark chapter. I’m using this library https://github.com/marcj/topsort.php to sort anything by entity dependencies and grouping by equal type allowing us to have bulk insert/update.
Repository
The repository pattern gives us the ability to hook during runtime into entity lifecycle events. We have events like pre/post save/update/insert/commit. In a behavior you can still inject code into those events by just returning actual php code in the old behavior hook method. However, it’s sometimes handy to hook during runtime in some events, which is now possible. The $configuration has those event dispatcher - you can hook into all events there as well, which aren’t bound then to any repository/entity.
Examples
<database name=“mongo” platform=“mongodb”>
<entity name=“MyBundle\Model\User”>
<field name=“id” autoIncrement=“true” primaryKey=“true”/>
<field name=“name” type=“varchar”
<relation name=“group” target=“MyBundle\Model\Group” />
</entity>
<entity name=“MyBundle\Model\Group”>
<field name=“id” autoIncrement=“true” primaryKey=“true”/>
<field name=“name” type=“varchar”
</entity>
</database>
<database name=“default” platform=“mysql” namespace=“System/Model/”>
<entity name=“Page”>
<field name=“id” autoIncrement=“true” primaryKey=“true”/>
<field name=“title” type=“varchar”/>
<relation name=“content” target=“Content” />
</entity>
<entity name=“Content”>
<field name=“id” autoIncrement=“true” primaryKey=“true”/>
<field name=“name” type=“text” />
</entity>
</database>
// create the $configuration manually
$configuration = new Configuration(‘./path/to/propel.yml’);
//or through `config:convert` command
$configuration = include ‘generated-conf.php’;
$session = $configuration->createSession(); //which is basically done by a framework on each request or controller call
$car = new Car(‘Ford Mustang’);
$session->persist($car); //same as in doctrine’s EntityManager::persist
$session->commit(); //same as in doctrine’s EntityManager::flush
$carRepository = $configuration->getRepository(‘\Car’); //provided usually per services/DI.
$car = $carRepository->find(23);
$brand = new Brand(‘Ford’);
$car->setBrand($brand);
$session->persist($brand);
$session->persist($car);
$session->commit();
//or
$car->save(); //cascade save as usual.
$cars = $carRepository->createQuery()
->filterByTitle('bla')
->limit(5)
->find();
foreach ($cars as $car) {
$car->setBla('x');
$session->persist($car);
}
$session->commit();
Why not just use Doctrine
Because we have now a much more simple way of defining entities and we are incredible much more faster during code generation and fully utilizing the unit of work pattern. Also we have way less magic in our classes.
Doctrine's UnitOfWork has 3.000 LOC, we only 100, although this will increase to few hundreds because of the missing circular references check.
Benchmark
Compatibility
Static creation of query classes are still possible. It uses the same static global configuration object as active record instances. This means the test suite with all its static invokations will still be compatible.
ToDo
[x] Implement Fetch
[ ] Implement lazy loading completely
[x] Implement INSERT
[x] Implement UPDATE (almost done)
[x] Implement DELETE
[ ] Implement circular references persisting fallback
[ ] Convert more behavior to new architecture
[ ] Implement Annotations support
[ ] Make test suite green again
[ ] More tests for repository and unitOfWork
I'm working on it now since some weeks and it will take some weeks. I hope to get some stuff done during the symfony conf.