Initial commit
This commit is contained in:
+210
@@ -0,0 +1,210 @@
|
||||
Blending the ORM and MongoDB ODM
|
||||
================================
|
||||
|
||||
Since the start of the `Doctrine MongoDB Object Document Mapper`_ project people have asked how it can be integrated with the `ORM`_. This article will demonstrates how you can integrate the two transparently, maintaining a clean domain model.
|
||||
|
||||
This example will have a `Product` that is stored in MongoDB and the `Order` stored in a MySQL database.
|
||||
|
||||
Define Product
|
||||
--------------
|
||||
|
||||
First lets define our `Product` document:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace Documents;
|
||||
|
||||
/** @Document */
|
||||
class Product
|
||||
{
|
||||
/** @Id */
|
||||
private $id;
|
||||
|
||||
/** @Field(type="string") */
|
||||
private $title;
|
||||
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle($title)
|
||||
{
|
||||
$this->title = $title;
|
||||
}
|
||||
}
|
||||
|
||||
Define Entity
|
||||
-------------
|
||||
|
||||
Next create the `Order` entity that has a `$product` and `$productId` property linking it to the `Product` that is stored with MongoDB:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace Entities;
|
||||
|
||||
use Documents\Product;
|
||||
|
||||
/**
|
||||
* @Entity
|
||||
* @Table(name="orders")
|
||||
*/
|
||||
class Order
|
||||
{
|
||||
/**
|
||||
* @Id @Column(type="integer")
|
||||
* @GeneratedValue(strategy="AUTO")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @Column(type="string")
|
||||
*/
|
||||
private $productId;
|
||||
|
||||
/**
|
||||
* @var Documents\Product
|
||||
*/
|
||||
private $product;
|
||||
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getProductId()
|
||||
{
|
||||
return $this->productId;
|
||||
}
|
||||
|
||||
public function setProduct(Product $product)
|
||||
{
|
||||
$this->productId = $product->getId();
|
||||
$this->product = $product;
|
||||
}
|
||||
|
||||
public function getProduct()
|
||||
{
|
||||
return $this->product;
|
||||
}
|
||||
}
|
||||
|
||||
Event Subscriber
|
||||
----------------
|
||||
|
||||
Now we need to setup an event subscriber that will set the `$product` property of all `Order` instances to a reference to the document product so it can be lazily loaded when it is accessed the first time. So first register a new event subscriber:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$eventManager = $em->getEventManager();
|
||||
$eventManager->addEventListener(
|
||||
array(\Doctrine\ORM\Events::postLoad), new MyEventSubscriber($dm)
|
||||
);
|
||||
|
||||
So now we need to define a class named `MyEventSubscriber` and pass a dependency to the `DocumentManager`. It will have a `postLoad()` method that sets the product document reference:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Doctrine\ODM\MongoDB\DocumentManager;
|
||||
use Doctrine\ORM\Event\LifecycleEventArgs;
|
||||
|
||||
class MyEventSubscriber
|
||||
{
|
||||
public function __construct(DocumentManager $dm)
|
||||
{
|
||||
$this->dm = $dm;
|
||||
}
|
||||
|
||||
public function postLoad(LifecycleEventArgs $eventArgs)
|
||||
{
|
||||
$order = $eventArgs->getEntity();
|
||||
$em = $eventArgs->getEntityManager();
|
||||
$productReflProp = $em->getClassMetadata('Entities\Order')
|
||||
->reflClass->getProperty('product');
|
||||
$productReflProp->setAccessible(true);
|
||||
$productReflProp->setValue(
|
||||
$order, $this->dm->getReference('Documents\Product', $order->getProductId())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
The `postLoad` method will be invoked after an ORM entity is loaded from the database. This allows us to use the `DocumentManager` to set the `$product` property with a reference to the `Product` document with the product id we previously stored.
|
||||
|
||||
Working with Products and Orders
|
||||
--------------------------------
|
||||
|
||||
First create a new `Product`:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$product = new \Documents\Product();
|
||||
$product->setTitle('Test Product');
|
||||
$dm->persist($product);
|
||||
$dm->flush();
|
||||
|
||||
Now create a new `Order` and link it to a `Product` in MySQL:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$order = new \Entities\Order();
|
||||
$order->setProduct($product);
|
||||
$em->persist($order);
|
||||
$em->flush();
|
||||
|
||||
Later we can retrieve the entity and lazily load the reference to the document in MongoDB:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$order = $em->find('Order', $order->getId());
|
||||
|
||||
// Instance of an uninitialized product proxy
|
||||
$product = $order->getProduct();
|
||||
|
||||
// Initializes proxy and queries the database
|
||||
echo "Order Title: " . $product->getTitle();
|
||||
|
||||
If you were to print the `$order` you would see that we got back regular PHP objects:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
print_r($order);
|
||||
|
||||
The above would output the following:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
Order Object
|
||||
(
|
||||
[id:Entities\Order:private] => 53
|
||||
[productId:Entities\Order:private] => 4c74a1868ead0ed7a9000000
|
||||
[product:Entities\Order:private] => Proxies\DocumentsProductProxy Object
|
||||
(
|
||||
[__isInitialized__] => 1
|
||||
[id:Documents\Product:private] => 4c74a1868ead0ed7a9000000
|
||||
[title:Documents\Product:private] => Test Product
|
||||
)
|
||||
)
|
||||
|
||||
.. _Doctrine MongoDB Object Document Mapper: http://www.doctrine-project.org/projects/mongodb_odm
|
||||
.. _ORM: http://www.doctrine-project.org/projects/orm
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
Implementing ArrayAccess for Domain Objects
|
||||
===========================================
|
||||
|
||||
.. sectionauthor:: Roman Borschel (roman@code-factory.org)
|
||||
|
||||
This recipe will show you how to implement ArrayAccess for your
|
||||
domain objects in order to allow more uniform access, for example
|
||||
in templates. In these examples we will implement ArrayAccess on a
|
||||
`Layer Supertype <http://martinfowler.com/eaaCatalog/layerSupertype.html>`_
|
||||
for all our domain objects.
|
||||
|
||||
Option 1
|
||||
--------
|
||||
|
||||
In this implementation we will make use of PHPs highly dynamic
|
||||
nature to dynamically access properties of a subtype in a supertype
|
||||
at runtime. Note that this implementation has 2 main caveats:
|
||||
|
||||
- It will not work with private fields
|
||||
- It will not go through any getters/setters
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
abstract class DomainObject implements ArrayAccess
|
||||
{
|
||||
public function offsetExists($offset)
|
||||
{
|
||||
return isset($this->$offset);
|
||||
}
|
||||
|
||||
public function offsetSet($offset, $value)
|
||||
{
|
||||
$this->$offset = $value;
|
||||
}
|
||||
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
return $this->$offset;
|
||||
}
|
||||
|
||||
public function offsetUnset($offset)
|
||||
{
|
||||
$this->$offset = null;
|
||||
}
|
||||
}
|
||||
|
||||
Option 2
|
||||
--------
|
||||
|
||||
In this implementation we will dynamically invoke getters/setters.
|
||||
Again we use PHPs dynamic nature to invoke methods on a subtype
|
||||
from a supertype at runtime. This implementation has the following
|
||||
caveats:
|
||||
|
||||
- It relies on a naming convention
|
||||
- The semantics of offsetExists can differ
|
||||
- offsetUnset will not work with typehinted setters
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
abstract class DomainObject implements ArrayAccess
|
||||
{
|
||||
public function offsetExists($offset)
|
||||
{
|
||||
// In this example we say that exists means it is not null
|
||||
$value = $this->{"get$offset"}();
|
||||
return $value !== null;
|
||||
}
|
||||
|
||||
public function offsetSet($offset, $value)
|
||||
{
|
||||
$this->{"set$offset"}($value);
|
||||
}
|
||||
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
return $this->{"get$offset"}();
|
||||
}
|
||||
|
||||
public function offsetUnset($offset)
|
||||
{
|
||||
$this->{"set$offset"}(null);
|
||||
}
|
||||
}
|
||||
|
||||
Read-only
|
||||
---------
|
||||
|
||||
You can slightly tweak option 1 or option 2 in order to make array
|
||||
access read-only. This will also circumvent some of the caveats of
|
||||
each option. Simply make offsetSet and offsetUnset throw an
|
||||
exception (i.e. BadMethodCallException).
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
abstract class DomainObject implements ArrayAccess
|
||||
{
|
||||
public function offsetExists($offset)
|
||||
{
|
||||
// option 1 or option 2
|
||||
}
|
||||
|
||||
public function offsetSet($offset, $value)
|
||||
{
|
||||
throw new BadMethodCallException("Array access of class " . get_class($this) . " is read-only!");
|
||||
}
|
||||
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
// option 1 or option 2
|
||||
}
|
||||
|
||||
public function offsetUnset($offset)
|
||||
{
|
||||
throw new BadMethodCallException("Array access of class " . get_class($this) . " is read-only!");
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
Implementing the Notify ChangeTracking Policy
|
||||
=============================================
|
||||
|
||||
.. sectionauthor:: Roman Borschel (roman@code-factory.org)
|
||||
|
||||
The NOTIFY change-tracking policy is the most effective
|
||||
change-tracking policy provided by Doctrine but it requires some
|
||||
boilerplate code. This recipe will show you how this boilerplate
|
||||
code should look like. We will implement it on a
|
||||
`Layer Supertype <http://martinfowler.com/eaaCatalog/layerSupertype.html>`_
|
||||
for all our domain objects.
|
||||
|
||||
Implementing NotifyPropertyChanged
|
||||
----------------------------------
|
||||
|
||||
The NOTIFY policy is based on the assumption that the entities
|
||||
notify interested listeners of changes to their properties. For
|
||||
that purpose, a class that wants to use this policy needs to
|
||||
implement the ``NotifyPropertyChanged`` interface from the
|
||||
``Doctrine\Common`` namespace.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Doctrine\Common\NotifyPropertyChanged,
|
||||
Doctrine\Common\PropertyChangedListener;
|
||||
|
||||
abstract class DomainObject implements NotifyPropertyChanged
|
||||
{
|
||||
private $_listeners = array();
|
||||
|
||||
public function addPropertyChangedListener(PropertyChangedListener $listener)
|
||||
{
|
||||
$this->_listeners[] = $listener;
|
||||
}
|
||||
|
||||
/** Notifies listeners of a change. */
|
||||
protected function _onPropertyChanged($propName, $oldValue, $newValue)
|
||||
{
|
||||
if ($this->_listeners) {
|
||||
foreach ($this->_listeners as $listener) {
|
||||
$listener->propertyChanged($this, $propName, $oldValue, $newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Then, in each property setter of concrete, derived domain classes,
|
||||
you need to invoke \_onPropertyChanged as follows to notify
|
||||
listeners:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
// Mapping not shown, either in annotations, xml or yaml as usual
|
||||
class MyEntity extends DomainObject
|
||||
{
|
||||
private $data;
|
||||
// ... other fields as usual
|
||||
|
||||
public function setData($data)
|
||||
{
|
||||
if ($data != $this->data) { // check: is it actually modified?
|
||||
$this->_onPropertyChanged('data', $this->data, $data);
|
||||
$this->data = $data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
The check whether the new value is different from the old one is
|
||||
not mandatory but recommended. That way you can avoid unnecessary
|
||||
updates and also have full control over when you consider a
|
||||
property changed.
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
Implementing Wakeup or Clone
|
||||
============================
|
||||
|
||||
.. sectionauthor:: Roman Borschel (roman@code-factory.org)
|
||||
|
||||
As explained in the
|
||||
:doc:`restrictions for document classes in the manual <../reference/architecture>`.
|
||||
it is usually not allowed for a document to implement ``__wakeup``
|
||||
or ``__clone``, because Doctrine makes special use of them.
|
||||
However, it is quite easy to make use of these methods in a safe
|
||||
way by guarding the custom wakeup or clone code with a document
|
||||
identity check, as demonstrated in the following sections.
|
||||
|
||||
Safely implementing \_\_wakeup
|
||||
------------------------------
|
||||
|
||||
To safely implement ``__wakeup``, simply enclose your
|
||||
implementation code in an identity check as follows:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
class MyDocument
|
||||
{
|
||||
private $id; // This is the identifier of the document.
|
||||
//...
|
||||
|
||||
public function __wakeup()
|
||||
{
|
||||
// If the document has an identity, proceed as normal.
|
||||
if ($this->id) {
|
||||
// ... Your code here as normal ...
|
||||
}
|
||||
// otherwise do nothing, do NOT throw an exception!
|
||||
}
|
||||
|
||||
//...
|
||||
}
|
||||
|
||||
Safely implementing \_\_clone
|
||||
-----------------------------
|
||||
|
||||
Safely implementing ``__clone`` is pretty much the same:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class MyDocument
|
||||
{
|
||||
private $id; // This is the identifier of the document.
|
||||
//...
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
// If the document has an identity, proceed as normal.
|
||||
if ($this->id) {
|
||||
// ... Your code here as normal ...
|
||||
}
|
||||
// otherwise do nothing, do NOT throw an exception!
|
||||
}
|
||||
|
||||
//...
|
||||
}
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
As you have seen, it is quite easy to safely make use of
|
||||
``__wakeup`` and ``__clone`` in your documents without adding any
|
||||
really Doctrine-specific or Doctrine-dependant code.
|
||||
|
||||
These implementations are possible and safe because when Doctrine
|
||||
invokes these methods, the documents never have an identity (yet).
|
||||
Furthermore, it is possibly a good idea to check for the identity
|
||||
in your code anyway, since it's rarely the case that you want to
|
||||
unserialize or clone a document with no identity.
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
Mapping Classes to the ORM and ODM
|
||||
==================================
|
||||
|
||||
Because of the non intrusive design of Doctrine it is possible for you to have plain PHP classes
|
||||
that are mapped to both a relational database with the Doctrine2 Object Relational Mapper and
|
||||
MongoDB with the Doctrine MongoDB Object Document Mapper, or any other persistence layer that
|
||||
implements the Doctrine Common `persistence`_ interfaces.
|
||||
|
||||
Test Subject
|
||||
------------
|
||||
|
||||
For this cookbook entry we need to define a class that can be persisted to both MySQL and MongoDB.
|
||||
We'll use a ``BlogPost`` as you may want to write some generic blogging functionality that has support
|
||||
for multiple Doctrine persistence layers:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Blog;
|
||||
|
||||
class BlogPost
|
||||
{
|
||||
private $id;
|
||||
private $title;
|
||||
private $body;
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
Mapping Information
|
||||
-------------------
|
||||
|
||||
Now we just need to provide the mapping information for the Doctrine persistence layers so they know
|
||||
how to consume the objects and persist them to the database.
|
||||
|
||||
ORM
|
||||
~~~
|
||||
|
||||
First define the mapping for the ORM:
|
||||
|
||||
.. configuration-block::
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Blog;
|
||||
|
||||
/** @Entity(repositoryClass="Doctrine\Blog\ORM\BlogPostRepository") */
|
||||
class BlogPost
|
||||
{
|
||||
/** @Id @Column(type="integer") */
|
||||
private $id;
|
||||
|
||||
/** @Column(type="string") */
|
||||
private $title;
|
||||
|
||||
/** @Column(type="text") */
|
||||
private $body;
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
|
||||
http://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
|
||||
|
||||
<entity name="Documents\BlogPost" repository-class="Doctrine\Blog\ORM\BlogPostRepository">
|
||||
<id name="id" type="integer" />
|
||||
<field name="name" type="string" />
|
||||
<field name="email" type="text" />
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Documents\BlogPost:
|
||||
repositoryClass: Doctrine\Blog\ORM\BlogPostRepository
|
||||
id:
|
||||
id:
|
||||
type: integer
|
||||
fields:
|
||||
title:
|
||||
type: string
|
||||
body:
|
||||
type: text
|
||||
|
||||
Now you are able to persist the ``Documents\BlogPost`` with an instance of ``EntityManager``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$blogPost = new BlogPost()
|
||||
$blogPost->setTitle('test');
|
||||
|
||||
$em->persist($blogPost);
|
||||
$em->flush();
|
||||
|
||||
You can find the blog post:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$blogPost = $em->getRepository('Documents\BlogPost')->findOneByTitle('test');
|
||||
|
||||
MongoDB ODM
|
||||
~~~~~~~~~~~
|
||||
|
||||
Now map the same class to the Doctrine MongoDB ODM:
|
||||
|
||||
.. configuration-block::
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace Documents;
|
||||
|
||||
/** @Document(repositoryClass="Doctrine\Blog\ODM\MongoDB\BlogPostRepository") */
|
||||
class BlogPost
|
||||
{
|
||||
/** @Id */
|
||||
private $id;
|
||||
|
||||
/** @Field(type="string") */
|
||||
private $title;
|
||||
|
||||
/** @Field(type="string") */
|
||||
private $body;
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
|
||||
http://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
|
||||
|
||||
<document name="Documents\BlogPost" repository-class="Doctrine\Blog\ODM\MongoDB\BlogPostRepository">
|
||||
<field fieldName="id" type="id" />
|
||||
<field fieldName="name" type="string" />
|
||||
<field fieldName="email" type="text" />
|
||||
</document>
|
||||
</doctrine-mongo-mapping>
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Documents\BlogPost:
|
||||
repositoryClass: Doctrine\Blog\ODM\MongoDB\BlogPostRepository
|
||||
fields:
|
||||
id:
|
||||
type: id
|
||||
title:
|
||||
type: string
|
||||
body:
|
||||
type: text
|
||||
|
||||
Now the same class is able to be persisted in the same way using an instance of ``DocumentManager``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$blogPost = new BlogPost()
|
||||
$blogPost->setTitle('test');
|
||||
|
||||
$dm->persist($blogPost);
|
||||
$dm->flush();
|
||||
|
||||
You can find the blog post:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$blogPost = $dm->getRepository('Documents\BlogPost')->findOneByTitle('test');
|
||||
|
||||
Repository Classes
|
||||
------------------
|
||||
|
||||
You can implement the same repository interface for the ORM and MongoDB ODM easily:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Blog\ORM;
|
||||
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
class BlogPostRepository extends EntityRepository
|
||||
{
|
||||
public function findPostById($id)
|
||||
{
|
||||
return $this->findOneBy(array('id' => $id));
|
||||
}
|
||||
}
|
||||
|
||||
Now define the same repository methods for the MongoDB ODM:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Blog\ODM\MongoDB;
|
||||
|
||||
use Doctrine\ODM\MongoDB\DocumentRepository;
|
||||
|
||||
class BlogPostRepository extends DocumentRepository
|
||||
{
|
||||
public function findPostById($id)
|
||||
{
|
||||
return $this->findOneBy(array('id' => $id));
|
||||
}
|
||||
}
|
||||
|
||||
As you can see the repositories are the same and the final returned data is the same vanilla
|
||||
PHP objects. The data is transparently injected to the objects for you automatically so you
|
||||
are not forced to extend some base class or shape your domain in any certain way for it to work
|
||||
with the Doctrine persistence layers.
|
||||
|
||||
.. _persistence: https://github.com/doctrine/common/tree/master/lib/Doctrine/Common/Persistence
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
Keeping Your Modules Independent
|
||||
================================
|
||||
|
||||
One of the goals of using modules is to create discrete units of functionality
|
||||
that do not have many (if any) dependencies, allowing you to use that
|
||||
functionality in other applications without including unnecessary items.
|
||||
|
||||
Doctrine MongoDB ODM includes a utility called
|
||||
``ResolveTargetDocumentListener``, that functions by intercepting certain calls
|
||||
inside Doctrine and rewriting ``targetDocument`` parameters in your metadata
|
||||
mapping at runtime. This allows your bundle to use an interface or abstract
|
||||
class in its mappings while still allowing the mapping to resolve to a concrete
|
||||
document class at runtime.
|
||||
|
||||
This functionality allows you to define relationships between different
|
||||
documents without creating hard dependencies.
|
||||
|
||||
Background
|
||||
----------
|
||||
|
||||
In the following example, we have an `InvoiceModule` that provides invoicing
|
||||
functionality, and a `CustomerModule` that contains customer management tools.
|
||||
We want to keep these separated, because they can be used in other systems
|
||||
without each other; however, we'd like to use them together in our application.
|
||||
|
||||
In this case, we have an ``Invoice`` document with a relationship to a
|
||||
non-existent object, an ``InvoiceSubjectInterface``. The goal is to get
|
||||
the ``ResolveTargetDocumentListener`` to replace any mention of the interface
|
||||
with a real class that implements that interface.
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
We're going to use the following basic documents (which are incomplete
|
||||
for brevity) to explain how to set up and use the
|
||||
``ResolveTargetDocumentListener``.
|
||||
|
||||
A Customer document:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// src/Acme/AppModule/Document/Customer.php
|
||||
|
||||
namespace Acme\AppModule\Document;
|
||||
|
||||
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
|
||||
use Acme\CustomerModule\Document\Customer as BaseCustomer;
|
||||
use Acme\InvoiceModule\Model\InvoiceSubjectInterface;
|
||||
|
||||
/**
|
||||
* @ODM\Document
|
||||
*/
|
||||
class Customer extends BaseCustomer implements InvoiceSubjectInterface
|
||||
{
|
||||
// In our example, any methods defined in the InvoiceSubjectInterface
|
||||
// are already implemented in the BaseCustomer
|
||||
}
|
||||
|
||||
An Invoice document:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// src/Acme/InvoiceModule/Document/Invoice.php
|
||||
|
||||
namespace Acme\InvoiceModule\Document;
|
||||
|
||||
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
|
||||
use Acme\InvoiceModule\Model\InvoiceSubjectInterface;
|
||||
|
||||
/**
|
||||
* @ODM\Document
|
||||
*/
|
||||
class Invoice
|
||||
{
|
||||
/**
|
||||
* @ODM\ReferenceOne(targetDocument="Acme\InvoiceModule\Model\InvoiceSubjectInterface")
|
||||
* @var InvoiceSubjectInterface
|
||||
*/
|
||||
protected $subject;
|
||||
}
|
||||
|
||||
An InvoiceSubjectInterface:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// src/Acme/InvoiceModule/Model/InvoiceSubjectInterface.php
|
||||
|
||||
namespace Acme\InvoiceModule\Model;
|
||||
|
||||
/**
|
||||
* An interface that the invoice Subject object should implement.
|
||||
* In most circumstances, only a single object should implement
|
||||
* this interface as the ResolveTargetDocumentListener can only
|
||||
* change the target to a single object.
|
||||
*/
|
||||
interface InvoiceSubjectInterface
|
||||
{
|
||||
// List any additional methods that your InvoiceModule
|
||||
// will need to access on the subject so that you can
|
||||
// be sure that you have access to those methods.
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName();
|
||||
}
|
||||
|
||||
Next, we need to configure the listener. Add this to the area where you setup
|
||||
Doctrine MongoDB ODM. You must set this up in the way outlined below, otherwise
|
||||
you cannot be guaranteed that the targetDocument resolution will occur reliably:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$evm = new \Doctrine\Common\EventManager;
|
||||
$rtdl = new \Doctrine\ODM\MongoDB\Tools\ResolveTargetDocumentListener;
|
||||
|
||||
// Adds a target-document class
|
||||
$rtdl->addResolveTargetDocument(
|
||||
'Acme\\InvoiceModule\\Model\\InvoiceSubjectInterface',
|
||||
'Acme\\CustomerModule\\Document\\Customer',
|
||||
array()
|
||||
);
|
||||
|
||||
// Add the ResolveTargetDocumentListener
|
||||
$evm->addEventListener(\Doctrine\ODM\MongoDB\Events::loadClassMetadata, $rtdl);
|
||||
|
||||
// Create the document manager as you normally would
|
||||
$dm = \Doctrine\ODM\MongoDB\DocumentManager::create($connectionOptions, $config, $evm);
|
||||
|
||||
Final Thoughts
|
||||
--------------
|
||||
|
||||
With ``ResolveTargetDocumentListener``, we are able to decouple our bundles so
|
||||
that they are usable by themselves and easier to maintain independently, while
|
||||
still being able to define relationships between different objects.
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
Simple Search Engine
|
||||
====================
|
||||
|
||||
It is very easy to implement a simple keyword search engine with MongoDB. Because of
|
||||
its flexible schema less nature we can store the keywords we want to search through directly
|
||||
on the document. MongoDB is capable of indexing the embedded documents so the results are fast
|
||||
and scalable.
|
||||
|
||||
Sample Model: Product
|
||||
---------------------
|
||||
|
||||
Imagine you had a ``Product`` document and you want to search the products by keywords. You can
|
||||
setup a document like the following with a ``$keywords`` property that is mapped as a collection:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace Documents;
|
||||
|
||||
/** @Document */
|
||||
class Product
|
||||
{
|
||||
/** @Id */
|
||||
private $id;
|
||||
|
||||
/** @Field(type="string") */
|
||||
private $title;
|
||||
|
||||
/** @Field(type="collection") @Index */
|
||||
private $keywords = array();
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
Working with Keywords
|
||||
---------------------
|
||||
|
||||
Now, create a product and add some keywords:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$product = new Product();
|
||||
$product->setTitle('Nike Air Jordan 2011');
|
||||
$product->addKeyword('nike shoes');
|
||||
$product->addKeyword('jordan shoes');
|
||||
$product->addKeyword('air jordan');
|
||||
$product->addKeyword('shoes');
|
||||
$product->addKeyword('2011');
|
||||
|
||||
$dm->persist($product);
|
||||
$dm->flush();
|
||||
|
||||
The above example populates the keywords manually but you could very easily write some code which
|
||||
automatically generates your keywords from a string built by the Product that may include the title,
|
||||
description and other fields. You could also use a tool like the `AlchemyAPI`_ if you want to do
|
||||
some more intelligent keyword extraction.
|
||||
|
||||
Searching Keywords
|
||||
------------------
|
||||
|
||||
Searching the keywords in the ``Product`` collection is easy! You can run a query like the following
|
||||
to find documents that have at least one of the keywords:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$keywords = array('nike shoes', 'air jordan');
|
||||
|
||||
$qb = $dm->createQueryBuilder('Product')
|
||||
->field('keywords')->in($keywords);
|
||||
|
||||
You can make the query more strict by using the ``all()`` method instead of ``in()``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$keywords = array('nike shoes', 'air jordan');
|
||||
|
||||
$qb = $dm->createQueryBuilder('Product')
|
||||
->field('keywords')->all($keywords);
|
||||
|
||||
The above query would only return products that have both of the keywords!
|
||||
|
||||
User Input
|
||||
~~~~~~~~~~
|
||||
|
||||
You can easily build keywords from a user search form by exploding whitespace and passing
|
||||
the results to your query. Here is an example:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$queryString = $_REQUEST['q'];
|
||||
$keywords = explode(' ', $queryString);
|
||||
|
||||
$qb = $dm->createQueryBuilder('Product')
|
||||
->field('keywords')->all($keywords);
|
||||
|
||||
Embedded Documents
|
||||
------------------
|
||||
|
||||
If you want to use an embedded document instead of just an array then you can. It will allow you to store
|
||||
additional information with each keyword, like its weight.
|
||||
|
||||
Definition
|
||||
~~~~~~~~~~
|
||||
|
||||
You can setup a ``Keyword`` document like the following:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
/** @EmbeddedDocument */
|
||||
class Keyword
|
||||
{
|
||||
/** @Field(type="string") @Index */
|
||||
private $keyword;
|
||||
|
||||
/** @Field(type="int") */
|
||||
private $weight;
|
||||
|
||||
public function __construct($keyword, $weight)
|
||||
{
|
||||
$this->keyword = $keyword;
|
||||
$this->weight = $weight;
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
Now you can embed the ``Keyword`` document many times in the ``Product``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace Documents;
|
||||
|
||||
/** @Document */
|
||||
class Product
|
||||
{
|
||||
// ...
|
||||
|
||||
/** @EmbedMany(targetDocument="Keyword") */
|
||||
private $keywords;
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
With the new embedded document to add a keyword to a ``Product`` the API is a little different,
|
||||
you would have to do the following:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$product->addKeyword(new Keyword('nike shoes', 1));
|
||||
|
||||
This is a very basic search engine example and can work for many small and simple applications. If you
|
||||
need better searching functionality you can look at integrating something like `Solr`_ in your project.
|
||||
|
||||
.. _AlchemyAPI: http://www.alchemyapi.com
|
||||
.. _Solr: http://lucene.apache.org/solr
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
Soft Delete Extension
|
||||
=====================
|
||||
|
||||
Sometimes you may not want to delete data from your database completely, but you want to
|
||||
disable or temporarily delete some records so they do not appear anymore in your frontend.
|
||||
Then, later you might want to restore that deleted data like it was never deleted.
|
||||
|
||||
This is possible with the ``SoftDelete`` extension which can be found on `github`_.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
First you just need to get the code by cloning the `github`_ repository:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ git clone git://github.com/doctrine/mongodb-odm-softdelete.git
|
||||
|
||||
Now once you have the code you can setup the autoloader for it:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$classLoader = new ClassLoader('Doctrine\ODM\MongoDB\SoftDelete', 'mongodb-odm-softdelete/lib');
|
||||
$classLoader->register();
|
||||
|
||||
Setup
|
||||
-----
|
||||
|
||||
Now you can autoload the classes you need to setup the ``SoftDeleteManager`` instance you need to manage
|
||||
the soft delete state of your documents:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Doctrine\ODM\MongoDB\SoftDelete\Configuration;
|
||||
use Doctrine\ODM\MongoDB\SoftDelete\UnitOfWork;
|
||||
use Doctrine\ODM\MongoDB\SoftDelete\SoftDeleteManager;
|
||||
use Doctrine\Common\EventManager;
|
||||
|
||||
// $dm is a DocumentManager instance we should already have
|
||||
|
||||
$config = new Configuration();
|
||||
$evm = new EventManager();
|
||||
$sdm = new SoftDeleteManager($dm, $config, $evm);
|
||||
|
||||
SoftDeleteable Interface
|
||||
------------------------
|
||||
|
||||
In order for your documents to work with the SoftDelete functionality they must implement
|
||||
the ``SoftDeleteable`` interface:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
interface SoftDeleteable
|
||||
{
|
||||
function getDeletedAt();
|
||||
}
|
||||
|
||||
Example Implementation
|
||||
----------------------
|
||||
|
||||
An implementation might look like this in a ``User`` document:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Doctrine\ODM\MongoDB\SoftDelete\SoftDeleteable;
|
||||
|
||||
/** @mongodb:Document */
|
||||
class User implements SoftDeleteable
|
||||
{
|
||||
// ...
|
||||
|
||||
/** @mongodb:Date @mongodb:Index */
|
||||
private $deletedAt;
|
||||
|
||||
public function getDeletedAt()
|
||||
{
|
||||
return $this->deletedAt;
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
Once you have the ``$sdm`` you can start managing the soft delete state of your documents:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$jwage = $dm->getRepository('User')->findOneByUsername('jwage');
|
||||
$fabpot = $dm->getRepository('User')->findOneByUsername('fabpot');
|
||||
$sdm->delete($jwage);
|
||||
$sdm->delete($fabpot);
|
||||
$sdm->flush();
|
||||
|
||||
The call to ``SoftDeleteManager#flush()`` would persist the deleted state to the database
|
||||
for all the documents it knows about and run a query like the following:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
db.users.update({ _id : { $in : userIds }}, { $set : { deletedAt : new Date() } })
|
||||
|
||||
Now if we were to restore the documents:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$sdm->restore($jwage);
|
||||
$sdm->flush();
|
||||
|
||||
It would execute a query like the following:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
db.users.update({ _id : { $in : userIds }}, { $unset : { deletedAt : true } })
|
||||
|
||||
Events
|
||||
------
|
||||
|
||||
We trigger some additional lifecycle events when documents are soft deleted and restored:
|
||||
|
||||
- Events::preSoftDelete
|
||||
- Events::postSoftDelete
|
||||
- Events::preRestore
|
||||
- Events::postRestore
|
||||
|
||||
Using the events is easy, just define a class like the following:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
class TestEventSubscriber implements \Doctrine\Common\EventSubscriber
|
||||
{
|
||||
public function preSoftDelete(LifecycleEventArgs $args)
|
||||
{
|
||||
$document = $args->getDocument();
|
||||
$sdm = $args->getSoftDeleteManager();
|
||||
}
|
||||
|
||||
public function getSubscribedEvents()
|
||||
{
|
||||
return array(Events::preSoftDelete);
|
||||
}
|
||||
}
|
||||
|
||||
Now we just need to add the event subscriber to the EventManager:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$eventSubscriber = new TestEventSubscriber();
|
||||
$evm->addEventSubscriber($eventSubscriber);
|
||||
|
||||
When we soft delete something the preSoftDelete() method will be invoked before any queries are sent
|
||||
to the database:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$sdm->delete($fabpot);
|
||||
$sdm->flush();
|
||||
|
||||
Cascading Soft Deletes
|
||||
----------------------
|
||||
|
||||
You can easily implement cascading soft deletes by using events in a certain way. Imagine you have
|
||||
a User and Post document and you want to soft delete a users posts when you delete him.
|
||||
|
||||
You just need to setup an event listener like the following:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Doctrine\ODM\MongoDB\SoftDelete\Event\LifecycleEventArgs;
|
||||
|
||||
class CascadingSoftDeleteListener implements EventSubscriber
|
||||
{
|
||||
public function preSoftDelete(LifecycleEventArgs $args)
|
||||
{
|
||||
$sdm = $args->getSoftDeleteManager();
|
||||
$document = $args->getDocument();
|
||||
if ($document instanceof User) {
|
||||
$sdm->deleteBy('Post', array('user.id' => $document->getId()));
|
||||
}
|
||||
}
|
||||
|
||||
public function preRestore(LifecycleEventArgs $args)
|
||||
{
|
||||
$sdm = $args->getSoftDeleteManager();
|
||||
$document = $args->getDocument();
|
||||
if ($document instanceof User) {
|
||||
$sdm->restoreBy('Post', array('user.id' => $document->getId()));
|
||||
}
|
||||
}
|
||||
|
||||
public function getSubscribedEvents()
|
||||
{
|
||||
return array(
|
||||
Events::preSoftDelete,
|
||||
Events::preRestore
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Now when you delete an instance of User it will also delete any Post documents where they
|
||||
reference the User being deleted. If you restore the User, his Post documents will also be restored.
|
||||
|
||||
.. _github: https://github.com/doctrine/mongodb-odm-softdelete
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
Validation of Documents
|
||||
=======================
|
||||
|
||||
.. sectionauthor:: Benjamin Eberlei <kontakt@beberlei.de>
|
||||
|
||||
Doctrine does not ship with any internal validators, the reason
|
||||
being that we think all the frameworks out there already ship with
|
||||
quite decent ones that can be integrated into your Domain easily.
|
||||
What we offer are hooks to execute any kind of validation.
|
||||
|
||||
.. note::
|
||||
|
||||
You don't need to validate your documents in the lifecycle
|
||||
events. Its only one of many options. Of course you can also
|
||||
perform validations in value setters or any other method of your
|
||||
documents that are used in your code.
|
||||
|
||||
Documents can register lifecycle event methods with Doctrine that
|
||||
are called on different occasions. For validation we would need to
|
||||
hook into the events called before persisting and updating. Even
|
||||
though we don't support validation out of the box, the
|
||||
implementation is even simpler than in Doctrine 1 and you will get
|
||||
the additional benefit of being able to re-use your validation in
|
||||
any other part of your domain.
|
||||
|
||||
Say we have an ``Order`` with several ``OrderLine`` instances. We
|
||||
never want to allow any customer to order for a larger sum than he
|
||||
is allowed to:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
class Order
|
||||
{
|
||||
public function assertCustomerAllowedBuying()
|
||||
{
|
||||
$orderLimit = $this->customer->getOrderLimit();
|
||||
|
||||
$amount = 0;
|
||||
foreach ($this->orderLines AS $line) {
|
||||
$amount += $line->getAmount();
|
||||
}
|
||||
|
||||
if ($amount > $orderLimit) {
|
||||
throw new CustomerOrderLimitExceededException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Now this is some pretty important piece of business logic in your
|
||||
code, enforcing it at any time is important so that customers with
|
||||
a unknown reputation don't owe your business too much money.
|
||||
|
||||
We can enforce this constraint in any of the metadata drivers.
|
||||
First Annotations:
|
||||
|
||||
.. configuration-block::
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
/** @Document @HasLifecycleCallbacks */
|
||||
class Order
|
||||
{
|
||||
/** @PrePersist @PreUpdate */
|
||||
public function assertCustomerAllowedBuying() {}
|
||||
}
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<doctrine-mapping>
|
||||
<document name="Order">
|
||||
<lifecycle-callbacks>
|
||||
<lifecycle-callback type="prePersist" method="assertCustomerallowedBuying" />
|
||||
<lifecycle-callback type="preUpdate" method="assertCustomerallowedBuying" />
|
||||
</lifecycle-callbacks>
|
||||
</document>
|
||||
</doctrine-mapping>
|
||||
|
||||
Now validation is performed whenever you call
|
||||
``DocumentManager#persist($order)`` or when you call
|
||||
``DocumentManager#flush()`` and an order is about to be updated. Any
|
||||
Exception that happens in the lifecycle callbacks will be cached by
|
||||
the DocumentManager and the current transaction is rolled back.
|
||||
|
||||
Of course you can do any type of primitive checks, not null,
|
||||
email-validation, string size, integer and date ranges in your
|
||||
validation callbacks.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
/** @Document @HasLifecycleCallbacks */
|
||||
class Order
|
||||
{
|
||||
/** @PrePersist @PreUpdate */
|
||||
public function validate()
|
||||
{
|
||||
if (!($this->plannedShipDate instanceof DateTime)) {
|
||||
throw new ValidateException();
|
||||
}
|
||||
|
||||
if ($this->plannedShipDate->format('U') < time()) {
|
||||
throw new ValidateException();
|
||||
}
|
||||
|
||||
if ($this->customer == null) {
|
||||
throw new OrderRequiresCustomerException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
What is nice about lifecycle events is, you can also re-use the
|
||||
methods at other places in your domain, for example in combination
|
||||
with your form library. Additionally there is no limitation in the
|
||||
number of methods you register on one particular event, i.e. you
|
||||
can register multiple methods for validation in "PrePersist" or
|
||||
"PreUpdate" or mix and share them in any combinations between those
|
||||
two events.
|
||||
|
||||
There is no limit to what you can and can't validate in
|
||||
"PrePersist" and "PreUpdate" as long as you don't create new document
|
||||
instances. This was already discussed in the previous blog post on
|
||||
the Versionable extension, which requires another type of event
|
||||
called "onFlush".
|
||||
|
||||
Further readings: :doc:`Lifecycle Events <../reference/events>`
|
||||
Reference in New Issue
Block a user