ZF & Doctrine 2 ODM Doctrine
WHO IS RYAN MAUGER? • Zend Framework Contributor • Zend Framework CR Team Member • Co-Author of Zend Framework 2 in Action (With Rob Allen) • Technical Editor for Zend Framework: A Beginners Guide • Community Supporter • Zend Certified PHP5 Engineer • Lead Developer at Lupimedia and trainer at Lupijuice
What is the ODM?
What is the ODM? Object Document Mapper
What is the ODM? Object Document Mapper Similar to an ORM
What is the ODM? Object Document Mapper Similar to an ORM But works with a Document Database
What is the ODM? Object Document Mapper Similar to an ORM But works with a Document Database
Whats the advantage? • Schema-less design (no complicated ALTER TABLE statements, just save your data! • Works with JSON objects, familiar with what your already doing • Simple!
Whats the advantage? • Schema-less design (no complicated ALTER TABLE statements, just save your data! • Works with JSON objects, familiar with what your already doing • Simple! (at least to get started)
Whats the advantage? Doctrine 2 • Uses the Data Mapper pattern • Simplifies your domain, removes the persistence logic • Entities are free to do their job • Defines a framework by which your domain model will work, improving consistency
Getting started with the ODM Persistence operations revolve around the Document Manager Mapping of properties is carried out through annotations in your entities, or through xml/yaml mappings
<?php namespace ApplicationBlog; /** * @Document(db="blog", collection="posts") */ class Post { /** * @Id */ private $id; An example entity /** * @Field(type="string") */ private $title; /** * @String */ Properties private $content; /** /** * @EmbedMany(targetDocument="ApplicationBlogComment") */ * @Id private $comments = array(); */ public function getId() private $id; { return $this->id; } /** public function setId($id) * @Field(type="string") { $this->id = (string) $id; */ return $this; private $title; } public function getTitle() { /** return $this->title; * @String } */ public function setTitle($title) private $content; { $this->title = (string) $title; } return $this; /** * @EmbedMany(targetDocument="ApplicationBlogComment") public function getContent() { */ return $this->content; private $comments = array(); } public function setContent($content) { $this->content = (string) $content; return $this; } public function getComments() { return $this->comments; } public function addComment(ApplicationBlogComment $comment) { $this->comments[] = $comment; return $this; } }
<?php namespace ApplicationBlog; /** * @Document(db="blog", collection="posts") */ class Post { /** * @Id */ private $id; An example entity /** * @Field(type="string") */ private $title; /** * @String */ Methods private $content; /** * @EmbedMany(targetDocument="ApplicationBlogComment") public function getContent() */ { private $comments = array(); return $this->content; public function getId() } { return $this->id; } public function setContent($content) public function setId($id) { { $this->id = (string) $id; $this->content = (string) $content; return $this; return $this; } } public function getTitle() { return $this->title; public function getComments() } { public function setTitle($title) return $this->comments; { $this->title = (string) $title; } return $this; } public function addComment(ApplicationBlogComment $comment) public function getContent() { { return $this->content; $this->comments[] = $comment; } return $this; public function setContent($content) { } $this->content = (string) $content; return $this; } public function getComments() { return $this->comments; } public function addComment(ApplicationBlogComment $comment) { $this->comments[] = $comment; return $this; } }
Using your entity Create <?php class IndexController extends Zend_Controller_Action { public function init() { /* Initialize action controller here */ } public function indexAction() { $documentManager = $this->getInvokeArg('bootstrap')->getResource('odm'); $post = new ApplicationBlogPost(); $post->setTitle('My Interesting Post') ->setContent('I have somethnng very interesting to say'); $documentManager->persist($post); $documentManager->flush(); } } > use blog switched to db blog > db.posts.find(); { "_id" : ObjectId("4dde4067fbd2237df1000000"), "title" : "My Interesting Post", "content" : "I have somethnng very interesting to say" }
Using your entity Update <?php class IndexController extends Zend_Controller_Action { public function init() { /* Initialize action controller here */ } public function indexAction() { $documentManager = $this->getInvokeArg('bootstrap')->getResource('odm'); $post = $documentManager->getRepository('ApplicationBlogPost') ->find('4dde4067fbd2237df1000000'); $post->setTitle('My Interesting Post') ->setContent('I have something very interesting to say'); $documentManager->persist($post); $documentManager->flush(); } } > db.posts.find(); { "_id" : ObjectId("4dde4067fbd2237df1000000"), "title" : "My Interesting Post", "content" : "I have something very interesting to say" }
Using your entity Add an comment <?php class IndexController extends Zend_Controller_Action { public function init() { /* Initialize action controller here */ } public function indexAction() { $documentManager = $this->getInvokeArg('bootstrap')->getResource('odm'); $post = $documentManager->getRepository('ApplicationBlogPost') ->find('4dde4067fbd2237df1000000'); $comment = new ApplicationBlogComment(); $comment->setEmail('foo@example.com') ->setComment('Nice post!'); $post->addComment($comment); $documentManager->persist($post); $documentManager->flush(); } } > db.posts.find(); { "_id" : ObjectId("4dde4067fbd2237df1000000"), "comments" : [ { "_id" : ObjectId("4dde439afbd22380f1000000"), "email" : "foo@example.com", "comment" : "Nice post!" } ], "content" : "I have something very interesting to say", "title" : "My Interesting Post" }
Using your entity Delete <?php class IndexController extends Zend_Controller_Action { public function init() { /* Initialize action controller here */ } public function indexAction() { $documentManager = $this->getInvokeArg('bootstrap')->getResource('odm'); $post = $documentManager->getRepository('ApplicationBlogPost') ->find('4dde4067fbd2237df1000000'); $documentManager->remove($post); $documentManager->flush(); } } > db.posts.find(); >
Available Annotations http://www.doctrine-project.org/docs/mongodb_odm/1.0/en/ reference/annotations-reference.html
Available Annotations http://www.doctrine-project.org/docs/mongodb_odm/1.0/en/ reference/annotations-reference.html @Field(type="string" name="origin") Field Mappings. Many aliases are available: @String, @Date, @Int, @Float etc. @EmbedOne / @EmbedMany Embedded Documents, stores the document inside the current document @ReferenceOne / @ReferenceMany Referenced Documents, stores a reference to one or more documents outside the current document @Document / @EmbeddedDocument Marks entities which are allowed to be managed by the document manager. additionally EmbeddedDocuments may not be saved independently, and must be a child of an @Document @HasLifecycleCallbacks & @PrePersist, @PostPersist etc. Marks an entity as having callbacks such as PrePersist which will be called automatically by the DocumentManager during the entities lifetime. (see reference for list)
Integrating with ZF
Integrating with ZF Example Sandbox: http://github.com/bittarman/zf-d2-odm Includes all (PHP) dependencies Requires the mongo pecl extension $ sudo pecl install mongo Install Doctrine from PEAR $ sudo pear channel-discover pear.doctrine-project.org $ sudo pear install pear.doctrine-project.org/DoctrineMongoDBODM-1.0.0BETA3 Checkout from Github $ git clone git://github.com/doctrine/mongodb-odm.git mongodb_odm $ cd mongodb_odm $ git checkout 1.0.0BETA3
The Plugin Resource <?php /* Import namespaces */ use DoctrineCommonClassLoader, DoctrineCommonAnnotationsAnnotationReader, DoctrineODMMongoDB, DoctrineODMMongoDBDocumentManager, DoctrineODMMongoDBMongo, DoctrineODMMongoDBMappingDriverAnnotationDriver; /** * Doctrine 2 ODM Resource Plugin * * @author Ryan Mauger * @copyright Ryan Mauger 2012 */ class Lupi_Resource_Odm extends Zend_Application_Resource_ResourceAbstract { public function init() { $options = $this->getOptions(); $this->registerAutoloaders($options); // Config $config = new DoctrineODMMongoDBConfiguration(); foreach ($options['config'] as $option => $value) { $method = "set" . ucfirst($option); $config->{$method}($value); } // Annotation reader $reader = new AnnotationReader(); $reader->setDefaultAnnotationNamespace('DoctrineODMMongoDBMapping'); $config->setMetadataDriverImpl(new AnnotationDriver($reader, $options['documents']['dir'])); $dm = DocumentManager::create(new DoctrineMongoDBConnection(new Mongo), $config); return $dm; } public function registerAutoloaders($options) { $autoloader = Zend_Loader_Autoloader::getInstance(); // Document classes $classLoader = new ClassLoader($options['documents']['namespace'], $options['documents']['dir']); $autoloader->pushAutoloader(array($classLoader, 'loadClass'), $options['documents']['namespace']); } }
The Plugin Resource <?php /* Import namespaces */ use DoctrineCommonClassLoader, DoctrineCommonAnnotationsAnnotationReader, DoctrineODMMongoDB, DoctrineODMMongoDBDocumentManager, DoctrineODMMongoDBMongo, DoctrineODMMongoDBMappingDriverAnnotationDriver; /** * Doctrine 2 ODM Resource Plugin * * @author Ryan Mauger * @copyright Ryan Mauger 2012 */ class Lupi_Resource_Odm extends Zend_Application_Resource_ResourceAbstract { public function init() { $options = $this->getOptions(); $this->registerAutoloaders($options); Set up class loaders. // Config $config = new DoctrineODMMongoDBConfiguration(); foreach ($options['config'] as $option => $value) { $method = "set" . ucfirst($option); Use the Doctrine $config->{$method}($value); } // Annotation reader loaders for ease $reader = new AnnotationReader(); $reader->setDefaultAnnotationNamespace('DoctrineODMMongoDBMapping'); $config->setMetadataDriverImpl(new AnnotationDriver($reader, $options['documents']['dir'])); $dm = DocumentManager::create(new DoctrineMongoDBConnection(new Mongo), $config); return $dm; } public function registerAutoloaders($options) { $autoloader = Zend_Loader_Autoloader::getInstance(); // Document classes $classLoader = new ClassLoader($options['documents']['namespace'], $options['documents']['dir']); $autoloader->pushAutoloader(array($classLoader, 'loadClass'), $options['documents']['namespace']); } }
The Plugin Resource <?php /* Import namespaces */ use DoctrineCommonClassLoader, DoctrineCommonAnnotationsAnnotationReader, DoctrineODMMongoDB, DoctrineODMMongoDBDocumentManager, DoctrineODMMongoDBMongo, DoctrineODMMongoDBMappingDriverAnnotationDriver; /** * Doctrine 2 ODM Resource Plugin * * @author Ryan Mauger * @copyright Ryan Mauger 2012 */ class Lupi_Resource_Odm extends Zend_Application_Resource_ResourceAbstract { public function init() { $options = $this->getOptions(); $this->registerAutoloaders($options); // Config $config = new DoctrineODMMongoDBConfiguration(); foreach ($options['config'] as $option => $value) { Set up the config object $method = "set" . ucfirst($option); $config->{$method}($value); } // Annotation reader $reader = new AnnotationReader(); $reader->setDefaultAnnotationNamespace('DoctrineODMMongoDBMapping'); $config->setMetadataDriverImpl(new AnnotationDriver($reader, $options['documents']['dir'])); $dm = DocumentManager::create(new DoctrineMongoDBConnection(new Mongo), $config); return $dm; } public function registerAutoloaders($options) { $autoloader = Zend_Loader_Autoloader::getInstance(); // Document classes $classLoader = new ClassLoader($options['documents']['namespace'], $options['documents']['dir']); $autoloader->pushAutoloader(array($classLoader, 'loadClass'), $options['documents']['namespace']); } }
The Plugin Resource <?php /* Import namespaces */ use DoctrineCommonClassLoader, DoctrineCommonAnnotationsAnnotationReader, DoctrineODMMongoDB, DoctrineODMMongoDBDocumentManager, DoctrineODMMongoDBMongo, DoctrineODMMongoDBMappingDriverAnnotationDriver; /** * Doctrine 2 ODM Resource Plugin * * @author Ryan Mauger * @copyright Ryan Mauger 2012 */ class Lupi_Resource_Odm extends Zend_Application_Resource_ResourceAbstract { public function init() { Set up the Annotation $options = $this->getOptions(); $this->registerAutoloaders($options); // Config Reader, or mapping driver $config = new DoctrineODMMongoDBConfiguration(); foreach ($options['config'] as $option => $value) { $method = "set" . ucfirst($option); $config->{$method}($value); should you prefer YAML / } // Annotation reader XML $reader = new AnnotationReader(); $reader->setDefaultAnnotationNamespace('DoctrineODMMongoDBMapping'); $config->setMetadataDriverImpl(new AnnotationDriver($reader, $options['documents']['dir'])); $dm = DocumentManager::create(new DoctrineMongoDBConnection(new Mongo), $config); return $dm; } public function registerAutoloaders($options) { $autoloader = Zend_Loader_Autoloader::getInstance(); // Document classes $classLoader = new ClassLoader($options['documents']['namespace'], $options['documents']['dir']); $autoloader->pushAutoloader(array($classLoader, 'loadClass'), $options['documents']['namespace']); } }
The Plugin Resource <?php /* Import namespaces */ use DoctrineCommonClassLoader, DoctrineCommonAnnotationsAnnotationReader, DoctrineODMMongoDB, DoctrineODMMongoDBDocumentManager, DoctrineODMMongoDBMongo, DoctrineODMMongoDBMappingDriverAnnotationDriver; /** * Doctrine 2 ODM Resource Plugin * * @author Ryan Mauger * @copyright Ryan Mauger 2012 */ class Lupi_Resource_Odm extends Zend_Application_Resource_ResourceAbstract { public function init() { $options = $this->getOptions(); $this->registerAutoloaders($options); // Config Create and return the $config = new DoctrineODMMongoDBConfiguration(); foreach ($options['config'] as $option => $value) { $method = "set" . ucfirst($option); $config->{$method}($value); Document manager } // Annotation reader $reader = new AnnotationReader(); $reader->setDefaultAnnotationNamespace('DoctrineODMMongoDBMapping'); $config->setMetadataDriverImpl(new AnnotationDriver($reader, $options['documents']['dir'])); $dm = DocumentManager::create(new DoctrineMongoDBConnection(new Mongo), $config); return $dm; } public function registerAutoloaders($options) { $autoloader = Zend_Loader_Autoloader::getInstance(); // Document classes $classLoader = new ClassLoader($options['documents']['namespace'], $options['documents']['dir']); $autoloader->pushAutoloader(array($classLoader, 'loadClass'), $options['documents']['namespace']); } }
[production] ; PHP settings phpSettings.display_startup_errors = 0 phpSettings.display_errors = 0 ; Set Plugin path for lupi mongoodm resource Configuration pluginPaths.Lupi_Resource = APPLICATION_PATH "/../library/Lupi/Resource" ; Library include paths & namespaces includePaths.library = APPLICATION_PATH "/../library" autoloaderNamespaces[] = "Lupi" autoloaderNamespaces[] = "Doctrine" ; Bootstrap options bootstrap.path = APPLICATION_PATH "/Bootstrap.php" bootstrap.class = "Bootstrap" ; Applicaiton Setup appnamespace = "Application" resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers" resources.frontController.params.displayExceptions = 0 ; ODM settings resources.odm.documents.dir = APPLICATION_PATH "/entities" resources.odm.documents.namespace = "Application" resources.odm.config.proxyDir = APPLICATION_PATH "/cache/proxies" resources.odm.config.proxyNamespace = "proxies" resources.odm.config.hydratorDir = APPLICATION_PATH "/cache/hydrators" resources.odm.config.hydratorNamespace = "hydrators" [staging : production] [testing : production] phpSettings.display_startup_errors = 1 phpSettings.display_errors = 1 [development : production] phpSettings.display_startup_errors = 1 phpSettings.display_errors = 1 resources.frontController.params.displayExceptions = 1
[production] ; PHP settings phpSettings.display_startup_errors = 0 phpSettings.display_errors = 0 ; Set Plugin path for lupi mongoodm resource Configuration pluginPaths.Lupi_Resource = APPLICATION_PATH "/../library/Lupi/Resource" ; Library include paths & namespaces includePaths.library = APPLICATION_PATH "/../library" autoloaderNamespaces[] = "Lupi" autoloaderNamespaces[] = "Doctrine" ; Bootstrap options Tell it where to load the plugin resource bootstrap.path = APPLICATION_PATH "/Bootstrap.php" bootstrap.class = "Bootstrap" ; Applicaiton Setup appnamespace = "Application" resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers" resources.frontController.params.displayExceptions = 0 ; ODM settings resources.odm.documents.dir = APPLICATION_PATH "/entities" resources.odm.documents.namespace = "Application" resources.odm.config.proxyDir = APPLICATION_PATH "/cache/proxies" resources.odm.config.proxyNamespace = "proxies" resources.odm.config.hydratorDir = APPLICATION_PATH "/cache/hydrators" resources.odm.config.hydratorNamespace = "hydrators" [staging : production] [testing : production] phpSettings.display_startup_errors = 1 phpSettings.display_errors = 1 [development : production] phpSettings.display_startup_errors = 1 phpSettings.display_errors = 1 resources.frontController.params.displayExceptions = 1
[production] ; PHP settings phpSettings.display_startup_errors = 0 phpSettings.display_errors = 0 ; Set Plugin path for lupi mongoodm resource Configuration pluginPaths.Lupi_Resource = APPLICATION_PATH "/../library/Lupi/Resource" ; Library include paths & namespaces includePaths.library = APPLICATION_PATH "/../library" autoloaderNamespaces[] = "Lupi" autoloaderNamespaces[] = "Doctrine" ; Bootstrap options Minimal config to get started bootstrap.path = APPLICATION_PATH "/Bootstrap.php" bootstrap.class = "Bootstrap" ; Applicaiton Setup appnamespace = "Application" resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers" resources.frontController.params.displayExceptions = 0 ; ODM settings resources.odm.documents.dir = APPLICATION_PATH "/entities" resources.odm.documents.namespace = "Application" resources.odm.config.proxyDir = APPLICATION_PATH "/cache/proxies" resources.odm.config.proxyNamespace = "proxies" resources.odm.config.hydratorDir = APPLICATION_PATH "/cache/hydrators" resources.odm.config.hydratorNamespace = "hydrators" [staging : production] [testing : production] phpSettings.display_startup_errors = 1 phpSettings.display_errors = 1 [development : production] phpSettings.display_startup_errors = 1 phpSettings.display_errors = 1 resources.frontController.params.displayExceptions = 1
Base Controller • Provide an easy method to access the DocumentManager, and perhaps a service layer / repository • Not using an action helper because we will be likely to be using this tightly everywhere, so it avoids writing _helper a few hundred times! • You could use an action helper and use IOC or similar to replace this, this is for simplicity
<?php namespace LupiController; Base Controller abstract class Action extends Zend_Controller_Action { /** * @var Zend_Application */ Override the constructor at this point! protected $bootstrap; /** * @var DoctrineODMMongoDBDocumentManager */ protected $dm; It’s useful to leave public function __construct(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response, array $invokeArgs = array()) init() for the final { $this->setRequest($request) ->setResponse($response) implementation so ->_setInvokeArgs($invokeArgs); $this->_helper = new Zend_Controller_Action_HelperBroker($this); $this->dm = $this->getBootstrap()->getResource('odm'); you don’t need to $this->init(); } remember to always public function getBootstrap() { if (null === $this->bootstrap) { call parent::init() $this->bootstrap = $this->getInvokeArg('bootstrap'); } return $this->bootstrap; } }
Simplified controller code <?php use LupiController as controller; class IndexController extends controllerAction { Common repository for /** * @var DoctrineODMMongoDBDocumentRepository */ the controller, set it in protected $repository; public function init() the init { $this->repository = $this->dm->getRepository('ApplicationBlogPost'); } public function indexAction() { $post = $this->repository->find('4dde4067fbd2237df1000000'); $comment = new ApplicationBlogComment(); $comment->setEmail('foo@test.com') ->setComment('nice post!'); $post->addComment($comment); $this->dm->persist($post); $this->dm->flush(); } }
Simplified controller code <?php use LupiController as controller; class IndexController extends controllerAction { Common repository for /** * @var DoctrineODMMongoDBDocumentRepository */ the controller, set it in protected $repository; public function init() the init { $this->repository = $this->dm->getRepository('ApplicationBlogPost'); } public function indexAction() { $post = $this->repository->find('4dde4067fbd2237df1000000'); $comment = new ApplicationBlogComment(); $comment->setEmail('foo@test.com') ->setComment('nice post!'); $post->addComment($comment); $this->dm->persist($post); $this->dm->flush(); } } Nice verbose code!
Managing change
Managing change • For adding a new property, mostly, you don’t need to do much
Managing change • The db is schema-less
Managing change • The db is schema-less • You can add properties, without needing to update the old objects until your ready
Managing change • The db is schema-less • You can add properties, without needing to update the old objects until your ready • Simply add the property, and update old objects through your usual admin interface
Managing change • The db is schema-less • You can add properties, without needing to update the old objects until your ready • Simply add the property, and update old objects through your usual admin interface • Objects without the new property will simply have a null value until you change them
Managing change Make use of the annotations!
Changing a property name From To <?php <?php namespace ApplicationBlog; namespace ApplicationBlog; /** /** * @Document(db="blog", collection="posts") * @Document(db="blog", collection="posts") */ */ class Post class Post { { /** /** * @Id * @Id */ */ private $id; private $id; /** /** * @Field(type="string") * @Field(type="string") */ */ private $name; private $title; /** /** * @String * @String */ */ private $content; private $content; } }
Changing a property name From To <?php <?php namespace ApplicationBlog; namespace ApplicationBlog; /** /** * @Document(db="blog", collection="posts") * @Document(db="blog", collection="posts") */ */ class Post class Post { { /** * @Id Add @AlsoLoad /** * @Id */ */ private $id; private $id; /** /** * @Field(type="string") * @Field(type="string") * @AlsoLoad({“name”}) */ */ private $name; private $title; /** /** * @String * @String */ */ private $content; private $content; } }
Transforming a property From To <?php namespace ApplicationBlog; <?php /** namespace ApplicationBlog; * @Document(db="blog", collection="posts") */ /** class User * @Document(db="blog", collection="posts") { */ /** class User * @Id { */ /** private $id; * @Id */ /** private $id; * @Field(type="string") */ /** private $firstname; * @Field(type="string") */ /** private $fullname; * @Field(type="string") */ /** private $lastname; * @ReferenceMany(targetDocument=”ApplicationBlogPost”) */ /** private $posts; * @ReferenceMany(targetDocument=”ApplicationBlogPost”) } */ private $posts; }
Transforming a property From <?php To namespace ApplicationBlog; /** * @Document(db="blog", collection="posts") <?php */ namespace ApplicationBlog; class User { /** /** * @Id * @Document(db="blog", collection="posts") */ */ private $id; class User { /** * @Field(type="string") /** */ * @Id private $firstname; */ private $id; /** * @Field(type="string") */ /** private $lastname; * @Field(type="string") */ /** * @ReferenceMany(targetDocument=”ApplicationBlogPost”) private $fulllname; */ private $posts; /** * @ReferenceMany(targetDocument=”ApplicationBlogPost”) /** * @AlsoLoad({“fullname”}) */ */ private $posts; public function populateNameProperties($fullname) } { $name = explode(‘ ‘, $fullname); $this->firstname = $name[0]; $this->lastname = $name[1]; Use a method to migrate using } } @AlsoLoad
NOTE! • When searching, your queries will not know about the old name! • Include old and new fields in your searches if possible until all data has been migrated
NOTE! • The ODM is still in BETA3, there are still some glitches, but its well worth having a good look now. • http://www.doctrine-project.org/ and follow progress
Useful information • Official docs: http://www.doctrine-project.org/ docs/mongodb_odm/1.0/en/ • Mongodb: http://www.mongodb.org/ excellent intro to mongodb • The example sandbox: http://github.com/ bittarman/zf-d2-odm
Thanks for listening Catch me on twitter: @Bittarman Slides will be available on slideshare http://slideshare.com/rmauger ZF & Doctrine 2 ODM Doctrine

Zend Framework and the Doctrine2 MongoDB ODM (ZF1)

  • 1.
    ZF & Doctrine2 ODM Doctrine
  • 2.
    WHO IS RYANMAUGER? • Zend Framework Contributor • Zend Framework CR Team Member • Co-Author of Zend Framework 2 in Action (With Rob Allen) • Technical Editor for Zend Framework: A Beginners Guide • Community Supporter • Zend Certified PHP5 Engineer • Lead Developer at Lupimedia and trainer at Lupijuice
  • 3.
  • 4.
    What is theODM? Object Document Mapper
  • 5.
    What is theODM? Object Document Mapper Similar to an ORM
  • 6.
    What is theODM? Object Document Mapper Similar to an ORM But works with a Document Database
  • 7.
    What is theODM? Object Document Mapper Similar to an ORM But works with a Document Database
  • 8.
    Whats the advantage? •Schema-less design (no complicated ALTER TABLE statements, just save your data! • Works with JSON objects, familiar with what your already doing • Simple!
  • 9.
    Whats the advantage? •Schema-less design (no complicated ALTER TABLE statements, just save your data! • Works with JSON objects, familiar with what your already doing • Simple! (at least to get started)
  • 10.
    Whats the advantage? Doctrine 2 • Uses the Data Mapper pattern • Simplifies your domain, removes the persistence logic • Entities are free to do their job • Defines a framework by which your domain model will work, improving consistency
  • 11.
    Getting started withthe ODM Persistence operations revolve around the Document Manager Mapping of properties is carried out through annotations in your entities, or through xml/yaml mappings
  • 12.
    <?php namespace ApplicationBlog; /** * @Document(db="blog", collection="posts") */ class Post { /** * @Id */ private $id; An example entity /** * @Field(type="string") */ private $title; /** * @String */ Properties private $content; /** /** * @EmbedMany(targetDocument="ApplicationBlogComment") */ * @Id private $comments = array(); */ public function getId() private $id; { return $this->id; } /** public function setId($id) * @Field(type="string") { $this->id = (string) $id; */ return $this; private $title; } public function getTitle() { /** return $this->title; * @String } */ public function setTitle($title) private $content; { $this->title = (string) $title; } return $this; /** * @EmbedMany(targetDocument="ApplicationBlogComment") public function getContent() { */ return $this->content; private $comments = array(); } public function setContent($content) { $this->content = (string) $content; return $this; } public function getComments() { return $this->comments; } public function addComment(ApplicationBlogComment $comment) { $this->comments[] = $comment; return $this; } }
  • 13.
    <?php namespace ApplicationBlog; /** * @Document(db="blog", collection="posts") */ class Post { /** * @Id */ private $id; An example entity /** * @Field(type="string") */ private $title; /** * @String */ Methods private $content; /** * @EmbedMany(targetDocument="ApplicationBlogComment") public function getContent() */ { private $comments = array(); return $this->content; public function getId() } { return $this->id; } public function setContent($content) public function setId($id) { { $this->id = (string) $id; $this->content = (string) $content; return $this; return $this; } } public function getTitle() { return $this->title; public function getComments() } { public function setTitle($title) return $this->comments; { $this->title = (string) $title; } return $this; } public function addComment(ApplicationBlogComment $comment) public function getContent() { { return $this->content; $this->comments[] = $comment; } return $this; public function setContent($content) { } $this->content = (string) $content; return $this; } public function getComments() { return $this->comments; } public function addComment(ApplicationBlogComment $comment) { $this->comments[] = $comment; return $this; } }
  • 14.
    Using your entity Create <?php class IndexController extends Zend_Controller_Action { public function init() { /* Initialize action controller here */ } public function indexAction() { $documentManager = $this->getInvokeArg('bootstrap')->getResource('odm'); $post = new ApplicationBlogPost(); $post->setTitle('My Interesting Post') ->setContent('I have somethnng very interesting to say'); $documentManager->persist($post); $documentManager->flush(); } } > use blog switched to db blog > db.posts.find(); { "_id" : ObjectId("4dde4067fbd2237df1000000"), "title" : "My Interesting Post", "content" : "I have somethnng very interesting to say" }
  • 15.
    Using your entity Update <?php class IndexController extends Zend_Controller_Action { public function init() { /* Initialize action controller here */ } public function indexAction() { $documentManager = $this->getInvokeArg('bootstrap')->getResource('odm'); $post = $documentManager->getRepository('ApplicationBlogPost') ->find('4dde4067fbd2237df1000000'); $post->setTitle('My Interesting Post') ->setContent('I have something very interesting to say'); $documentManager->persist($post); $documentManager->flush(); } } > db.posts.find(); { "_id" : ObjectId("4dde4067fbd2237df1000000"), "title" : "My Interesting Post", "content" : "I have something very interesting to say" }
  • 16.
    Using your entity Add an comment <?php class IndexController extends Zend_Controller_Action { public function init() { /* Initialize action controller here */ } public function indexAction() { $documentManager = $this->getInvokeArg('bootstrap')->getResource('odm'); $post = $documentManager->getRepository('ApplicationBlogPost') ->find('4dde4067fbd2237df1000000'); $comment = new ApplicationBlogComment(); $comment->setEmail('foo@example.com') ->setComment('Nice post!'); $post->addComment($comment); $documentManager->persist($post); $documentManager->flush(); } } > db.posts.find(); { "_id" : ObjectId("4dde4067fbd2237df1000000"), "comments" : [ { "_id" : ObjectId("4dde439afbd22380f1000000"), "email" : "foo@example.com", "comment" : "Nice post!" } ], "content" : "I have something very interesting to say", "title" : "My Interesting Post" }
  • 17.
    Using your entity Delete <?php class IndexController extends Zend_Controller_Action { public function init() { /* Initialize action controller here */ } public function indexAction() { $documentManager = $this->getInvokeArg('bootstrap')->getResource('odm'); $post = $documentManager->getRepository('ApplicationBlogPost') ->find('4dde4067fbd2237df1000000'); $documentManager->remove($post); $documentManager->flush(); } } > db.posts.find(); >
  • 18.
  • 19.
    Available Annotations http://www.doctrine-project.org/docs/mongodb_odm/1.0/en/ reference/annotations-reference.html @Field(type="string" name="origin") Field Mappings. Many aliases are available: @String, @Date, @Int, @Float etc. @EmbedOne / @EmbedMany Embedded Documents, stores the document inside the current document @ReferenceOne / @ReferenceMany Referenced Documents, stores a reference to one or more documents outside the current document @Document / @EmbeddedDocument Marks entities which are allowed to be managed by the document manager. additionally EmbeddedDocuments may not be saved independently, and must be a child of an @Document @HasLifecycleCallbacks & @PrePersist, @PostPersist etc. Marks an entity as having callbacks such as PrePersist which will be called automatically by the DocumentManager during the entities lifetime. (see reference for list)
  • 20.
  • 21.
    Integrating with ZF ExampleSandbox: http://github.com/bittarman/zf-d2-odm Includes all (PHP) dependencies Requires the mongo pecl extension $ sudo pecl install mongo Install Doctrine from PEAR $ sudo pear channel-discover pear.doctrine-project.org $ sudo pear install pear.doctrine-project.org/DoctrineMongoDBODM-1.0.0BETA3 Checkout from Github $ git clone git://github.com/doctrine/mongodb-odm.git mongodb_odm $ cd mongodb_odm $ git checkout 1.0.0BETA3
  • 22.
    The Plugin Resource <?php /*Import namespaces */ use DoctrineCommonClassLoader, DoctrineCommonAnnotationsAnnotationReader, DoctrineODMMongoDB, DoctrineODMMongoDBDocumentManager, DoctrineODMMongoDBMongo, DoctrineODMMongoDBMappingDriverAnnotationDriver; /** * Doctrine 2 ODM Resource Plugin * * @author Ryan Mauger * @copyright Ryan Mauger 2012 */ class Lupi_Resource_Odm extends Zend_Application_Resource_ResourceAbstract { public function init() { $options = $this->getOptions(); $this->registerAutoloaders($options); // Config $config = new DoctrineODMMongoDBConfiguration(); foreach ($options['config'] as $option => $value) { $method = "set" . ucfirst($option); $config->{$method}($value); } // Annotation reader $reader = new AnnotationReader(); $reader->setDefaultAnnotationNamespace('DoctrineODMMongoDBMapping'); $config->setMetadataDriverImpl(new AnnotationDriver($reader, $options['documents']['dir'])); $dm = DocumentManager::create(new DoctrineMongoDBConnection(new Mongo), $config); return $dm; } public function registerAutoloaders($options) { $autoloader = Zend_Loader_Autoloader::getInstance(); // Document classes $classLoader = new ClassLoader($options['documents']['namespace'], $options['documents']['dir']); $autoloader->pushAutoloader(array($classLoader, 'loadClass'), $options['documents']['namespace']); } }
  • 23.
    The Plugin Resource <?php /*Import namespaces */ use DoctrineCommonClassLoader, DoctrineCommonAnnotationsAnnotationReader, DoctrineODMMongoDB, DoctrineODMMongoDBDocumentManager, DoctrineODMMongoDBMongo, DoctrineODMMongoDBMappingDriverAnnotationDriver; /** * Doctrine 2 ODM Resource Plugin * * @author Ryan Mauger * @copyright Ryan Mauger 2012 */ class Lupi_Resource_Odm extends Zend_Application_Resource_ResourceAbstract { public function init() { $options = $this->getOptions(); $this->registerAutoloaders($options); Set up class loaders. // Config $config = new DoctrineODMMongoDBConfiguration(); foreach ($options['config'] as $option => $value) { $method = "set" . ucfirst($option); Use the Doctrine $config->{$method}($value); } // Annotation reader loaders for ease $reader = new AnnotationReader(); $reader->setDefaultAnnotationNamespace('DoctrineODMMongoDBMapping'); $config->setMetadataDriverImpl(new AnnotationDriver($reader, $options['documents']['dir'])); $dm = DocumentManager::create(new DoctrineMongoDBConnection(new Mongo), $config); return $dm; } public function registerAutoloaders($options) { $autoloader = Zend_Loader_Autoloader::getInstance(); // Document classes $classLoader = new ClassLoader($options['documents']['namespace'], $options['documents']['dir']); $autoloader->pushAutoloader(array($classLoader, 'loadClass'), $options['documents']['namespace']); } }
  • 24.
    The Plugin Resource <?php /*Import namespaces */ use DoctrineCommonClassLoader, DoctrineCommonAnnotationsAnnotationReader, DoctrineODMMongoDB, DoctrineODMMongoDBDocumentManager, DoctrineODMMongoDBMongo, DoctrineODMMongoDBMappingDriverAnnotationDriver; /** * Doctrine 2 ODM Resource Plugin * * @author Ryan Mauger * @copyright Ryan Mauger 2012 */ class Lupi_Resource_Odm extends Zend_Application_Resource_ResourceAbstract { public function init() { $options = $this->getOptions(); $this->registerAutoloaders($options); // Config $config = new DoctrineODMMongoDBConfiguration(); foreach ($options['config'] as $option => $value) { Set up the config object $method = "set" . ucfirst($option); $config->{$method}($value); } // Annotation reader $reader = new AnnotationReader(); $reader->setDefaultAnnotationNamespace('DoctrineODMMongoDBMapping'); $config->setMetadataDriverImpl(new AnnotationDriver($reader, $options['documents']['dir'])); $dm = DocumentManager::create(new DoctrineMongoDBConnection(new Mongo), $config); return $dm; } public function registerAutoloaders($options) { $autoloader = Zend_Loader_Autoloader::getInstance(); // Document classes $classLoader = new ClassLoader($options['documents']['namespace'], $options['documents']['dir']); $autoloader->pushAutoloader(array($classLoader, 'loadClass'), $options['documents']['namespace']); } }
  • 25.
    The Plugin Resource <?php /*Import namespaces */ use DoctrineCommonClassLoader, DoctrineCommonAnnotationsAnnotationReader, DoctrineODMMongoDB, DoctrineODMMongoDBDocumentManager, DoctrineODMMongoDBMongo, DoctrineODMMongoDBMappingDriverAnnotationDriver; /** * Doctrine 2 ODM Resource Plugin * * @author Ryan Mauger * @copyright Ryan Mauger 2012 */ class Lupi_Resource_Odm extends Zend_Application_Resource_ResourceAbstract { public function init() { Set up the Annotation $options = $this->getOptions(); $this->registerAutoloaders($options); // Config Reader, or mapping driver $config = new DoctrineODMMongoDBConfiguration(); foreach ($options['config'] as $option => $value) { $method = "set" . ucfirst($option); $config->{$method}($value); should you prefer YAML / } // Annotation reader XML $reader = new AnnotationReader(); $reader->setDefaultAnnotationNamespace('DoctrineODMMongoDBMapping'); $config->setMetadataDriverImpl(new AnnotationDriver($reader, $options['documents']['dir'])); $dm = DocumentManager::create(new DoctrineMongoDBConnection(new Mongo), $config); return $dm; } public function registerAutoloaders($options) { $autoloader = Zend_Loader_Autoloader::getInstance(); // Document classes $classLoader = new ClassLoader($options['documents']['namespace'], $options['documents']['dir']); $autoloader->pushAutoloader(array($classLoader, 'loadClass'), $options['documents']['namespace']); } }
  • 26.
    The Plugin Resource <?php /*Import namespaces */ use DoctrineCommonClassLoader, DoctrineCommonAnnotationsAnnotationReader, DoctrineODMMongoDB, DoctrineODMMongoDBDocumentManager, DoctrineODMMongoDBMongo, DoctrineODMMongoDBMappingDriverAnnotationDriver; /** * Doctrine 2 ODM Resource Plugin * * @author Ryan Mauger * @copyright Ryan Mauger 2012 */ class Lupi_Resource_Odm extends Zend_Application_Resource_ResourceAbstract { public function init() { $options = $this->getOptions(); $this->registerAutoloaders($options); // Config Create and return the $config = new DoctrineODMMongoDBConfiguration(); foreach ($options['config'] as $option => $value) { $method = "set" . ucfirst($option); $config->{$method}($value); Document manager } // Annotation reader $reader = new AnnotationReader(); $reader->setDefaultAnnotationNamespace('DoctrineODMMongoDBMapping'); $config->setMetadataDriverImpl(new AnnotationDriver($reader, $options['documents']['dir'])); $dm = DocumentManager::create(new DoctrineMongoDBConnection(new Mongo), $config); return $dm; } public function registerAutoloaders($options) { $autoloader = Zend_Loader_Autoloader::getInstance(); // Document classes $classLoader = new ClassLoader($options['documents']['namespace'], $options['documents']['dir']); $autoloader->pushAutoloader(array($classLoader, 'loadClass'), $options['documents']['namespace']); } }
  • 27.
    [production] ; PHP settings phpSettings.display_startup_errors= 0 phpSettings.display_errors = 0 ; Set Plugin path for lupi mongoodm resource Configuration pluginPaths.Lupi_Resource = APPLICATION_PATH "/../library/Lupi/Resource" ; Library include paths & namespaces includePaths.library = APPLICATION_PATH "/../library" autoloaderNamespaces[] = "Lupi" autoloaderNamespaces[] = "Doctrine" ; Bootstrap options bootstrap.path = APPLICATION_PATH "/Bootstrap.php" bootstrap.class = "Bootstrap" ; Applicaiton Setup appnamespace = "Application" resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers" resources.frontController.params.displayExceptions = 0 ; ODM settings resources.odm.documents.dir = APPLICATION_PATH "/entities" resources.odm.documents.namespace = "Application" resources.odm.config.proxyDir = APPLICATION_PATH "/cache/proxies" resources.odm.config.proxyNamespace = "proxies" resources.odm.config.hydratorDir = APPLICATION_PATH "/cache/hydrators" resources.odm.config.hydratorNamespace = "hydrators" [staging : production] [testing : production] phpSettings.display_startup_errors = 1 phpSettings.display_errors = 1 [development : production] phpSettings.display_startup_errors = 1 phpSettings.display_errors = 1 resources.frontController.params.displayExceptions = 1
  • 28.
    [production] ; PHP settings phpSettings.display_startup_errors= 0 phpSettings.display_errors = 0 ; Set Plugin path for lupi mongoodm resource Configuration pluginPaths.Lupi_Resource = APPLICATION_PATH "/../library/Lupi/Resource" ; Library include paths & namespaces includePaths.library = APPLICATION_PATH "/../library" autoloaderNamespaces[] = "Lupi" autoloaderNamespaces[] = "Doctrine" ; Bootstrap options Tell it where to load the plugin resource bootstrap.path = APPLICATION_PATH "/Bootstrap.php" bootstrap.class = "Bootstrap" ; Applicaiton Setup appnamespace = "Application" resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers" resources.frontController.params.displayExceptions = 0 ; ODM settings resources.odm.documents.dir = APPLICATION_PATH "/entities" resources.odm.documents.namespace = "Application" resources.odm.config.proxyDir = APPLICATION_PATH "/cache/proxies" resources.odm.config.proxyNamespace = "proxies" resources.odm.config.hydratorDir = APPLICATION_PATH "/cache/hydrators" resources.odm.config.hydratorNamespace = "hydrators" [staging : production] [testing : production] phpSettings.display_startup_errors = 1 phpSettings.display_errors = 1 [development : production] phpSettings.display_startup_errors = 1 phpSettings.display_errors = 1 resources.frontController.params.displayExceptions = 1
  • 29.
    [production] ; PHP settings phpSettings.display_startup_errors= 0 phpSettings.display_errors = 0 ; Set Plugin path for lupi mongoodm resource Configuration pluginPaths.Lupi_Resource = APPLICATION_PATH "/../library/Lupi/Resource" ; Library include paths & namespaces includePaths.library = APPLICATION_PATH "/../library" autoloaderNamespaces[] = "Lupi" autoloaderNamespaces[] = "Doctrine" ; Bootstrap options Minimal config to get started bootstrap.path = APPLICATION_PATH "/Bootstrap.php" bootstrap.class = "Bootstrap" ; Applicaiton Setup appnamespace = "Application" resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers" resources.frontController.params.displayExceptions = 0 ; ODM settings resources.odm.documents.dir = APPLICATION_PATH "/entities" resources.odm.documents.namespace = "Application" resources.odm.config.proxyDir = APPLICATION_PATH "/cache/proxies" resources.odm.config.proxyNamespace = "proxies" resources.odm.config.hydratorDir = APPLICATION_PATH "/cache/hydrators" resources.odm.config.hydratorNamespace = "hydrators" [staging : production] [testing : production] phpSettings.display_startup_errors = 1 phpSettings.display_errors = 1 [development : production] phpSettings.display_startup_errors = 1 phpSettings.display_errors = 1 resources.frontController.params.displayExceptions = 1
  • 30.
    Base Controller • Providean easy method to access the DocumentManager, and perhaps a service layer / repository • Not using an action helper because we will be likely to be using this tightly everywhere, so it avoids writing _helper a few hundred times! • You could use an action helper and use IOC or similar to replace this, this is for simplicity
  • 31.
    <?php namespace LupiController; Base Controller abstract class Action extends Zend_Controller_Action { /** * @var Zend_Application */ Override the constructor at this point! protected $bootstrap; /** * @var DoctrineODMMongoDBDocumentManager */ protected $dm; It’s useful to leave public function __construct(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response, array $invokeArgs = array()) init() for the final { $this->setRequest($request) ->setResponse($response) implementation so ->_setInvokeArgs($invokeArgs); $this->_helper = new Zend_Controller_Action_HelperBroker($this); $this->dm = $this->getBootstrap()->getResource('odm'); you don’t need to $this->init(); } remember to always public function getBootstrap() { if (null === $this->bootstrap) { call parent::init() $this->bootstrap = $this->getInvokeArg('bootstrap'); } return $this->bootstrap; } }
  • 32.
    Simplified controller code <?php useLupiController as controller; class IndexController extends controllerAction { Common repository for /** * @var DoctrineODMMongoDBDocumentRepository */ the controller, set it in protected $repository; public function init() the init { $this->repository = $this->dm->getRepository('ApplicationBlogPost'); } public function indexAction() { $post = $this->repository->find('4dde4067fbd2237df1000000'); $comment = new ApplicationBlogComment(); $comment->setEmail('foo@test.com') ->setComment('nice post!'); $post->addComment($comment); $this->dm->persist($post); $this->dm->flush(); } }
  • 33.
    Simplified controller code <?php useLupiController as controller; class IndexController extends controllerAction { Common repository for /** * @var DoctrineODMMongoDBDocumentRepository */ the controller, set it in protected $repository; public function init() the init { $this->repository = $this->dm->getRepository('ApplicationBlogPost'); } public function indexAction() { $post = $this->repository->find('4dde4067fbd2237df1000000'); $comment = new ApplicationBlogComment(); $comment->setEmail('foo@test.com') ->setComment('nice post!'); $post->addComment($comment); $this->dm->persist($post); $this->dm->flush(); } } Nice verbose code!
  • 34.
  • 35.
    Managing change • Foradding a new property, mostly, you don’t need to do much
  • 36.
    Managing change • Thedb is schema-less
  • 37.
    Managing change • Thedb is schema-less • You can add properties, without needing to update the old objects until your ready
  • 38.
    Managing change • Thedb is schema-less • You can add properties, without needing to update the old objects until your ready • Simply add the property, and update old objects through your usual admin interface
  • 39.
    Managing change • Thedb is schema-less • You can add properties, without needing to update the old objects until your ready • Simply add the property, and update old objects through your usual admin interface • Objects without the new property will simply have a null value until you change them
  • 40.
    Managing change Make useof the annotations!
  • 41.
    Changing a propertyname From To <?php <?php namespace ApplicationBlog; namespace ApplicationBlog; /** /** * @Document(db="blog", collection="posts") * @Document(db="blog", collection="posts") */ */ class Post class Post { { /** /** * @Id * @Id */ */ private $id; private $id; /** /** * @Field(type="string") * @Field(type="string") */ */ private $name; private $title; /** /** * @String * @String */ */ private $content; private $content; } }
  • 42.
    Changing a propertyname From To <?php <?php namespace ApplicationBlog; namespace ApplicationBlog; /** /** * @Document(db="blog", collection="posts") * @Document(db="blog", collection="posts") */ */ class Post class Post { { /** * @Id Add @AlsoLoad /** * @Id */ */ private $id; private $id; /** /** * @Field(type="string") * @Field(type="string") * @AlsoLoad({“name”}) */ */ private $name; private $title; /** /** * @String * @String */ */ private $content; private $content; } }
  • 43.
    Transforming a property From To <?php namespace ApplicationBlog; <?php /** namespace ApplicationBlog; * @Document(db="blog", collection="posts") */ /** class User * @Document(db="blog", collection="posts") { */ /** class User * @Id { */ /** private $id; * @Id */ /** private $id; * @Field(type="string") */ /** private $firstname; * @Field(type="string") */ /** private $fullname; * @Field(type="string") */ /** private $lastname; * @ReferenceMany(targetDocument=”ApplicationBlogPost”) */ /** private $posts; * @ReferenceMany(targetDocument=”ApplicationBlogPost”) } */ private $posts; }
  • 44.
    Transforming a property From <?php To namespace ApplicationBlog; /** * @Document(db="blog", collection="posts") <?php */ namespace ApplicationBlog; class User { /** /** * @Id * @Document(db="blog", collection="posts") */ */ private $id; class User { /** * @Field(type="string") /** */ * @Id private $firstname; */ private $id; /** * @Field(type="string") */ /** private $lastname; * @Field(type="string") */ /** * @ReferenceMany(targetDocument=”ApplicationBlogPost”) private $fulllname; */ private $posts; /** * @ReferenceMany(targetDocument=”ApplicationBlogPost”) /** * @AlsoLoad({“fullname”}) */ */ private $posts; public function populateNameProperties($fullname) } { $name = explode(‘ ‘, $fullname); $this->firstname = $name[0]; $this->lastname = $name[1]; Use a method to migrate using } } @AlsoLoad
  • 45.
    NOTE! • When searching,your queries will not know about the old name! • Include old and new fields in your searches if possible until all data has been migrated
  • 46.
    NOTE! • The ODMis still in BETA3, there are still some glitches, but its well worth having a good look now. • http://www.doctrine-project.org/ and follow progress
  • 47.
    Useful information • Officialdocs: http://www.doctrine-project.org/ docs/mongodb_odm/1.0/en/ • Mongodb: http://www.mongodb.org/ excellent intro to mongodb • The example sandbox: http://github.com/ bittarman/zf-d2-odm
  • 48.
    Thanks for listening Catch me on twitter: @Bittarman Slides will be available on slideshare http://slideshare.com/rmauger ZF & Doctrine 2 ODM Doctrine