In this post I will explain how to create a new Doctrine2 extension. Extensions fulfill the role played in Doctrine1 by behaviors.
In a app that I am currently developing I faced the following situation:
People can create projects. A project can have tasks, comments, etc.
Projects have a version number.
When the client is browsing the project, he makes calls every few seconds saying: I am in version X.
When the client does an action (comment, open a new task…), it sends “I have just performed this action: blablabla”
When the server receives a new action from the client, it increments the version of the project.
When the server receives a “I am in version X” message and the version in the server is greater than X, it answers back saying: “user Y did action blablabla”. And the client will hopefully make use of this information to update his copy of the project using backbone.js or whatever.
When the server receives a “I am in version X” and the version in the server is X, it answers: “Ok, you are fine”.
So, in this scenario, what I need is an entity (History) that contains the version number of the project (that I will call status instead of version to avoid name collisions and confusions with other extensions), the id of the project, and a field to store the action messages. And I would like Doctrine to take care of the version number and increment it whenever I insert a new History entity. It is a simple behavior extension.
Why not simply using the versionable extension? Because versionable stores new versions of the entities, and I just want to store a description of the changes in between versions.
Let’s start writing some tests, to have a clear idea of the requirements that what we need to fulfill.
Is it clear enough? First two functions help us to set up the environment, to be able to flush entities to the database, and have our HistoryableListener listening, and the last one contains our tests. We test three tings: that the first status will be 0, that the status is incremented when we create actions for the same resource, and that actions created to another resource don’t affect the last status count of this resource.
We need a fixture:
We are using three custom annotations: @gedmo:Historyable marks this entity as historyable, @gedmo:status tells our extension that that field contains the status (or versions) and @gedmo:refVersion tells our extension that that field contains the id of the entity to which this status refers.
And the repository:
Just an empty repository extending BaseHistoryRepository, that we will define later.
With this code, we have our tests prepared.
We need some code to take care of these new annotations. Three definitions:
Now, we need a driver to transform these annotations to configuration options. This class checks that the annotations are not wrong. For instance, it checks that there is only one status field, one refVersion field, that they are integers, and so on. I won’t paste the full code here, (you can check the full source code here), because with all the checks it is quite long and repetitive.
Now we define an Adapter that will contain the logic needed to get the new status number.
First, we write an interface for the adapter. This interface specifies the functions that the adapter must implement. This is because you will have usually two adapters, one for ORM (relational databases) and one for ODM (MongoDb). In this case, I will implement the ORM, but having an interface for the adapter is a good practice. It contains only a function:
This function will be the only and killer feature of our simple extension :). How does this function look like?
All this work for a single query that will tell us what will be the next status number of this History! Check out how we can make use of the array $config, that holds the names of the fields that are relevant to us, to build the query.
And now what? We are close to the end. We have a function that will tell us what the next status number must be, but how can we set this field whenever a new Historyable entity is inserted? Now is when the Event Subscriber comes to help us.
We will listen to the onFlush event, retrieve the objects that are marked for insertion (because they are new), and compute the status number based on the refVersion field. With this, we don’t have to care about updating manually the status field anymore.
Now, won’t it be useful to have a function in our repository that retrieves the last status for a project? We can make a repository class and make our real repositories (like the one that we wrote in fixtures) to extend this, having access to that function:
And that is all! We have just finished our extension! Now we can run the tests and see if everything went fine.