Howdy! I’m writing a series of blog posts based around my experiences with Zend Framework 2 and Doctrine ODM (Object Document Manager) library. I’ve found this to be an awesome stack that allows for quick development, reliable testing, and reusable code. I’ll refer to code at github which has the code for this basic project. This is definitely not an advanced topics course, as I’ll be covering more advanced techniques in future blog posts.
ZF2 is a bit of a burden to learn at first. Some people have dismissed it before making it the whole way through the example application, myself included. However, once the framework’s weaknesses and strengths are considered, it’s actually very solid. A lot of the configuration is confusing and clunky at first, but becomes understood very easily and allows for extremely customizable modules. I personally do not use the native dependency injection techniques and instead find that doing all injection through the service manager works extremely well. Give it the time it deserves and you won’t regret it.
There is no silver bullet in the data world, and Mongo is definitely no exception. Mongo is a schema-less NoSQL database which works really well for certain types of data and situations. In my case, I had 3 pieces of data which were all very loosely connected, and SQL joins were very expensive. My data was not being utilized to its full extent because of this. Switching to Mongo should help me utilize it. Also, the schema-less nature leads to a much faster development cadence where migrations do not need to occur.
Disclaimer: Some people have reported issues scaling Mongo up in larger systems due to a 12 server scaling limit and how write locking works. That’s a much more advanced topic.
Doctrine is one of the most popular ORMs in PHP right now. It’s extremely easy to use and well-maintained. The ORM (made for relational dbs like MySQL) is complimented by an ODM (made for Mongo and NoSQL systems). The beautiful thing about this is that one can switch from ODM to ORM or vice-versa by simply changing how things are mapped out (very little application code change).
Doctrine is well-tested and will make your application easier to test (don’t need to test trivial aspects)
There are a few assumptions about setup that I’m going to make because they’re outside the scope of this introduction:
- PHP >= 5.3 (built in server makes life easier)
- Mongo installed on localhost:27017 (default)
- Composer is installed and accessible by alias “composer”. If on linux, replace that with php composer.phar
And with that, let’s get the show on the road! Let’s create a new project called fred-demo using composer:
composer create-project -sdev --repository-url="http://packages.zendframework.com" zendframework/skeleton-application fred-demo
This will allow create a new ZF2 project with some skeleton stuff there. Let’s add a few more modules using composer which will load doctrine and zftool
composer require doctrine/mongodb-odm:dev-master doctrine/doctrine-mongo-odm-module:dev-master zendframework/zftool:dev-master
Now let’s copy the distribution configuration file for doctrine odm into our application’s autoload folder
cp vendor/doctrine/doctrine-mongo-odm-module/config/module.doctrine-mongo-odm.local.php.dist config/autoload/module.doctrine-mongo-odm.global.php
This configuration file is documented on github. You should replace the contents of your file with the contents of the file on github. The trickiest part is the AnnotationDriver which must be enabled in order to tell Doctrine how to handle our PHP objects in the database (through annotations).
The last step is to add ‘DoctrineModule’ and ‘DoctrineMongoODMModule’ to your config/application.config.php file. Any modules that you add here will be loaded by ZF2 and will be accessible in your application. If you install modules but do not register them here, then you will get errors when you try to use them. When you create a module using ZFTool (like we’ll do shortly), it will be automatically appended to this list!
Run and Ensure Configuration
I mentioned earlier that php built in server will be used for running this example application. This is a great tool that I’ve been using to isolate my php application runtime and to let it run on any url or port that I have in my operating system’s hosts file. Run the following commands to start a server (assuming you’re in the project root):
cd public php -S localhost:8080 index.php
This will start a server that routes all requests through index.php! This is really convenient as no .htaccess file is available with the built in server; any rules would have to be written in PHP and then run through a router. ZF2 handles all of this magic for us! If you run via apache, it will also work great but you have to setup everything correctly which has time overhead (and time is money).
At this point you should be able to visit localhost:8080 and see the ZF2 welcome page. If you have an Exception page visible, then check your configurations and module loads. You may need to run “composer install” if modules are not present (they should be if you followed this post).
Write some Code!
I’m going to skip over some of the code and assume that you are following along with the github project. A lot of it is pretty self-explanatory for this very simple controller example. I’ll instead be focusing on some of the configuration quirks and design patterns we could / should use.
The first step is to create a module that we will be working in (instead of Application). I prefer to not work in Application because it’s too generic for my liking. I would rather have a descriptive module which has meaning and no fluff. We’ll create the module and also a controller:
./vendor/bin/zf.php create module Demo ./vendor/bin/zf.php create controller Entry Demo rm -rf module/Demo/view
The view folder was deleted because this will just be simple JSON output. You actually just accomplished a lot with these generators. You managed to create a new module called demo and then also a controller inside of it called Entry. However, we now have to do some tedious configuration house keeping. This is always the least fun part of any application, but it becomes very quick after just a few times.
Check out the commented module.config.php file and notice the controllers, router, and view_manager entries. The doctrine entry will make more sense after we add an entity. These are all briefly commented. The biggest mistake is to not register a controller under the controllers entry or to not add a route for a new resource. All of these aspects are pretty heavily documented.
Sweet Sweet Entities
The Entity and Mapper is the core of the db integration. An Entity defines fields and how they should be mapped, but does not handling any of the actual DB integration. A mapper can take an entity and perform operations on it. Create a new folder/file combination that will end up mapping to module/Demo/src/Demo/Entity/Entry.php. This file is just a POPO (Plain Ol’ PHP Object). There are two properties (public for brevity’s sake) and some annotations.
Notice that the namespace ‘Doctrine\ODM\MongoDB\Mapping\Annotations’ is used as ODM. It is very important to reference the namespace fully when defining fields like @ODM\Id, and so a shortcut is used in the namespacing. Every Entity saved in Doctrine ODM must have a primary / id field.
The default Mongo ID is really cool. It is based on the date/time and so you can extract an entity’s creation DateTime based on it (without an extra field). You can also query on it like a date time which is a more advanced topic. I recommend just sticking to default, but there are also other cool identifiers which can be used when necessary.
Going back to module.config.php, the bottom ‘doctrine’ entry may make more sense now. Every entity that is mappable / managed by doctrine must be registered to a driver of some sort. Our ODM_Driver is an Annotation based driver and so we must register it’s use for Demo\Entity\Entry. If we did not do this, we would get an error when we use the entity.
Even Sweeter Mappers
Now we can create our mapper interface and class. The reason that this should be interfaced is that I could easily create a mapper which used something other than Doctrine ODM without changing references to the concrete class. I briefly mention in the comments that this could easily be abstracted into a GenericMapper. In fact, I will be doing this for sure in the next blog post which will present a simple design pattern for a GenericMapper and extended mappers. It’s a really good idea to follow up with it when available.
The mapper is for the most part pretty readable and self-explanatory. Notice that the ObjectManager and ObjectRepository are passed in as constructor requirements. This allows for easy injection using the service manager and prevents a mapper from being created without a manager or repository (which would be useless). Also, ObjectMapper and ObjectRepository are common Doctrine classes shared across all implementations…cool!
Can I get some Service, Please?
The service manager is the heart of any application module. It allows for an easy to reference service to be registered which can handle dependencies and a bunch of other things. We have to add the service configuration to our Module.php file. Check out the fact that the Entity is invokable but the Mapper is a factory. This allows the entity to be called without any dependencies but the mapper can have its dependencies resolved. Also notice that the DemoEntry is shared=>false. This means that anytime $serviceManager->get(‘DemoEntry’) is called, a new object is returned. Alternatively, any time that ‘DemoEntryMapper’ is invoked, a shared instance will be returned like a singleton pattern.
Lastly a Controller
Now we are ready to fill in the EntryController that we created. I have decided on a list (all), list (single), remove, and add action. These can be accessed by:
list(all) => localhost:8080/entry/list
list(single) => localhost:8080/entry/list/SomeID
remove => localhost:8080/entry/SomeID
add => localhost:8080/entry/add (with post data ‘text’ entered)
Check out the controller and actions. Notice that the JsonModel is returned with an array. This will automatically return JSON data instead of HTML views. Also notice how the id is retrieved from the route and can be checked to differentiate list(all) and list(single).
That’s All Folks
There will be more in the future about some cool patterns and techniques I’ve discovered which make my apps super slick (such as filters and generic base classes). Filter documentation is hard to come by, so that will be a great topic. Also check me out at Frederick, Maryland Meetup on January 7.