All sample code is available on github and can be cloned out to follow along with.
Base Entity and Mapper
A BaseEntity is not as important as the mapper. The base entity allows for defining custom magic __set and __get functions that could be applied to all children. It also requires validate which can be used to on PrePersist / PreUpdate hooks. PHP doesn’t have annotation inheritance, or else this class could become much more useful.
I played around with the idea of extracting some common functionality from the old entry mapper class into a generic class. In fact, this will clean up mappers all around your application. A Generic Mapper Interface has been created which defines all common mapping methods that you would want to use in your application. These methods are not specific to Mongo DB, and could be used with any database mapper.
The Generic Mapper does all of the heavy lifting in the application. Notice that this class makes use of Doctrine\Common\Persistence classes which mean that this is compatible with any Doctrine ObjectManager[s] or ObjectRepository[s]. The majority of the functionality is fairly straightforward in this mapper. There are a few important caveats, however:
- When you save an entity, the mapper will look to see if that object is managed which can tell if it is new or not.
- The object is utf8 encoded on save. Mongo requires this to be true, and enforcing it on the application level can be a burden. This could be moved into setters, but then arrays and deep objects have more limited usage. I choose to keep it in the Generic Mapper and to make all fields public.
- sortBy is passed into all multi-entity queries. This can be overridden in children to define custom sorting.
- The Mapper is EventManagerAware and will trigger events on save.pre, save.post, remove.pre, remove.post. This is extremely useful and one of the main benefits of a centralized mapper base class like this.
- There is an attempt to check for duplicate keys on a failed save. This is touchy but does infact work for all data I’ve thrown at it. There’s no specific Unique Exception thrown by Mongo Driver.
This class has been very useful for my development, and has made adding new mappers easier than anything. There are also tests written for it, and so new mappers do not need to cover these tests unless they add new functionality to the mapper.
Extending Base Functionality
The new Entry entity isn’t all that much different from the previous version. However, there is now a validation function which will run whenever any document is persisted or updated. This is extremely helpful in preventing sanity checking in many different places!
The EntryMapperInterface adds a new require function. Notice that the previous functions are inherited through the extension of GenericMapperInterface. Now your EntryMapper doesn’t need to inherit from two interfaces directly.
The EntryMapper is much cleaner now. Notice that the custom sortBy is defined, but doesn’t have to be. The new functionality has been added and can make use of the repository and manager that is present in the parent class. The Module.php service coniguration has not changed at all (ignore the init function until later)! The same injection happens and there are no more dependencies in the service factory.
There is now a Controller Test which tests some of our functionality! Testing is pretty easy with ZF2, but remember that the service locator is your friend. Do not directly instantiate classes unless they’re closed off. When you do this, it becomes much more difficult/impossible to reliably test the class in all ways. Notice how mock mappers are used in the test instead of a connected mongodb instance. This makes it possible to test your code without any dependencies! Perfect.
Hooks are one of my favorite features of this generic mapper. Hooks are extremely powerful and allow you to de-couple code while increasing modularity. Here’s a real world application:
You are developing an ecommerce application. Whenever a payment is made (saved) a number of things need to happen: emails need to send, inventory needs to be updated, and so on. You could write your own observer pattern to handle these events (giving you an event manager like Zend) or you could call those subroutines in the same routine that saves or invokes save on the payment. Anytime that you want to add new subroutines, the code becomes more complex and more tightly coupled (bad). The Event Manager gives you an easy to use observer pattern which lets you save in the save method and email in your email event! Everything is decoupled and the save routine has no dependencies on the events.
Hooks are setup in the Module.php. The init() hook is called when the module is initialized, and the Service Manager and Event Manager can be extracted from the event. Hooks are “attached” to a listener and in our case, we are listening for any class to emit the Demo\Entity\Entry::save.post event. We can check in the hook if the entity is new (passed in our hook invocation) and then act on it. This trivial example will just append <p></p> to the entry text if it is a new entity.
I also added a Hook Test that checks for the consequences of the hook to happen. Init is a bad of a hard thing to test, because init only happens once per test suite. Notice that I recommend running this test with the –group and –exclude-group option to prevent the init method from being called in other tests, locking out the ability to set the mock mapper instance correctly. I use a little test running script which keeps resources clean and runs all tests when needed.
I might add another installment or two in the ZF2 + Doctrine ODM series! Check back again