Initial commit
This commit is contained in:
commit
897a334bf6
18
build.docset/Contents/Info.plist
Normal file
18
build.docset/Contents/Info.plist
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>build</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>build</string>
|
||||||
|
<key>DashDocSetFamily</key>
|
||||||
|
<string>python</string>
|
||||||
|
<key>DocSetPlatformFamily</key>
|
||||||
|
<string>build</string>
|
||||||
|
<key>isDashDocset</key>
|
||||||
|
<true/>
|
||||||
|
<key>isJavaScriptEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
4
build.docset/Contents/Resources/Documents/.buildinfo
Normal file
4
build.docset/Contents/Resources/Documents/.buildinfo
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# Sphinx build info version 1
|
||||||
|
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||||
|
config: b9a0aa75b165bda23dea90cac5cdb554
|
||||||
|
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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
|
|
@ -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!");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
|
@ -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.
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -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
|
|
@ -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>`
|
107
build.docset/Contents/Resources/Documents/_sources/index.rst.txt
Normal file
107
build.docset/Contents/Resources/Documents/_sources/index.rst.txt
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
Doctrine MongoDB ODM's documentation!
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
The Doctrine MongoDB ODM documentation is comprised of tutorials, a reference section and
|
||||||
|
cookbook articles that explain different parts of the Object Document mapper.
|
||||||
|
|
||||||
|
Getting Help
|
||||||
|
------------
|
||||||
|
|
||||||
|
If this documentation is not helping to answer questions you have about
|
||||||
|
Doctrine MongoDB ODM don't panic. You can get help from different sources:
|
||||||
|
|
||||||
|
- The `Doctrine Mailing List <http://groups.google.com/group/doctrine-user>`_
|
||||||
|
- Internet Relay Chat (IRC) in `#doctrine on Freenode <irc://irc.freenode.net/doctrine>`_
|
||||||
|
- Report a bug on `GitHub <https://github.com/doctrine/mongodb-odm/issues>`_.
|
||||||
|
- On `StackOverflow <http://stackoverflow.com/questions/tagged/doctrine-odm>`_
|
||||||
|
|
||||||
|
Getting Started
|
||||||
|
---------------
|
||||||
|
|
||||||
|
:doc:`Getting Started <tutorials/getting-started>` |
|
||||||
|
:doc:`Introduction <reference/introduction>` |
|
||||||
|
:doc:`Architecture <reference/architecture>`
|
||||||
|
|
||||||
|
Mapping Objects onto a Database
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
* **Basic Reference**:
|
||||||
|
:doc:`Objects and Fields <reference/basic-mapping>` |
|
||||||
|
:doc:`References <reference/reference-mapping>` |
|
||||||
|
:doc:`Bi-Directional References <reference/bidirectional-references>` |
|
||||||
|
:doc:`Complex References <reference/complex-references>` |
|
||||||
|
:doc:`Indexes <reference/indexes>` |
|
||||||
|
:doc:`Inheritance <reference/inheritance-mapping>`
|
||||||
|
|
||||||
|
* **Embedded Data**:
|
||||||
|
:doc:`Embedded <reference/embedded-mapping>` |
|
||||||
|
:doc:`Trees <reference/trees>`
|
||||||
|
|
||||||
|
* **GridFS**:
|
||||||
|
:doc:`Storing Files in GridFS <reference/storing-files-with-mongogridfs>`
|
||||||
|
|
||||||
|
* **Mapping Driver References**:
|
||||||
|
:doc:`XML <reference/xml-mapping>` |
|
||||||
|
:doc:`YAML <reference/yml-mapping>` |
|
||||||
|
:doc:`Docblock Annotations <reference/annotations-reference>` |
|
||||||
|
:doc:`Metadata Drivers <reference/metadata-drivers>`
|
||||||
|
|
||||||
|
Working with Objects
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
* **Basic Reference**:
|
||||||
|
:doc:`Documents <reference/working-with-objects>` |
|
||||||
|
:doc:`Repositories <reference/document-repositories>` |
|
||||||
|
:doc:`Events <reference/events>` |
|
||||||
|
:doc:`Migrations <reference/migrating-schemas>`
|
||||||
|
|
||||||
|
* **Query Reference**:
|
||||||
|
:doc:`Query Builder API <reference/query-builder-api>` |
|
||||||
|
:doc:`Aggregation Pipeline queries <reference/aggregation-builder>` |
|
||||||
|
:doc:`Geo Spatial Queries <reference/geospatial-queries>` |
|
||||||
|
:doc:`Slave Okay Queries <reference/slave-okay-queries>` |
|
||||||
|
:doc:`Find and Update <reference/find-and-update>` |
|
||||||
|
:doc:`Filters <reference/filters>` |
|
||||||
|
:doc:`Priming References <reference/priming-references>` |
|
||||||
|
:doc:`Eager Cursors <reference/eager-cursors>` |
|
||||||
|
:doc:`Map Reduce <reference/map-reduce>`
|
||||||
|
|
||||||
|
Advanced Topics
|
||||||
|
---------------
|
||||||
|
|
||||||
|
* **Collections**:
|
||||||
|
:doc:`Capped Collections <reference/capped-collections>` |
|
||||||
|
:doc:`Storage Strategies <reference/storage-strategies>` |
|
||||||
|
:doc:`Custom Collections <reference/custom-collections>` |
|
||||||
|
:doc:`Sharded setups <reference/sharding>`
|
||||||
|
|
||||||
|
* **Transactions and Concurrency**:
|
||||||
|
:doc:`Transactions and Concurrency <reference/transactions-and-concurrency>`
|
||||||
|
|
||||||
|
* **Best Practices**:
|
||||||
|
:doc:`Best Practices <reference/best-practices>`
|
||||||
|
|
||||||
|
* **Performance**:
|
||||||
|
:doc:`Change Tracking Policies <reference/change-tracking-policies>`
|
||||||
|
|
||||||
|
* **Logging**:
|
||||||
|
:doc:`Logging <reference/logging>`
|
||||||
|
|
||||||
|
Cookbook
|
||||||
|
--------
|
||||||
|
|
||||||
|
* **Examples**:
|
||||||
|
:doc:`Soft Delete <cookbook/soft-delete-extension>` |
|
||||||
|
:doc:`Simple Search Engine <cookbook/simple-search-engine>`
|
||||||
|
|
||||||
|
* **Tricks**:
|
||||||
|
:doc:`Blending ORM and MongoDB ODM <cookbook/blending-orm-and-mongodb-odm>` |
|
||||||
|
:doc:`Mapping classes to ORM and ODM <cookbook/mapping-classes-to-orm-and-odm>`
|
||||||
|
|
||||||
|
* **Implementation**:
|
||||||
|
:doc:`Array Access <cookbook/implementing-array-access-for-domain-objects>` |
|
||||||
|
:doc:`Notify ChangeTracking Example <cookbook/implementing-the-notify-changetracking-policy>` |
|
||||||
|
:doc:`Using Wakeup Or Clone <cookbook/implementing-wakeup-or-clone>` |
|
||||||
|
:doc:`Validation <cookbook/validation-of-documents>` |
|
||||||
|
:doc:`Simple Search Engine <cookbook/simple-search-engine>` |
|
||||||
|
:doc:`Keeping Your Modules Independent <cookbook/resolve-target-document-listener>`
|
|
@ -0,0 +1,814 @@
|
||||||
|
Aggregation builder
|
||||||
|
===================
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
This feature is introduced in version 1.2
|
||||||
|
|
||||||
|
The aggregation framework provides an easy way to process records and return
|
||||||
|
computed results. The aggregation builder helps to build complex aggregation
|
||||||
|
pipelines.
|
||||||
|
|
||||||
|
Creating an Aggregation Builder
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
You can easily create a new ``Aggregation\Builder`` object with the
|
||||||
|
``DocumentManager::createAggregationBuilder()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\User::class);
|
||||||
|
|
||||||
|
The first argument indicates the document for which you want to create the
|
||||||
|
builder.
|
||||||
|
|
||||||
|
Adding pipeline stages
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
To add a pipeline stage to the builder, call the corresponding method on the
|
||||||
|
builder object:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder
|
||||||
|
->match()
|
||||||
|
->field('purchaseDate')
|
||||||
|
->gte($from)
|
||||||
|
->lt($to)
|
||||||
|
->field('user')
|
||||||
|
->references($user)
|
||||||
|
->group()
|
||||||
|
->field('id')
|
||||||
|
->expression('$user')
|
||||||
|
->field('numPurchases')
|
||||||
|
->sum(1)
|
||||||
|
->field('amount')
|
||||||
|
->sum('$amount');
|
||||||
|
|
||||||
|
Just like the query builder, the aggregation builder takes care of converting
|
||||||
|
``DateTime`` objects into ``MongoDate`` objects.
|
||||||
|
|
||||||
|
Nesting expressions
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
You can create more complex aggregation stages by using the ``expr()`` method in
|
||||||
|
the aggregation builder.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder
|
||||||
|
->match()
|
||||||
|
->field('purchaseDate')
|
||||||
|
->gte($from)
|
||||||
|
->lt($to)
|
||||||
|
->field('user')
|
||||||
|
->references($user)
|
||||||
|
->group()
|
||||||
|
->field('id')
|
||||||
|
->expression(
|
||||||
|
$builder->expr()
|
||||||
|
->field('month')
|
||||||
|
->month('purchaseDate')
|
||||||
|
->field('year')
|
||||||
|
->year('purchaseDate')
|
||||||
|
)
|
||||||
|
->field('numPurchases')
|
||||||
|
->sum(1)
|
||||||
|
->field('amount')
|
||||||
|
->sum('$amount');
|
||||||
|
|
||||||
|
This aggregation would group all purchases by their month and year by projecting
|
||||||
|
those values into an embedded object for the ``id`` field. For example:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
_id: {
|
||||||
|
month: 1,
|
||||||
|
year: 2016
|
||||||
|
},
|
||||||
|
numPurchases: 1,
|
||||||
|
amount: 27.89
|
||||||
|
}
|
||||||
|
|
||||||
|
Executing an aggregation pipeline
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
You can execute a pipeline using the ``execute()`` method. This will run the
|
||||||
|
aggregation pipeline and return a cursor for you to iterate over the results:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\User::class);
|
||||||
|
$result = $builder->execute();
|
||||||
|
|
||||||
|
If you instead want to look at the built aggregation pipeline, call the
|
||||||
|
``Builder::getPipeline()`` method.
|
||||||
|
|
||||||
|
Hydration
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
By default, aggregation results are returned as PHP arrays. This is because the
|
||||||
|
result of an aggregation pipeline may look completely different from the source
|
||||||
|
document. In order to get hydrated aggregation results, you first have to map
|
||||||
|
a ``QueryResultDocument``. These are written like regular mapped documents, but
|
||||||
|
they can't be persisted to the database.
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/** @QueryResultDocument */
|
||||||
|
class UserPurchases
|
||||||
|
{
|
||||||
|
/** @ReferenceOne(targetDocument="User", name="_id") */
|
||||||
|
private $user;
|
||||||
|
|
||||||
|
/** @Field(type="int") */
|
||||||
|
private $numPurchases;
|
||||||
|
|
||||||
|
/** @Field(type="float") */
|
||||||
|
private $amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
<query-result-document name="Documents\UserPurchases">
|
||||||
|
<field fieldName="numPurchases" type="int" />
|
||||||
|
<field fieldName="amount" type="float" />
|
||||||
|
<reference-one field="user" target-document="Documents\User" name="_id" />
|
||||||
|
</query-result-document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
Documents\User:
|
||||||
|
type: queryResultDocument
|
||||||
|
fields:
|
||||||
|
user:
|
||||||
|
name: _id
|
||||||
|
targetDocument: Documents\User
|
||||||
|
numPurchases:
|
||||||
|
type: int
|
||||||
|
amount:
|
||||||
|
type: float
|
||||||
|
|
||||||
|
Once you have mapped the document, use the ``hydrate()`` method to tell the
|
||||||
|
aggregation builder about this document:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder
|
||||||
|
->hydrate(\Documents\UserPurchases::class)
|
||||||
|
->match()
|
||||||
|
->field('purchaseDate')
|
||||||
|
->gte($from)
|
||||||
|
->lt($to)
|
||||||
|
->field('user')
|
||||||
|
->references($user)
|
||||||
|
->group()
|
||||||
|
->field('id')
|
||||||
|
->expression('$user')
|
||||||
|
->field('numPurchases')
|
||||||
|
->sum(1)
|
||||||
|
->field('amount')
|
||||||
|
->sum('$amount');
|
||||||
|
|
||||||
|
When you run the queries, all results will be returned as instances of the
|
||||||
|
specified document.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Query result documents can use all features regular documents can use: you
|
||||||
|
can map embedded documents, define references, and even use discriminators
|
||||||
|
to get different result documents according to the aggregation result.
|
||||||
|
|
||||||
|
Aggregation pipeline stages
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
MongoDB provides the following aggregation pipeline stages:
|
||||||
|
|
||||||
|
- `$addFields <https://docs.mongodb.com/manual/reference/operator/aggregation/addFields/>`_
|
||||||
|
- `$bucket <https://docs.mongodb.com/manual/reference/operator/aggregation/bucket/>`_
|
||||||
|
- `$bucketAuto <https://docs.mongodb.com/manual/reference/operator/aggregation/bucketAuto/>`_
|
||||||
|
- `$collStats <https://docs.mongodb.com/manual/reference/operator/aggregation/collStats/>`_
|
||||||
|
- `$count <https://docs.mongodb.com/manual/reference/operator/aggregation/count/>`_
|
||||||
|
- `$facet <https://docs.mongodb.com/manual/reference/operator/aggregation/facet/>`_
|
||||||
|
- `$geoNear <https://docs.mongodb.com/manual/reference/operator/aggregation/geoNear/>`_
|
||||||
|
- `$graphLookup <https://docs.mongodb.com/manual/reference/operator/aggregation/graphLookup/>`_
|
||||||
|
- `$group <https://docs.mongodb.com/manual/reference/operator/aggregation/group/>`_
|
||||||
|
- `$indexStats <https://docs.mongodb.com/manual/reference/operator/aggregation/indexStats/>`_
|
||||||
|
- `$limit <https://docs.mongodb.com/manual/reference/operator/aggregation/limit/>`_
|
||||||
|
- `$lookup <https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/>`_
|
||||||
|
- `$match <https://docs.mongodb.com/manual/reference/operator/aggregation/match/>`_
|
||||||
|
- `$out <https://docs.mongodb.com/manual/reference/operator/aggregation/out/>`_
|
||||||
|
- `$project <https://docs.mongodb.com/manual/reference/operator/aggregation/project/>`_
|
||||||
|
- `$redact <https://docs.mongodb.com/manual/reference/operator/aggregation/redact/>`_
|
||||||
|
- `$replaceRoot <https://docs.mongodb.com/manual/reference/operator/aggregation/replaceRoot/>`_
|
||||||
|
- `$sample <https://docs.mongodb.com/manual/reference/operator/aggregation/sample/>`_
|
||||||
|
- `$skip <https://docs.mongodb.com/manual/reference/operator/aggregation/skip/>`_
|
||||||
|
- `$sort <https://docs.mongodb.com/manual/reference/operator/aggregation/project/>`_
|
||||||
|
- `$sortByCount <https://docs.mongodb.com/manual/reference/operator/aggregation/sortByCount/>`_
|
||||||
|
- `$unwind <https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/>`_
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The ``$lookup``, ``$sample`` and ``$indexStats`` stages were added in MongoDB
|
||||||
|
3.2. The ``$addFields``, ``$bucket``, ``$bucketAuto``, ``$sortByCount``,
|
||||||
|
``$replaceRoot``, ``$facet``, ``$graphLookup``, ``$coun`` and ``$collStats``
|
||||||
|
stages were added in MongoDB 3.4.
|
||||||
|
|
||||||
|
$addFields
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
Adds new fields to documents. ``$addFields`` outputs documents that contain all
|
||||||
|
existing fields from the input documents and newly added fields.
|
||||||
|
|
||||||
|
The ``$addFields`` stage is equivalent to a ``$project`` stage that explicitly
|
||||||
|
specifies all existing fields in the input documents and adds the new fields.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder
|
||||||
|
->addFields()
|
||||||
|
->field('purchaseYear')
|
||||||
|
->year('$purchaseDate');
|
||||||
|
|
||||||
|
$bucket
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
Categorizes incoming documents into groups, called buckets, based on a specified
|
||||||
|
expression and bucket boundaries.
|
||||||
|
|
||||||
|
Each bucket is represented as a document in the output. The document for each
|
||||||
|
bucket contains an _id field, whose value specifies the inclusive lower bound of
|
||||||
|
the bucket and a count field that contains the number of documents in the bucket.
|
||||||
|
The count field is included by default when the output is not specified.
|
||||||
|
|
||||||
|
``$bucket`` only produces output documents for buckets that contain at least one
|
||||||
|
input document.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder
|
||||||
|
->bucket()
|
||||||
|
->groupBy('$itemCount')
|
||||||
|
->boundaries(1, 2, 3, 4, 5, '5+')
|
||||||
|
->defaultBucket('5+')
|
||||||
|
->output()
|
||||||
|
->field('lowestValue')
|
||||||
|
->min('$value')
|
||||||
|
->field('highestValue')
|
||||||
|
->max('$value')
|
||||||
|
;
|
||||||
|
|
||||||
|
$bucketAuto
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
Similar to ``$bucket``, except that boundaries are automatically determined in
|
||||||
|
an attempt to evenly distribute the documents into the specified number of
|
||||||
|
buckets.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder
|
||||||
|
->bucketAuto()
|
||||||
|
->groupBy('$itemCount')
|
||||||
|
->buckets(5)
|
||||||
|
->output()
|
||||||
|
->field('lowestValue')
|
||||||
|
->min('$value')
|
||||||
|
->field('highestValue')
|
||||||
|
->max('$value')
|
||||||
|
;
|
||||||
|
|
||||||
|
$collStats
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
The ``$collStats`` stage returns statistics regarding a collection or view.
|
||||||
|
|
||||||
|
$count
|
||||||
|
~~~~~~
|
||||||
|
|
||||||
|
Returns a document that contains a count of the number of documents input to the
|
||||||
|
stage.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder
|
||||||
|
->match()
|
||||||
|
->field('itemCount')
|
||||||
|
->eq(1)
|
||||||
|
->count('numSingleItemOrders')
|
||||||
|
;
|
||||||
|
|
||||||
|
The example above returns a single document with the ``numSingleItemOrders``
|
||||||
|
containing the number of orders found.
|
||||||
|
|
||||||
|
$facet
|
||||||
|
~~~~~~
|
||||||
|
|
||||||
|
Processes multiple aggregation pipelines within a single stage on the same set
|
||||||
|
of input documents. Each sub-pipeline has its own field in the output document
|
||||||
|
where its results are stored as an array of documents.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder
|
||||||
|
->facet()
|
||||||
|
->field('groupedByItemCount')
|
||||||
|
->pipeline(
|
||||||
|
$dm->createAggregationBuilder(\Documents\Orders::class)->group()
|
||||||
|
->field('id')
|
||||||
|
->expression('$itemCount')
|
||||||
|
->field('lowestValue')
|
||||||
|
->min('$value')
|
||||||
|
->field('highestValue')
|
||||||
|
->max('$value')
|
||||||
|
->field('totalValue')
|
||||||
|
->sum('$value')
|
||||||
|
->field('averageValue')
|
||||||
|
->avg('$value')
|
||||||
|
)
|
||||||
|
->field('groupedByYear')
|
||||||
|
->pipeline(
|
||||||
|
$dm->createAggregationBuilder(\Documents\Orders::class)->group()
|
||||||
|
->field('id')
|
||||||
|
->year('purchaseDate')
|
||||||
|
->field('lowestValue')
|
||||||
|
->min('$value')
|
||||||
|
->field('highestValue')
|
||||||
|
->max('$value')
|
||||||
|
->field('totalValue')
|
||||||
|
->sum('$value')
|
||||||
|
->field('averageValue')
|
||||||
|
->avg('$value')
|
||||||
|
)
|
||||||
|
;
|
||||||
|
|
||||||
|
$geoNear
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
The ``$geoNear`` stage finds and outputs documents in order of nearest to
|
||||||
|
farthest from a specified point.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $this->dm->createAggregationBuilder(\Documents\City::class);
|
||||||
|
$builder
|
||||||
|
->geoNear(120, 40)
|
||||||
|
->spherical(true)
|
||||||
|
->distanceField('distance')
|
||||||
|
// Convert radians to kilometers (use 3963.192 for miles)
|
||||||
|
->distanceMultiplier(6378.137);
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The ``$geoNear`` stage must be the first stage in the pipeline and the
|
||||||
|
collection must contain a single geospatial index. You must include the
|
||||||
|
``distanceField`` option for the stage to work.
|
||||||
|
|
||||||
|
$graphLookup
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Performs a recursive search on a collection, with options for restricting the
|
||||||
|
search by recursion depth and query filter. The ``$graphLookup`` stage can be
|
||||||
|
used to resolve association graphs and flatten them into a single list.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $this->dm->createAggregationBuilder(\Documents\Traveller::class);
|
||||||
|
$builder
|
||||||
|
->graphLookup('nearestAirport')
|
||||||
|
->connectFromField('connections')
|
||||||
|
->maxDepth(2)
|
||||||
|
->depthField('numConnections')
|
||||||
|
->alias('destinations');
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The target document of the reference used in ``connectFromField`` must be
|
||||||
|
the very same document. The aggregation builder will throw an exception if
|
||||||
|
you try to resolve a different document.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Due to a limitation in MongoDB, the ``$graphLookup`` stage can not be used
|
||||||
|
with references that are stored as DBRef. To use references in a
|
||||||
|
``$graphLookup`` stage, store the reference as ID or ``ref``. This is
|
||||||
|
explained in the :doc:`Reference mapping <reference-mapping>` chapter.
|
||||||
|
|
||||||
|
.. _aggregation_builder_group:
|
||||||
|
|
||||||
|
$group
|
||||||
|
~~~~~~
|
||||||
|
|
||||||
|
The ``$group`` stage is used to do calculations based on previously matched
|
||||||
|
documents:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder
|
||||||
|
->match()
|
||||||
|
->field('user')
|
||||||
|
->references($user)
|
||||||
|
->group()
|
||||||
|
->field('id')
|
||||||
|
->expression(
|
||||||
|
$builder->expr()
|
||||||
|
->field('month')
|
||||||
|
->month('purchaseDate')
|
||||||
|
->field('year')
|
||||||
|
->year('purchaseDate')
|
||||||
|
)
|
||||||
|
->field('numPurchases')
|
||||||
|
->sum(1)
|
||||||
|
->field('amount')
|
||||||
|
->sum('$amount');
|
||||||
|
|
||||||
|
$indexStats
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
The ``$indexStats`` stage returns statistics regarding the use of each index for
|
||||||
|
the collection. More information can be found in the `official Documentation <https://docs.mongodb.com/manual/reference/operator/aggregation/indexStats/>`_
|
||||||
|
|
||||||
|
$lookup
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The ``$lookup`` stage was introduced in MongoDB 3.2. Using it on older servers
|
||||||
|
will result in an error.
|
||||||
|
|
||||||
|
The ``$lookup`` stage is used to fetch documents from different collections in
|
||||||
|
pipeline stages. Take the following relationship for example:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceMany(
|
||||||
|
* targetDocument="Documents\Item",
|
||||||
|
* cascade="all",
|
||||||
|
* storeAs="id"
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
private $items;
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder
|
||||||
|
->lookup('items')
|
||||||
|
->alias('items');
|
||||||
|
|
||||||
|
The resulting array will contain all matched item documents in an array. This has
|
||||||
|
to be considered when looking up one-to-one relationships:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceOne(
|
||||||
|
* targetDocument="Documents\Item",
|
||||||
|
* cascade="all",
|
||||||
|
* storeAs="id"
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
private $items;
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder
|
||||||
|
->lookup('user')
|
||||||
|
->alias('user')
|
||||||
|
->unwind('$user');
|
||||||
|
|
||||||
|
MongoDB will always return an array, even if the lookup only returned a single
|
||||||
|
document. Thus, when looking up one-to-one references the result must be flattened
|
||||||
|
using the ``$unwind`` operator.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Due to a limitation in MongoDB, the ``$lookup`` stage can not be used with
|
||||||
|
references that are stored as DBRef. To use references in a ``$lookup``
|
||||||
|
stage, store the reference as ID or ``ref``. This is explained in the
|
||||||
|
:doc:`Reference mapping <reference-mapping>` chapter.
|
||||||
|
|
||||||
|
You can also configure your lookup manually if you don't have it mapped in your
|
||||||
|
document:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder
|
||||||
|
->lookup('unmappedCollection')
|
||||||
|
->localField('_id')
|
||||||
|
->foreignField('userId')
|
||||||
|
->alias('items');
|
||||||
|
|
||||||
|
$match
|
||||||
|
~~~~~~
|
||||||
|
|
||||||
|
The ``$match`` stage lets you filter documents according to certain criteria. It
|
||||||
|
works just like the query builder:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder
|
||||||
|
->match()
|
||||||
|
->field('purchaseDate')
|
||||||
|
->gte($from)
|
||||||
|
->lt($to)
|
||||||
|
->field('user')
|
||||||
|
->references($user);
|
||||||
|
|
||||||
|
You can also use fields defined in previous stages:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder
|
||||||
|
->project()
|
||||||
|
->excludeIdField()
|
||||||
|
->includeFields(['purchaseDate', 'user'])
|
||||||
|
->field('purchaseYear')
|
||||||
|
->year('$purchaseDate')
|
||||||
|
->match()
|
||||||
|
->field('purchaseYear')
|
||||||
|
->equals(2016);
|
||||||
|
|
||||||
|
$out
|
||||||
|
~~~~
|
||||||
|
|
||||||
|
The ``$out`` stage is used to store the result of the aggregation pipeline in a
|
||||||
|
collection instead of returning an iterable cursor of results. This must be the
|
||||||
|
last stage in an aggregation pipeline.
|
||||||
|
|
||||||
|
If the collection specified by the ``$out`` operation already exists, then upon
|
||||||
|
completion of the aggregation, the existing collection is atomically replaced.
|
||||||
|
Any indexes that existed on the collection are left intact. If the aggregation
|
||||||
|
fails, the ``$out`` operation does not remove the data from an existing
|
||||||
|
collection.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The aggregation pipeline will fail to complete if the result would violate
|
||||||
|
any unique index constraints, including those on the ``id`` field.
|
||||||
|
|
||||||
|
$project
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
The ``$project`` stage lets you reshape the current document or define a completely
|
||||||
|
new one:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder
|
||||||
|
->project()
|
||||||
|
->excludeIdField()
|
||||||
|
->includeFields(['purchaseDate', 'user'])
|
||||||
|
->field('purchaseYear')
|
||||||
|
->year('$purchaseDate');
|
||||||
|
|
||||||
|
$redact
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
The redact stage can be used to restrict the contents of the documents based on
|
||||||
|
information stored in the documents themselves. You can read more about the
|
||||||
|
``$redact`` stage in the `MongoDB documentation <https://docs.mongodb.com/manual/reference/operator/aggregation/redact/>`_.
|
||||||
|
|
||||||
|
The following example taken from the official documentation checks the ``level``
|
||||||
|
field on all document levels and evaluates it to grant or deny access:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
_id: 1,
|
||||||
|
level: 1,
|
||||||
|
acct_id: "xyz123",
|
||||||
|
cc: {
|
||||||
|
level: 5,
|
||||||
|
type: "yy",
|
||||||
|
num: 000000000000,
|
||||||
|
exp_date: ISODate("2015-11-01T00:00:00.000Z"),
|
||||||
|
billing_addr: {
|
||||||
|
level: 5,
|
||||||
|
addr1: "123 ABC Street",
|
||||||
|
city: "Some City"
|
||||||
|
},
|
||||||
|
shipping_addr: [
|
||||||
|
{
|
||||||
|
level: 3,
|
||||||
|
addr1: "987 XYZ Ave",
|
||||||
|
city: "Some City"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 3,
|
||||||
|
addr1: "PO Box 0123",
|
||||||
|
city: "Some City"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
status: "A"
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder
|
||||||
|
->redact()
|
||||||
|
->cond(
|
||||||
|
$builder->expr()->gte('$$level', 5),
|
||||||
|
'$$PRUNE',
|
||||||
|
'$$DESCEND'
|
||||||
|
)
|
||||||
|
|
||||||
|
$replaceRoot
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Promotes a specified document to the top level and replaces all other fields.
|
||||||
|
The operation replaces all existing fields in the input document, including the
|
||||||
|
``_id`` field. You can promote an existing embedded document to the top level,
|
||||||
|
or create a new document for promotion.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder
|
||||||
|
->replaceRoot('$embeddedField');
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder
|
||||||
|
->replaceRoot()
|
||||||
|
->field('averagePricePerItem')
|
||||||
|
->divide('$value', '$itemCount');
|
||||||
|
|
||||||
|
$sample
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
The sample stage can be used to randomly select a subset of documents in the
|
||||||
|
aggregation pipeline. It behaves like the ``$limit`` stage, but instead of
|
||||||
|
returning the first ``n`` documents it returns ``n`` random documents.
|
||||||
|
|
||||||
|
$sort, $limit and $skip
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The ``$sort``, ``$limit`` and ``$skip`` stages behave like the corresponding
|
||||||
|
query options, allowing you to control the order and subset of results returned
|
||||||
|
by the aggregation pipeline.
|
||||||
|
|
||||||
|
$sortByCount
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Groups incoming documents based on the value of a specified expression, then
|
||||||
|
computes the count of documents in each distinct group.
|
||||||
|
|
||||||
|
Each output document contains two fields: an _id field containing the distinct
|
||||||
|
grouping value, and a count field containing the number of documents belonging
|
||||||
|
to that grouping or category.
|
||||||
|
|
||||||
|
The documents are sorted by count in descending order.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder->sortByCount('$items');
|
||||||
|
|
||||||
|
The example above is equivalent to the following pipeline:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\Orders::class);
|
||||||
|
$builder
|
||||||
|
->group()
|
||||||
|
->field('_id')
|
||||||
|
->expression('$items')
|
||||||
|
->field('count')
|
||||||
|
->sum(1)
|
||||||
|
->sort(['count' => -1])
|
||||||
|
;
|
||||||
|
|
||||||
|
$unwind
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
The ``$unwind`` stage flattens an array in a document, returning a copy for each
|
||||||
|
item. Take this sample document:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
_id: {
|
||||||
|
month: 1,
|
||||||
|
year: 2016
|
||||||
|
},
|
||||||
|
purchaseDates: [
|
||||||
|
'2016-01-07',
|
||||||
|
'2016-03-10',
|
||||||
|
'2016-06-25'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
To flatten the ``purchaseDates`` array, we would apply the following pipeline
|
||||||
|
stage:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$builder = $dm->createAggregationBuilder(\Documents\User::class);
|
||||||
|
$builder->unwind('$purchaseDates');
|
||||||
|
|
||||||
|
The stage would return three documents, each containing a single purchase date:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
_id: {
|
||||||
|
month: 1,
|
||||||
|
year: 2016
|
||||||
|
},
|
||||||
|
purchaseDates: '2016-01-07'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: {
|
||||||
|
month: 1,
|
||||||
|
year: 2016
|
||||||
|
},
|
||||||
|
purchaseDates: '2016-03-10'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: {
|
||||||
|
month: 1,
|
||||||
|
year: 2016
|
||||||
|
},
|
||||||
|
purchaseDates: '2016-06-25'
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,124 @@
|
||||||
|
Architecture
|
||||||
|
============
|
||||||
|
|
||||||
|
This chapter gives an overview of the overall architecture,
|
||||||
|
terminology and constraints of Doctrine. It is recommended to
|
||||||
|
read this chapter carefully.
|
||||||
|
|
||||||
|
Documents
|
||||||
|
---------
|
||||||
|
|
||||||
|
A document is a lightweight, persistent domain object. A document can
|
||||||
|
be any regular PHP class observing the following restrictions:
|
||||||
|
|
||||||
|
- A document class must not be final or contain final methods.
|
||||||
|
- All persistent properties/field of any document class should
|
||||||
|
always be private or protected, otherwise lazy-loading might not
|
||||||
|
work as expected.
|
||||||
|
- A document class must not implement ``__clone`` or
|
||||||
|
:doc:`do so safely <../cookbook/implementing-wakeup-or-clone>`.
|
||||||
|
- A document class must not implement ``__wakeup`` or
|
||||||
|
:doc:`do so safely <../cookbook/implementing-wakeup-or-clone>`.
|
||||||
|
Also consider implementing
|
||||||
|
`Serializable <http://de3.php.net/manual/en/class.serializable.php>`_
|
||||||
|
instead.
|
||||||
|
- Any two document classes in a class hierarchy that inherit
|
||||||
|
directly or indirectly from one another must not have a mapped
|
||||||
|
property with the same name. That is, if B inherits from A then B
|
||||||
|
must not have a mapped field with the same name as an already
|
||||||
|
mapped field that is inherited from A.
|
||||||
|
|
||||||
|
Documents support inheritance, polymorphic associations, and
|
||||||
|
polymorphic queries. Both abstract and concrete classes can be
|
||||||
|
documents. Documents may extend non-document classes as well as document
|
||||||
|
classes, and non-document classes may extend document classes.
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
|
||||||
|
The constructor of a document is only ever invoked when
|
||||||
|
*you* construct a new instance with the *new* keyword. Doctrine
|
||||||
|
never calls document constructors, thus you are free to use them as
|
||||||
|
you wish and even have it require arguments of any type.
|
||||||
|
|
||||||
|
Document states
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
A document instance can be characterized as being NEW, MANAGED, DETACHED or REMOVED.
|
||||||
|
|
||||||
|
- A NEW document instance has no persistent identity, and is not yet
|
||||||
|
associated with a DocumentManager and a UnitOfWork (i.e. those just
|
||||||
|
created with the "new" operator).
|
||||||
|
- A MANAGED document instance is an instance with a persistent
|
||||||
|
identity that is associated with a DocumentManager and whose
|
||||||
|
persistence is thus managed.
|
||||||
|
- A DETACHED document instance is an instance with a persistent
|
||||||
|
identity that is not (or no longer) associated with a
|
||||||
|
DocumentManager and a UnitOfWork.
|
||||||
|
- A REMOVED document instance is an instance with a persistent
|
||||||
|
identity, associated with a DocumentManager, that will be removed
|
||||||
|
from the database upon transaction commit.
|
||||||
|
|
||||||
|
Persistent fields
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The persistent state of a document is represented by instance
|
||||||
|
variables. An instance variable must be directly accessed only from
|
||||||
|
within the methods of the document by the document instance itself.
|
||||||
|
Instance variables must not be accessed by clients of the document.
|
||||||
|
The state of the document is available to clients only through the
|
||||||
|
document's methods, i.e. accessor methods (getter/setter methods) or
|
||||||
|
other business methods.
|
||||||
|
|
||||||
|
Collection-valued persistent fields and properties must be defined
|
||||||
|
in terms of the ``Doctrine\Common\Collections\Collection``
|
||||||
|
interface. The collection implementation type may be used by the
|
||||||
|
application to initialize fields or properties before the document is
|
||||||
|
made persistent. Once the document becomes managed (or detached),
|
||||||
|
subsequent access must be through the interface type.
|
||||||
|
|
||||||
|
Serializing documents
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Serializing documents can be problematic and is not really
|
||||||
|
recommended, at least not as long as a document instance still holds
|
||||||
|
references to proxy objects or is still managed by an
|
||||||
|
DocumentManager. If you intend to serialize (and unserialize) document
|
||||||
|
instances that still hold references to proxy objects you may run
|
||||||
|
into problems with private properties because of technical
|
||||||
|
limitations. Proxy objects implement ``__sleep`` and it is not
|
||||||
|
possible for ``__sleep`` to return names of private properties in
|
||||||
|
parent classes. On the other hand it is not a solution for proxy
|
||||||
|
objects to implement ``Serializable`` because Serializable does not
|
||||||
|
work well with any potential cyclic object references (at least we
|
||||||
|
did not find a way yet, if you did, please contact us).
|
||||||
|
|
||||||
|
The DocumentManager
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
The ``DocumentManager`` class is a central access point to the ODM
|
||||||
|
functionality provided by Doctrine. The ``DocumentManager`` API is
|
||||||
|
used to manage the persistence of your objects and to query for
|
||||||
|
persistent objects.
|
||||||
|
|
||||||
|
Transactional write-behind
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
An ``DocumentManager`` and the underlying ``UnitOfWork`` employ a
|
||||||
|
strategy called "transactional write-behind" that delays the
|
||||||
|
execution of query statements in order to execute them in the most
|
||||||
|
efficient way and to execute them at the end of a transaction so
|
||||||
|
that all write locks are quickly released. You should see Doctrine
|
||||||
|
as a tool to synchronize your in-memory objects with the database
|
||||||
|
in well defined units of work. Work with your objects and modify
|
||||||
|
them as usual and when you're done call ``DocumentManager#flush()``
|
||||||
|
to make your changes persistent.
|
||||||
|
|
||||||
|
The Unit of Work
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Internally an ``DocumentManager`` uses a ``UnitOfWork``, which is a
|
||||||
|
typical implementation of the
|
||||||
|
`Unit of Work pattern <http://martinfowler.com/eaaCatalog/unitOfWork.html>`_,
|
||||||
|
to keep track of all the things that need to be done the next time
|
||||||
|
``flush`` is invoked. You usually do not directly interact with a
|
||||||
|
``UnitOfWork`` but with the ``DocumentManager`` instead.
|
|
@ -0,0 +1,621 @@
|
||||||
|
Basic Mapping
|
||||||
|
=============
|
||||||
|
|
||||||
|
This chapter explains the basic mapping of objects and properties.
|
||||||
|
Mapping of references and embedded documents will be covered in the
|
||||||
|
next chapter "Reference Mapping".
|
||||||
|
|
||||||
|
Mapping Drivers
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Doctrine provides several different ways for specifying object
|
||||||
|
document mapping metadata:
|
||||||
|
|
||||||
|
- Docblock Annotations
|
||||||
|
- XML
|
||||||
|
- YAML
|
||||||
|
- Raw PHP Code
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you're wondering which mapping driver gives the best
|
||||||
|
performance, the answer is: None. Once the metadata of a class has
|
||||||
|
been read from the source (annotations, xml or yaml) it is stored
|
||||||
|
in an instance of the
|
||||||
|
``Doctrine\ODM\MongoDB\Mapping\ClassMetadata`` class and these
|
||||||
|
instances are stored in the metadata cache. Therefore at the end of
|
||||||
|
the day all drivers perform equally well. If you're not using a
|
||||||
|
metadata cache (not recommended!) then the XML driver might have a
|
||||||
|
slight edge in performance due to the powerful native XML support
|
||||||
|
in PHP.
|
||||||
|
|
||||||
|
Introduction to Docblock Annotations
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
You've probably used docblock annotations in some form already,
|
||||||
|
most likely to provide documentation metadata for a tool like
|
||||||
|
``PHPDocumentor`` (@author, @link, ...). Docblock annotations are a
|
||||||
|
tool to embed metadata inside the documentation section which can
|
||||||
|
then be processed by some tool. Doctrine generalizes the concept of
|
||||||
|
docblock annotations so that they can be used for any kind of
|
||||||
|
metadata and so that it is easy to define new docblock annotations.
|
||||||
|
In order to allow more involved annotation values and to reduce the
|
||||||
|
chances of clashes with other docblock annotations, the Doctrine
|
||||||
|
docblock annotations feature an alternative syntax that is heavily
|
||||||
|
inspired by the Annotation syntax introduced in Java 5.
|
||||||
|
|
||||||
|
The implementation of these enhanced docblock annotations is
|
||||||
|
located in the ``Doctrine\Common\Annotations`` namespace and
|
||||||
|
therefore part of the Common package. Doctrine docblock annotations
|
||||||
|
support namespaces and nested annotations among other things. The
|
||||||
|
Doctrine MongoDB ODM defines its own set of docblock annotations
|
||||||
|
for supplying object document mapping metadata.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you're not comfortable with the concept of docblock
|
||||||
|
annotations, don't worry, as mentioned earlier Doctrine 2 provides
|
||||||
|
XML and YAML alternatives and you could easily implement your own
|
||||||
|
favorite mechanism for defining ORM metadata.
|
||||||
|
|
||||||
|
Persistent classes
|
||||||
|
------------------
|
||||||
|
|
||||||
|
In order to mark a class for object-relational persistence it needs
|
||||||
|
to be designated as a document. This can be done through the
|
||||||
|
``@Document`` marker annotation.
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
<document name="Documents\User">
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
Documents\User:
|
||||||
|
type: document
|
||||||
|
|
||||||
|
By default, the document will be persisted to a database named
|
||||||
|
doctrine and a collection with the same name as the class name. In
|
||||||
|
order to change that, you can use the ``db`` and ``collection``
|
||||||
|
option as follows:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/** @Document(db="my_db", collection="users") */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
<document name="Documents\User" db="my_db" collection="users">
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
Documents\User:
|
||||||
|
type: document
|
||||||
|
db: my_db
|
||||||
|
collection: users
|
||||||
|
|
||||||
|
Now instances of ``Documents\User`` will be persisted into a
|
||||||
|
collection named ``users`` in the database ``my_db``.
|
||||||
|
|
||||||
|
If you want to omit the db attribute you can configure the default db
|
||||||
|
to use with the ``setDefaultDB`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$config->setDefaultDB('my_db');
|
||||||
|
|
||||||
|
.. _doctrine_mapping_types:
|
||||||
|
|
||||||
|
Doctrine Mapping Types
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
A Doctrine Mapping Type defines the mapping between a PHP type and
|
||||||
|
an MongoDB type. You can even write your own custom mapping types.
|
||||||
|
|
||||||
|
Here is a quick overview of the built-in mapping types:
|
||||||
|
|
||||||
|
- ``bin``
|
||||||
|
- ``bin_bytearray``
|
||||||
|
- ``bin_custom``
|
||||||
|
- ``bin_func``
|
||||||
|
- ``bin_md5``
|
||||||
|
- ``bin_uuid``
|
||||||
|
- ``boolean``
|
||||||
|
- ``collection``
|
||||||
|
- ``custom_id``
|
||||||
|
- ``date``
|
||||||
|
- ``file``
|
||||||
|
- ``float``
|
||||||
|
- ``hash``
|
||||||
|
- ``id``
|
||||||
|
- ``int``
|
||||||
|
- ``key``
|
||||||
|
- ``object_id``
|
||||||
|
- ``raw``
|
||||||
|
- ``string``
|
||||||
|
- ``timestamp``
|
||||||
|
|
||||||
|
You can read more about the available MongoDB types on `php.net <http://us.php.net/manual/en/mongo.types.php>`_.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The Doctrine mapping types are used to convert the local PHP types to the MongoDB types
|
||||||
|
when persisting so that your domain is not bound to MongoDB-specific types. For example a
|
||||||
|
DateTime instance may be converted to MongoDate when you persist your documents, and vice
|
||||||
|
versa during hydration.
|
||||||
|
|
||||||
|
Generally, the name of each built-in mapping type hints as to how the value will be converted.
|
||||||
|
This list explains some of the less obvious mapping types:
|
||||||
|
|
||||||
|
- ``bin``: string to MongoBinData instance with a "generic" type (default)
|
||||||
|
- ``bin_bytearray``: string to MongoBinData instance with a "byte array" type
|
||||||
|
- ``bin_custom``: string to MongoBinData instance with a "custom" type
|
||||||
|
- ``bin_func``: string to MongoBinData instance with a "function" type
|
||||||
|
- ``bin_md5``: string to MongoBinData instance with a "md5" type
|
||||||
|
- ``bin_uuid``: string to MongoBinData instance with a "uuid" type
|
||||||
|
- ``collection``: numerically indexed array to MongoDB array
|
||||||
|
- ``date``: DateTime to MongoDate
|
||||||
|
- ``hash``: associative array to MongoDB object
|
||||||
|
- ``id``: string to MongoId by default, but other formats are possible
|
||||||
|
- ``timestamp``: string to MongoTimestamp
|
||||||
|
- ``raw``: any type
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you are using the hash type, values within the associative array are
|
||||||
|
passed to MongoDB directly, without being prepared. Only formats suitable for
|
||||||
|
the Mongo driver should be used. If your hash contains values which are not
|
||||||
|
suitable you should either use an embedded document or use formats provided
|
||||||
|
by the MongoDB driver (e.g. ``\MongoDate`` instead of ``\DateTime``).
|
||||||
|
|
||||||
|
Property Mapping
|
||||||
|
----------------
|
||||||
|
|
||||||
|
After a class has been marked as a document it can specify
|
||||||
|
mappings for its instance fields. Here we will only look at simple
|
||||||
|
fields that hold scalar values like strings, numbers, etc.
|
||||||
|
References to other objects and embedded objects are covered in the
|
||||||
|
chapter "Reference Mapping".
|
||||||
|
|
||||||
|
.. _basic_mapping_identifiers:
|
||||||
|
|
||||||
|
Identifiers
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
Every document class needs an identifier. You designate the field
|
||||||
|
that serves as the identifier with the ``@Id`` marker annotation.
|
||||||
|
Here is an example:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
private $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
<document name="Documents\User">
|
||||||
|
<field fieldName="id" id="true" />
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
Documents\User:
|
||||||
|
fields:
|
||||||
|
id:
|
||||||
|
type: id
|
||||||
|
id: true
|
||||||
|
|
||||||
|
You can configure custom ID strategies if you don't want to use the default MongoId.
|
||||||
|
The available strategies are:
|
||||||
|
|
||||||
|
- ``AUTO`` - Uses the native generated MongoId.
|
||||||
|
- ``ALNUM`` - Generates an alpha-numeric string (based on an incrementing value).
|
||||||
|
- ``CUSTOM`` - Defers generation to a AbstractIdGenerator implementation specified in the ``class`` option.
|
||||||
|
- ``INCREMENT`` - Uses another collection to auto increment an integer identifier.
|
||||||
|
- ``UUID`` - Generates a UUID identifier.
|
||||||
|
- ``NONE`` - Do not generate any identifier. ID must be manually set.
|
||||||
|
|
||||||
|
Here is an example how to manually set a string identifier for your documents:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** Document */
|
||||||
|
class MyPersistentClass
|
||||||
|
{
|
||||||
|
/** @Id(strategy="NONE", type="string") */
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
public function setId($id)
|
||||||
|
{
|
||||||
|
$this->id = $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
//...
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
|
||||||
|
<document name="MyPersistentClass">
|
||||||
|
<field name="id" id="true" strategy="NONE" type="string" />
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
MyPersistentClass:
|
||||||
|
fields:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
id: true
|
||||||
|
strategy: NONE
|
||||||
|
|
||||||
|
When using the ``NONE`` strategy you will have to explicitly set an id before persisting the document:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
//...
|
||||||
|
|
||||||
|
$document = new MyPersistentClass();
|
||||||
|
$document->setId('my_unique_identifier');
|
||||||
|
$dm->persist($document);
|
||||||
|
$dm->flush();
|
||||||
|
|
||||||
|
Now you can retrieve the document later:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
//...
|
||||||
|
|
||||||
|
$document = $dm->find('MyPersistentClass', 'my_unique_identifier');
|
||||||
|
|
||||||
|
You can define your own ID generator by extending the
|
||||||
|
``Doctrine\ODM\MongoDB\Id\AbstractIdGenerator`` class and specifying the class
|
||||||
|
as an option for the ``CUSTOM`` strategy:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** Document */
|
||||||
|
class MyPersistentClass
|
||||||
|
{
|
||||||
|
/** @Id(strategy="CUSTOM", type="string", options={"class"="Vendor\Specific\Generator"}) */
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
public function setId($id)
|
||||||
|
{
|
||||||
|
$this->id = $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
//...
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
|
||||||
|
<document name="MyPersistentClass">
|
||||||
|
<field name="id" id="true" strategy="CUSTOM" type="string">
|
||||||
|
<id-generator-option name="class" value="Vendor\Specific\Generator" />
|
||||||
|
</field>
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
MyPersistentClass:
|
||||||
|
fields:
|
||||||
|
id:
|
||||||
|
id: true
|
||||||
|
strategy: CUSTOM
|
||||||
|
type: string
|
||||||
|
options:
|
||||||
|
class: Vendor\Specific\Generator
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Fields
|
||||||
|
~~~~~~
|
||||||
|
|
||||||
|
To mark a property for document persistence the ``@Field`` docblock
|
||||||
|
annotation can be used. This annotation usually requires at least 1
|
||||||
|
attribute to be set, the ``type``. The ``type`` attribute specifies
|
||||||
|
the Doctrine Mapping Type to use for the field. If the type is not
|
||||||
|
specified, 'string' is used as the default mapping type since it is
|
||||||
|
the most flexible.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
private $username;
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
<document name="Documents\User">
|
||||||
|
<field fieldName="id" id="true" />
|
||||||
|
<field fieldName="username" type="string" />
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
Documents\User:
|
||||||
|
fields:
|
||||||
|
id:
|
||||||
|
type: id
|
||||||
|
id: true
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
In that example we mapped the property ``id`` to the field ``id``
|
||||||
|
using the mapping type ``id`` and the property ``name`` is mapped
|
||||||
|
to the field ``name`` with the default mapping type ``string``. As
|
||||||
|
you can see, by default the mongo field names are assumed to be the
|
||||||
|
same as the property names. To specify a different name for the
|
||||||
|
field, you can use the ``name`` attribute of the Field annotation
|
||||||
|
as follows:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Field(name="db_name") */
|
||||||
|
private $name;
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<field fieldName="name" name="db_name" />
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
name:
|
||||||
|
name: db_name
|
||||||
|
|
||||||
|
Custom Mapping Types
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Doctrine allows you to create new mapping types. This can come in
|
||||||
|
handy when you're missing a specific mapping type or when you want
|
||||||
|
to replace the existing implementation of a mapping type.
|
||||||
|
|
||||||
|
In order to create a new mapping type you need to subclass
|
||||||
|
``Doctrine\ODM\MongoDB\Types\Type`` and implement/override
|
||||||
|
the methods. Here is an example skeleton of such a custom type
|
||||||
|
class:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace My\Project\Types;
|
||||||
|
|
||||||
|
use Doctrine\ODM\MongoDB\Types\Type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* My custom datatype.
|
||||||
|
*/
|
||||||
|
class MyType extends Type
|
||||||
|
{
|
||||||
|
public function convertToPHPValue($value)
|
||||||
|
{
|
||||||
|
// Note: this function is only called when your custom type is used
|
||||||
|
// as an identifier. For other cases, closureToPHP() will be called.
|
||||||
|
return new \DateTime('@' . $value->sec);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function closureToPHP()
|
||||||
|
{
|
||||||
|
// Return the string body of a PHP closure that will receive $value
|
||||||
|
// and store the result of a conversion in a $return variable
|
||||||
|
return '$return = new \DateTime($value);';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function convertToDatabaseValue($value)
|
||||||
|
{
|
||||||
|
// This is called to convert a PHP value to its Mongo equivalent
|
||||||
|
return new \MongoDate($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Restrictions to keep in mind:
|
||||||
|
|
||||||
|
-
|
||||||
|
If the value of the field is *NULL* the method
|
||||||
|
``convertToDatabaseValue()`` is not called.
|
||||||
|
-
|
||||||
|
The ``UnitOfWork`` never passes values to the database convert
|
||||||
|
method that did not change in the request.
|
||||||
|
|
||||||
|
When you have implemented the type you still need to let Doctrine
|
||||||
|
know about it. This can be achieved through the
|
||||||
|
``Doctrine\ODM\MongoDB\Types\Type#registerType($name, $class)``
|
||||||
|
method.
|
||||||
|
|
||||||
|
Here is an example:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// in bootstrapping code
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
use Doctrine\ODM\MongoDB\Types\Type;
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// Register my type
|
||||||
|
Type::addType('mytype', 'My\Project\Types\MyType');
|
||||||
|
|
||||||
|
As can be seen above, when registering the custom types in the
|
||||||
|
configuration you specify a unique name for the mapping type and
|
||||||
|
map that to the corresponding |FQCN|. Now you can use your new
|
||||||
|
type in your mapping like this:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class MyPersistentClass
|
||||||
|
{
|
||||||
|
/** @Field(type="mytype") */
|
||||||
|
private $field;
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<field fieldName="field" type="mytype" />
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
field:
|
||||||
|
type: mytype
|
||||||
|
|
||||||
|
Multiple Document Types in a Collection
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
You can easily store multiple types of documents in a single collection. This
|
||||||
|
requires specifying the same collection name, ``discriminatorField``, and
|
||||||
|
(optionally) ``discriminatorMap`` mapping options for each class that will share
|
||||||
|
the collection. Here is an example:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document(collection="my_documents")
|
||||||
|
* @DiscriminatorField("type")
|
||||||
|
* @DiscriminatorMap({"article"="Article", "album"="Album"})
|
||||||
|
*/
|
||||||
|
class Article
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document(collection="my_documents")
|
||||||
|
* @DiscriminatorField("type")
|
||||||
|
* @DiscriminatorMap({"article"="Article", "album"="Album"})
|
||||||
|
*/
|
||||||
|
class Album
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
All instances of ``Article`` and ``Album`` will be stored in the
|
||||||
|
``my_documents`` collection. You can query for the documents of a particular
|
||||||
|
class just like you normally would and the results will automatically be limited
|
||||||
|
based on the discriminator value for that class.
|
||||||
|
|
||||||
|
If you wish to query for multiple types of documents from the collection, you
|
||||||
|
may pass an array of document class names when creating a query builder:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$query = $dm->createQuery(array('Article', 'Album'));
|
||||||
|
$documents = $query->execute();
|
||||||
|
|
||||||
|
The above will return a cursor that will allow you to iterate over all
|
||||||
|
``Article`` and ``Album`` documents in the collections.
|
||||||
|
|
||||||
|
.. |FQCN| raw:: html
|
||||||
|
<abbr title="Fully-Qualified Class Name">FQCN</abbr>
|
|
@ -0,0 +1,70 @@
|
||||||
|
Best Practices
|
||||||
|
==============
|
||||||
|
|
||||||
|
Here are some best practices you can follow when working with the Doctrine MongoDB ODM.
|
||||||
|
|
||||||
|
Constrain relationships as much as possible
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
It is important to constrain relationships as much as possible. This means:
|
||||||
|
|
||||||
|
- Impose a traversal direction (avoid bidirectional associations if possible)
|
||||||
|
- Eliminate nonessential associations
|
||||||
|
|
||||||
|
This has several benefits:
|
||||||
|
|
||||||
|
- Reduced coupling in your domain model
|
||||||
|
- Simpler code in your domain model (no need to maintain bidirectionality properly)
|
||||||
|
- Less work for Doctrine
|
||||||
|
|
||||||
|
Use events judiciously
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
The event system of Doctrine is great and fast. Even though making
|
||||||
|
heavy use of events, especially lifecycle events, can have a
|
||||||
|
negative impact on the performance of your application. Thus you
|
||||||
|
should use events judiciously.
|
||||||
|
|
||||||
|
Use cascades judiciously
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Automatic cascades of the persist/remove/merge/etc. operations are
|
||||||
|
very handy but should be used wisely. Do NOT simply add all
|
||||||
|
cascades to all associations. Think about which cascades actually
|
||||||
|
do make sense for you for a particular association, given the
|
||||||
|
scenarios it is most likely used in.
|
||||||
|
|
||||||
|
Don't use special characters
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Avoid using any non-ASCII characters in class, field, table or
|
||||||
|
column names. Doctrine itself is not unicode-safe in many places
|
||||||
|
and will not be until PHP itself is fully unicode-aware.
|
||||||
|
|
||||||
|
Initialize collections in the constructor
|
||||||
|
-----------------------------------------
|
||||||
|
|
||||||
|
It is recommended best practice to initialize any business
|
||||||
|
collections in documents in the constructor.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MyProject\Model;
|
||||||
|
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
private $addresses;
|
||||||
|
private $articles;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->addresses = new ArrayCollection;
|
||||||
|
$this->articles = new ArrayCollection;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,218 @@
|
||||||
|
Bi-Directional References
|
||||||
|
=========================
|
||||||
|
|
||||||
|
By default when you map a bi-directional reference, the reference is maintained on both sides
|
||||||
|
of the relationship and there is not a single "owning side". Both sides are considered owning
|
||||||
|
and changes are tracked and persisted separately. Here is an example:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class BlogPost
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
/** @ReferenceOne(targetDocument="User") */
|
||||||
|
private $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
/** @ReferenceMany(targetDocument="BlogPost") */
|
||||||
|
private $posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
When I persist some instances of the above classes the references would exist on both sides! The
|
||||||
|
``BlogPost`` collection would have a `DBRef`_ stored on the ``$user`` property and the ``User``
|
||||||
|
collection would have a `DBRef`_ stored in the ``$posts`` property.
|
||||||
|
|
||||||
|
Owning and Inverse Sides
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
A user may have lots of posts and we don't need to store a reference to each post on the user, we
|
||||||
|
can get the users post by running a query like the following:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
db.BlogPost.find({ 'user.$id' : user.id })
|
||||||
|
|
||||||
|
In order to map this you can use the ``inversedBy`` and ``mappedBy`` options. Here is the same
|
||||||
|
example above where we implement this:
|
||||||
|
|
||||||
|
One to Many
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class BlogPost
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
/** @ReferenceOne(targetDocument="User", inversedBy="posts") */
|
||||||
|
private $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
/** @ReferenceMany(targetDocument="BlogPost", mappedBy="user") */
|
||||||
|
private $posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
So now when we persist a ``User`` and multiple ``BlogPost`` instances for that ``User``:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$user = new User();
|
||||||
|
|
||||||
|
$post1 = new BlogPost();
|
||||||
|
$post1->setUser($user);
|
||||||
|
|
||||||
|
$post2 = new BlogPost();
|
||||||
|
$post2->setUser($user);
|
||||||
|
|
||||||
|
$post3 = new BlogPost();
|
||||||
|
$post3->setUser($user);
|
||||||
|
|
||||||
|
$dm->persist($post1);
|
||||||
|
$dm->persist($post2);
|
||||||
|
$dm->persist($post3);
|
||||||
|
$dm->flush();
|
||||||
|
|
||||||
|
And we retrieve the ``User`` later to access the posts for that user:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$user = $dm->find('User', $user->id);
|
||||||
|
|
||||||
|
$posts = $user->getPosts();
|
||||||
|
foreach ($posts as $post) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
The above will execute a query like the following to lazily load the collection of posts to
|
||||||
|
iterate over:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
db.BlogPost.find( { 'user.$id' : user.id } )
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Remember that the inverse side, the side which specified ``mappedBy`` is immutable and
|
||||||
|
any changes to the state of the reference will not be persisted.
|
||||||
|
|
||||||
|
Other Examples
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Here are several examples which implement the ``inversedBy`` and ``mappedBy`` options:
|
||||||
|
|
||||||
|
One to One
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
Here is an example where we have a one to one relationship between ``Cart`` and ``Customer``:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class Cart
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceOne(targetDocument="Customer", inversedBy="cart")
|
||||||
|
*/
|
||||||
|
public $customer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class Customer
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceOne(targetDocument="Cart", mappedBy="customer")
|
||||||
|
*/
|
||||||
|
public $cart;
|
||||||
|
}
|
||||||
|
|
||||||
|
The owning side is on ``Cart.customer`` and the ``Customer.cart`` referenced is loaded with a query
|
||||||
|
like this:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
db.Cart.find( { 'customer.$id' : customer.id } )
|
||||||
|
|
||||||
|
If you want to nullify the relationship between a ``Cart`` instance and ``Customer`` instance
|
||||||
|
you must null it out on the ``Cart.customer`` side:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$cart->setCustomer(null);
|
||||||
|
$dm->flush();
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
When specifying inverse one-to-one relationships the referenced document is
|
||||||
|
loaded directly when the owning document is hydrated instead of using a
|
||||||
|
proxy. In the example above, loading a ``Customer`` object from the database
|
||||||
|
would also cause the corresponding ``Cart`` to be loaded. This can cause
|
||||||
|
performance issues when loading many ``Customer`` objects at once.
|
||||||
|
|
||||||
|
Self-Referencing Many to Many
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceMany(targetDocument="User", mappedBy="myFriends")
|
||||||
|
*/
|
||||||
|
public $friendsWithMe;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceMany(targetDocument="User", inversedBy="friendsWithMe")
|
||||||
|
*/
|
||||||
|
public $myFriends;
|
||||||
|
|
||||||
|
public function __construct($name)
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
$this->friendsWithMe = new \Doctrine\Common\Collections\ArrayCollection();
|
||||||
|
$this->myFriends = new \Doctrine\Common\Collections\ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addFriend(User $user)
|
||||||
|
{
|
||||||
|
$user->friendsWithMe[] = $this;
|
||||||
|
$this->myFriends[] = $user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.. _DBRef: https://docs.mongodb.com/manual/reference/database-references/#dbrefs
|
|
@ -0,0 +1,92 @@
|
||||||
|
Capped Collections
|
||||||
|
==================
|
||||||
|
|
||||||
|
Capped collections are fixed sized collections that have a very
|
||||||
|
high performance auto-LRU age-out feature (age out is based on
|
||||||
|
insertion order).
|
||||||
|
|
||||||
|
In addition, capped collections automatically, with high
|
||||||
|
performance, maintain insertion order for the objects in the
|
||||||
|
collection; this is very powerful for certain use cases such as
|
||||||
|
logging.
|
||||||
|
|
||||||
|
Mapping
|
||||||
|
-------
|
||||||
|
|
||||||
|
You can configure the collection in the ``collection`` attribute of
|
||||||
|
the ``@Document`` annotation:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document(collection={
|
||||||
|
* "name"="collname",
|
||||||
|
* "capped"=true,
|
||||||
|
* "size"=100000,
|
||||||
|
* "max"=1000
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
class Category
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
public $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
<document name="Documents\Category" collection="collname" capped-collection="true" capped-collection-size="100000" capped-collection-max="1000">
|
||||||
|
<field fieldName="id" id="true" />
|
||||||
|
<field fieldName="name" type="string" />
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
Documents\Category:
|
||||||
|
type: document
|
||||||
|
collection:
|
||||||
|
name: collname
|
||||||
|
capped: true
|
||||||
|
size: 100000
|
||||||
|
max: 1000
|
||||||
|
fields:
|
||||||
|
id:
|
||||||
|
type: id
|
||||||
|
id: true
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
Creating
|
||||||
|
--------
|
||||||
|
|
||||||
|
Remember that you must manually create the collections. If you let
|
||||||
|
MongoDB create the collection lazily the first time it is selected,
|
||||||
|
it will not be created with the capped configuration. You can
|
||||||
|
create the collection for a document with the ``SchemaManager``
|
||||||
|
that can be acquired from your ``DocumentManager`` instance:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$documentManager->getSchemaManager()->createDocumentCollection('Category');
|
||||||
|
|
||||||
|
You can drop the collection too if it already exists:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$documentManager->getSchemaManager()->dropDocumentCollection('Category');
|
|
@ -0,0 +1,150 @@
|
||||||
|
.. _change_tracking_policies:
|
||||||
|
|
||||||
|
Change Tracking Policies
|
||||||
|
========================
|
||||||
|
|
||||||
|
Change tracking is the process of determining what has changed in
|
||||||
|
managed documents since the last time they were synchronized with
|
||||||
|
the database.
|
||||||
|
|
||||||
|
Doctrine provides 3 different change tracking policies, each having
|
||||||
|
its particular advantages and disadvantages. The change tracking
|
||||||
|
policy can be defined on a per-class basis (or more precisely,
|
||||||
|
per-hierarchy).
|
||||||
|
|
||||||
|
Deferred Implicit
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The deferred implicit policy is the default change tracking policy
|
||||||
|
and the most convenient one. With this policy, Doctrine detects the
|
||||||
|
changes by a property-by-property comparison at commit time and
|
||||||
|
also detects changes to documents or new documents that are
|
||||||
|
referenced by other managed documents. Although the most convenient policy,
|
||||||
|
it can have negative effects on performance if you are dealing with large units
|
||||||
|
of work. Since Doctrine can't know what has changed, it needs to check
|
||||||
|
all managed documents for changes every time you invoke DocumentManager#flush(),
|
||||||
|
making this operation rather costly.
|
||||||
|
|
||||||
|
Deferred Explicit
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The deferred explicit policy is similar to the deferred implicit
|
||||||
|
policy in that it detects changes through a property-by-property
|
||||||
|
comparison at commit time. The difference is that only documents are
|
||||||
|
considered that have been explicitly marked for change detection
|
||||||
|
through a call to DocumentManager#persist(document) or through a save
|
||||||
|
cascade. All other documents are skipped. This policy therefore
|
||||||
|
gives improved performance for larger units of work while
|
||||||
|
sacrificing the behavior of "automatic dirty checking".
|
||||||
|
|
||||||
|
Therefore, flush() operations are potentially cheaper with this
|
||||||
|
policy. The negative aspect this has is that if you have a rather
|
||||||
|
large application and you pass your objects through several layers
|
||||||
|
for processing purposes and business tasks you may need to track
|
||||||
|
yourself which documents have changed on the way so you can pass
|
||||||
|
them to DocumentManager#persist().
|
||||||
|
|
||||||
|
This policy can be configured as follows:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document
|
||||||
|
* @ChangeTrackingPolicy("DEFERRED_EXPLICIT")
|
||||||
|
*/
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
Notify
|
||||||
|
~~~~~~
|
||||||
|
|
||||||
|
This policy is based on the assumption that the documents 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
|
||||||
|
namespace. As a guideline, such an implementation can look as
|
||||||
|
follows:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Doctrine\Common\NotifyPropertyChanged,
|
||||||
|
Doctrine\Common\PropertyChangedListener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document
|
||||||
|
* @ChangeTrackingPolicy("NOTIFY")
|
||||||
|
*/
|
||||||
|
class MyDocument implements NotifyPropertyChanged
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
private $_listeners = array();
|
||||||
|
|
||||||
|
public function addPropertyChangedListener(PropertyChangedListener $listener)
|
||||||
|
{
|
||||||
|
$this->_listeners[] = $listener;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Then, in each property setter of this class or derived classes, you
|
||||||
|
need to notify all the ``PropertyChangedListener`` instances. As an
|
||||||
|
example we add a convenience method on ``MyDocument`` that shows this
|
||||||
|
behavior:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
class MyDocument implements NotifyPropertyChanged
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
protected function _onPropertyChanged($propName, $oldValue, $newValue)
|
||||||
|
{
|
||||||
|
if ($this->_listeners) {
|
||||||
|
foreach ($this->_listeners as $listener) {
|
||||||
|
$listener->propertyChanged($this, $propName, $oldValue, $newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setData($data)
|
||||||
|
{
|
||||||
|
if ($data != $this->data) {
|
||||||
|
$this->_onPropertyChanged('data', $this->data, $data);
|
||||||
|
$this->data = $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
You have to invoke ``_onPropertyChanged`` inside every method that
|
||||||
|
changes the persistent state of ``MyDocument``.
|
||||||
|
|
||||||
|
The check whether the new value is different from the old one is
|
||||||
|
not mandatory but recommended. That way you also have full control
|
||||||
|
over when you consider a property changed.
|
||||||
|
|
||||||
|
The negative point of this policy is obvious: You need implement an
|
||||||
|
interface and write some plumbing code. But also note that we tried
|
||||||
|
hard to keep this notification functionality abstract. Strictly
|
||||||
|
speaking, it has nothing to do with the persistence layer. You may
|
||||||
|
find that property notification events come in handy in many other
|
||||||
|
scenarios as well. As mentioned earlier, the ``Doctrine\Common``
|
||||||
|
namespace is not that evil and consists solely of very small classes
|
||||||
|
and interfaces that have almost no external dependencies and that you can easily take with you should
|
||||||
|
you want to swap out the persistence layer. This change tracking policy
|
||||||
|
does not introduce a dependency on the Doctrine persistence
|
||||||
|
layer.
|
||||||
|
|
||||||
|
The positive point and main advantage of this policy is its
|
||||||
|
effectiveness. It has the best performance characteristics of the 3
|
||||||
|
policies with larger units of work and a flush() operation is very
|
||||||
|
cheap when nothing has changed.
|
|
@ -0,0 +1,152 @@
|
||||||
|
Complex References
|
||||||
|
==================
|
||||||
|
|
||||||
|
Sometimes you may want to access related documents using custom criteria or from
|
||||||
|
the inverse side of a relationship.
|
||||||
|
|
||||||
|
You can create an `immutable`_ reference to one or many documents and specify
|
||||||
|
how that reference is to be loaded. The reference is immutable in that it is
|
||||||
|
defined only in the mapping, unlike a typical reference where a `MongoDBRef`_ or
|
||||||
|
identifier (see :ref:`storing_references`) is stored on the document itself.
|
||||||
|
|
||||||
|
The following options may be used for :ref:`one <reference_one>` and
|
||||||
|
:ref:`many <reference_many>` reference mappings:
|
||||||
|
|
||||||
|
- ``criteria`` - Query criteria to apply to the cursor.
|
||||||
|
- ``repositoryMethod`` - The repository method used to create the cursor.
|
||||||
|
- ``sort`` - Sort criteria for the cursor.
|
||||||
|
- ``skip`` - Skip offset to apply to the cursor.
|
||||||
|
- ``limit`` - Limit to apply to the cursor.
|
||||||
|
|
||||||
|
Basic Example
|
||||||
|
-------------
|
||||||
|
|
||||||
|
In the following example, ``$comments`` will refer to all Comments for the
|
||||||
|
BlogPost and ``$last5Comments`` will refer to only the last five Comments. The
|
||||||
|
``mappedBy`` field is used to determine which Comment field should be used for
|
||||||
|
querying by the BlogPost's ID.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class BlogPost
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
/** @ReferenceMany(targetDocument="Comment", mappedBy="blogPost") */
|
||||||
|
private $comments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceMany(
|
||||||
|
* targetDocument="Comment",
|
||||||
|
* mappedBy="blogPost",
|
||||||
|
* sort={"date"="desc"},
|
||||||
|
* limit=5
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
private $last5Comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class Comment
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
/** @ReferenceOne(targetDocument="BlogPost", inversedBy="comments") */
|
||||||
|
private $blogPost;
|
||||||
|
}
|
||||||
|
|
||||||
|
You can also use ``mappedBy`` for referencing a single document, as in the
|
||||||
|
following example:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceOne(
|
||||||
|
* targetDocument="Comment",
|
||||||
|
* mappedBy="blogPost",
|
||||||
|
* sort={"date"="desc"}
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
private $lastComment;
|
||||||
|
|
||||||
|
|
||||||
|
``criteria`` Example
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Use ``criteria`` to further match referenced documents. In the following
|
||||||
|
example, ``$commentsByAdmin`` will refer only comments created by
|
||||||
|
administrators:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceMany(
|
||||||
|
* targetDocument="Comment",
|
||||||
|
* mappedBy="blogPost",
|
||||||
|
* criteria={"isByAdmin" : true}
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
private $commentsByAdmin;
|
||||||
|
|
||||||
|
``repositoryMethod`` Example
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Alternatively, you can use ``repositoryMethod`` to specify a custom method to
|
||||||
|
call on the Comment repository class to populate the reference.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceMany(
|
||||||
|
* targetDocument="Comment",
|
||||||
|
* mappedBy="blogPost",
|
||||||
|
* repositoryMethod="findSomeComments"
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
private $someComments;
|
||||||
|
|
||||||
|
The ``Comment`` class will need to have a custom repository class configured:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document(repositoryClass="CommentRepository") */
|
||||||
|
class Comment
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
Lastly, the ``CommentRepository`` class will need a ``findSomeComments()``
|
||||||
|
method which shall return ``Doctrine\MongoDB\CursorInterface``. When this method
|
||||||
|
is called to populate the reference, Doctrine will provide the Blogpost instance
|
||||||
|
(i.e. owning document) as the first argument:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class CommentRepository extends \Doctrine\ODM\MongoDB\DocumentRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return \Doctrine\ODM\MongoDB\Cursor
|
||||||
|
*/
|
||||||
|
public function findSomeComments(BlogPost $blogPost)
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder()
|
||||||
|
->field('blogPost')->references($blogPost);
|
||||||
|
->getQuery()->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.. _MongoDBRef: http://php.net/manual/en/class.mongodbref.php
|
||||||
|
.. _immutable: http://en.wikipedia.org/wiki/Immutable
|
|
@ -0,0 +1,54 @@
|
||||||
|
Console Commands
|
||||||
|
================
|
||||||
|
|
||||||
|
Doctrine MongoDB ODM offers some console commands, which utilize Symfony2's
|
||||||
|
Console component, to ease your development process:
|
||||||
|
|
||||||
|
- ``odm:clear-cache:metadata`` - Clear all metadata cache of the various cache drivers.
|
||||||
|
- ``odm:query`` - Query mongodb and inspect the outputted results from your document classes.
|
||||||
|
- ``odm:generate:documents`` - Generate document classes and method stubs from your mapping information.
|
||||||
|
- ``odm:generate:hydrators`` - Generates hydrator classes for document classes.
|
||||||
|
- ``odm:generate:proxies`` - Generates proxy classes for document classes.
|
||||||
|
- ``odm:generate:repositories`` - Generate repository classes from your mapping information.
|
||||||
|
- ``odm:schema:create`` - Allows you to create databases, collections and indexes for your documents
|
||||||
|
- ``odm:schema:drop`` - Allows you to drop databases, collections and indexes for your documents
|
||||||
|
- ``odm:schema:update`` - Allows you to update indexes for your documents
|
||||||
|
- ``odm:schema:shard`` - Allows you to enable sharding for your documents
|
||||||
|
|
||||||
|
Provided you have an existing ``DocumentManager`` instance, you can setup a
|
||||||
|
console command easily with the following code:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// mongodb.php
|
||||||
|
|
||||||
|
// ... include Composer autoloader and configure DocumentManager instance
|
||||||
|
|
||||||
|
$helperSet = new \Symfony\Component\Console\Helper\HelperSet(array(
|
||||||
|
'dm' => new \Doctrine\ODM\MongoDB\Tools\Console\Helper\DocumentManagerHelper($dm),
|
||||||
|
));
|
||||||
|
|
||||||
|
$app = new Application('Doctrine MongoDB ODM');
|
||||||
|
$app->setHelperSet($helperSet);
|
||||||
|
$app->addCommands(array(
|
||||||
|
new \Doctrine\ODM\MongoDB\Tools\Console\Command\GenerateDocumentsCommand(),
|
||||||
|
new \Doctrine\ODM\MongoDB\Tools\Console\Command\GenerateHydratorsCommand(),
|
||||||
|
new \Doctrine\ODM\MongoDB\Tools\Console\Command\GenerateProxiesCommand(),
|
||||||
|
new \Doctrine\ODM\MongoDB\Tools\Console\Command\GenerateRepositoriesCommand(),
|
||||||
|
new \Doctrine\ODM\MongoDB\Tools\Console\Command\QueryCommand(),
|
||||||
|
new \Doctrine\ODM\MongoDB\Tools\Console\Command\ClearCache\MetadataCommand(),
|
||||||
|
new \Doctrine\ODM\MongoDB\Tools\Console\Command\Schema\CreateCommand(),
|
||||||
|
new \Doctrine\ODM\MongoDB\Tools\Console\Command\Schema\DropCommand(),
|
||||||
|
new \Doctrine\ODM\MongoDB\Tools\Console\Command\Schema\UpdateCommand(),
|
||||||
|
new \Doctrine\ODM\MongoDB\Tools\Console\Command\Schema\ShardCommand(),
|
||||||
|
));
|
||||||
|
|
||||||
|
$app->run();
|
||||||
|
|
||||||
|
A reference implementation of the console command may be found in the
|
||||||
|
``tools/sandbox`` directory of the project repository. That command is
|
||||||
|
configured to store generated hydrators and proxies in the same directory, and
|
||||||
|
relies on the main project's Composer dependencies. You will want to customize
|
||||||
|
its configuration files if you intend to use it in your own project.
|
|
@ -0,0 +1,170 @@
|
||||||
|
.. _custom_collection:
|
||||||
|
|
||||||
|
Custom Collections
|
||||||
|
==================
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
This feature was introduced in version 1.1
|
||||||
|
|
||||||
|
By default, Doctrine uses ``ArrayCollection`` implementation of its ``Collection``
|
||||||
|
interface to hold both embedded and referenced documents. That collection may then
|
||||||
|
be wrapped by a ``PersistentCollection`` to allow for change tracking and other
|
||||||
|
persistence-related features.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class Application
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @EmbedMany(targetDocument="Section")
|
||||||
|
*/
|
||||||
|
private $sections;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->sections = new ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
For most cases this solution is sufficient but more sophisticated domains could use
|
||||||
|
their own collections (e.g. a collection that ensures its contained objects are sorted)
|
||||||
|
or to simply add common filtering methods that otherwise would otherwise be added to
|
||||||
|
owning document's class.
|
||||||
|
|
||||||
|
Custom Collection Classes
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
You may want to check `malarzm/collections <https://github.com/malarzm/collections>`_
|
||||||
|
which provides alternative implementations of Doctrine's ``Collection`` interface and
|
||||||
|
aims to kickstart development of your own collections.
|
||||||
|
|
||||||
|
Using your own ``Collection`` implementation is as simple as specifying the
|
||||||
|
``collectionClass`` parameter in the ``@EmbedMany`` or ``@ReferenceMany`` mapping
|
||||||
|
and ensuring that your custom class is initialized in the owning class' constructor:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class Application
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @EmbedMany(
|
||||||
|
* collectionClass="SectionCollection"
|
||||||
|
* targetDocument="Section"
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
private $sections;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->sections = new SectionCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
If you are satisfied with ``ArrayCollection`` and only want
|
||||||
|
to sprinkle it with some filtering methods, you may just extend it:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
|
||||||
|
class SectionCollection extends ArrayCollection
|
||||||
|
{
|
||||||
|
public function getEnabled()
|
||||||
|
{
|
||||||
|
return $this->filter(function(Section $s) {
|
||||||
|
return $s->isEnabled();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Alternatively, you may want to implement the whole class from scratch:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
|
||||||
|
class SectionCollection implements Collection
|
||||||
|
{
|
||||||
|
private $elements = array();
|
||||||
|
|
||||||
|
public function __construct(array $elements = array())
|
||||||
|
{
|
||||||
|
$this->elements = $elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
// your implementation of all methods interface requires
|
||||||
|
}
|
||||||
|
|
||||||
|
Taking Control of the Collection's Constructor
|
||||||
|
----------------------------------------------
|
||||||
|
|
||||||
|
By default, Doctrine assumes that it can instantiate your collections in same
|
||||||
|
manner as an ``ArrayCollection`` (i.e. the only parameter is an optional PHP
|
||||||
|
array); however, you may want to inject additional dependencies into your
|
||||||
|
custom collection class(es). This will require you to create a
|
||||||
|
`PersistentCollectionFactory implementation <https://github.com/doctrine/mongodb-odm/blob/master/lib/Doctrine/ODM/MongoDB/PersistentCollection/PersistentCollectionFactory.php>`_,
|
||||||
|
which Doctrine will then use to construct its persistent collections.
|
||||||
|
You may decide to implement this class from scratch or extend our
|
||||||
|
``AbstractPersistentCollectionFactory``:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Doctrine\ODM\MongoDB\PersistentCollection\AbstractPersistentCollectionFactory;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
|
||||||
|
final class YourPersistentCollectionFactory extends AbstractPersistentCollectionFactory
|
||||||
|
{
|
||||||
|
private $eventDispatcher;
|
||||||
|
|
||||||
|
public function __construct(EventDispatcherInterface $eventDispatcher)
|
||||||
|
{
|
||||||
|
$this->eventDispatcher = $eventDispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createCollectionClass($collectionClass)
|
||||||
|
{
|
||||||
|
switch ($collectionClass) {
|
||||||
|
case SectionCollection::class:
|
||||||
|
return new $collectionClass(array(), $this->eventDispatcher);
|
||||||
|
default:
|
||||||
|
return new $collectionClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
The factory class must then be registered in the ``Configuration``:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$eventDispatcher = $container->get('event_dispatcher');
|
||||||
|
$collFactory = new YourPersistentCollectionFactory($eventDispatcher);
|
||||||
|
$configuration = new Configuration();
|
||||||
|
// your other config here
|
||||||
|
$configuration->setPersistentCollectionFactory($collFactory);
|
|
@ -0,0 +1,210 @@
|
||||||
|
.. _document_repositories:
|
||||||
|
|
||||||
|
Document Repositories
|
||||||
|
=====================
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
A repository mediates between the domain and data mapping layers using a
|
||||||
|
collection-like interface for accessing domain objects.
|
||||||
|
|
||||||
|
In Doctrine, a repository is a class that concentrates code responsible for
|
||||||
|
querying and filtering your documents. ODM provides you with a default
|
||||||
|
``DocumentRepository`` for all of your documents:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/* @var $repository \Doctrine\ODM\MongoDB\DocumentRepository */
|
||||||
|
$repository = $documentManager->getRepository(User::class);
|
||||||
|
$disabledUsers = $repository->findBy(['disabled' => true, 'activated' => true]);
|
||||||
|
|
||||||
|
The array passed to ``findBy`` specifies the criteria for which documents are matched.
|
||||||
|
ODM will assist with converting PHP values to equivalent BSON types whenever possible:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$group = $documentManager->find(Group::class, 123);
|
||||||
|
/* @var $repository \Doctrine\ODM\MongoDB\DocumentRepository */
|
||||||
|
$repository = $documentManager->getRepository(User::class);
|
||||||
|
$usersInGroup = $repository->findBy(['group' => $group]);
|
||||||
|
|
||||||
|
The default repository implementation provides the following methods:
|
||||||
|
|
||||||
|
- ``find()`` - finds one document by its identifier. This may skip a database query
|
||||||
|
if the document is already managed by ODM.
|
||||||
|
- ``findAll()`` - finds all documents in the collection.
|
||||||
|
- ``findBy()`` - finds all documents matching the given criteria. Additional query
|
||||||
|
options may be specified (e.g. sort, limit, skip).
|
||||||
|
- ``findOneBy()`` - finds one document matching the given criteria.
|
||||||
|
- ``matching()`` - Finds all documents matching the given criteria, as expressed
|
||||||
|
with Doctrine's Criteria API.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
All above methods will include additional criteria specified by :ref:`Filters <filters>`.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Magic ``findBy`` and ``findOneBy`` calls described below are deprecated in 1.2 and
|
||||||
|
will be removed in 2.0.
|
||||||
|
|
||||||
|
Additional methods that are not defined explicitly in the repository class may also be
|
||||||
|
used if they follow a specific naming convention:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$group = $documentManager->find(Group::class, 123);
|
||||||
|
/* @var $repository \Doctrine\ODM\MongoDB\DocumentRepository */
|
||||||
|
$repository = $documentManager->getRepository(User::class);
|
||||||
|
$usersInGroup = $repository->findByGroup($group);
|
||||||
|
$randomUser = $repository->findOneByStatus('active');
|
||||||
|
|
||||||
|
In the above example, ``findByGroup()`` and ``findOneByStatus()`` will be handled by
|
||||||
|
the ``__call`` method, which intercepts calls to undefined methods. If the invoked
|
||||||
|
method's name starts with "findBy" or "findOneBy", ODM will attempt to infer mapped
|
||||||
|
properties from the remainder of the method name ("Group" or "Status" as per example).
|
||||||
|
The above calls are equivalent to:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$group = $documentManager->find(Group::class, 123);
|
||||||
|
/* @var $repository \Doctrine\ODM\MongoDB\DocumentRepository */
|
||||||
|
$repository = $documentManager->getRepository(User::class);
|
||||||
|
$usersInGroup = $repository->findBy(['group' => $group]);
|
||||||
|
$randomUser = $repository->findOneBy(['status' => 'active']);
|
||||||
|
|
||||||
|
Custom Repositories
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
A custom repository allows filtering logic to be consolidated into a single class instead
|
||||||
|
of spreading it throughout a project. A custom repository class may be specified for a
|
||||||
|
document class like so:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/** @Document(repositoryClass="Repositories\UserRepository") */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
/* ... */
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<document name="Documents\User" repository-class="Repositories\UserRepository">
|
||||||
|
<!-- ... -->
|
||||||
|
</document>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
Documents\User:
|
||||||
|
repositoryClass: Repositories\\UserRepository
|
||||||
|
collection: user
|
||||||
|
# ...
|
||||||
|
|
||||||
|
The next step is implementing your repository class. In most cases, ODM's default
|
||||||
|
``DocumentRepository`` class may be extended with additional methods that you need.
|
||||||
|
More complex cases that require passing additional dependencies to a custom repository
|
||||||
|
class will be discussed in the next section.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Repositories;
|
||||||
|
|
||||||
|
class UserRepository extends DocumentRepository
|
||||||
|
{
|
||||||
|
public function findDisabled()
|
||||||
|
{
|
||||||
|
return $this->findBy(['disabled' => true, 'activated' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
It is also possible to change ODM's default ``DocumentRepository`` to your own
|
||||||
|
implementation for all documents (unless overridden by the mapping):
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
$documentManager->getConfiguration()
|
||||||
|
->setDefaultRepositoryClassName(MyDefaultRepository::class);
|
||||||
|
|
||||||
|
Repositories with Additional Dependencies
|
||||||
|
-----------------------------------------
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Implementing your own RepositoryFactory is possible since version 1.0, but the
|
||||||
|
``AbstractRepositoryFactory`` class used in this example is only available since 1.2.
|
||||||
|
|
||||||
|
By default, Doctrine assumes that it can instantiate your repositories in same manner
|
||||||
|
as its default one:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Repositories;
|
||||||
|
|
||||||
|
class UserRepository extends DocumentRepository
|
||||||
|
{
|
||||||
|
public function __construct(DocumentManager $dm, UnitOfWork $uow, ClassMetadata $classMetadata)
|
||||||
|
{
|
||||||
|
/* constructor is inherited from DocumentRepository */
|
||||||
|
/* ... */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
In order to change the way Doctrine instantiates repositories, you will need to implement your own
|
||||||
|
`RepositoryFactory <https://github.com/doctrine/mongodb-odm/blob/master/lib/Doctrine/ODM/MongoDB/Repository/RepositoryFactory.php>`_
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Doctrine\ODM\MongoDB\Repository\AbstractRepositoryFactory;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
|
||||||
|
final class YourRepositoryFactory extends AbstractRepositoryFactory
|
||||||
|
{
|
||||||
|
private $eventDispatcher;
|
||||||
|
|
||||||
|
public function __construct(EventDispatcherInterface $eventDispatcher)
|
||||||
|
{
|
||||||
|
$this->eventDispatcher = $eventDispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function instantiateRepository($repositoryClassName, DocumentManager $documentManager, ClassMetadata $metadata)
|
||||||
|
{
|
||||||
|
switch ($repositoryClassName) {
|
||||||
|
case UserRepository::class:
|
||||||
|
return new UserRepository($this->eventDispatcher, $documentManager, $metadata);
|
||||||
|
default:
|
||||||
|
return new $repositoryClassName($documentManager, $documentManager->getUnitOfWork(), $metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
The factory class must then be registered in the ``Configuration``:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$eventDispatcher = $container->get('event_dispatcher');
|
||||||
|
$repoFactory = new YourRepositoryFactory($eventDispatcher);
|
||||||
|
$configuration = new Configuration();
|
||||||
|
// your other config here
|
||||||
|
$configuration->setRepositoryFactory($repoFactory);
|
|
@ -0,0 +1,46 @@
|
||||||
|
Eager Cursors
|
||||||
|
-------------
|
||||||
|
|
||||||
|
With a typical MongoDB cursor, it stays open during iteration and fetches
|
||||||
|
batches of documents as you iterate over the cursor. This isn't bad,
|
||||||
|
but sometimes you want to fetch all of the data eagerly. For example
|
||||||
|
when dealing with web applications, and you want to only show 50
|
||||||
|
documents from a collection you should fetch all the data in your
|
||||||
|
controller first before going on to the view.
|
||||||
|
|
||||||
|
Benefits:
|
||||||
|
|
||||||
|
- The cursor stays open for a much shorter period of time.
|
||||||
|
|
||||||
|
- Data retrieval and hydration are consolidated operations.
|
||||||
|
|
||||||
|
- Doctrine has the ability to retry the cursor when exceptions during interaction with mongodb are encountered.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('User')
|
||||||
|
->eagerCursor(true);
|
||||||
|
$query = $qb->getQuery();
|
||||||
|
$users = $query->execute(); // returns instance of Doctrine\MongoDB\ODM\EagerCursor
|
||||||
|
|
||||||
|
At this point all data is loaded from the database and cursors to MongoDB
|
||||||
|
have been closed but hydration of the data in to objects has not begun. Once
|
||||||
|
insertion starts the data will be hydrated in to PHP objects.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
foreach ($users as $user) {
|
||||||
|
echo $user->getUsername()."\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
Not all documents are converted to objects at once, the hydration is still done
|
||||||
|
one document at a time during iteration. The only change is that all data is retrieved
|
||||||
|
first.
|
|
@ -0,0 +1,283 @@
|
||||||
|
Embedded Mapping
|
||||||
|
================
|
||||||
|
|
||||||
|
This chapter explains how embedded documents are mapped in
|
||||||
|
Doctrine.
|
||||||
|
|
||||||
|
.. _embed_one:
|
||||||
|
|
||||||
|
Embed One
|
||||||
|
---------
|
||||||
|
|
||||||
|
Embed a single document:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
/** @EmbedOne(targetDocument="Address") */
|
||||||
|
private $address;
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @EmbeddedDocument */
|
||||||
|
class Address
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
<document name="Documents\User">
|
||||||
|
<embed-one field="address" target-document="Address" />
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
User:
|
||||||
|
type: document
|
||||||
|
embedOne:
|
||||||
|
address:
|
||||||
|
targetDocument: Address
|
||||||
|
|
||||||
|
Address:
|
||||||
|
type: embeddedDocument
|
||||||
|
|
||||||
|
.. _embed_many:
|
||||||
|
|
||||||
|
Embed Many
|
||||||
|
----------
|
||||||
|
|
||||||
|
Embed many documents:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
/** @EmbedMany(targetDocument="Phonenumber") */
|
||||||
|
private $phonenumbers = array();
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @EmbeddedDocument */
|
||||||
|
class Phonenumber
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
<document name="Documents\User">
|
||||||
|
<embed-many field="phonenumbers" target-document="Phonenumber" />
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
User:
|
||||||
|
type: document
|
||||||
|
embedMany:
|
||||||
|
phonenumbers:
|
||||||
|
targetDocument: Phonenumber
|
||||||
|
|
||||||
|
Phonenumber:
|
||||||
|
type: embeddedDocument
|
||||||
|
|
||||||
|
.. _embed_mixing_document_types:
|
||||||
|
|
||||||
|
Mixing Document Types
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
If you want to store different types of embedded documents in the same field,
|
||||||
|
you can simply omit the ``targetDocument`` option:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
// ..
|
||||||
|
|
||||||
|
/** @EmbedMany */
|
||||||
|
private $tasks = array();
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<embed-many field="tasks" />
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
embedMany:
|
||||||
|
tasks: ~
|
||||||
|
|
||||||
|
Now the ``$tasks`` property can store any type of document! The class name will
|
||||||
|
be automatically stored in a field named ``_doctrine_class_name`` within
|
||||||
|
the embedded document. The field name can be customized with the
|
||||||
|
``discriminatorField`` option:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
// ..
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @EmbedMany(discriminatorField="type")
|
||||||
|
*/
|
||||||
|
private $tasks = array();
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<embed-many field="tasks">
|
||||||
|
<discriminator-field name="type" />
|
||||||
|
</embed-many>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
embedMany:
|
||||||
|
tasks:
|
||||||
|
discriminatorField: type
|
||||||
|
|
||||||
|
You can also specify a discriminator map to avoid storing the |FQCN|
|
||||||
|
in each embedded document:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
// ..
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @EmbedMany(
|
||||||
|
* discriminatorMap={
|
||||||
|
* "download"="DownloadTask",
|
||||||
|
* "build"="BuildTask"
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
private $tasks = array();
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<embed-many field="tasks">
|
||||||
|
<discriminator-map>
|
||||||
|
<discriminator-mapping value="download" class="DownloadTask" />
|
||||||
|
<discriminator-mapping value="build" class="BuildTask" />
|
||||||
|
</discriminator-map>
|
||||||
|
</embed-many>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
embedMany:
|
||||||
|
tasks:
|
||||||
|
discriminatorMap:
|
||||||
|
download: DownloadTask
|
||||||
|
build: BuildTask
|
||||||
|
|
||||||
|
If you have embedded documents without a discriminator value that need to be
|
||||||
|
treated correctly you can optionally specify a default value for the
|
||||||
|
discriminator:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
// ..
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @EmbedMany(
|
||||||
|
* discriminatorMap={
|
||||||
|
* "download"="DownloadTask",
|
||||||
|
* "build"="BuildTask"
|
||||||
|
* },
|
||||||
|
* defaultDiscriminatorValue="download"
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
private $tasks = array();
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<embed-many field="tasks">
|
||||||
|
<discriminator-map>
|
||||||
|
<discriminator-mapping value="download" class="DownloadTask" />
|
||||||
|
<discriminator-mapping value="build" class="BuildTask" />
|
||||||
|
</discriminator-map>
|
||||||
|
<default-discriminator-value value="download" />
|
||||||
|
</embed-many>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
embedMany:
|
||||||
|
tasks:
|
||||||
|
discriminatorMap:
|
||||||
|
download: DownloadTask
|
||||||
|
build: BuildTask
|
||||||
|
defaultDiscriminatorValue: download
|
||||||
|
|
||||||
|
Cascading Operations
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
All operations on embedded documents are automatically cascaded.
|
||||||
|
This is because embedded documents are part of their parent
|
||||||
|
document and cannot exist without those by nature.
|
||||||
|
|
||||||
|
.. |FQCN| raw:: html
|
||||||
|
<abbr title="Fully-Qualified Class Name">FQCN</abbr>
|
|
@ -0,0 +1,717 @@
|
||||||
|
Events
|
||||||
|
======
|
||||||
|
|
||||||
|
Doctrine features a lightweight event system that is part of the
|
||||||
|
Common package.
|
||||||
|
|
||||||
|
The Event System
|
||||||
|
----------------
|
||||||
|
|
||||||
|
The event system is controlled by the ``EventManager``. It is the
|
||||||
|
central point of Doctrine's event listener system. Listeners are
|
||||||
|
registered on the manager and events are dispatched through the
|
||||||
|
manager.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$evm = new EventManager();
|
||||||
|
|
||||||
|
Now we can add some event listeners to the ``$evm``. Let's create a
|
||||||
|
``EventTest`` class to play around with.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class EventTest
|
||||||
|
{
|
||||||
|
const preFoo = 'preFoo';
|
||||||
|
const postFoo = 'postFoo';
|
||||||
|
|
||||||
|
private $_evm;
|
||||||
|
|
||||||
|
public $preFooInvoked = false;
|
||||||
|
public $postFooInvoked = false;
|
||||||
|
|
||||||
|
public function __construct($evm)
|
||||||
|
{
|
||||||
|
$evm->addEventListener(array(self::preFoo, self::postFoo), $this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function preFoo(EventArgs $e)
|
||||||
|
{
|
||||||
|
$this->preFooInvoked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function postFoo(EventArgs $e)
|
||||||
|
{
|
||||||
|
$this->postFooInvoked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new instance
|
||||||
|
$test = new EventTest($evm);
|
||||||
|
|
||||||
|
Events can be dispatched by using the ``dispatchEvent()`` method.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$evm->dispatchEvent(EventTest::preFoo);
|
||||||
|
$evm->dispatchEvent(EventTest::postFoo);
|
||||||
|
|
||||||
|
You can easily remove a listener with the ``removeEventListener()``
|
||||||
|
method.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$evm->removeEventListener(array(self::preFoo, self::postFoo), $this);
|
||||||
|
|
||||||
|
The Doctrine event system also has a simple concept of event
|
||||||
|
subscribers. We can define a simple ``TestEventSubscriber`` class
|
||||||
|
which implements the ``\Doctrine\Common\EventSubscriber`` interface
|
||||||
|
and implements a ``getSubscribedEvents()`` method which returns an
|
||||||
|
array of events it should be subscribed to.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class TestEventSubscriber implements \Doctrine\Common\EventSubscriber
|
||||||
|
{
|
||||||
|
const preFoo = 'preFoo';
|
||||||
|
|
||||||
|
public $preFooInvoked = false;
|
||||||
|
|
||||||
|
public function preFoo()
|
||||||
|
{
|
||||||
|
$this->preFooInvoked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSubscribedEvents()
|
||||||
|
{
|
||||||
|
return array(self::preFoo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$eventSubscriber = new TestEventSubscriber();
|
||||||
|
$evm->addEventSubscriber($eventSubscriber);
|
||||||
|
|
||||||
|
Now when you dispatch an event any event subscribers will be
|
||||||
|
notified for that event.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$evm->dispatchEvent(TestEventSubscriber::preFoo);
|
||||||
|
|
||||||
|
Now test the ``$eventSubscriber`` instance to see if the
|
||||||
|
``preFoo()`` method was invoked.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
if ($eventSubscriber->preFooInvoked) {
|
||||||
|
echo 'pre foo invoked!';
|
||||||
|
}
|
||||||
|
|
||||||
|
.. _lifecycle_events:
|
||||||
|
|
||||||
|
Lifecycle Events
|
||||||
|
----------------
|
||||||
|
|
||||||
|
The DocumentManager and UnitOfWork trigger several events during
|
||||||
|
the life-time of their registered documents.
|
||||||
|
|
||||||
|
-
|
||||||
|
preRemove - The preRemove event occurs for a given document before
|
||||||
|
the respective DocumentManager remove operation for that document
|
||||||
|
is executed.
|
||||||
|
-
|
||||||
|
postRemove - The postRemove event occurs for a document after the
|
||||||
|
document has been removed. It will be invoked after the database
|
||||||
|
delete operations.
|
||||||
|
-
|
||||||
|
prePersist - The prePersist event occurs for a given document
|
||||||
|
before the respective DocumentManager persist operation for that
|
||||||
|
document is executed.
|
||||||
|
-
|
||||||
|
postPersist - The postPersist event occurs for a document after
|
||||||
|
the document has been made persistent. It will be invoked after the
|
||||||
|
database insert operations. Generated primary key values are
|
||||||
|
available in the postPersist event.
|
||||||
|
-
|
||||||
|
preUpdate - The preUpdate event occurs before the database update
|
||||||
|
operations to document data.
|
||||||
|
-
|
||||||
|
postUpdate - The postUpdate event occurs after the database update
|
||||||
|
operations to document data.
|
||||||
|
-
|
||||||
|
preLoad - The preLoad event occurs for a document before the
|
||||||
|
document has been loaded into the current DocumentManager from the
|
||||||
|
database or after the refresh operation has been applied to it.
|
||||||
|
-
|
||||||
|
postLoad - The postLoad event occurs for a document after the
|
||||||
|
document has been loaded into the current DocumentManager from the
|
||||||
|
database or after the refresh operation has been applied to it.
|
||||||
|
-
|
||||||
|
loadClassMetadata - The loadClassMetadata event occurs after the
|
||||||
|
mapping metadata for a class has been loaded from a mapping source
|
||||||
|
(annotations/xml/yaml).
|
||||||
|
-
|
||||||
|
preFlush - The preFlush event occurs before the change-sets of all
|
||||||
|
managed documents are computed. This both a lifecycle call back and
|
||||||
|
and listener.
|
||||||
|
-
|
||||||
|
postFlush - The postFlush event occurs after the change-sets of all
|
||||||
|
managed documents are computed.
|
||||||
|
-
|
||||||
|
onFlush - The onFlush event occurs after the change-sets of all
|
||||||
|
managed documents are computed. This event is not a lifecycle
|
||||||
|
callback.
|
||||||
|
-
|
||||||
|
onClear - The onClear event occurs after the UnitOfWork has had
|
||||||
|
its state cleared.
|
||||||
|
-
|
||||||
|
documentNotFound - The documentNotFound event occurs when a proxy object
|
||||||
|
could not be initialized. This event is not a lifecycle callback.
|
||||||
|
-
|
||||||
|
postCollectionLoad - The postCollectionLoad event occurs just after
|
||||||
|
collection has been initialized (loaded) and before new elements
|
||||||
|
are re-added to it.
|
||||||
|
|
||||||
|
You can access the Event constants from the ``Events`` class in the
|
||||||
|
ODM package.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Doctrine\ODM\MongoDB\Events;
|
||||||
|
|
||||||
|
echo Events::preUpdate;
|
||||||
|
|
||||||
|
These can be hooked into by two different types of event
|
||||||
|
listeners:
|
||||||
|
|
||||||
|
-
|
||||||
|
Lifecycle Callbacks are methods on the document classes that are
|
||||||
|
called when the event is triggered. They receive instances
|
||||||
|
of ``Doctrine\ODM\MongoDB\Event\LifecycleEventArgs`` (see relevant
|
||||||
|
examples below) as arguments and are specifically designed to allow
|
||||||
|
changes inside the document classes state.
|
||||||
|
-
|
||||||
|
Lifecycle Event Listeners are classes with specific callback
|
||||||
|
methods that receives some kind of ``EventArgs`` instance which
|
||||||
|
give access to the document, DocumentManager or other relevant
|
||||||
|
data.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
All Lifecycle events that happen during the ``flush()`` of
|
||||||
|
a DocumentManager have very specific constraints on the allowed
|
||||||
|
operations that can be executed. Please read the
|
||||||
|
*Implementing Event Listeners* section very carefully to understand
|
||||||
|
which operations are allowed in which lifecycle event.
|
||||||
|
|
||||||
|
Lifecycle Callbacks
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
A lifecycle event is a regular event with the additional feature of
|
||||||
|
providing a mechanism to register direct callbacks inside the
|
||||||
|
corresponding document classes that are executed when the lifecycle
|
||||||
|
event occurs.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document @HasLifecycleCallbacks */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Field
|
||||||
|
*/
|
||||||
|
public $value;
|
||||||
|
|
||||||
|
/** @Field */
|
||||||
|
private $createdAt;
|
||||||
|
|
||||||
|
/** @PrePersist */
|
||||||
|
public function doStuffOnPrePersist(\Doctrine\ODM\MongoDB\Event\LifecycleEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
$this->createdAt = date('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @PrePersist */
|
||||||
|
public function doOtherStuffOnPrePersist(\Doctrine\ODM\MongoDB\Event\LifecycleEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
$this->value = 'changed from prePersist callback!';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @PostPersist */
|
||||||
|
public function doStuffOnPostPersist(\Doctrine\ODM\MongoDB\Event\LifecycleEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
$this->value = 'changed from postPersist callback!';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @PreLoad */
|
||||||
|
public function doStuffOnPreLoad(\Doctrine\ODM\MongoDB\Event\PreLoadEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
$data =& $eventArgs->getData();
|
||||||
|
$data['value'] = 'changed from preLoad callback';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @PostLoad */
|
||||||
|
public function doStuffOnPostLoad(\Doctrine\ODM\MongoDB\Event\LifecycleEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
$this->value = 'changed from postLoad callback!';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @PreUpdate */
|
||||||
|
public function doStuffOnPreUpdate(\Doctrine\ODM\MongoDB\Event\PreUpdateEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
$this->value = 'changed from preUpdate callback!';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @PreFlush */
|
||||||
|
public function preFlush(\Doctrine\ODM\MongoDB\Event\PreFlushEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
$this->value = 'changed from preFlush callback!';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Note that when using annotations you have to apply the
|
||||||
|
@HasLifecycleCallbacks marker annotation on the document class.
|
||||||
|
|
||||||
|
Listening to Lifecycle Events
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
Lifecycle event listeners are much more powerful than the simple
|
||||||
|
lifecycle callbacks that are defined on the document classes. They
|
||||||
|
allow to implement re-usable behaviours between different document
|
||||||
|
classes, yet require much more detailed knowledge about the inner
|
||||||
|
workings of the DocumentManager and UnitOfWork. Please read the
|
||||||
|
*Implementing Event Listeners* section carefully if you are trying
|
||||||
|
to write your own listener.
|
||||||
|
|
||||||
|
To register an event listener you have to hook it into the
|
||||||
|
EventManager that is passed to the DocumentManager factory:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$eventManager = new EventManager();
|
||||||
|
$eventManager->addEventListener(array(Events::preUpdate), new MyEventListener());
|
||||||
|
$eventManager->addEventSubscriber(new MyEventSubscriber());
|
||||||
|
|
||||||
|
$documentManager = DocumentManager::create($mongo, $config, $eventManager);
|
||||||
|
|
||||||
|
You can also retrieve the event manager instance after the
|
||||||
|
DocumentManager was created:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$documentManager->getEventManager()->addEventListener(array(Events::preUpdate), new MyEventListener());
|
||||||
|
$documentManager->getEventManager()->addEventSubscriber(new MyEventSubscriber());
|
||||||
|
|
||||||
|
Implementing Event Listeners
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
This section explains what is and what is not allowed during
|
||||||
|
specific lifecycle events of the UnitOfWork. Although you get
|
||||||
|
passed the DocumentManager in all of these events, you have to
|
||||||
|
follow this restrictions very carefully since operations in the
|
||||||
|
wrong event may produce lots of different errors, such as
|
||||||
|
inconsistent data and lost updates/persists/removes.
|
||||||
|
|
||||||
|
prePersist
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
Listen to the ``prePersist`` event:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$test = new EventTest();
|
||||||
|
$evm = $dm->getEventManager();
|
||||||
|
$evm->addEventListener(Events::prePersist, $test);
|
||||||
|
|
||||||
|
Define the ``EventTest`` class:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class EventTest
|
||||||
|
{
|
||||||
|
public function prePersist(\Doctrine\ODM\MongoDB\Event\LifecycleEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
$document = $eventArgs->getDocument();
|
||||||
|
$document->setSomething();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preLoad
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$test = new EventTest();
|
||||||
|
$evm = $dm->getEventManager();
|
||||||
|
$evm->addEventListener(Events::preLoad, $test);
|
||||||
|
|
||||||
|
Define the ``EventTest`` class with a ``preLoad()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class EventTest
|
||||||
|
{
|
||||||
|
public function preLoad(\Doctrine\ODM\MongoDB\Event\PreLoadEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
$data =& $eventArgs->getData();
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
postLoad
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$test = new EventTest();
|
||||||
|
$evm = $dm->getEventManager();
|
||||||
|
$evm->addEventListener(Events::postLoad, $test);
|
||||||
|
|
||||||
|
Define the ``EventTest`` class with a ``postLoad()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class EventTest
|
||||||
|
{
|
||||||
|
public function postLoad(\Doctrine\ODM\MongoDB\Event\LifecycleEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
$document = $eventArgs->getDocument();
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preRemove
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$test = new EventTest();
|
||||||
|
$evm = $dm->getEventManager();
|
||||||
|
$evm->addEventListener(Events::preRemove, $test);
|
||||||
|
|
||||||
|
Define the ``EventTest`` class with a ``preRemove()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class EventTest
|
||||||
|
{
|
||||||
|
public function preRemove(\Doctrine\ODM\MongoDB\Event\LifecycleEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
$document = $eventArgs->getDocument();
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preFlush
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$test = new EventTest();
|
||||||
|
$evm = $dm->getEventManager();
|
||||||
|
$evm->addEventListener(Events::preFlush, $test);
|
||||||
|
|
||||||
|
Define the ``EventTest`` class with a ``preFlush()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class EventTest
|
||||||
|
{
|
||||||
|
public function preFlush(\Doctrine\ODM\MongoDB\Event\PreFlushEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
$dm = $eventArgs->getDocumentManager();
|
||||||
|
$uow = $dm->getUnitOfWork();
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFlush
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$test = new EventTest();
|
||||||
|
$evm = $dm->getEventManager();
|
||||||
|
$evm->addEventListener(Events::onFlush, $test);
|
||||||
|
|
||||||
|
Define the ``EventTest`` class with a ``onFlush()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class EventTest
|
||||||
|
{
|
||||||
|
public function onFlush(\Doctrine\ODM\MongoDB\Event\OnFlushEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
$dm = $eventArgs->getDocumentManager();
|
||||||
|
$uow = $dm->getUnitOfWork();
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
postFlush
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$test = new EventTest();
|
||||||
|
$evm = $dm->getEventManager();
|
||||||
|
$evm->addEventListener(Events::postFlush, $test);
|
||||||
|
|
||||||
|
Define the ``EventTest`` class with a ``postFlush()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class EventTest
|
||||||
|
{
|
||||||
|
public function postFlush(\Doctrine\ODM\MongoDB\Event\PostFlushEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
$dm = $eventArgs->getDocumentManager();
|
||||||
|
$uow = $dm->getUnitOfWork();
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preUpdate
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$test = new EventTest();
|
||||||
|
$evm = $dm->getEventManager();
|
||||||
|
$evm->addEventListener(Events::preUpdate, $test);
|
||||||
|
|
||||||
|
Define the ``EventTest`` class with a ``preUpdate()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class EventTest
|
||||||
|
{
|
||||||
|
public function preUpdate(\Doctrine\ODM\MongoDB\Event\LifecycleEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
$document = $eventArgs->getDocument();
|
||||||
|
$document->setSomething();
|
||||||
|
$dm = $eventArgs->getDocumentManager();
|
||||||
|
$class = $dm->getClassMetadata(get_class($document));
|
||||||
|
$dm->getUnitOfWork()->recomputeSingleDocumentChangeSet($class, $document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you modify a document in the preUpdate event you must call ``recomputeSingleDocumentChangeSet``
|
||||||
|
for the modified document in order for the changes to be persisted.
|
||||||
|
|
||||||
|
onClear
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$test = new EventTest();
|
||||||
|
$evm = $dm->getEventManager();
|
||||||
|
$evm->addEventListener(Events::onClear, $test);
|
||||||
|
|
||||||
|
Define the ``EventTest`` class with a ``onClear()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class EventTest
|
||||||
|
{
|
||||||
|
public function onClear(\Doctrine\ODM\MongoDB\Event\OnClearEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
$class = $eventArgs->getDocumentClass();
|
||||||
|
$dm = $eventArgs->getDocumentManager();
|
||||||
|
$uow = $dm->getUnitOfWork();
|
||||||
|
|
||||||
|
// Check if event clears all documents.
|
||||||
|
if ($eventArgs->clearsAllDocuments()) {
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
documentNotFound
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$test = new EventTest();
|
||||||
|
$evm = $dm->getEventManager();
|
||||||
|
$evm->addEventListener(Events::documentNotFound, $test);
|
||||||
|
|
||||||
|
Define the ``EventTest`` class with a ``documentNotFound()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class EventTest
|
||||||
|
{
|
||||||
|
public function documentNotFound(\Doctrine\ODM\MongoDB\Event\DocumentNotFoundEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
$proxy = $eventArgs->getObject();
|
||||||
|
$identifier = $eventArgs->getIdentifier();
|
||||||
|
// do something
|
||||||
|
// To prevent the documentNotFound exception from being thrown, call the disableException() method:
|
||||||
|
$eventArgs->disableException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
postUpdate, postRemove, postPersist
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$test = new EventTest();
|
||||||
|
$evm = $dm->getEventManager();
|
||||||
|
$evm->addEventListener(Events::postUpdate, $test);
|
||||||
|
$evm->addEventListener(Events::postRemove, $test);
|
||||||
|
$evm->addEventListener(Events::postPersist, $test);
|
||||||
|
|
||||||
|
Define the ``EventTest`` class with a ``postUpdate()``, ``postRemove()`` and ``postPersist()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class EventTest
|
||||||
|
{
|
||||||
|
public function postUpdate(\Doctrine\ODM\MongoDB\Event\LifecycleEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function postRemove(\Doctrine\ODM\MongoDB\Event\LifecycleEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function postPersist(\Doctrine\ODM\MongoDB\Event\LifecycleEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
postCollectionLoad
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
This event was introduced in version 1.1
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$test = new EventTest();
|
||||||
|
$evm = $dm->getEventManager();
|
||||||
|
$evm->addEventListener(Events::postCollectionLoad, $test);
|
||||||
|
|
||||||
|
Define the ``EventTest`` class with a ``postCollectionLoad()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class EventTest
|
||||||
|
{
|
||||||
|
public function postCollectionLoad(\Doctrine\ODM\MongoDB\Event\PostCollectionLoadEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
$collection = $eventArgs->getCollection();
|
||||||
|
if ($collection instanceof \Malarzm\Collections\DiffableCollection) {
|
||||||
|
$collection->snapshot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Load ClassMetadata Event
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
When the mapping information for a document is read, it is
|
||||||
|
populated in to a ``ClassMetadata`` instance. You can hook in to
|
||||||
|
this process and manipulate the instance with the ``loadClassMetadata`` event:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$test = new EventTest();
|
||||||
|
$metadataFactory = $dm->getMetadataFactory();
|
||||||
|
$evm = $dm->getEventManager();
|
||||||
|
$evm->addEventListener(Events::loadClassMetadata, $test);
|
||||||
|
|
||||||
|
class EventTest
|
||||||
|
{
|
||||||
|
public function loadClassMetadata(\Doctrine\ODM\MongoDB\Event\LoadClassMetadataEventArgs $eventArgs)
|
||||||
|
{
|
||||||
|
$classMetadata = $eventArgs->getClassMetadata();
|
||||||
|
$fieldMapping = array(
|
||||||
|
'fieldName' => 'about',
|
||||||
|
'type' => 'string'
|
||||||
|
);
|
||||||
|
$classMetadata->mapField($fieldMapping);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
.. _filters:
|
||||||
|
|
||||||
|
Filters
|
||||||
|
=======
|
||||||
|
|
||||||
|
Doctrine features a filter system that allows the developer to add additional
|
||||||
|
criteria to queries, regardless of where the query is generated within the
|
||||||
|
application (e.g. from a query builder, loading referenced documents). This is
|
||||||
|
useful for excluding documents at a low level, to ensure that they are neither
|
||||||
|
returned from MongoDB nor hydrated by ODM.
|
||||||
|
|
||||||
|
Example filter class
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Throughout this document, the example ``MyLocaleFilter`` class will be used to
|
||||||
|
illustrate how the filter feature works. A filter class must extend the base
|
||||||
|
``Doctrine\ODM\MongoDB\Query\Filter\BsonFilter`` class and implement the
|
||||||
|
``addFilterCriteria()`` method. This method receives ``ClassMetadata`` and is
|
||||||
|
invoked whenever a query is prepared for any class. Since filters are typically
|
||||||
|
designed with a specific class or interface in mind, ``addFilterCriteria()``
|
||||||
|
will frequently start by checking ``ClassMetadata`` and returning immediately if
|
||||||
|
it is not supported.
|
||||||
|
|
||||||
|
Parameters for the query should be set on the filter object by calling the
|
||||||
|
``BsonFilter::setParameter()`` method. Within the filter class, parameters
|
||||||
|
should be accessed via ``BsonFilter::getParameter()``.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Vendor\Filter;
|
||||||
|
|
||||||
|
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
|
||||||
|
use Doctrine\ODM\MongoDB\Query\Filter\BsonFilter;
|
||||||
|
|
||||||
|
class MyLocaleFilter extends BsonFilter
|
||||||
|
{
|
||||||
|
public function addFilterCriteria(ClassMetadata $targetDocument)
|
||||||
|
{
|
||||||
|
// Check if the entity implements the LocalAware interface
|
||||||
|
if ( ! $targetDocument->reflClass->implementsInterface('LocaleAware')) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
return array('locale' => $this->getParameter('locale'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
-------------
|
||||||
|
Filter classes are added to the configuration as following:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$config->addFilter('locale', '\Vendor\Filter\MyLocaleFilter');
|
||||||
|
|
||||||
|
The ``Configuration#addFilter()`` method takes a name for the filter and the
|
||||||
|
name of the filter class, which will be constructed as necessary.
|
||||||
|
|
||||||
|
An optional third parameter may be used to set parameters at configuration time:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$config->addFilter('locale', '\Vendor\Filter\MyLocaleFilter', array('locale' => 'en'));
|
||||||
|
|
||||||
|
Disabling/Enabling Filters and Setting Parameters
|
||||||
|
-------------------------------------------------
|
||||||
|
|
||||||
|
Filters can be disabled and enabled via the ``FilterCollection``, which is
|
||||||
|
stored in the ``DocumentManager``. The ``FilterCollection#enable($name)`` method
|
||||||
|
may be used to enabled and return a filter, after which you may set parameters.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$filter = $dm->getFilterCollection()->enable("locale");
|
||||||
|
$filter->setParameter('locale', array('$in' => array('en', 'fr'));
|
||||||
|
|
||||||
|
// Disable the filter (perhaps temporarily to run an unfiltered query)
|
||||||
|
$filter = $dm->getFilterCollection()->disable("locale");
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Disabling and enabling filters has no effect on managed documents. If you
|
||||||
|
want to refresh or reload an object after having modified a filter or the
|
||||||
|
FilterCollection, then you should clear the DocumentManager and re-fetch
|
||||||
|
your documents so the new filtering rules may be applied.
|
|
@ -0,0 +1,84 @@
|
||||||
|
Find and Modify
|
||||||
|
===============
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
From MongoDB.org:
|
||||||
|
|
||||||
|
MongoDB supports a "find, modify, and return" command. This command
|
||||||
|
can be used to atomically modify a document (at most one) and
|
||||||
|
return it. Note that, by default, the document returned will not
|
||||||
|
include the modifications made on the update.
|
||||||
|
|
||||||
|
Doctrine fully integrates the find and modify functionality to the
|
||||||
|
query builder object so you can easily run these types of queries!
|
||||||
|
|
||||||
|
Update
|
||||||
|
------
|
||||||
|
|
||||||
|
For example you can update a job and return it:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$job = $dm->createQueryBuilder('Job')
|
||||||
|
// Find the job
|
||||||
|
->findAndUpdate()
|
||||||
|
->field('in_progress')->equals(false)
|
||||||
|
->sort('priority', 'desc')
|
||||||
|
|
||||||
|
// Update found job
|
||||||
|
->field('started')->set(new \MongoDate())
|
||||||
|
->field('in_progress')->set(true)
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
If you want to update a job and return the new document you can
|
||||||
|
call the ``returnNew()`` method.
|
||||||
|
|
||||||
|
Here is an example where we return the new updated job document:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$job = $dm->createQueryBuilder('Job')
|
||||||
|
// Find the job
|
||||||
|
->findAndUpdate()
|
||||||
|
->returnNew()
|
||||||
|
->field('in_progress')->equals(false)
|
||||||
|
->sort('priority', 'desc')
|
||||||
|
|
||||||
|
// Update found job
|
||||||
|
->field('started')->set(new \MongoDate())
|
||||||
|
->field('in_progress')->set(true)
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
The returned ``$job`` will be a managed ``Job`` instance with the
|
||||||
|
``started`` and ``in_progress`` fields updated.
|
||||||
|
|
||||||
|
Remove
|
||||||
|
------
|
||||||
|
|
||||||
|
You can also remove a document and return it:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$job = $dm->createQueryBuilder('Job')
|
||||||
|
->findAndRemove()
|
||||||
|
->sort('priority', 'desc')
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
You can read more about the find and modify functionality on the
|
||||||
|
`MongoDB website <https://docs.mongodb.com/manual/reference/method/db.collection.findAndModify/>`_.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you don't need to return the document, you can use just run a normal update which can
|
||||||
|
affect multiple documents, as well. For multiple update to happen you need to use
|
||||||
|
``->updateMany()`` method of the builder (or ``update()->multiple()`` combination that
|
||||||
|
was deprecated in version 1.2).
|
|
@ -0,0 +1,141 @@
|
||||||
|
Geospatial Queries
|
||||||
|
==================
|
||||||
|
|
||||||
|
You can execute some special queries when using geospatial indexes
|
||||||
|
like checking for documents within a rectangle or circle.
|
||||||
|
|
||||||
|
Mapping
|
||||||
|
-------
|
||||||
|
|
||||||
|
First, setup some documents like the following:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document
|
||||||
|
* @Index(keys={"coordinates"="2d"})
|
||||||
|
*/
|
||||||
|
class City
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
public $name;
|
||||||
|
|
||||||
|
/** @EmbedOne(targetDocument="Coordinates") */
|
||||||
|
public $coordinates;
|
||||||
|
|
||||||
|
/** @Distance */
|
||||||
|
public $distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @EmbeddedDocument */
|
||||||
|
class Coordinates
|
||||||
|
{
|
||||||
|
/** @Field(type="float") */
|
||||||
|
public $x;
|
||||||
|
|
||||||
|
/** @Field(type="float") */
|
||||||
|
public $y;
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<indexes>
|
||||||
|
<index>
|
||||||
|
<key name="coordinates" order="2d" />
|
||||||
|
</index>
|
||||||
|
</indexes>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
indexes:
|
||||||
|
coordinates:
|
||||||
|
keys:
|
||||||
|
coordinates: 2d
|
||||||
|
|
||||||
|
Near Query
|
||||||
|
----------
|
||||||
|
|
||||||
|
Now you can execute queries against these documents like the
|
||||||
|
following. Check for the 10 nearest cities to a given longitude
|
||||||
|
and latitude with the ``near($longitude, $latitude)`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$cities = $this->dm->createQuery('City')
|
||||||
|
->field('coordinates')->near(-120, 40)
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
.. _geonear:
|
||||||
|
|
||||||
|
GeoNear Command
|
||||||
|
---------------
|
||||||
|
|
||||||
|
You can also execute the `geoNear command`_ using the query builder's
|
||||||
|
``geoNear()`` method. Additional builder methods can be used to set options for
|
||||||
|
this command (e.g. ``distanceMultipler()``, ``maxDistance()``, ``spherical()``).
|
||||||
|
Unlike ``near()``, which uses a query operator, ``geoNear()`` does not require
|
||||||
|
the location field to be specified in the builder, as MongoDB will use the
|
||||||
|
single geospatial index for the collection. Documents will be returned in order
|
||||||
|
of nearest to farthest.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$cities = $this->dm->createQuery('City')
|
||||||
|
->geoNear(-120, 40)
|
||||||
|
->spherical(true)
|
||||||
|
// Convert radians to kilometers (use 3963.192 for miles)
|
||||||
|
->distanceMultiplier(6378.137)
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
If the model has a property mapped with :ref:`@Distance <annotation_distance>`,
|
||||||
|
that field will be set with the calculated distance between the document and the
|
||||||
|
query coordinates.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
foreach ($cities as $city) {
|
||||||
|
printf("%s is %f kilometers away.\n", $city->name, $city->distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
.. _`geoNear command`: https://docs.mongodb.com/manual/reference/command/geoNear/
|
||||||
|
|
||||||
|
Within Box
|
||||||
|
----------
|
||||||
|
|
||||||
|
You can also query for cities within a given rectangle using the
|
||||||
|
``withinBox($x1, $y1, $x2, $y2)`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$cities = $this->dm->createQuery('City')
|
||||||
|
->field('coordinates')->withinBox(41, 41, 72, 72)
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
Within Center
|
||||||
|
-------------
|
||||||
|
|
||||||
|
In addition to boxes you can check for cities within a circle using
|
||||||
|
the ``withinCenter($x, $y, $radius)`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$cities = $this->dm->createQuery('City')
|
||||||
|
->field('coordinates')->withinCenter(50, 50, 20)
|
||||||
|
->execute();
|
|
@ -0,0 +1,549 @@
|
||||||
|
Indexes
|
||||||
|
=======
|
||||||
|
|
||||||
|
Working with indexes in the MongoDB ODM is pretty straight forward.
|
||||||
|
You can have multiple indexes, they can consist of multiple fields,
|
||||||
|
they can be unique and you can give them an order. In this chapter
|
||||||
|
we'll show you examples of indexes using annotations.
|
||||||
|
|
||||||
|
First here is an example where we put an index on a single
|
||||||
|
property:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/** @Field(type="string") @Index */
|
||||||
|
public $username;
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<field name="username" index="true" />
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
fields:
|
||||||
|
username:
|
||||||
|
index: true
|
||||||
|
|
||||||
|
|
||||||
|
Index Options
|
||||||
|
-------------
|
||||||
|
|
||||||
|
You can customize the index with some additional options:
|
||||||
|
|
||||||
|
-
|
||||||
|
**name** - The name of the index. This can be useful if you are
|
||||||
|
indexing many keys and Mongo complains about the index name being
|
||||||
|
too long.
|
||||||
|
-
|
||||||
|
**dropDups** - If a unique index is being created and duplicate
|
||||||
|
values exist, drop all but one duplicate value.
|
||||||
|
-
|
||||||
|
**background** - Create indexes in the background while other
|
||||||
|
operations are taking place. By default, index creation happens
|
||||||
|
synchronously. If you specify TRUE with this option, index creation
|
||||||
|
will be asynchronous.
|
||||||
|
-
|
||||||
|
**safe** - You can specify a boolean value for checking if the
|
||||||
|
index creation succeeded. The driver will throw a
|
||||||
|
MongoCursorException if index creation failed.
|
||||||
|
-
|
||||||
|
**expireAfterSeconds** - If you specify this option then the associated
|
||||||
|
document will be automatically removed when the provided time (in seconds)
|
||||||
|
has passed. This option is bound to a number of limitations, which
|
||||||
|
are documented at https://docs.mongodb.com/manual/tutorial/expire-data/.
|
||||||
|
-
|
||||||
|
**order** - The order of the index (asc or desc).
|
||||||
|
-
|
||||||
|
**unique** - Create a unique index.
|
||||||
|
-
|
||||||
|
**sparse** - Create a sparse index. If a unique index is being created
|
||||||
|
the sparse option will allow duplicate null entries, but the field must be
|
||||||
|
unique otherwise.
|
||||||
|
-
|
||||||
|
**partialFilterExpression** - Create a partial index. Partial indexes only
|
||||||
|
index the documents in a collection that meet a specified filter expression.
|
||||||
|
By indexing a subset of the documents in a collection, partial indexes have
|
||||||
|
lower storage requirements and reduced performance costs for index creation
|
||||||
|
and maintenance. This feature was introduced with MongoDB 3.2 and is not
|
||||||
|
available on older versions.
|
||||||
|
|
||||||
|
Unique Index
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/** @Field(type="string") @Index(unique=true, order="asc") */
|
||||||
|
public $username;
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<field fieldName="username" index="true" unique="true" order="asc" />
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
fields:
|
||||||
|
username:
|
||||||
|
index: true
|
||||||
|
unique: true
|
||||||
|
order: true
|
||||||
|
|
||||||
|
For your convenience you can quickly specify a unique index with
|
||||||
|
``@UniqueIndex``:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/** @Field(type="string") @UniqueIndex(order="asc") */
|
||||||
|
public $username;
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<field fieldName="username" unique="true" order="asc" />
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
fields:
|
||||||
|
username:
|
||||||
|
unique: true
|
||||||
|
order: true
|
||||||
|
|
||||||
|
If you want to specify an index that consists of multiple fields
|
||||||
|
you can specify them on the class doc block:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document
|
||||||
|
* @UniqueIndex(keys={"accountId"="asc", "username"="asc"})
|
||||||
|
*/
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/** @Field(type="int") */
|
||||||
|
public $accountId;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
public $username;
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/orm/doctrine-mongo-mapping.xsd">
|
||||||
|
|
||||||
|
<document name="Documents\User">
|
||||||
|
<indexes>
|
||||||
|
<index>
|
||||||
|
<option name="unique" value="true" />
|
||||||
|
<key name="accountId" order="asc" />
|
||||||
|
<key name="username" order="asc" />
|
||||||
|
</index>
|
||||||
|
</indexes>
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
Documents\User:
|
||||||
|
indexes:
|
||||||
|
usernameacctid:
|
||||||
|
options:
|
||||||
|
unique: true
|
||||||
|
keys:
|
||||||
|
accountId:
|
||||||
|
order: asc
|
||||||
|
username:
|
||||||
|
order: asc
|
||||||
|
|
||||||
|
To specify multiple indexes you must use the ``@Indexes``
|
||||||
|
annotation:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document
|
||||||
|
* @Indexes({
|
||||||
|
* @Index(keys={"accountId"="asc"}),
|
||||||
|
* @Index(keys={"username"="asc"})
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/** @Field(type="int") */
|
||||||
|
public $accountId;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
public $username;
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/orm/doctrine-mongo-mapping.xsd">
|
||||||
|
|
||||||
|
<document name="Documents\User">
|
||||||
|
<indexes>
|
||||||
|
<index>
|
||||||
|
<key name="accountId" order="asc" />
|
||||||
|
</index>
|
||||||
|
<index>
|
||||||
|
<key name="username" order="asc" />
|
||||||
|
</index>
|
||||||
|
</indexes>
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
Documents\User:
|
||||||
|
indexes:
|
||||||
|
accountId:
|
||||||
|
keys:
|
||||||
|
accountId:
|
||||||
|
order: asc
|
||||||
|
username:
|
||||||
|
keys:
|
||||||
|
username:
|
||||||
|
order: asc
|
||||||
|
|
||||||
|
Embedded Indexes
|
||||||
|
----------------
|
||||||
|
|
||||||
|
You can specify indexes on embedded documents just like you do on normal documents. When Doctrine
|
||||||
|
creates the indexes for a document it will also create all the indexes from its mapped embedded
|
||||||
|
documents.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/** @EmbeddedDocument */
|
||||||
|
class Comment
|
||||||
|
{
|
||||||
|
/** @Field(type="date") @Index */
|
||||||
|
private $date;
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
Now if we had a ``BlogPost`` document with the ``Comment`` document embedded many times:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class BlogPost
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
/** @Field(type="string") @Index */
|
||||||
|
private $slug;
|
||||||
|
|
||||||
|
/** @EmbedMany(targetDocument="Comment") */
|
||||||
|
private $comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
If we were to create the indexes with the ``SchemaManager``:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$sm->ensureIndexes();
|
||||||
|
|
||||||
|
It will create the indexes from the ``BlogPost`` document but will also create the indexes that are
|
||||||
|
defined on the ``Comment`` embedded document. The following would be executed on the underlying MongoDB
|
||||||
|
database:
|
||||||
|
|
||||||
|
..
|
||||||
|
|
||||||
|
db.BlogPost.ensureIndexes({ 'slug' : 1, 'comments.date': 1 })
|
||||||
|
|
||||||
|
Also, for your convenience you can create the indexes for your mapped documents from the
|
||||||
|
:doc:`console <console-commands>`:
|
||||||
|
|
||||||
|
..
|
||||||
|
|
||||||
|
$ php mongodb.php mongodb:schema:create --index
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you are :ref:`mixing document types <embed_mixing_document_types>` for your
|
||||||
|
embedded documents, ODM will not be able to create indexes for their fields
|
||||||
|
unless you specify a discriminator map for the :ref:`embed-one <embed_one>`
|
||||||
|
or :ref:`embed-many <embed_many>` relationship.
|
||||||
|
|
||||||
|
Geospatial Indexing
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
You can specify a geospatial index by just specifying the keys and
|
||||||
|
options structures manually:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document
|
||||||
|
* @Index(keys={"coordinates"="2d"})
|
||||||
|
*/
|
||||||
|
class Place
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/** @EmbedOne(targetDocument="Coordinates") */
|
||||||
|
public $coordinates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @EmbeddedDocument */
|
||||||
|
class Coordinates
|
||||||
|
{
|
||||||
|
/** @Field(type="float") */
|
||||||
|
public $latitude;
|
||||||
|
|
||||||
|
/** @Field(type="float") */
|
||||||
|
public $longitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<indexes>
|
||||||
|
<index>
|
||||||
|
<key name="coordinates" order="2d" />
|
||||||
|
</index>
|
||||||
|
</indexes>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
indexes:
|
||||||
|
coordinates:
|
||||||
|
keys:
|
||||||
|
coordinates: 2d
|
||||||
|
|
||||||
|
Partial indexes
|
||||||
|
---------------
|
||||||
|
|
||||||
|
You can create a partial index by adding a ``partialFilterExpression`` to any
|
||||||
|
index.
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document
|
||||||
|
* @Index(keys={"city"="asc"}, partialFilterExpression={"version"={"$gt"=1}})
|
||||||
|
*/
|
||||||
|
class Place
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
public $city;
|
||||||
|
|
||||||
|
/** @Field(type="int") */
|
||||||
|
public $version;
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<indexes>
|
||||||
|
<index>
|
||||||
|
<key name="city" order="asc" />
|
||||||
|
<partial-filter-expression>
|
||||||
|
<field name="version" value="1" operator="gt" />
|
||||||
|
</partial-filter-expression>
|
||||||
|
</index>
|
||||||
|
</indexes>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
indexes:
|
||||||
|
partialIndexExample:
|
||||||
|
keys:
|
||||||
|
coordinates: asc
|
||||||
|
options:
|
||||||
|
partialFilterExpression:
|
||||||
|
version: { $gt: 1 }
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Partial indexes are only available with MongoDB 3.2 or newer. For more
|
||||||
|
information on partial filter expressions, read the
|
||||||
|
`official MongoDB documentation <https://docs.mongodb.com/manual/core/index-partial/>`_.
|
||||||
|
|
||||||
|
Requiring Indexes
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Requiring Indexes was deprecated in 1.2 and will be removed in 2.0.
|
||||||
|
|
||||||
|
Sometimes you may want to require indexes for all your queries to ensure you don't let stray unindexed queries
|
||||||
|
make it to the database and cause performance problems.
|
||||||
|
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document(requireIndexes=true)
|
||||||
|
*/
|
||||||
|
class Place
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/** @Field(type="string") @Index */
|
||||||
|
public $city;
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
// Documents.Place.dcm.xml
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/orm/doctrine-mongo-mapping.xsd">
|
||||||
|
|
||||||
|
<document name="Documents\Place" require-indexes="true">
|
||||||
|
<field fieldName="id" id="true" />
|
||||||
|
<field fieldName="city" type="string" />
|
||||||
|
<indexes>
|
||||||
|
<index>
|
||||||
|
<key name="city">
|
||||||
|
</index>
|
||||||
|
</indexes>
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
# Documents.Place.dcm.yml
|
||||||
|
|
||||||
|
Documents\Place:
|
||||||
|
fields:
|
||||||
|
id:
|
||||||
|
id: true
|
||||||
|
city:
|
||||||
|
type: string
|
||||||
|
indexes:
|
||||||
|
index1:
|
||||||
|
keys:
|
||||||
|
city: asc
|
||||||
|
|
||||||
|
When you run queries it will check that it is indexed and throw an exception if it is not indexed:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('Documents\Place')
|
||||||
|
->field('city')->equals('Nashville');
|
||||||
|
$query = $qb->getQuery();
|
||||||
|
$places = $query->execute();
|
||||||
|
|
||||||
|
When you execute the query it will throw an exception if `city` was not indexed in the database. You can control
|
||||||
|
whether or not an exception will be thrown by using the `requireIndexes()` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb->requireIndexes(false);
|
||||||
|
|
||||||
|
You can also check if the query is indexed and with the `isIndexed()` method and use it to display your
|
||||||
|
own notification when a query is unindexed:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$query = $qb->getQuery();
|
||||||
|
if (!$query->isIndexed()) {
|
||||||
|
$notifier->addError('Cannot execute queries that are not indexed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
If you don't want to require indexes for all queries you can set leave `requireIndexes` as false and control
|
||||||
|
it on a per query basis:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb->requireIndexes(true);
|
||||||
|
$query = $qb->getQuery();
|
||||||
|
$results = $query->execute();
|
|
@ -0,0 +1,280 @@
|
||||||
|
.. _inheritance_mapping:
|
||||||
|
|
||||||
|
Inheritance Mapping
|
||||||
|
===================
|
||||||
|
|
||||||
|
Doctrine currently offers two supported methods of inheritance:
|
||||||
|
:ref:`single collection <single_collection_inheritance>` and
|
||||||
|
:ref:`collection per class <collection_per_class_inheritance>` inheritance.
|
||||||
|
|
||||||
|
Mapped Superclasses
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
A mapped superclass is an abstract or concrete class that provides mapping
|
||||||
|
information for its subclasses, but is not itself a document. Typically, the
|
||||||
|
purpose of such a mapped superclass is to define state and mapping information
|
||||||
|
that is common to multiple document classes.
|
||||||
|
|
||||||
|
Just like non-mapped classes, mapped superclasses may appear in the middle of
|
||||||
|
an otherwise mapped inheritance hierarchy (through
|
||||||
|
:ref:`single collection <single_collection_inheritance>` or
|
||||||
|
:ref:`collection per class <collection_per_class_inheritance>`) inheritance.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
A mapped superclass cannot be a document and is not queryable.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/** @MappedSuperclass */
|
||||||
|
abstract class BaseDocument
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
<mapped-superclass name="Documents\BaseDocument">
|
||||||
|
</mapped-superclass>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
Documents\BaseDocument:
|
||||||
|
type: mappedSuperclass
|
||||||
|
|
||||||
|
.. _single_collection_inheritance:
|
||||||
|
|
||||||
|
Single Collection Inheritance
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
In single collection inheritance, each document is stored in a single collection
|
||||||
|
and a discriminator field is used to distinguish one document type from another.
|
||||||
|
|
||||||
|
Simple example:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document
|
||||||
|
* @InheritanceType("SINGLE_COLLECTION")
|
||||||
|
* @DiscriminatorField("type")
|
||||||
|
* @DiscriminatorMap({"person"="Person", "employee"="Employee"})
|
||||||
|
*/
|
||||||
|
class Person
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document
|
||||||
|
*/
|
||||||
|
class Employee extends Person
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
<document name="Documents\Person" inheritance-type="SINGLE_COLLECTION">
|
||||||
|
<discriminator-field name="type" />
|
||||||
|
<discriminator-map>
|
||||||
|
<discriminator-mapping value="person" class="Person" />
|
||||||
|
<discriminator-mapping value="employee" class="Employee" />
|
||||||
|
</discriminator-map>
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
<document name="Documents\Employee">
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
Documents\Person:
|
||||||
|
type: document
|
||||||
|
inheritanceType: SINGLE_COLLECTION
|
||||||
|
discriminatorField: type
|
||||||
|
discriminatorMap:
|
||||||
|
person: Person
|
||||||
|
employee: Employee
|
||||||
|
|
||||||
|
The discriminator value allows Doctrine to infer the class name to instantiate
|
||||||
|
when hydrating a document. If a discriminator map is used, the discriminator
|
||||||
|
value will be used to look up the class name in the map.
|
||||||
|
|
||||||
|
Now, if we query for a Person and its discriminator value is ``employee``, we
|
||||||
|
would get an Employee instance back:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$employee = new Employee();
|
||||||
|
// ...
|
||||||
|
$dm->persist($employee);
|
||||||
|
$dm->flush();
|
||||||
|
|
||||||
|
$employee = $dm->find('Person', $employee->getId()); // instanceof Employee
|
||||||
|
|
||||||
|
Even though we queried for a Person, Doctrine will know to return an Employee
|
||||||
|
instance because of the discriminator map!
|
||||||
|
|
||||||
|
If your document structure has changed and you've added discriminators after
|
||||||
|
already having a bunch of documents, you can specify a default value for the
|
||||||
|
discriminator field:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document
|
||||||
|
* @InheritanceType("SINGLE_COLLECTION")
|
||||||
|
* @DiscriminatorField("type")
|
||||||
|
* @DiscriminatorMap({"person"="Person", "employee"="Employee"})
|
||||||
|
* @DefaultDiscriminatorValue("person")
|
||||||
|
*/
|
||||||
|
class Person
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document
|
||||||
|
*/
|
||||||
|
class Employee extends Person
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
<document name="Documents\Person" inheritance-type="SINGLE_COLLECTION">
|
||||||
|
<discriminator-field name="type" />
|
||||||
|
<discriminator-map>
|
||||||
|
<discriminator-mapping value="person" class="Person" />
|
||||||
|
<discriminator-mapping value="employee" class="Employee" />
|
||||||
|
</discriminator-map>
|
||||||
|
<default-discriminator-value value="person" />
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
<document name="Documents\Employee">
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
Documents\Person:
|
||||||
|
type: document
|
||||||
|
inheritanceType: SINGLE_COLLECTION
|
||||||
|
discriminatorField: type
|
||||||
|
defaultDiscriminatorValue: person
|
||||||
|
discriminatorMap:
|
||||||
|
person: Person
|
||||||
|
employee: Employee
|
||||||
|
|
||||||
|
.. _collection_per_class_inheritance:
|
||||||
|
|
||||||
|
Collection Per Class Inheritance
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
With collection per class inheritance, each document is stored in its own
|
||||||
|
collection and contains all inherited fields:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document
|
||||||
|
* @InheritanceType("COLLECTION_PER_CLASS")
|
||||||
|
*/
|
||||||
|
class Person
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document
|
||||||
|
*/
|
||||||
|
class Employee extends Person
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
<document name="Documents\Person" inheritance-type="COLLECTION_PER_CLASS">
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
<document name="Documents\Employee">
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
Documents\Person:
|
||||||
|
type: document
|
||||||
|
inheritanceType: COLLECTION_PER_CLASS
|
||||||
|
|
||||||
|
A discriminator is not needed with this type of inheritance since the data is
|
||||||
|
separated in different collections.
|
|
@ -0,0 +1,494 @@
|
||||||
|
Introduction
|
||||||
|
============
|
||||||
|
|
||||||
|
Doctrine MongoDB Object Document Mapper is built for PHP 5.3.0+ and
|
||||||
|
provides transparent persistence for PHP objects to the popular `MongoDB`_ database by `10gen`_.
|
||||||
|
|
||||||
|
Features Overview
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
- Transparent persistence.
|
||||||
|
- Map one or many embedded documents.
|
||||||
|
- Map one or many referenced documents.
|
||||||
|
- Create references between documents in different databases.
|
||||||
|
- Map documents with Annotations, XML, YAML or plain old PHP code.
|
||||||
|
- Documents can be stored on the `MongoGridFS <http://www.php.net/MongoGridFS>`_.
|
||||||
|
- Collection per class(concrete) and single collection inheritance supported.
|
||||||
|
- Map your Doctrine 2 ORM Entities to the ODM and use mixed data stores.
|
||||||
|
- Inserts are performed using `MongoCollection::batchInsert() <http://us.php.net/manual/en/mongocollection.batchinsert.php>`_
|
||||||
|
- Updates are performed using atomic operators.
|
||||||
|
|
||||||
|
Here is a quick example of some PHP object documents that demonstrates a few of the features:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
|
||||||
|
use DateTime;
|
||||||
|
|
||||||
|
/** @ODM\MappedSuperclass */
|
||||||
|
abstract class BaseEmployee
|
||||||
|
{
|
||||||
|
/** @ODM\Id */
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
/** @ODM\Field(type="int", strategy="increment") */
|
||||||
|
private $changes = 0;
|
||||||
|
|
||||||
|
/** @ODM\Field(type="collection") */
|
||||||
|
private $notes = array();
|
||||||
|
|
||||||
|
/** @ODM\Field(type="string") */
|
||||||
|
private $name;
|
||||||
|
|
||||||
|
/** @ODM\Field(type="int") */
|
||||||
|
private $salary;
|
||||||
|
|
||||||
|
/** @ODM\Field(type="date") */
|
||||||
|
private $started;
|
||||||
|
|
||||||
|
/** @ODM\Field(type="date") */
|
||||||
|
private $left;
|
||||||
|
|
||||||
|
/** @ODM\EmbedOne(targetDocument="Address") */
|
||||||
|
private $address;
|
||||||
|
|
||||||
|
public function getId() { return $this->id; }
|
||||||
|
|
||||||
|
public function getChanges() { return $this->changes; }
|
||||||
|
public function incrementChanges() { $this->changes++; }
|
||||||
|
|
||||||
|
public function getNotes() { return $this->notes; }
|
||||||
|
public function addNote($note) { $this->notes[] = $note; }
|
||||||
|
|
||||||
|
public function getName() { return $this->name; }
|
||||||
|
public function setName($name) { $this->name = $name; }
|
||||||
|
|
||||||
|
public function getSalary() { return $this->salary; }
|
||||||
|
public function setSalary($salary) { $this->salary = (int) $salary; }
|
||||||
|
|
||||||
|
public function getStarted() { return $this->started; }
|
||||||
|
public function setStarted(DateTime $started) { $this->started = $started; }
|
||||||
|
|
||||||
|
public function getLeft() { return $this->left; }
|
||||||
|
public function setLeft(DateTime $left) { $this->left = $left; }
|
||||||
|
|
||||||
|
public function getAddress() { return $this->address; }
|
||||||
|
public function setAddress(Address $address) { $this->address = $address; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @ODM\Document */
|
||||||
|
class Employee extends BaseEmployee
|
||||||
|
{
|
||||||
|
/** @ODM\ReferenceOne(targetDocument="Documents\Manager") */
|
||||||
|
private $manager;
|
||||||
|
|
||||||
|
public function getManager() { return $this->manager; }
|
||||||
|
public function setManager(Manager $manager) { $this->manager = $manager; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @ODM\Document */
|
||||||
|
class Manager extends BaseEmployee
|
||||||
|
{
|
||||||
|
/** @ODM\ReferenceMany(targetDocument="Documents\Project") */
|
||||||
|
private $projects;
|
||||||
|
|
||||||
|
public __construct() { $this->projects = new ArrayCollection(); }
|
||||||
|
|
||||||
|
public function getProjects() { return $this->projects; }
|
||||||
|
public function addProject(Project $project) { $this->projects[] = $project; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @ODM\EmbeddedDocument */
|
||||||
|
class Address
|
||||||
|
{
|
||||||
|
/** @ODM\Field(type="string") */
|
||||||
|
private $address;
|
||||||
|
|
||||||
|
/** @ODM\Field(type="string") */
|
||||||
|
private $city;
|
||||||
|
|
||||||
|
/** @ODM\Field(type="string") */
|
||||||
|
private $state;
|
||||||
|
|
||||||
|
/** @ODM\Field(type="string") */
|
||||||
|
private $zipcode;
|
||||||
|
|
||||||
|
public function getAddress() { return $this->address; }
|
||||||
|
public function setAddress($address) { $this->address = $address; }
|
||||||
|
|
||||||
|
public function getCity() { return $this->city; }
|
||||||
|
public function setCity($city) { $this->city = $city; }
|
||||||
|
|
||||||
|
public function getState() { return $this->state; }
|
||||||
|
public function setState($state) { $this->state = $state; }
|
||||||
|
|
||||||
|
public function getZipcode() { return $this->zipcode; }
|
||||||
|
public function setZipcode($zipcode) { $this->zipcode = $zipcode; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @ODM\Document */
|
||||||
|
class Project
|
||||||
|
{
|
||||||
|
/** @ODM\Id */
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
/** @ODM\Field(type="string") */
|
||||||
|
private $name;
|
||||||
|
|
||||||
|
public function __construct($name) { $this->name = $name; }
|
||||||
|
|
||||||
|
public function getId() { return $this->id; }
|
||||||
|
|
||||||
|
public function getName() { return $this->name; }
|
||||||
|
public function setName($name) { $this->name = $name; }
|
||||||
|
}
|
||||||
|
|
||||||
|
Now those objects can be used just like you weren't using any
|
||||||
|
persistence layer at all and can be persisted transparently by
|
||||||
|
Doctrine:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Documents\Employee;
|
||||||
|
use Documents\Address;
|
||||||
|
use Documents\Project;
|
||||||
|
use Documents\Manager;
|
||||||
|
use DateTime;
|
||||||
|
|
||||||
|
$employee = new Employee();
|
||||||
|
$employee->setName('Employee');
|
||||||
|
$employee->setSalary(50000);
|
||||||
|
$employee->setStarted(new DateTime());
|
||||||
|
|
||||||
|
$address = new Address();
|
||||||
|
$address->setAddress('555 Doctrine Rd.');
|
||||||
|
$address->setCity('Nashville');
|
||||||
|
$address->setState('TN');
|
||||||
|
$address->setZipcode('37209');
|
||||||
|
$employee->setAddress($address);
|
||||||
|
|
||||||
|
$project = new Project('New Project');
|
||||||
|
$manager = new Manager();
|
||||||
|
$manager->setName('Manager');
|
||||||
|
$manager->setSalary(100000);
|
||||||
|
$manager->setStarted(new DateTime());
|
||||||
|
$manager->addProject($project);
|
||||||
|
|
||||||
|
$dm->persist($employee);
|
||||||
|
$dm->persist($address);
|
||||||
|
$dm->persist($project);
|
||||||
|
$dm->persist($manager);
|
||||||
|
$dm->flush();
|
||||||
|
|
||||||
|
The above would insert the following:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
Array
|
||||||
|
(
|
||||||
|
[000000004b0a33690000000001c304c6] => Array
|
||||||
|
(
|
||||||
|
[name] => New Project
|
||||||
|
)
|
||||||
|
|
||||||
|
)
|
||||||
|
Array
|
||||||
|
(
|
||||||
|
[000000004b0a33660000000001c304c6] => Array
|
||||||
|
(
|
||||||
|
[changes] => 0
|
||||||
|
[notes] => Array
|
||||||
|
(
|
||||||
|
)
|
||||||
|
|
||||||
|
[name] => Manager
|
||||||
|
[salary] => 100000
|
||||||
|
[started] => MongoDate Object
|
||||||
|
(
|
||||||
|
[sec] => 1275265048
|
||||||
|
[usec] => 0
|
||||||
|
)
|
||||||
|
|
||||||
|
[projects] => Array
|
||||||
|
(
|
||||||
|
[0] => Array
|
||||||
|
(
|
||||||
|
[$ref] => projects
|
||||||
|
[$id] => 4c0300188ead0e947a000000
|
||||||
|
[$db] => my_db
|
||||||
|
)
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
)
|
||||||
|
Array
|
||||||
|
(
|
||||||
|
[000000004b0a336a0000000001c304c6] => Array
|
||||||
|
(
|
||||||
|
[changes] => 0
|
||||||
|
[notes] => Array
|
||||||
|
(
|
||||||
|
)
|
||||||
|
|
||||||
|
[name] => Employee
|
||||||
|
[salary] => 50000
|
||||||
|
[started] => MongoDate Object
|
||||||
|
(
|
||||||
|
[sec] => 1275265048
|
||||||
|
[usec] => 0
|
||||||
|
)
|
||||||
|
|
||||||
|
[address] => Array
|
||||||
|
(
|
||||||
|
[address] => 555 Doctrine Rd.
|
||||||
|
[city] => Nashville
|
||||||
|
[state] => TN
|
||||||
|
[zipcode] => 37209
|
||||||
|
)
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
If we update a property and call ``->flush()`` again we'll get an
|
||||||
|
efficient update query using the atomic operators:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$newProject = new Project('Another Project');
|
||||||
|
$manager->setSalary(200000);
|
||||||
|
$manager->addNote('Gave user 100k a year raise');
|
||||||
|
$manager->incrementChanges(2);
|
||||||
|
$manager->addProject($newProject);
|
||||||
|
|
||||||
|
$dm->persist($newProject);
|
||||||
|
$dm->flush();
|
||||||
|
|
||||||
|
The above could would produce an update that looks something like
|
||||||
|
this:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
Array
|
||||||
|
(
|
||||||
|
[$inc] => Array
|
||||||
|
(
|
||||||
|
[changes] => 2
|
||||||
|
)
|
||||||
|
|
||||||
|
[$pushAll] => Array
|
||||||
|
(
|
||||||
|
[notes] => Array
|
||||||
|
(
|
||||||
|
[0] => Gave user 100k a year raise
|
||||||
|
)
|
||||||
|
|
||||||
|
[projects] => Array
|
||||||
|
(
|
||||||
|
[0] => Array
|
||||||
|
(
|
||||||
|
[$ref] => projects
|
||||||
|
[$id] => 4c0310718ead0e767e030000
|
||||||
|
[$db] => my_db
|
||||||
|
)
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
[$set] => Array
|
||||||
|
(
|
||||||
|
[salary] => 200000
|
||||||
|
)
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
This is a simple example, but it demonstrates well that you can
|
||||||
|
transparently persist PHP objects while still utilizing the
|
||||||
|
atomic operators for updating documents! Continue reading to learn
|
||||||
|
how to get the Doctrine MongoDB Object Document Mapper setup and
|
||||||
|
running!
|
||||||
|
|
||||||
|
Setup
|
||||||
|
-----
|
||||||
|
|
||||||
|
Before we can begin, we'll need to install the Doctrine MongoDB ODM library and
|
||||||
|
its dependencies. The easiest way to do this is with `Composer`_:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
$ composer require "doctrine/mongodb-odm"
|
||||||
|
|
||||||
|
Once ODM and its dependencies have been downloaded, we can begin by creating a
|
||||||
|
``bootstrap.php`` file in our project's root directory, where Composer's
|
||||||
|
``vendor/`` directory also resides. Let's start by importing some of the classes
|
||||||
|
we'll use:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Doctrine\MongoDB\Connection;
|
||||||
|
use Doctrine\ODM\MongoDB\Configuration;
|
||||||
|
use Doctrine\ODM\MongoDB\DocumentManager;
|
||||||
|
use Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver;
|
||||||
|
|
||||||
|
The first bit of code will be to import Composer's autoloader, so these classes
|
||||||
|
can actually be loaded:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
if ( ! file_exists($file = __DIR__.'/vendor/autoload.php')) {
|
||||||
|
throw new RuntimeException('Install dependencies to run this script.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$loader = require_once $file;
|
||||||
|
|
||||||
|
Note that instead of simply requiring the file, we assign its return value to
|
||||||
|
the ``$loader`` variable. Assuming document classes will be stored in the
|
||||||
|
``Documents/`` directory (with a namespace to match), we can register them with
|
||||||
|
the autoloader like so:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
$loader->add('Documents', __DIR__);
|
||||||
|
|
||||||
|
Ultimately, our application will utilize ODM through its ``DocumentManager``
|
||||||
|
class. Before we can instantiate a ``DocumentManager``, we need to construct the
|
||||||
|
``Connection`` and ``Configuration`` objects required by its factory method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
$connection = new Connection();
|
||||||
|
$config = new Configuration();
|
||||||
|
|
||||||
|
Next, we'll specify some essential configuration options. The following assumes
|
||||||
|
that we will store generated proxy and hydrator classes in the ``Proxies/`` and
|
||||||
|
``Hydrators/`` directories, respectively. Additionally, we'll define a default
|
||||||
|
database name to use for document classes that do not specify a database in
|
||||||
|
their mapping.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
$config->setProxyDir(__DIR__ . '/Proxies');
|
||||||
|
$config->setProxyNamespace('Proxies');
|
||||||
|
$config->setHydratorDir(__DIR__ . '/Hydrators');
|
||||||
|
$config->setHydratorNamespace('Hydrators');
|
||||||
|
$config->setDefaultDB('doctrine_odm');
|
||||||
|
|
||||||
|
The easiest way to define mappings for our document classes is with annotations.
|
||||||
|
We'll need to specify an annotation driver in our configuration (with one or
|
||||||
|
more paths) and register the annotations for the driver:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
$config->setMetadataDriverImpl(AnnotationDriver::create(__DIR__ . '/Documents'));
|
||||||
|
|
||||||
|
AnnotationDriver::registerAnnotationClasses();
|
||||||
|
|
||||||
|
At this point, we have everything necessary to construct a ``DocumentManager``:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
$dm = DocumentManager::create($connection, $config);
|
||||||
|
|
||||||
|
The final ``bootstrap.php`` file should look like this:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Doctrine\MongoDB\Connection;
|
||||||
|
use Doctrine\ODM\MongoDB\Configuration;
|
||||||
|
use Doctrine\ODM\MongoDB\DocumentManager;
|
||||||
|
use Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver;
|
||||||
|
|
||||||
|
if ( ! file_exists($file = __DIR__.'/vendor/autoload.php')) {
|
||||||
|
throw new RuntimeException('Install dependencies to run this script.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$loader = require_once $file;
|
||||||
|
$loader->add('Documents', __DIR__);
|
||||||
|
|
||||||
|
$connection = new Connection();
|
||||||
|
|
||||||
|
$config = new Configuration();
|
||||||
|
$config->setProxyDir(__DIR__ . '/Proxies');
|
||||||
|
$config->setProxyNamespace('Proxies');
|
||||||
|
$config->setHydratorDir(__DIR__ . '/Hydrators');
|
||||||
|
$config->setHydratorNamespace('Hydrators');
|
||||||
|
$config->setDefaultDB('doctrine_odm');
|
||||||
|
$config->setMetadataDriverImpl(AnnotationDriver::create(__DIR__ . '/Documents'));
|
||||||
|
|
||||||
|
AnnotationDriver::registerAnnotationClasses();
|
||||||
|
|
||||||
|
$dm = DocumentManager::create($connection, $config);
|
||||||
|
|
||||||
|
That is it! Your ``DocumentManager`` instance is ready to be used!
|
||||||
|
|
||||||
|
Using PHP 7
|
||||||
|
-----------
|
||||||
|
|
||||||
|
You can use Doctrine MongoDB ODM with PHP 7, but there are a few extra steps during
|
||||||
|
the installation. Since the legacy driver (referred to as ``ext-mongo``) is not
|
||||||
|
available on PHP 7, you will need the new driver (``ext-mongodb``) installed and
|
||||||
|
use a polyfill to provide the API of the legacy driver.
|
||||||
|
|
||||||
|
To do this, you have to require ``alcaeus/mongo-php-adapter`` before adding a composer
|
||||||
|
dependency to ODM. To do this, run the following command:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
$ composer require "alcaeus/mongo-php-adapter"
|
||||||
|
|
||||||
|
Next, manually add a ``provide`` section to your ``composer.json``:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
"provide": {
|
||||||
|
"ext-mongo": "1.6.14"
|
||||||
|
}
|
||||||
|
|
||||||
|
This section needs to be added to work around a composer issue with libraries
|
||||||
|
providing platform packages (such as ``ext-mongo``). Now, you may install ODM as
|
||||||
|
described above:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
$ composer require "doctrine/mongodb-odm"
|
||||||
|
|
||||||
|
.. _MongoDB: https://www.mongodb.com/
|
||||||
|
.. _10gen: http://www.10gen.com
|
||||||
|
.. _Composer: http://getcomposer.org/
|
|
@ -0,0 +1,31 @@
|
||||||
|
Logging
|
||||||
|
=======
|
||||||
|
|
||||||
|
If you want to turn on logging and receive information about
|
||||||
|
queries made to the database you can do so on your
|
||||||
|
``Doctrine\ODM\MongoDB\Configuration`` instance:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
$config->setLoggerCallable(function(array $log) {
|
||||||
|
print_r($log);
|
||||||
|
});
|
||||||
|
|
||||||
|
You can register any PHP callable and it will be notified with a
|
||||||
|
single argument that is an array of information about the query
|
||||||
|
being sent to the database.
|
||||||
|
|
||||||
|
Just like the anonymous function above, you could pass an array
|
||||||
|
with a object instance and a method to call:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
$config->setLoggerCallable(array($obj, 'method'));
|
|
@ -0,0 +1,117 @@
|
||||||
|
Map Reduce
|
||||||
|
==========
|
||||||
|
|
||||||
|
The Doctrine MongoDB ODM fully supports the `map reduce`_ functionality via its
|
||||||
|
:doc:`Query Builder API <query-builder-api>`.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
From the MongoDB manual:
|
||||||
|
|
||||||
|
Map-reduce is a data processing paradigm for condensing large volumes of
|
||||||
|
data into useful aggregated results. In MongoDB, map-reduce operations use
|
||||||
|
custom JavaScript functions to map, or associate, values to a key. If a key
|
||||||
|
has multiple values mapped to it, the operation reduces the values for the
|
||||||
|
key to a single object.
|
||||||
|
|
||||||
|
Imagine a situation where you had an application with a document
|
||||||
|
named ``Event`` and it was related to a ``User`` document:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class Event
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
/** @ReferenceOne(targetDocument="Documents\User") */
|
||||||
|
private $user;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
private $type;
|
||||||
|
|
||||||
|
/** @Field(type="date") */
|
||||||
|
private $date;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
private $description;
|
||||||
|
|
||||||
|
// getters and setters
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
We may have a situation where we want to run a query that tells us how many
|
||||||
|
sales events each user has had. We can easily use the map reduce functionality
|
||||||
|
of MongoDB via the ODM's query builder. Here is a simple map reduce example:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('Documents\User')
|
||||||
|
->field('type')
|
||||||
|
->equals('sale')
|
||||||
|
->map('function() { emit(this.user.$id, 1); }')
|
||||||
|
->reduce('function(k, vals) {
|
||||||
|
var sum = 0;
|
||||||
|
for (var i in vals) {
|
||||||
|
sum += vals[i];
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}');
|
||||||
|
$query = $qb->getQuery();
|
||||||
|
$results = $query->execute();
|
||||||
|
|
||||||
|
foreach ($results as $user) {
|
||||||
|
printf("User %s had %d sale(s).\n", $user['_id'], $user['value']);
|
||||||
|
}
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The query builder also has a ``finalize()`` method, which may be used to
|
||||||
|
specify a `finalize function`_ to be executed after the reduce step.
|
||||||
|
|
||||||
|
When using map reduce with Doctrine, the results are not hydrated into objects.
|
||||||
|
Instead, the raw results are returned directly from MongoDB.
|
||||||
|
|
||||||
|
The preceding example is equivalent to executing the following command via the
|
||||||
|
PHP driver directly:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$db = $mongoClient->selectDB('my_db');
|
||||||
|
|
||||||
|
$map = new MongoCode('function() { emit(this.user.$id, 1); }');
|
||||||
|
$reduce = new MongoCode('function(k, vals) {
|
||||||
|
var sum = 0;
|
||||||
|
for (var i in vals) {
|
||||||
|
sum += vals[i];
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}');
|
||||||
|
|
||||||
|
$result = $db->command(array(
|
||||||
|
'mapreduce' => 'events',
|
||||||
|
'map' => $map,
|
||||||
|
'reduce' => $reduce,
|
||||||
|
'query' => array('type' => 'sale'),
|
||||||
|
));
|
||||||
|
|
||||||
|
foreach ($result['results'] as $user) {
|
||||||
|
printf("User %s had %d sale(s).\n", $user['_id'], $user['value']);
|
||||||
|
}
|
||||||
|
|
||||||
|
.. _`map reduce`: https://docs.mongodb.com/manual/core/map-reduce/
|
||||||
|
.. _`finalize function`: https://docs.mongodb.com/master/reference/command/mapReduce/#mapreduce-finalize-cmd
|
|
@ -0,0 +1,196 @@
|
||||||
|
Metadata Drivers
|
||||||
|
================
|
||||||
|
|
||||||
|
The heart of an object mapper is the mapping information
|
||||||
|
that glues everything together. It instructs the DocumentManager how
|
||||||
|
it should behave when dealing with the different documents.
|
||||||
|
|
||||||
|
Core Metadata Drivers
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Doctrine provides a few different ways for you to specify your
|
||||||
|
metadata:
|
||||||
|
|
||||||
|
- **XML files** (XmlDriver)
|
||||||
|
- **Class DocBlock Annotations** (AnnotationDriver)
|
||||||
|
- **YAML files** (YamlDriver)
|
||||||
|
- **PHP Code in files or static functions** (PhpDriver)
|
||||||
|
|
||||||
|
Something important to note about the above drivers is they are all
|
||||||
|
an intermediate step to the same end result. The mapping
|
||||||
|
information is populated to ``Doctrine\ODM\MongoDB\Mapping\ClassMetadata``
|
||||||
|
instances. So in the end, Doctrine only ever has to work with the
|
||||||
|
API of the ``ClassMetadata`` class to get mapping information for
|
||||||
|
a document.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The populated ``ClassMetadata`` instances are also cached
|
||||||
|
so in a production environment the parsing and populating only ever
|
||||||
|
happens once. You can configure the metadata cache implementation
|
||||||
|
using the ``setMetadataCacheImpl()`` method on the
|
||||||
|
``Doctrine\ODM\MongoDB\Configuration`` class:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$em->getConfiguration()->setMetadataCacheImpl(new ApcCache());
|
||||||
|
|
||||||
|
If you want to use one of the included core metadata drivers you
|
||||||
|
just need to configure it. All the drivers are in the
|
||||||
|
``Doctrine\ODM\MongoDB\Mapping\Driver`` namespace:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$driver = new \Doctrine\ODM\MongoDB\Mapping\Driver\XmlDriver('/path/to/mapping/files');
|
||||||
|
$em->getConfiguration()->setMetadataDriverImpl($driver);
|
||||||
|
|
||||||
|
Implementing Metadata Drivers
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
In addition to the included metadata drivers you can very easily
|
||||||
|
implement your own. All you need to do is define a class which
|
||||||
|
implements the ``Driver`` interface:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Doctrine\ODM\MongoDB\Mapping\Driver;
|
||||||
|
|
||||||
|
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo;
|
||||||
|
|
||||||
|
interface Driver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Loads the metadata for the specified class into the provided container.
|
||||||
|
*
|
||||||
|
* @param string $className
|
||||||
|
* @param ClassMetadataInfo $metadata
|
||||||
|
*/
|
||||||
|
function loadMetadataForClass($className, ClassMetadataInfo $metadata);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the names of all mapped classes known to this driver.
|
||||||
|
*
|
||||||
|
* @return array The names of all mapped classes known to this driver.
|
||||||
|
*/
|
||||||
|
function getAllClassNames();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the class with the specified name should have its metadata loaded.
|
||||||
|
* This is only the case if it is either mapped as a Document or a
|
||||||
|
* MappedSuperclass.
|
||||||
|
*
|
||||||
|
* @param string $className
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
function isTransient($className);
|
||||||
|
}
|
||||||
|
|
||||||
|
If you want to write a metadata driver to parse information from
|
||||||
|
some file format we've made your life a little easier by providing
|
||||||
|
the ``AbstractFileDriver`` implementation for you to extend from:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class MyMetadataDriver extends AbstractFileDriver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected $_fileExtension = '.dcm.ext';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function loadMetadataForClass($className, ClassMetadataInfo $metadata)
|
||||||
|
{
|
||||||
|
$data = $this->_loadMappingFile($file);
|
||||||
|
|
||||||
|
// populate ClassMetadataInfo instance from $data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function _loadMappingFile($file)
|
||||||
|
{
|
||||||
|
// parse contents of $file and return php data structure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
When using the ``AbstractFileDriver`` it requires that you
|
||||||
|
only have one document defined per file and the file named after the
|
||||||
|
class described inside where namespace separators are replaced by
|
||||||
|
periods. So if you have a document named ``Documents\User`` and you
|
||||||
|
wanted to write a mapping file for your driver above you would need
|
||||||
|
to name the file ``Documents.User.dcm.ext`` for it to be
|
||||||
|
recognized.
|
||||||
|
|
||||||
|
Now you can use your ``MyMetadataDriver`` implementation by setting
|
||||||
|
it with the ``setMetadataDriverImpl()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$driver = new MyMetadataDriver('/path/to/mapping/files');
|
||||||
|
$em->getConfiguration()->setMetadataDriverImpl($driver);
|
||||||
|
|
||||||
|
ClassMetadata
|
||||||
|
-------------
|
||||||
|
|
||||||
|
The last piece you need to know and understand about metadata in
|
||||||
|
Doctrine is the API of the ``ClassMetadata`` classes. You need to
|
||||||
|
be familiar with them in order to implement your own drivers but
|
||||||
|
more importantly to retrieve mapping information for a certain
|
||||||
|
document when needed.
|
||||||
|
|
||||||
|
You have all the methods you need to manually specify the mapping
|
||||||
|
information instead of using some mapping file to populate it from.
|
||||||
|
The base ``ClassMetadataInfo`` class is responsible for only data
|
||||||
|
storage and is not meant for runtime use. It does not require that
|
||||||
|
the class actually exists yet so it is useful for describing some
|
||||||
|
document before it exists and using that information to generate for
|
||||||
|
example the documents themselves. The class ``ClassMetadata``
|
||||||
|
extends ``ClassMetadataInfo`` and adds some functionality required
|
||||||
|
for runtime usage and requires that the PHP class is present and
|
||||||
|
can be autoloaded.
|
||||||
|
|
||||||
|
You can read more about the API of the ``ClassMetadata`` classes in
|
||||||
|
the PHP Mapping chapter.
|
||||||
|
|
||||||
|
Getting ClassMetadata Instances
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
If you want to get the ``ClassMetadata`` instance for a document in
|
||||||
|
your project to programmatically use some mapping information to
|
||||||
|
generate some HTML or something similar you can retrieve it through
|
||||||
|
the ``ClassMetadataFactory``:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$cmf = $em->getMetadataFactory();
|
||||||
|
$class = $cmf->getMetadataFor('MyDocumentName');
|
||||||
|
|
||||||
|
Now you can learn about the document and use the data stored in the
|
||||||
|
``ClassMetadata`` instance to get all mapped fields for example and
|
||||||
|
iterate over them:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
foreach ($class->fieldMappings as $fieldMapping) {
|
||||||
|
echo $fieldMapping['fieldName'] . "\n";
|
||||||
|
}
|
|
@ -0,0 +1,219 @@
|
||||||
|
Migrating Schemas
|
||||||
|
=================
|
||||||
|
|
||||||
|
Even though MongoDB is schemaless, introducing some kind of object mapper means
|
||||||
|
that your object definitions become your schema. You may have a situation where
|
||||||
|
you rename a property in your object model but need to load values from older
|
||||||
|
documents where the field is still using the former name. While you could use
|
||||||
|
MongoDB's `$rename`_ operator to migrate everything, sometimes a lazy migration
|
||||||
|
is preferable. Doctrine offers a few different methods for dealing with this
|
||||||
|
problem!
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The features in this chapter were inspired by `Objectify`_, an object mapper
|
||||||
|
for the Google App Engine datastore. Additional information may be found in
|
||||||
|
the `Objectify schema migration`_ documentation.
|
||||||
|
|
||||||
|
Renaming a Field
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Let's say you have a simple document that starts off with the following fields:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class Person
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
public $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
Later on, you need rename ``name`` to ``fullName``; however, you'd like to
|
||||||
|
hydrate ``fullName`` from ``name`` if the new field doesn't exist.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class Person
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/** @Field(type="string") @AlsoLoad("name") */
|
||||||
|
public $fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
When a Person is loaded, the ``fullName`` field will be populated with the value
|
||||||
|
of ``name`` if ``fullName`` is not found. When the Person is persisted, this
|
||||||
|
value will then be stored in the ``fullName`` field.
|
||||||
|
|
||||||
|
.. caution::
|
||||||
|
|
||||||
|
A caveat of this feature is that it only affects hydration. Queries will not
|
||||||
|
know about the rename, so a query on ``fullName`` will only match documents
|
||||||
|
with the new field name. You can still query using the ``name`` field to
|
||||||
|
find older documents. The `$or`_ query operator could be used to match both.
|
||||||
|
|
||||||
|
Transforming Data
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
You may have a situation where you want to migrate a Person's name to separate
|
||||||
|
``firstName`` and ``lastName`` fields. This is also possible by specifying the
|
||||||
|
``@AlsoLoad`` annotation on a method, which will then be invoked immediately
|
||||||
|
before normal hydration.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document @HasLifecycleCallbacks */
|
||||||
|
class Person
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
public $firstName;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
public $lastName;
|
||||||
|
|
||||||
|
/** @AlsoLoad({"name", "fullName"}) */
|
||||||
|
public function populateFirstAndLastName($fullName)
|
||||||
|
{
|
||||||
|
list($this->firstName, $this->lastName) = explode(' ', $fullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
The annotation is defined with one or a list of field names. During hydration,
|
||||||
|
these fields will be checked in order and, for each field present, the annotated
|
||||||
|
method will be invoked with its value as a single argument. Since the
|
||||||
|
``firstName`` and ``lastName`` fields are mapped, they would then be updated
|
||||||
|
when the Person was persisted back to MongoDB.
|
||||||
|
|
||||||
|
Unlike lifecycle callbacks, the ``@AlsoLoad`` method annotation does not require
|
||||||
|
the :ref:`haslifecyclecallbacks` class annotation to be present.
|
||||||
|
|
||||||
|
Moving Fields
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Migrating your schema can be a difficult task, but Doctrine provides a few
|
||||||
|
different methods for dealing with it:
|
||||||
|
|
||||||
|
- **@AlsoLoad** - load values from old fields or transform data through methods
|
||||||
|
- **@NotSaved** - load values into fields without saving them again
|
||||||
|
- **@PostLoad** - execute code after all fields have been loaded
|
||||||
|
- **@PrePersist** - execute code before your document gets saved
|
||||||
|
|
||||||
|
Imagine you have some address-related fields on a Person document:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class Person
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
public $name;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
public $street;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
public $city;
|
||||||
|
}
|
||||||
|
|
||||||
|
Later on, you may want to migrate this data into an embedded Address document:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @EmbeddedDocument */
|
||||||
|
class Address
|
||||||
|
{
|
||||||
|
/** @Field(type="string") */
|
||||||
|
public $street;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
public $city;
|
||||||
|
|
||||||
|
public function __construct($street, $city)
|
||||||
|
{
|
||||||
|
$this->street = $street;
|
||||||
|
$this->city = $city;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @Document @HasLifecycleCallbacks */
|
||||||
|
class Person
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
public $name;
|
||||||
|
|
||||||
|
/** @NotSaved */
|
||||||
|
public $street;
|
||||||
|
|
||||||
|
/** @NotSaved */
|
||||||
|
public $city;
|
||||||
|
|
||||||
|
/** @EmbedOne(targetDocument="Address") */
|
||||||
|
public $address;
|
||||||
|
|
||||||
|
/** @PostLoad */
|
||||||
|
public function postLoad()
|
||||||
|
{
|
||||||
|
if ($this->street !== null || $this->city !== null)
|
||||||
|
{
|
||||||
|
$this->address = new Address($this->street, $this->city);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Person's ``street`` and ``city`` fields will be hydrated, but not saved. Once
|
||||||
|
the Person has loaded, the ``postLoad()`` method will be invoked and construct
|
||||||
|
a new Address object, which is mapped and will be persisted.
|
||||||
|
|
||||||
|
Alternatively, you could defer this migration until the Person is saved:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document @HasLifecycleCallbacks */
|
||||||
|
class Person
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
/** @PrePersist */
|
||||||
|
public function prePersist()
|
||||||
|
{
|
||||||
|
if ($this->street !== null || $this->city !== null)
|
||||||
|
{
|
||||||
|
$this->address = new Address($this->street, $this->city);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
The :ref:`haslifecyclecallbacks` annotation must be present on the class in
|
||||||
|
which the method is declared for the lifecycle callback to be registered.
|
||||||
|
|
||||||
|
.. _`$rename`: https://docs.mongodb.com/manual/reference/operator/update/rename/
|
||||||
|
.. _`Objectify`: https://github.com/objectify/objectify
|
||||||
|
.. _`Objectify schema migration`: https://github.com/objectify/objectify/wiki/SchemaMigration
|
||||||
|
.. _`$or`: https://docs.mongodb.com/manual/reference/operator/query/or/
|
|
@ -0,0 +1,168 @@
|
||||||
|
Priming References
|
||||||
|
==================
|
||||||
|
|
||||||
|
Priming references allows you to consolidate database queries when working with
|
||||||
|
:ref:`one <reference_one>` and :ref:`many <reference_many>` reference mappings.
|
||||||
|
This is useful for avoiding the
|
||||||
|
`n+1 problem <http://stackoverflow.com/q/97197/162228>`_ in your application.
|
||||||
|
|
||||||
|
Query Builder
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Consider the following abbreviated model:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
/** @ReferenceMany(targetDocument="Account") */
|
||||||
|
private $accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
We would like to query for 100 users and then iterate over their referenced
|
||||||
|
accounts.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('User')
|
||||||
|
->limit(100);
|
||||||
|
$query = $qb->getQuery();
|
||||||
|
$users = $query->execute();
|
||||||
|
|
||||||
|
foreach ($users as $user) {
|
||||||
|
/* PersistentCollection::initialize() will be invoked when we begin
|
||||||
|
* iterating through the user's accounts. Any accounts not already
|
||||||
|
* managed by the unit of work will need to be queried.
|
||||||
|
*/
|
||||||
|
foreach ($user->getAccounts() as $account) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
In this example, ODM would query the database once for the result set of users
|
||||||
|
and then, for each user, issue a separate query to load any accounts that are
|
||||||
|
not already being managed by the unit of work. This could result in as many as
|
||||||
|
100 additional database queries!
|
||||||
|
|
||||||
|
If we expect to iterate through all users and their accounts, we could optimize
|
||||||
|
this process by loading all of the referenced accounts with one query. The query
|
||||||
|
builder's ``prime()`` method allows us to do just that.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('User')
|
||||||
|
->field('accounts')->prime(true)
|
||||||
|
->limit(100);
|
||||||
|
$query = $qb->getQuery();
|
||||||
|
|
||||||
|
/* After querying for the users, ODM will collect the IDs of all referenced
|
||||||
|
* accounts and load them with a single additional query.
|
||||||
|
*/
|
||||||
|
$users = $query->execute();
|
||||||
|
|
||||||
|
foreach ($users as $user) {
|
||||||
|
/* Accounts have already been loaded, so iterating through accounts will
|
||||||
|
* not query an additional query.
|
||||||
|
*/
|
||||||
|
foreach ($user->getAccounts() as $account) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
In this case, priming will allow us to load all users and referenced accounts in
|
||||||
|
two database queries. If the accounts had used an
|
||||||
|
:ref:`inheritance mapping <inheritance_mapping>`, priming might require several
|
||||||
|
queries (one per discriminated class name).
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Priming is also compatible with :ref:`simple references <storing_references>`
|
||||||
|
and discriminated references. When priming discriminated references, ODM
|
||||||
|
will issue one query per distinct class among the referenced document(s).
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Hydration must be enabled in the query builder for priming to work properly.
|
||||||
|
Disabling hydration will cause the DBRef to be returned for a referenced
|
||||||
|
document instead of the hydrated document object.
|
||||||
|
|
||||||
|
Inverse references
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This feature was added in version 1.2.
|
||||||
|
|
||||||
|
When using inverse references (references mapped using ``mappedBy`` or
|
||||||
|
``repositoryMethod``) you can also enable primers on one-to-many references by
|
||||||
|
specifying them in the mapping:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
/** @ReferenceMany(targetDocument="Account", prime={"user"}) */
|
||||||
|
private $accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
When the collection is initialized, the configured primers are automatically
|
||||||
|
added to the query.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
When using inverse references with ``repositoryMethod``, be sure to return
|
||||||
|
an eager cursor from the repository method if you want to rely on primers
|
||||||
|
defined in the mapping. If the result is not an eager cursor, an exception
|
||||||
|
will be thrown and the collection won't be loaded. Also, any primers you
|
||||||
|
might have added in the ``repositoryMethod`` are overwritten with those
|
||||||
|
specified in the mapping.
|
||||||
|
|
||||||
|
Primer Callback
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Passing ``true`` to ``prime()`` instructs ODM to load the referenced document(s)
|
||||||
|
on its own; however, we can also pass a custom callable (e.g. Closure instance)
|
||||||
|
to ``prime()``, which allows more control over the priming query.
|
||||||
|
|
||||||
|
As an example, we can look at the default callable, which is found in the
|
||||||
|
``ReferencePrimer`` class.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
function(DocumentManager $dm, ClassMetadata $class, array $ids, array $hints) {
|
||||||
|
$qb = $dm->createQueryBuilder($class->name)
|
||||||
|
->field($class->identifier)->in($ids);
|
||||||
|
|
||||||
|
if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
|
||||||
|
$qb->slaveOkay(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
|
||||||
|
$qb->setReadPreference(
|
||||||
|
$hints[Query::HINT_READ_PREFERENCE],
|
||||||
|
$hints[Query::HINT_READ_PREFERENCE_TAGS]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb->getQuery()->toArray();
|
||||||
|
};
|
||||||
|
|
||||||
|
Firstly, the callable is passed the ``DocumentManager`` of the main query. This
|
||||||
|
is necessary to create the query used for priming, and ensures that the results
|
||||||
|
will become managed in the same scope. The ``ClassMetadata`` argument provides
|
||||||
|
mapping information for the referenced class as well as its name, which is used
|
||||||
|
to create the query builder. An array of identifiers follows, which is used to
|
||||||
|
query for the documents to be primed. Lastly, the ``UnitOfWork`` hints from the
|
||||||
|
original query are provided so that the priming query can apply them as well.
|
|
@ -0,0 +1,998 @@
|
||||||
|
Query Builder API
|
||||||
|
=================
|
||||||
|
|
||||||
|
.. role:: math(raw)
|
||||||
|
:format: html latex
|
||||||
|
|
||||||
|
Querying for documents with Doctrine is just as simple as if you
|
||||||
|
weren't using Doctrine at all. Of course you always have your
|
||||||
|
traditional ``find()`` and ``findOne()`` methods but you also have
|
||||||
|
a ``Query`` object with a fluent API for defining the query that
|
||||||
|
should be executed.
|
||||||
|
|
||||||
|
The ``Query`` object supports several types of queries
|
||||||
|
|
||||||
|
- FIND
|
||||||
|
- FIND_AND_UPDATE
|
||||||
|
- FIND_AND_REMOVE
|
||||||
|
- INSERT
|
||||||
|
- UPDATE
|
||||||
|
- REMOVE
|
||||||
|
- GROUP
|
||||||
|
- MAP_REDUCE
|
||||||
|
- DISTINCT_FIELD
|
||||||
|
- GEO_LOCATION
|
||||||
|
|
||||||
|
This section will show examples for the different types of queries.
|
||||||
|
|
||||||
|
Finding Documents
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
You have a few different ways to find documents. You can use the ``find()`` method
|
||||||
|
to find a document by its identifier:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$users = $dm->find('User', $id);
|
||||||
|
|
||||||
|
The ``find()`` method is just a convenience shortcut method to:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$user = $dm->getRepository('User')->find($id);
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The ``find()`` method checks the local in memory identity map for the document
|
||||||
|
before querying the database for the document.
|
||||||
|
|
||||||
|
On the ``DocumentRepository`` you have a few other methods for finding documents:
|
||||||
|
|
||||||
|
- ``findBy`` - find documents by an array of criteria
|
||||||
|
- ``findOneBy`` - find one document by an array of criteria
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$users = $dm->getRepository('User')->findBy(array('type' => 'employee'));
|
||||||
|
$user = $dm->getRepository('User')->findOneBy(array('username' => 'jwage'));
|
||||||
|
|
||||||
|
Creating a Query Builder
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
You can easily create a new ``Query\Builder`` object with the
|
||||||
|
``DocumentManager::createQueryBuilder()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('User');
|
||||||
|
|
||||||
|
The first and only argument is optional, you can specify it later
|
||||||
|
with the ``find()``, ``update()`` (deprecated), ``updateOne()``,
|
||||||
|
``updateMany()`` or ``remove()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder();
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
$qb->find('User');
|
||||||
|
|
||||||
|
Executing Queries
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
You can execute a query by getting a ``Query`` through the ``getQuery()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('User');
|
||||||
|
$query = $qb->getQuery();
|
||||||
|
|
||||||
|
Now you can ``execute()`` that query and it will return a cursor for you to iterate over the results:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$users = $query->execute();
|
||||||
|
|
||||||
|
Debugging Queries
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
While building not complicated queries is really simple sometimes it might be hard to wrap your head
|
||||||
|
around more sophisticated queries that involves building separate expressions to work properly. If
|
||||||
|
you are not sure if your the query constructed with Builder is in fact correct you may want to ``debug()`` it
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('User');
|
||||||
|
$query = $qb->getQuery();
|
||||||
|
$debug = $query->debug();
|
||||||
|
|
||||||
|
At this point your query is *prepared* - that means ODM done all its job in renaming fields to match their
|
||||||
|
database name, added discriminator fields, applied filters, created correct references and all other things
|
||||||
|
you employ ODM to. The array returned by ``->debug()`` is what is passed to the underlying driver for the
|
||||||
|
query to be performed.
|
||||||
|
|
||||||
|
Eager Cursors
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
You can configure queries to return an eager cursor instead of a normal mongodb cursor using the ``Builder#eagerCursor()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('User')
|
||||||
|
->eagerCursor(true);
|
||||||
|
$query = $qb->getQuery();
|
||||||
|
$cursor = $query->execute(); // instanceof Doctrine\ODM\MongoDB\EagerCursor
|
||||||
|
|
||||||
|
Iterating over the ``$cursor`` will fetch all the data in a short and small cursor all at once and will hydrate
|
||||||
|
one document at a time in to an object as you iterate:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
foreach ($cursor as $user) { // queries for all users and data is held internally
|
||||||
|
// each User object is hydrated from the data one at a time.
|
||||||
|
}
|
||||||
|
|
||||||
|
Getting Single Result
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
If you want to just get a single result you can use the ``Query#getSingleResult()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$user = $dm->createQueryBuilder('User')
|
||||||
|
->field('username')->equals('jwage')
|
||||||
|
->getQuery()
|
||||||
|
->getSingleResult();
|
||||||
|
|
||||||
|
Selecting Fields
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
You can limit the fields that are returned in the results by using
|
||||||
|
the ``select()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('User')
|
||||||
|
->select('username', 'password');
|
||||||
|
$query = $qb->getQuery();
|
||||||
|
$users = $query->execute();
|
||||||
|
|
||||||
|
In the results only the data from the username and password will be
|
||||||
|
returned.
|
||||||
|
|
||||||
|
Index hints
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
You can force MongoDB to use a specific index for a query with the ``hint()`` method (see `hint <https://docs.mongodb.com/manual/reference/operator/meta/hint/>`_)
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('User')
|
||||||
|
->hint('user_pass_idx');
|
||||||
|
$query = $qb->getQuery();
|
||||||
|
$users = $query->execute();
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Combining ``select()`` and ``hint()`` on appropriate indexes can result in very fast
|
||||||
|
`covered queries <https://docs.mongodb.com/manual/core/query-optimization/#covered-query>`_
|
||||||
|
|
||||||
|
Selecting Distinct Values
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Sometimes you may want to get an array of distinct values in a
|
||||||
|
collection. You can accomplish this using the ``distinct()``
|
||||||
|
method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$ages = $dm->createQueryBuilder('User')
|
||||||
|
->distinct('age')
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
The above would give you an ``ArrayCollection`` of all the distinct user ages!
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
MongoDB's `distinct command <https://docs.mongodb.com/manual/reference/command/distinct/>`_
|
||||||
|
does not support sorting, so you cannot combine ``distinct()`` with
|
||||||
|
``sort()``. If you would like to sort the results of a distinct query, you
|
||||||
|
will need to do so in PHP after executing the query.
|
||||||
|
|
||||||
|
Refreshing Documents
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
When a query (e.g. geoNear, find) returns one or more hydrated documents whose
|
||||||
|
identifiers are already in the identity map, ODM returns the managed document
|
||||||
|
instances for those results. In this case, a managed document's data may differ
|
||||||
|
from whatever was just returned by the database query.
|
||||||
|
|
||||||
|
The query builder's ``refresh()`` method may be used to instruct ODM to override
|
||||||
|
the managed document with data from the query result. This is comparable to
|
||||||
|
calling ``DocumentManager::refresh()`` for a managed document. The document's
|
||||||
|
changeset will be reset in the process.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$user = $dm->createQueryBuilder('User')
|
||||||
|
->field('username')->equals('jwage')
|
||||||
|
->refresh()
|
||||||
|
->getQuery()
|
||||||
|
->getSingleResult();
|
||||||
|
|
||||||
|
// Jon's user will have the latest data, even if it was already managed
|
||||||
|
|
||||||
|
Refreshing is not applicable if hydration is disabled.
|
||||||
|
|
||||||
|
Fetching Documents as Read-Only
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Similar to ``refresh()``, ``readOnly()`` instructs ODM to not only hydrate the
|
||||||
|
latest data but also to create new document's instance (i.e. if found document
|
||||||
|
would be already managed by Doctrine, new instance will be returned) and not
|
||||||
|
register it in ``UnitOfWork``.
|
||||||
|
|
||||||
|
This technique can prove especially useful when using ``select()`` with no intent
|
||||||
|
to update fetched documents.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$user = $dm->createQueryBuilder('User')
|
||||||
|
->field('username')->equals('malarzm')
|
||||||
|
->readOnly()
|
||||||
|
->getQuery()
|
||||||
|
->getSingleResult();
|
||||||
|
|
||||||
|
// Maciej's user will have the latest data, and will not be the same object
|
||||||
|
// as the one that was already managed (if it was)
|
||||||
|
|
||||||
|
Read-Only is not applicable if hydration is disabled.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Read-only mode is not deep, i.e. any references (be it owning or inverse) of
|
||||||
|
fetched WILL be managed by Doctrine. This is a shortcoming of current
|
||||||
|
implementation, may change in future and will not be considered a BC break
|
||||||
|
(will be treated as a feature instead).
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
To manage a document previously fetched in read-only mode, always use the
|
||||||
|
`merge` method of the DocumentManager. Using `persist` in these cases can
|
||||||
|
have unwanted side effects.
|
||||||
|
|
||||||
|
Disabling Hydration
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
For find queries the results by default are hydrated and you get
|
||||||
|
document objects back instead of arrays. You can disable this and
|
||||||
|
get the raw results directly back from mongo by using the
|
||||||
|
``hydrate(false)`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$users = $dm->createQueryBuilder('User')
|
||||||
|
->hydrate(false)
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
print_r($users);
|
||||||
|
|
||||||
|
Limiting Results
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
You can limit results similar to how you would in a relational
|
||||||
|
database with a limit and offset by using the ``limit()`` and
|
||||||
|
``skip()`` method.
|
||||||
|
|
||||||
|
Here is an example where we get the third page of blog posts when
|
||||||
|
we show twenty at a time:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$blogPosts = $dm->createQueryBuilder('BlogPost')
|
||||||
|
->limit(20)
|
||||||
|
->skip(40)
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
Sorting Results
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
You can sort the results by using the ``sort()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('Article')
|
||||||
|
->sort('createdAt', 'desc');
|
||||||
|
|
||||||
|
If you want to an additional sort you can call ``sort()`` again. The calls are stacked and ordered
|
||||||
|
in the order you call the method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$query->sort('featured', 'desc');
|
||||||
|
|
||||||
|
Map Reduce
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
You can also run map reduced find queries using the ``Query``
|
||||||
|
object:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $this->dm->createQueryBuilder('Event')
|
||||||
|
->field('type')->equals('sale')
|
||||||
|
->map('function() { emit(this.userId, 1); }')
|
||||||
|
->reduce("function(k, vals) {
|
||||||
|
var sum = 0;
|
||||||
|
for (var i in vals) {
|
||||||
|
sum += vals[i];
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}");
|
||||||
|
$query = $qb->getQuery();
|
||||||
|
$results = $query->execute();
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
When you specify a ``map()`` and ``reduce()`` operation
|
||||||
|
the results will not be hydrated and the raw results from the map
|
||||||
|
reduce operation will be returned.
|
||||||
|
|
||||||
|
If you just want to reduce the results using a javascript function
|
||||||
|
you can just call the ``where()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('User')
|
||||||
|
->where("function() { return this.type == 'admin'; }");
|
||||||
|
|
||||||
|
You can read more about the `$where operator <https://docs.mongodb.com/manual/reference/operator/query/where/>`_ in the Mongo docs.
|
||||||
|
|
||||||
|
Conditional Operators
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The conditional operators in Mongo are available to limit the returned results through a easy to use API. Doctrine abstracts this to a fluent object oriented interface with a fluent API. Here is a list of all the conditional operation methods you can use on the `Query\Builder` object.
|
||||||
|
|
||||||
|
* ``where($javascript)``
|
||||||
|
* ``in($values)``
|
||||||
|
* ``notIn($values)``
|
||||||
|
* ``equals($value)``
|
||||||
|
* ``notEqual($value)``
|
||||||
|
* ``gt($value)``
|
||||||
|
* ``gte($value)``
|
||||||
|
* ``lt($value)``
|
||||||
|
* ``lte($value)``
|
||||||
|
* ``range($start, $end)``
|
||||||
|
* ``size($size)``
|
||||||
|
* ``exists($bool)``
|
||||||
|
* ``type($type)``
|
||||||
|
* ``all($values)``
|
||||||
|
* ``mod($mod)``
|
||||||
|
* ``addOr($expr)``
|
||||||
|
* ``references($document)``
|
||||||
|
* ``includesReferenceTo($document)``
|
||||||
|
|
||||||
|
Query for active administrator users:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('User')
|
||||||
|
->field('type')->equals('admin')
|
||||||
|
->field('active')->equals(true);
|
||||||
|
|
||||||
|
Query for articles that have some tags:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('Article')
|
||||||
|
->field('tags.name')->in(array('tag1', 'tag2'));
|
||||||
|
|
||||||
|
Read more about the
|
||||||
|
`$in operator <https://docs.mongodb.com/manual/reference/operator/query/in/>`_
|
||||||
|
in the Mongo docs
|
||||||
|
|
||||||
|
Query for articles that do not have some tags:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('Article')
|
||||||
|
->field('tags.name')->notIn(array('tag3'));
|
||||||
|
|
||||||
|
Read more about the
|
||||||
|
`$nin operator <https://docs.mongodb.com/manual/reference/operator/query/nin/>`_
|
||||||
|
in the Mongo docs.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('User')
|
||||||
|
->field('type')->notEqual('admin');
|
||||||
|
|
||||||
|
Read more about the
|
||||||
|
`$ne operator <https://docs.mongodb.com/manual/reference/operator/query/ne/>`_
|
||||||
|
in the Mongo docs.
|
||||||
|
|
||||||
|
Query for accounts with an amount due greater than 30:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('Account')
|
||||||
|
->field('amount_due')->gt(30);
|
||||||
|
|
||||||
|
Query for accounts with an amount due greater than or equal to 30:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('Account')
|
||||||
|
->field('amount_due')->gte(30);
|
||||||
|
|
||||||
|
Query for accounts with an amount due less than 30:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('Account')
|
||||||
|
->field('amount_due')->lt(30);
|
||||||
|
|
||||||
|
Query for accounts with an amount due less than or equal to 30:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('Account')
|
||||||
|
->field('amount_due')->lte(30);
|
||||||
|
|
||||||
|
Query for accounts with an amount due between 10 and 20:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('Account')
|
||||||
|
->field('amount_due')->range(10, 20);
|
||||||
|
|
||||||
|
Read more about
|
||||||
|
`conditional operators <http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-ConditionalOperators%3A%3C%2C%3C%3D%2C%3E%2C%3E%3D>`_
|
||||||
|
in the Mongo docs.
|
||||||
|
|
||||||
|
Query for articles with no comments:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('Article')
|
||||||
|
->field('comments')->size(0);
|
||||||
|
|
||||||
|
Read more about the
|
||||||
|
`$size operator <https://docs.mongodb.com/manual/reference/operator/query/size/>`_
|
||||||
|
in the Mongo docs.
|
||||||
|
|
||||||
|
Query for users that have a login field before it was renamed to
|
||||||
|
username:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('User')
|
||||||
|
->field('login')->exists(true);
|
||||||
|
|
||||||
|
Read more about the
|
||||||
|
`$exists operator <https://docs.mongodb.com/manual/reference/operator/query/exists/>`_
|
||||||
|
in the Mongo docs.
|
||||||
|
|
||||||
|
Query for users that have a type field that is of integer bson
|
||||||
|
type:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('User')
|
||||||
|
->field('type')->type('integer');
|
||||||
|
|
||||||
|
Read more about the
|
||||||
|
`$type operator <https://docs.mongodb.com/manual/reference/operator/query/type/>`_
|
||||||
|
in the Mongo docs.
|
||||||
|
|
||||||
|
Query for users that are in all the specified Groups:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('User')
|
||||||
|
->field('groups')->all(array('Group 1', 'Group 2'));
|
||||||
|
|
||||||
|
Read more about the
|
||||||
|
`$all operator <https://docs.mongodb.com/manual/reference/operator/query/all/>`_
|
||||||
|
in the Mongo docs.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('Transaction')
|
||||||
|
->field('field')->mod('field', array(10, 1));
|
||||||
|
|
||||||
|
Read more about the
|
||||||
|
`$mod operator <https://docs.mongodb.com/manual/reference/operator/query/mod/>`_ in the Mongo docs.
|
||||||
|
|
||||||
|
Query for users who have subscribed or are in a trial.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('User');
|
||||||
|
$qb->addOr($qb->expr()->field('subscriber')->equals(true));
|
||||||
|
$qb->addOr($qb->expr()->field('inTrial')->equals(true));
|
||||||
|
|
||||||
|
Read more about the
|
||||||
|
`$or operator <https://docs.mongodb.com/manual/reference/operator/query/or/>`_ in the Mongo docs.
|
||||||
|
|
||||||
|
The ``references()`` method may be used to query the owning side of a
|
||||||
|
:ref:`@ReferenceOne <annotations_reference_reference_one>` relationship. In the
|
||||||
|
following example, we query for all articles written by a particular user.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// Suppose $user has already been fetched from the database
|
||||||
|
$qb = $dm->createQueryBuilder('Article')
|
||||||
|
->field('user')->references($user);
|
||||||
|
|
||||||
|
The ``includesReferenceTo()`` method may be used to query the owning side of a
|
||||||
|
:ref:`@ReferenceMany <annotations_reference_reference_many>` relationship. In
|
||||||
|
the following example, we query for the user(s) that have access to a particular
|
||||||
|
account.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// Suppose $account has already been fetched from the database
|
||||||
|
$qb = $dm->createQueryBuilder('User')
|
||||||
|
->field('accounts')->includesReferenceTo($account);
|
||||||
|
|
||||||
|
Text Search
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
You can use the
|
||||||
|
`$text operator <https://docs.mongodb.com/manual/reference/operator/query/text/>`_
|
||||||
|
to run a text search against a field with a text index. To do so, create a
|
||||||
|
document with a text index:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document
|
||||||
|
* @Index(keys={"description"="text"})
|
||||||
|
*/
|
||||||
|
class Document
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
public $description;
|
||||||
|
|
||||||
|
/** @Field(type="float") @NotSaved */
|
||||||
|
public $score;
|
||||||
|
}
|
||||||
|
|
||||||
|
You can then run queries using the text operator:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// Run a text search against the index
|
||||||
|
$qb = $dm->createQueryBuilder('Document')
|
||||||
|
->text('words you are looking for');
|
||||||
|
|
||||||
|
To fetch the calculated score for the text search, use the ``selectMeta()``
|
||||||
|
method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// Run a text search against the index
|
||||||
|
$qb = $dm->createQueryBuilder('Document')
|
||||||
|
->selectMeta('score', 'textScore')
|
||||||
|
->text('words you are looking for');
|
||||||
|
|
||||||
|
You can also change the language used for stemming using the ``language()``
|
||||||
|
method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// Run a text search against the index
|
||||||
|
$qb = $dm->createQueryBuilder('Document')
|
||||||
|
->language('it')
|
||||||
|
->text('parole che stai cercando');
|
||||||
|
|
||||||
|
|
||||||
|
Update Queries
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Doctrine also supports executing atomic update queries using the `Query\Builder`
|
||||||
|
object. You can use the conditional operations in combination with the ability to
|
||||||
|
change document field values atomically. Additionally if you are modifying a field
|
||||||
|
that is a reference you can pass managed document to the Builder and let ODM build
|
||||||
|
``DBRef`` object for you.
|
||||||
|
|
||||||
|
You have several modifier operations
|
||||||
|
available to you that make it easy to update documents in Mongo:
|
||||||
|
|
||||||
|
* ``set($name, $value, $atomic = true)``
|
||||||
|
* ``setNewObj($newObj)``
|
||||||
|
* ``inc($name, $value)``
|
||||||
|
* ``unsetField($field)``
|
||||||
|
* ``push($field, $value)``
|
||||||
|
* ``pushAll($field, array $valueArray)``
|
||||||
|
* ``addToSet($field, $value)``
|
||||||
|
* ``addManyToSet($field, array $values)``
|
||||||
|
* ``popFirst($field)``
|
||||||
|
* ``popLast($field)``
|
||||||
|
* ``pull($field, $value)``
|
||||||
|
* ``pullAll($field, array $valueArray)``
|
||||||
|
|
||||||
|
Updating multiple documents
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
By default Mongo updates only one document unless ``multi`` option is provided and true.
|
||||||
|
In ODM the distinction is done by explicitly calling ``updateMany()`` method of the builder:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$dm->createQueryBuilder('User')
|
||||||
|
->updateMany()
|
||||||
|
->field('someField')->set('newValue')
|
||||||
|
->field('username')->equals('sgoettschkes')
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
``updateMany()`` and ``updateOne()`` methods were introduced in version 1.2. If you're
|
||||||
|
using one of previous version you need to use ``update()`` combined with ``multiple(true)``.
|
||||||
|
|
||||||
|
Modifier Operations
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Change a users password:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$dm->createQueryBuilder('User')
|
||||||
|
->updateOne()
|
||||||
|
->field('password')->set('newpassword')
|
||||||
|
->field('username')->equals('jwage')
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
If you want to just set the values of an entirely new object you
|
||||||
|
can do so by passing false as the third argument of ``set()`` to
|
||||||
|
tell it the update is not an atomic one:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$dm->createQueryBuilder('User')
|
||||||
|
->updateOne()
|
||||||
|
->field('username')->set('jwage', false)
|
||||||
|
->field('password')->set('password', false)
|
||||||
|
// ... set other remaining fields
|
||||||
|
->field('username')->equals('jwage')
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
Read more about the
|
||||||
|
`$set modifier <https://docs.mongodb.com/manual/reference/operator/update/set/>`_
|
||||||
|
in the Mongo docs.
|
||||||
|
|
||||||
|
You can set an entirely new object to update as well:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$dm->createQueryBuilder('User')
|
||||||
|
->setNewObj(array(
|
||||||
|
'username' => 'jwage',
|
||||||
|
'password' => 'password',
|
||||||
|
// ... other fields
|
||||||
|
))
|
||||||
|
->field('username')->equals('jwage')
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
Increment the value of a document:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$dm->createQueryBuilder('Package')
|
||||||
|
->field('id')->equals('theid')
|
||||||
|
->field('downloads')->inc(1)
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
Read more about the
|
||||||
|
`$inc modifier <https://docs.mongodb.com/manual/reference/operator/update/inc/>`_
|
||||||
|
in the Mongo docs.
|
||||||
|
|
||||||
|
Unset the login field from users where the login field still
|
||||||
|
exists:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$dm->createQueryBuilder('User')
|
||||||
|
->updateMany()
|
||||||
|
->field('login')->unsetField()->exists(true)
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
Read more about the
|
||||||
|
`$unset modifier <https://docs.mongodb.com/manual/reference/operator/update/unset/>`_
|
||||||
|
in the Mongo docs.
|
||||||
|
|
||||||
|
Append new tag to the tags array:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$dm->createQueryBuilder('Article')
|
||||||
|
->updateOne()
|
||||||
|
->field('tags')->push('tag5')
|
||||||
|
->field('id')->equals('theid')
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
Read more about the
|
||||||
|
`$push modifier <https://docs.mongodb.com/manual/reference/operator/update/push/>`_
|
||||||
|
in the Mongo docs.
|
||||||
|
|
||||||
|
Append new tags to the tags array:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$dm->createQueryBuilder('Article')
|
||||||
|
->updateOne()
|
||||||
|
->field('tags')->pushAll(array('tag6', 'tag7'))
|
||||||
|
->field('id')->equals('theid')
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
Read more about the
|
||||||
|
`$pushAll modifier <https://docs.mongodb.com/manual/reference/operator/update/pushAll/>`_
|
||||||
|
in the Mongo docs.
|
||||||
|
|
||||||
|
Add value to array only if its not in the array already:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$dm->createQueryBuilder('Article')
|
||||||
|
->updateOne()
|
||||||
|
->field('tags')->addToSet('tag1')
|
||||||
|
->field('id')->equals('theid')
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
Read more about the
|
||||||
|
`$addToSet modifier <https://docs.mongodb.com/manual/reference/operator/update/addToSet/>`_
|
||||||
|
in the Mongo docs.
|
||||||
|
|
||||||
|
Add many values to the array only if they do not exist in the array
|
||||||
|
already:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$dm->createQueryBuilder('Article')
|
||||||
|
->updateOne()
|
||||||
|
->field('tags')->addManyToSet(array('tag6', 'tag7'))
|
||||||
|
->field('id')->equals('theid')
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
Read more about the
|
||||||
|
`$addManyToSet modifier <http://www.mongodb.org/display/DOCS/Updating#Updating-%24addManyToSet>`_
|
||||||
|
in the Mongo docs.
|
||||||
|
|
||||||
|
Remove first element in an array:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$dm->createQueryBuilder('Article')
|
||||||
|
->updateOne()
|
||||||
|
->field('tags')->popFirst()
|
||||||
|
->field('id')->equals('theid')
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
Remove last element in an array:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$dm->createQueryBuilder('Article')
|
||||||
|
->updateOne()
|
||||||
|
->field('tags')->popLast()
|
||||||
|
->field('id')->equals('theid')
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
Read more about the
|
||||||
|
`$pop modifier <https://docs.mongodb.com/manual/reference/operator/update/pop/>`_
|
||||||
|
in the Mongo docs.
|
||||||
|
|
||||||
|
Remove all occurrences of value from array:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$dm->createQueryBuilder('Article')
|
||||||
|
->updateMany()
|
||||||
|
->field('tags')->pull('tag1')
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
Read more about the
|
||||||
|
`$pull modifier <https://docs.mongodb.com/manual/reference/operator/update/pull/>`_
|
||||||
|
in the Mongo docs.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$dm->createQueryBuilder('Article')
|
||||||
|
->updateMany()
|
||||||
|
->field('tags')->pullAll(array('tag1', 'tag2'))
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
Read more about the
|
||||||
|
`$pullAll modifier <https://docs.mongodb.com/manual/reference/operator/update/pullAll/>`_
|
||||||
|
in the Mongo docs.
|
||||||
|
|
||||||
|
Remove Queries
|
||||||
|
--------------
|
||||||
|
|
||||||
|
In addition to updating you can also issue queries to remove
|
||||||
|
documents from a collection. It works pretty much the same way as
|
||||||
|
everything else and you can use the conditional operations to
|
||||||
|
specify which documents you want to remove.
|
||||||
|
|
||||||
|
Here is an example where we remove users who have never logged in:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$dm->createQueryBuilder('User')
|
||||||
|
->remove()
|
||||||
|
->field('num_logins')->equals(0)
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
Group Queries
|
||||||
|
-------------
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Due to deprecation of ``group`` command in MongoDB 3.4 the ODM
|
||||||
|
also deprecates its usage through Query Builder in 1.2. Please
|
||||||
|
use :ref:`$group stage <aggregation_builder_group>` of the
|
||||||
|
Aggregation Builder instead.
|
||||||
|
|
||||||
|
The last type of supported query is a group query. It performs an
|
||||||
|
operation similar to SQL's GROUP BY command.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$result = $this->dm->createQueryBuilder('Documents\User')
|
||||||
|
->group(array(), array('count' => 0))
|
||||||
|
->reduce('function (obj, prev) { prev.count++; }')
|
||||||
|
->field('a')->gt(1)
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
This is the same as if we were to do the group with the raw PHP
|
||||||
|
code:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$reduce = 'function (obj, prev) { prev.count++; }';
|
||||||
|
$condition = array('a' => array( '$gt' => 1));
|
||||||
|
$result = $collection->group(array(), array('count' => 0), $reduce, $condition);
|
|
@ -0,0 +1,495 @@
|
||||||
|
Reference Mapping
|
||||||
|
=================
|
||||||
|
|
||||||
|
This chapter explains how references between documents are mapped with Doctrine.
|
||||||
|
|
||||||
|
Collections
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Examples of many-valued references in this manual make use of a ``Collection``
|
||||||
|
interface and a corresponding ``ArrayCollection`` implementation, which are
|
||||||
|
defined in the ``Doctrine\Common\Collections`` namespace. These classes have no
|
||||||
|
dependencies on ODM, and can therefore be used within your domain model and
|
||||||
|
elsewhere without introducing coupling to the persistence layer.
|
||||||
|
|
||||||
|
ODM also provides a ``PersistentCollection`` implementation of ``Collection``,
|
||||||
|
which incorporates change-tracking functionality; however, this class is
|
||||||
|
constructed internally during hydration. As a developer, you should develop with
|
||||||
|
the ``Collection`` interface in mind so that your code can operate with any
|
||||||
|
implementation.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
New in 1.1: you are no longer limited to using ``ArrayCollection`` and can
|
||||||
|
freely use your own ``Collection`` implementation. For more details please
|
||||||
|
see :doc:`Custom Collections <custom-collections>` chapter.
|
||||||
|
|
||||||
|
Why are these classes used over PHP arrays? Native arrays cannot be
|
||||||
|
transparently extended in PHP, which is necessary for many advanced features
|
||||||
|
provided by the ODM. Although PHP does provide various interfaces that allow
|
||||||
|
objects to operate like arrays (e.g. ``Traversable``, ``Countable``,
|
||||||
|
``ArrayAccess``), and even a concrete implementation in ``ArrayObject``, these
|
||||||
|
objects cannot always be used everywhere that a native array is accepted.
|
||||||
|
Doctrine's ``Collection`` interface and ``ArrayCollection`` implementation are
|
||||||
|
conceptually very similar to ``ArrayObject``, with some slight differences and
|
||||||
|
improvements.
|
||||||
|
|
||||||
|
.. _reference_one:
|
||||||
|
|
||||||
|
Reference One
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Reference one document:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class Product
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceOne(targetDocument="Shipping")
|
||||||
|
*/
|
||||||
|
private $shipping;
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class Shipping
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
<document name="Documents\Product">
|
||||||
|
<reference-one field="shipping" target-document="Documents\Shipping" />
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
Product:
|
||||||
|
type: document
|
||||||
|
referenceOne:
|
||||||
|
shipping:
|
||||||
|
targetDocument: Documents\Shipping
|
||||||
|
|
||||||
|
.. _reference_many:
|
||||||
|
|
||||||
|
Reference Many
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Reference many documents:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceMany(targetDocument="Account")
|
||||||
|
*/
|
||||||
|
private $accounts = array();
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class Account
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
<document name="Documents\Product">
|
||||||
|
<reference-many field="accounts" target-document="Documents\Account" />
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
User:
|
||||||
|
type: document
|
||||||
|
referenceMany:
|
||||||
|
accounts:
|
||||||
|
targetDocument: Documents\Account
|
||||||
|
|
||||||
|
.. _reference_mixing_document_types:
|
||||||
|
|
||||||
|
Mixing Document Types
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
If you want to store different types of documents in references, you can simply
|
||||||
|
omit the ``targetDocument`` option:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
// ..
|
||||||
|
|
||||||
|
/** @ReferenceMany */
|
||||||
|
private $favorites = array();
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<field fieldName="favorites" />
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
referenceMany:
|
||||||
|
favorites: ~
|
||||||
|
|
||||||
|
Now the ``$favorites`` property can store a reference to any type of document!
|
||||||
|
The class name will be automatically stored in a field named
|
||||||
|
``_doctrine_class_name`` within the `DBRef`_ object.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The MongoDB shell tends to ignore fields other than ``$id`` and ``$ref``
|
||||||
|
when displaying `DBRef`_ objects. You can verify the presence of any ``$db``
|
||||||
|
and discriminator fields by querying and examining the document with a
|
||||||
|
driver. See `SERVER-10777 <https://jira.mongodb.org/browse/SERVER-10777>`_
|
||||||
|
for additional discussion on this issue.
|
||||||
|
|
||||||
|
The name of the field within the DBRef object can be customized via the
|
||||||
|
``discriminatorField`` option:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
// ..
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceMany(discriminatorField="type")
|
||||||
|
*/
|
||||||
|
private $favorites = array();
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<reference-many fieldName="favorites">
|
||||||
|
<discriminator-field name="type" />
|
||||||
|
</reference-many>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
referenceMany:
|
||||||
|
favorites:
|
||||||
|
discriminatorField: type
|
||||||
|
|
||||||
|
You can also specify a discriminator map to avoid storing the |FQCN|
|
||||||
|
in each `DBRef`_ object:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
// ..
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceMany(
|
||||||
|
* discriminatorMap={
|
||||||
|
* "album"="Album",
|
||||||
|
* "song"="Song"
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
private $favorites = array();
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<reference-many fieldName="favorites">
|
||||||
|
<discriminator-map>
|
||||||
|
<discriminator-mapping value="album" class="Documents\Album" />
|
||||||
|
<discriminator-mapping value="song" class="Documents\Song" />
|
||||||
|
</discriminator-map>
|
||||||
|
</reference-many>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
referenceMany:
|
||||||
|
favorites:
|
||||||
|
discriminatorMap:
|
||||||
|
album: Documents\Album
|
||||||
|
song: Documents\Song
|
||||||
|
|
||||||
|
If you have references without a discriminator value that should be considered
|
||||||
|
a certain class, you can optionally specify a default discriminator value:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
// ..
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceMany(
|
||||||
|
* discriminatorMap={
|
||||||
|
* "album"="Album",
|
||||||
|
* "song"="Song"
|
||||||
|
* },
|
||||||
|
* defaultDiscriminatorValue="album"
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
private $favorites = array();
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<reference-many fieldName="favorites">
|
||||||
|
<discriminator-map>
|
||||||
|
<discriminator-mapping value="album" class="Documents\Album" />
|
||||||
|
<discriminator-mapping value="song" class="Documents\Song" />
|
||||||
|
</discriminator-map>
|
||||||
|
<default-discriminator-value value="album" />
|
||||||
|
</reference-many>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
referenceMany:
|
||||||
|
favorites:
|
||||||
|
discriminatorMap:
|
||||||
|
album: Documents\Album
|
||||||
|
song: Documents\Song
|
||||||
|
defaultDiscriminatorValue: album
|
||||||
|
|
||||||
|
.. _storing_references:
|
||||||
|
|
||||||
|
Storing References
|
||||||
|
------------------
|
||||||
|
|
||||||
|
By default all references are stored as a `DBRef`_ object with the traditional
|
||||||
|
``$ref``, ``$id``, and (optionally) ``$db`` fields (in that order). For references to
|
||||||
|
documents of a single collection, storing the collection (and database) names for
|
||||||
|
each reference may be redundant. You can use simple references to store the
|
||||||
|
referenced document's identifier (e.g. ``MongoId``) instead of a `DBRef`_.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceOne(targetDocument="Profile", storeAs="id")
|
||||||
|
*/
|
||||||
|
private $profile;
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<reference-one target-document="Documents\Profile", store-as="id" />
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
referenceOne:
|
||||||
|
profile:
|
||||||
|
storeAs: id
|
||||||
|
|
||||||
|
Now, the ``profile`` field will only store the ``MongoId`` of the referenced
|
||||||
|
Profile document.
|
||||||
|
|
||||||
|
Simple references reduce the amount of storage used, both for the document
|
||||||
|
itself and any indexes on the reference field; however, simple references cannot
|
||||||
|
be used with discriminators, since there is no `DBRef`_ object in which to store
|
||||||
|
a discriminator value.
|
||||||
|
|
||||||
|
In addition to saving references as `DBRef`_ with ``$ref``, ``$id``, and ``$db``
|
||||||
|
fields and as ``MongoId``, it is possible to save references as `DBRef`_ without
|
||||||
|
the ``$db`` field. This solves problems when the database name changes (and also
|
||||||
|
reduces the amount of storage used).
|
||||||
|
|
||||||
|
The ``storeAs`` option has the following possible values:
|
||||||
|
|
||||||
|
- **dbRefWithDb**: Uses a `DBRef`_ with ``$ref``, ``$id``, and ``$db`` fields (this is the default)
|
||||||
|
- **dbRef**: Uses a `DBRef`_ with ``$ref`` and ``$id``
|
||||||
|
- **ref**: Uses a custom embedded object with an ``id`` field
|
||||||
|
- **id**: Uses the identifier of the referenced object
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The ``storeAs=id`` option used to be called a "simple reference". The old syntax is
|
||||||
|
still recognized (so using ``simple=true`` will imply ``storeAs=id``).
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
For backwards compatibility ``storeAs=dbRefWithDb`` is the default, but
|
||||||
|
``storeAs=ref`` is the recommended setting.
|
||||||
|
|
||||||
|
|
||||||
|
Cascading Operations
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
By default, Doctrine will not cascade any ``UnitOfWork`` operations to
|
||||||
|
referenced documents. You must explicitly enable this functionality:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceOne(targetDocument="Profile", cascade={"persist"})
|
||||||
|
*/
|
||||||
|
private $profile;
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<reference-one target-document="Documents\Profile">
|
||||||
|
<cascade>
|
||||||
|
<persist/>
|
||||||
|
</cascade>
|
||||||
|
</reference-one>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
referenceOne:
|
||||||
|
profile:
|
||||||
|
cascade: [persist]
|
||||||
|
|
||||||
|
The valid values are:
|
||||||
|
|
||||||
|
- **all** - cascade all operations by default.
|
||||||
|
- **detach** - cascade detach operation to referenced documents.
|
||||||
|
- **merge** - cascade merge operation to referenced documents.
|
||||||
|
- **refresh** - cascade refresh operation to referenced documents.
|
||||||
|
- **remove** - cascade remove operation to referenced documents.
|
||||||
|
- **persist** - cascade persist operation to referenced documents.
|
||||||
|
|
||||||
|
Orphan Removal
|
||||||
|
--------------
|
||||||
|
|
||||||
|
There is another concept of cascading that is relevant only when removing documents
|
||||||
|
from collections. If a Document of type ``A`` contains references to privately
|
||||||
|
owned Documents ``B`` then if the reference from ``A`` to ``B`` is removed the
|
||||||
|
document ``B`` should also be removed, because it is not used anymore.
|
||||||
|
|
||||||
|
OrphanRemoval works with both reference one and many mapped fields.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
When using the ``orphanRemoval=true`` option Doctrine makes the assumption
|
||||||
|
that the documents are privately owned and will **NOT** be reused by other documents.
|
||||||
|
If you neglect this assumption your documents will get deleted by Doctrine even if
|
||||||
|
you assigned the orphaned documents to another one.
|
||||||
|
|
||||||
|
As a better example consider an Addressbook application where you have Contacts, Addresses
|
||||||
|
and StandingData:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Addressbook;
|
||||||
|
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document
|
||||||
|
*/
|
||||||
|
class Contact
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
/** @ReferenceOne(targetDocument="StandingData", orphanRemoval=true) */
|
||||||
|
private $standingData;
|
||||||
|
|
||||||
|
/** @ReferenceMany(targetDocument="Address", mappedBy="contact", orphanRemoval=true) */
|
||||||
|
private $addresses;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->addresses = new ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function newStandingData(StandingData $sd)
|
||||||
|
{
|
||||||
|
$this->standingData = $sd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeAddress($pos)
|
||||||
|
{
|
||||||
|
unset($this->addresses[$pos]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Now two examples of what happens when you remove the references:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$contact = $dm->find("Addressbook\Contact", $contactId);
|
||||||
|
$contact->newStandingData(new StandingData("Firstname", "Lastname", "Street"));
|
||||||
|
$contact->removeAddress(1);
|
||||||
|
|
||||||
|
$dm->flush();
|
||||||
|
|
||||||
|
In this case you have not only changed the ``Contact`` document itself but
|
||||||
|
you have also removed the references for standing data and as well as one
|
||||||
|
address reference. When flush is called not only are the references removed
|
||||||
|
but both the old standing data and the one address documents are also deleted
|
||||||
|
from the database.
|
||||||
|
|
||||||
|
.. _`DBRef`: https://docs.mongodb.com/manual/reference/database-references/#dbrefs
|
||||||
|
.. |FQCN| raw:: html
|
||||||
|
<abbr title="Fully-Qualified Class Name">FQCN</abbr>
|
|
@ -0,0 +1,69 @@
|
||||||
|
.. _sharding:
|
||||||
|
|
||||||
|
Sharding
|
||||||
|
========
|
||||||
|
|
||||||
|
MongoDB allows you to horizontally scale your database. In order to enable this,
|
||||||
|
Doctrine MongoDB ODM needs to know about your sharding setup. For basic information
|
||||||
|
about sharding, please refer to the `MongoDB docs <https://docs.mongodb.com/manual/sharding/>`_.
|
||||||
|
|
||||||
|
Once you have a `sharded cluster <https://docs.mongodb.com/manual/core/sharded-cluster-architectures-production/>`_,
|
||||||
|
you can enable sharding for a document. You can do this by defining a shard key in
|
||||||
|
the document:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Document
|
||||||
|
* @ShardKey(keys={"username"="asc"})
|
||||||
|
*/
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/** @Field(type="int") */
|
||||||
|
public $accountId;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
public $username;
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/orm/doctrine-mongo-mapping.xsd">
|
||||||
|
|
||||||
|
<document name="Documents\User">
|
||||||
|
<shard-key>
|
||||||
|
<key name="username" order="asc"/>
|
||||||
|
</shard-key>
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
Documents\User:
|
||||||
|
shardKey:
|
||||||
|
keys:
|
||||||
|
username: asc
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
When a shard key is defined for a document, Doctrine MongoDB ODM will no
|
||||||
|
longer persist changes to the shard key as these fields become immutable in
|
||||||
|
a sharded setup.
|
||||||
|
|
||||||
|
Once you've defined a shard key you need to enable sharding for the collection
|
||||||
|
where the document will be stored. To do this, use the ``odm:schema:shard``
|
||||||
|
command.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
For performance reasons, sharding is not enabled during the
|
||||||
|
``odm:schema:create`` and ``odm:schema:update`` commmands.
|
|
@ -0,0 +1,71 @@
|
||||||
|
Slave Okay Queries
|
||||||
|
==================
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
``slaveOkay`` was deprecated in 1.2 - please use `Read Preference <http://php.net/manual/en/mongo.readpreferences.php>`_
|
||||||
|
instead.
|
||||||
|
|
||||||
|
Documents
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
You can configure an entire document to send all reads to the slaves by using the ``slaveOkay`` flag:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document(slaveOkay=true) */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
private $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
Now all reads involving the ``User`` document will be sent to a slave.
|
||||||
|
|
||||||
|
Queries
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
If you want to instruct individual queries to read from a slave you can use the ``slaveOkay()`` method
|
||||||
|
on the query builder.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('User')
|
||||||
|
->slaveOkay(true);
|
||||||
|
$query = $qb->getQuery();
|
||||||
|
$users = $query->execute();
|
||||||
|
|
||||||
|
The data in the query above will be read from a slave. Even if you have a ``@ReferenceOne`` or
|
||||||
|
``@ReferenceMany`` resulting from the query above it will be initialized and loaded from a slave.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
/** @ReferenceMany(targetDocument="Account") */
|
||||||
|
private $accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
Now when you query and iterate over the accounts, they will be loaded from a slave:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$qb = $dm->createQueryBuilder('User')
|
||||||
|
->slaveOkay(true);
|
||||||
|
$query = $qb->getQuery();
|
||||||
|
$users = $query->execute();
|
||||||
|
|
||||||
|
foreach ($users as $user) {
|
||||||
|
foreach ($user->getAccounts() as $account) {
|
||||||
|
echo $account->getName();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
.. _storage_strategies:
|
||||||
|
|
||||||
|
Storage Strategies
|
||||||
|
==================
|
||||||
|
|
||||||
|
Doctrine MongoDB ODM implements several different strategies for persisting changes
|
||||||
|
to mapped fields. These strategies apply to the following mapping types:
|
||||||
|
|
||||||
|
- :ref:`int`
|
||||||
|
- :ref:`float`
|
||||||
|
- :ref:`embed_many`
|
||||||
|
- :ref:`reference_many`
|
||||||
|
|
||||||
|
For collections, Doctrine tracks changes via the PersistentCollection class. The
|
||||||
|
strategies described on this page are implemented by the CollectionPersister
|
||||||
|
class. The ``increment`` strategy cannot be used for collections.
|
||||||
|
|
||||||
|
increment
|
||||||
|
---------
|
||||||
|
|
||||||
|
The ``increment`` strategy does not apply to collections but can be used for
|
||||||
|
``int`` and ``float`` fields. When using the ``increment`` strategy, the field
|
||||||
|
value will be updated using the `$inc`_ operator.
|
||||||
|
|
||||||
|
addToSet
|
||||||
|
--------
|
||||||
|
|
||||||
|
The ``addToSet`` strategy uses MongoDB's `$addToSet`_ operator to insert
|
||||||
|
elements into the array. This strategy is useful for ensuring that duplicate
|
||||||
|
values will not be inserted into the collection. Like the `pushAll`_ strategy,
|
||||||
|
elements are inserted in a separate query after removing deleted elements.
|
||||||
|
|
||||||
|
set
|
||||||
|
---
|
||||||
|
|
||||||
|
The ``set`` strategy uses MongoDB's `$set`_ operator to update the entire
|
||||||
|
collection with a single update query.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Doctrine's Collection interface is modeled after PHP's associative arrays,
|
||||||
|
so they cannot always be represented as a BSON array. If the collection's
|
||||||
|
keys are not sequential integers starting with zero, the ``set`` strategy
|
||||||
|
will store the collection as a BSON object instead of an array. Use the
|
||||||
|
`setArray`_ strategy if you want to ensure that the collection is always
|
||||||
|
stored as a BSON array.
|
||||||
|
|
||||||
|
setArray
|
||||||
|
--------
|
||||||
|
|
||||||
|
The ``setArray`` strategy uses MongoDB's `$set`_ operator, just like the ``set``
|
||||||
|
strategy, but will first numerically reindex the collection to ensure that it is
|
||||||
|
stored as a BSON array.
|
||||||
|
|
||||||
|
pushAll
|
||||||
|
-------
|
||||||
|
|
||||||
|
The ``pushAll`` strategy uses MongoDB's `$pushAll`_ operator to insert
|
||||||
|
elements into the array. MongoDB does not allow elements to be added and removed
|
||||||
|
from an array in a single operation, so this strategy relies on multiple update
|
||||||
|
queries to remove and insert elements (in that order).
|
||||||
|
|
||||||
|
.. _atomic_set:
|
||||||
|
|
||||||
|
atomicSet
|
||||||
|
---------
|
||||||
|
|
||||||
|
The ``atomicSet`` strategy uses MongoDB's `$set`_ operator to update the entire
|
||||||
|
collection with a single update query. Unlike with ``set`` strategy there will
|
||||||
|
be only one query for updating both parent document and collection itself. This
|
||||||
|
strategy can be especially useful when dealing with high concurrency and
|
||||||
|
:ref:`versioned documents <annotations_reference_version>`.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The ``atomicSet`` and ``atomicSetArray`` strategies may only be used for
|
||||||
|
collections mapped directly in a top-level document.
|
||||||
|
|
||||||
|
.. _atomic_set_array:
|
||||||
|
|
||||||
|
atomicSetArray
|
||||||
|
--------------
|
||||||
|
|
||||||
|
The ``atomicSetArray`` strategy works exactly like ``atomicSet`` strategy, but
|
||||||
|
will first numerically reindex the collection to ensure that it is stored as a
|
||||||
|
BSON array.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The ``atomicSet`` and ``atomicSetArray`` strategies may only be used for
|
||||||
|
collections mapped directly in a top-level document.
|
||||||
|
|
||||||
|
.. _`$addToSet`: https://docs.mongodb.com/manual/reference/operator/update/addToSet/
|
||||||
|
.. _`$inc`: https://docs.mongodb.com/manual/reference/operator/update/inc/
|
||||||
|
.. _`$pushAll`: https://docs.mongodb.com/manual/reference/operator/update/pushAll/
|
||||||
|
.. _`$set`: https://docs.mongodb.com/manual/reference/operator/update/set/
|
||||||
|
.. _`$unset`: https://docs.mongodb.com/manual/reference/operator/update/unset/
|
|
@ -0,0 +1,190 @@
|
||||||
|
Storing Files with MongoGridFS
|
||||||
|
==============================
|
||||||
|
|
||||||
|
The PHP Mongo extension provides a nice and convenient way to store
|
||||||
|
files in chunks of data with the
|
||||||
|
`MongoGridFS <http://us.php.net/manual/en/class.mongogridfs.php>`_.
|
||||||
|
|
||||||
|
It uses two database collections, one to store the metadata for the
|
||||||
|
file, and another to store the contents of the file. The contents
|
||||||
|
are stored in chunks to avoid going over the maximum allowed size
|
||||||
|
of a MongoDB document.
|
||||||
|
|
||||||
|
You can easily setup a Document that is stored using the
|
||||||
|
MongoGridFS:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class Image
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
/** @Field */
|
||||||
|
private $name;
|
||||||
|
|
||||||
|
/** @File */
|
||||||
|
private $file;
|
||||||
|
|
||||||
|
/** @Field */
|
||||||
|
private $uploadDate;
|
||||||
|
|
||||||
|
/** @Field */
|
||||||
|
private $length;
|
||||||
|
|
||||||
|
/** @Field */
|
||||||
|
private $chunkSize;
|
||||||
|
|
||||||
|
/** @Field */
|
||||||
|
private $md5;
|
||||||
|
|
||||||
|
public function getId()
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName($name)
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFile()
|
||||||
|
{
|
||||||
|
return $this->file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFile($file)
|
||||||
|
{
|
||||||
|
$this->file = $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Notice how we annotated the $file property with @File. This is what
|
||||||
|
tells the Document that it is to be stored using the MongoGridFS
|
||||||
|
and the MongoGridFSFile instance is placed in the $file property
|
||||||
|
for you to access the actual file itself.
|
||||||
|
|
||||||
|
The $uploadDate, $chunkSize and $md5 properties are automatically filled in
|
||||||
|
for each file stored in GridFS (whether you like that or not).
|
||||||
|
Feel free to create getters in your document to actually make use of them,
|
||||||
|
but keep in mind that their values will be initially unset for new objects
|
||||||
|
until the next time the document is hydrated (fetched from the database).
|
||||||
|
|
||||||
|
First you need to create a new Image:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$image = new Image();
|
||||||
|
$image->setName('Test image');
|
||||||
|
$image->setFile('/path/to/image.png');
|
||||||
|
|
||||||
|
$dm->persist($image);
|
||||||
|
$dm->flush();
|
||||||
|
|
||||||
|
Now you can later query for the Image and render it:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$image = $dm->createQueryBuilder('Documents\Image')
|
||||||
|
->field('name')->equals('Test image')
|
||||||
|
->getQuery()
|
||||||
|
->getSingleResult();
|
||||||
|
|
||||||
|
header('Content-type: image/png;');
|
||||||
|
echo $image->getFile()->getBytes();
|
||||||
|
|
||||||
|
You can of course make references to this Image document from
|
||||||
|
another document. Imagine you had a Profile document and you wanted
|
||||||
|
every Profile to have a profile image:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Documents;
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class Profile
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
/** @Field */
|
||||||
|
private $name;
|
||||||
|
|
||||||
|
/** @ReferenceOne(targetDocument="Documents\Image") */
|
||||||
|
private $image;
|
||||||
|
|
||||||
|
public function getId()
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName($name)
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getImage()
|
||||||
|
{
|
||||||
|
return $this->image;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setImage(Image $image)
|
||||||
|
{
|
||||||
|
$this->image = $image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Now you can create a new Profile and give it an Image:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$image = new Image();
|
||||||
|
$image->setName('Test image');
|
||||||
|
$image->setFile('/path/to/image.png');
|
||||||
|
|
||||||
|
$profile = new Profile();
|
||||||
|
$profile->setName('Jonathan H. Wage');
|
||||||
|
$profile->setImage($image);
|
||||||
|
|
||||||
|
$dm->persist($profile);
|
||||||
|
$dm->flush();
|
||||||
|
|
||||||
|
If you want to query for the Profile and load the Image reference
|
||||||
|
in a query you can use:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$profile = $dm->createQueryBuilder('Profile')
|
||||||
|
->field('name')->equals('Jonathan H. Wage')
|
||||||
|
->getQuery()
|
||||||
|
->getSingleResult();
|
||||||
|
|
||||||
|
$image = $profile->getImage();
|
||||||
|
|
||||||
|
header('Content-type: image/png;');
|
||||||
|
echo $image->getFile()->getBytes();
|
|
@ -0,0 +1,306 @@
|
||||||
|
.. Heavily inspired by Doctrine 2 ORM documentation
|
||||||
|
|
||||||
|
Transactions and Concurrency
|
||||||
|
============================
|
||||||
|
|
||||||
|
Transactions
|
||||||
|
------------
|
||||||
|
|
||||||
|
As per the `documentation <https://docs.mongodb.com/manual/core/write-operations-atomicity/#atomicity-and-transactions>`_, MongoDB
|
||||||
|
write operations are "atomic on the level of a single document".
|
||||||
|
|
||||||
|
Even when updating multiple documents within a single write operation,
|
||||||
|
though the modification of each document is atomic,
|
||||||
|
the operation as a whole is not and other operations may interleave.
|
||||||
|
|
||||||
|
As stated in the `FAQ <https://docs.mongodb.com/manual/faq/fundamentals/#does-mongodb-support-transactions>`_,
|
||||||
|
"MongoDB does not support multi-document transactions" and neither does Doctrine MongoDB ODM.
|
||||||
|
|
||||||
|
Limitation
|
||||||
|
~~~~~~~~~~
|
||||||
|
At the moment, Doctrine MongoDB ODM does not provide any native strategy to emulate multi-document transactions.
|
||||||
|
|
||||||
|
Workaround
|
||||||
|
~~~~~~~~~~
|
||||||
|
To work around this limitation, one can utilize `two phase commits <https://docs.mongodb.com/manual/tutorial/perform-two-phase-commits/>`_.
|
||||||
|
|
||||||
|
Concurrency
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Doctrine MongoDB ODM offers native support for pessimistic and optimistic locking strategies.
|
||||||
|
This allows for very fine-grained control over what kind of locking is required for documents in your application.
|
||||||
|
|
||||||
|
.. _transactions_and_concurrency_optimistic_locking:
|
||||||
|
|
||||||
|
Optimistic Locking
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Approach
|
||||||
|
^^^^^^^^
|
||||||
|
|
||||||
|
Doctrine has integrated support for automatic optimistic locking
|
||||||
|
via a ``version`` field. Any document that should be
|
||||||
|
protected against concurrent modifications during long-running
|
||||||
|
business transactions gets a ``version`` field that is either a simple
|
||||||
|
number (mapping type: ``int``) or a date (mapping type: ``date``).
|
||||||
|
When changes to the document are persisted,
|
||||||
|
the expected version and version increment are incorporated into the update criteria and modifiers, respectively.
|
||||||
|
If this results in no document being modified by the update (i.e. expected version did not match),
|
||||||
|
a ``LockException`` is thrown, which indicates that the document was already modified by another query.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
| Versioning can only be used on *root* (top-level) documents.
|
||||||
|
|
||||||
|
Document Configuration
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The following example designates a version field using the ``int`` type:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
/** @Version @Field(type="int") */
|
||||||
|
private $version;
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<field fieldName="version" version="true" type="int" />
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
version:
|
||||||
|
type: int
|
||||||
|
version: true
|
||||||
|
|
||||||
|
|
||||||
|
Alternatively, the ``date`` type may be used:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
/** @Version @Field(type="date") */
|
||||||
|
private $version;
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<field fieldName="version" version="true" type="date" />
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
version:
|
||||||
|
type: date
|
||||||
|
version: true
|
||||||
|
|
||||||
|
Choosing the Field Type
|
||||||
|
"""""""""""""""""""""""
|
||||||
|
|
||||||
|
When using the ``date`` type in a high-concurrency environment, it is still possible to create multiple documents
|
||||||
|
with the same version and cause a conflict. This can be avoided by using the ``int`` type.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
"""""
|
||||||
|
|
||||||
|
When a version conflict is encountered during
|
||||||
|
``DocumentManager#flush()``, a ``LockException`` is thrown.
|
||||||
|
This exception can be caught and handled. Potential responses to a
|
||||||
|
``LockException`` are to present the conflict to the user or
|
||||||
|
to refresh or reload objects and then retry the update.
|
||||||
|
|
||||||
|
With PHP promoting a share-nothing architecture,
|
||||||
|
the worst case scenario for a delay between rendering an update form (with existing document data)
|
||||||
|
and modifying the document after a form submission may be your application's session timeout.
|
||||||
|
If the document is changed within that time frame by some other request,
|
||||||
|
it may be preferable to encounter a ``LockException`` when retrieving the document instead of executing the update.
|
||||||
|
|
||||||
|
You can specify the expected version of a document during a query with ``DocumentManager#find()``:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
use Doctrine\ODM\MongoDB\LockMode;
|
||||||
|
use Doctrine\ODM\MongoDB\LockException;
|
||||||
|
use Doctrine\ODM\MongoDB\DocumentManager;
|
||||||
|
|
||||||
|
$theDocumentId = 1;
|
||||||
|
$expectedVersion = 184;
|
||||||
|
|
||||||
|
/* @var $dm DocumentManager */
|
||||||
|
|
||||||
|
try {
|
||||||
|
$document = $dm->find('User', $theDocumentId, LockMode::OPTIMISTIC, $expectedVersion);
|
||||||
|
|
||||||
|
// do the work
|
||||||
|
|
||||||
|
$dm->flush();
|
||||||
|
} catch(LockException $e) {
|
||||||
|
echo "Sorry, but someone else has already changed this document. Please apply the changes again!";
|
||||||
|
}
|
||||||
|
|
||||||
|
Alternatively, an expected version may be specified for an existing document with ``DocumentManager#lock()``:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
use Doctrine\ODM\MongoDB\LockMode;
|
||||||
|
use Doctrine\ODM\MongoDB\LockException;
|
||||||
|
use Doctrine\ODM\MongoDB\DocumentManager;
|
||||||
|
|
||||||
|
$theDocumentId = 1;
|
||||||
|
$expectedVersion = 184;
|
||||||
|
|
||||||
|
/* @var $dm DocumentManager */
|
||||||
|
|
||||||
|
$document = $dm->find('User', $theDocumentId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// assert version
|
||||||
|
$dm->lock($document, LockMode::OPTIMISTIC, $expectedVersion);
|
||||||
|
|
||||||
|
} catch(LockException $e) {
|
||||||
|
echo "Sorry, but someone else has already changed this document. Please apply the changes again!";
|
||||||
|
}
|
||||||
|
|
||||||
|
Important Implementation Notes
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
You can easily get the optimistic locking workflow wrong if you
|
||||||
|
compare the wrong versions.
|
||||||
|
|
||||||
|
Workflow
|
||||||
|
""""""""
|
||||||
|
|
||||||
|
Say you have Alice and Bob editing a
|
||||||
|
hypothetical blog post:
|
||||||
|
|
||||||
|
- Alice reads the headline of the blog post being "Foo", at
|
||||||
|
optimistic lock version 1 (GET Request)
|
||||||
|
- Bob reads the headline of the blog post being "Foo", at
|
||||||
|
optimistic lock version 1 (GET Request)
|
||||||
|
- Bob updates the headline to "Bar", upgrading the optimistic lock
|
||||||
|
version to 2 (POST Request of a Form)
|
||||||
|
- Alice updates the headline to "Baz", ... (POST Request of a
|
||||||
|
Form)
|
||||||
|
|
||||||
|
At the last stage of this scenario the blog post has to be read
|
||||||
|
again from the database before Alice's headline can be applied. At
|
||||||
|
this point you will want to check if the blog post is still at
|
||||||
|
version 1 (which it is not in this scenario).
|
||||||
|
|
||||||
|
In order to correctly utilize optimistic locking, you *must* add the version as hidden form field or,
|
||||||
|
for more security, session attribute.
|
||||||
|
Otherwise, you cannot verify that the version at the time of update is the same as what was originally read
|
||||||
|
from the database when Alice performed her original GET request for the blog post.
|
||||||
|
Without correlating the version across form submissions, the application could lose updates.
|
||||||
|
|
||||||
|
Example Code
|
||||||
|
""""""""""""
|
||||||
|
|
||||||
|
The form (GET Request):
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
use Doctrine\ODM\MongoDB\DocumentManager;
|
||||||
|
|
||||||
|
/* @var $dm DocumentManager */
|
||||||
|
|
||||||
|
$post = $dm->find('BlogPost', 123456);
|
||||||
|
|
||||||
|
echo '<input type="hidden" name="id" value="' . $post->getId() . '" />';
|
||||||
|
echo '<input type="hidden" name="version" value="' . $post->getCurrentVersion() . '" />';
|
||||||
|
|
||||||
|
And the change headline action (POST Request):
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
use Doctrine\ODM\MongoDB\DocumentManager;
|
||||||
|
use Doctrine\ODM\MongoDB\LockMode;
|
||||||
|
|
||||||
|
/* @var $dm DocumentManager */
|
||||||
|
|
||||||
|
$postId = (int)$_POST['id'];
|
||||||
|
$postVersion = (int)$_POST['version'];
|
||||||
|
|
||||||
|
$post = $dm->find('BlogPost', $postId, LockMode::OPTIMISTIC, $postVersion);
|
||||||
|
|
||||||
|
.. _transactions_and_concurrency_pessimistic_locking:
|
||||||
|
|
||||||
|
Pessimistic Locking
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Doctrine MongoDB ODM also supports pessimistic locking via a configurable ``lock`` field.
|
||||||
|
This functionality is implemented entirely by Doctrine; MongoDB has no native support for pessimistic locking.
|
||||||
|
|
||||||
|
Document Configuration
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Pessimistic locking requires a document to designate a lock field using the ``int`` type:
|
||||||
|
|
||||||
|
.. configuration-block::
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
/** @Lock @Field(type="int") */
|
||||||
|
private $lock;
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<field fieldName="lock" lock="true" type="int" />
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
lock:
|
||||||
|
type: int
|
||||||
|
lock: true
|
||||||
|
|
||||||
|
Lock Modes
|
||||||
|
^^^^^^^^^^
|
||||||
|
|
||||||
|
Doctrine MongoDB ODM currently supports two pessimistic lock modes:
|
||||||
|
|
||||||
|
- Pessimistic Write
|
||||||
|
(``\Doctrine\ODM\MongoDB\LockMode::PESSIMISTIC_WRITE``): locks the
|
||||||
|
underlying document for concurrent read and write operations.
|
||||||
|
- Pessimistic Read (``\Doctrine\ODM\MongoDB\LockMode::PESSIMISTIC_READ``):
|
||||||
|
locks other concurrent requests that attempt to update or lock documents
|
||||||
|
in write mode.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
^^^^^
|
||||||
|
|
||||||
|
You can use pessimistic locks in two different scenarios:
|
||||||
|
|
||||||
|
1. Using
|
||||||
|
``DocumentManager#find($className, $id, \Doctrine\ODM\MongoDB\LockMode::PESSIMISTIC_WRITE)``
|
||||||
|
or
|
||||||
|
``DocumentManager#find($className, $id, \Doctrine\ODM\MongoDB\LockMode::PESSIMISTIC_READ)``
|
||||||
|
2. Using
|
||||||
|
``DocumentManager#lock($document, \Doctrine\ODM\MongoDB\LockMode::PESSIMISTIC_WRITE)``
|
||||||
|
or
|
||||||
|
``DocumentManager#lock($document, \Doctrine\ODM\MongoDB\LockMode::PESSIMISTIC_READ)``
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
| A few things could go wrong:
|
||||||
|
|
|
||||||
|
| If a request fails to complete (e.g. unhandled exception), you may end up with stale locks.
|
||||||
|
Said locks would need to be manually released or you would need to devise a strategy to automatically do so.
|
||||||
|
One way to mitigate stale locks after an application error would be to gracefully catch the exception
|
||||||
|
and ensure that relevant documents are unlocked before the request ends.
|
||||||
|
|
|
||||||
|
| `Deadlock <https://en.wikipedia.org/wiki/Deadlock>`_ situations are also possible.
|
||||||
|
Suppose process P1 needs resource R1 and has locked resource R2
|
||||||
|
and that another process P2 has locked resource R1 but also needs resource R2.
|
||||||
|
If both processes continue waiting for the respective resources, the application will be stuck.
|
||||||
|
When loading a document, Doctrine can immediately throw an exception if it is already locked.
|
||||||
|
A deadlock could be created by endlessly retrying attempts to acquire the lock.
|
||||||
|
One can avoid a possible deadlock by designating a maximum number of retry attempts
|
||||||
|
and automatically releasing any active locks with the request ends,
|
||||||
|
thereby allowing a process to end gracefully while another completes its task.
|
|
@ -0,0 +1,267 @@
|
||||||
|
Trees
|
||||||
|
=====
|
||||||
|
|
||||||
|
MongoDB lends itself quite well to storing hierarchical data. This
|
||||||
|
chapter will demonstrate some examples!
|
||||||
|
|
||||||
|
Full Tree in Single Document
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class BlogPost
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
private $title;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
private $body;
|
||||||
|
|
||||||
|
/** @EmbedMany(targetDocument="Comment") */
|
||||||
|
private $comments = array();
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @EmbeddedDocument */
|
||||||
|
class Comment
|
||||||
|
{
|
||||||
|
/** @Field(type="string") */
|
||||||
|
private $by;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
private $text;
|
||||||
|
|
||||||
|
/** @EmbedMany(targetDocument="Comment") */
|
||||||
|
private $replies = array();
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
Retrieve a blog post and only select the first 10 comments:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$post = $dm->createQueryBuilder('BlogPost')
|
||||||
|
->selectSlice('replies', 0, 10)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleResult();
|
||||||
|
|
||||||
|
$replies = $post->getReplies();
|
||||||
|
|
||||||
|
You can read more about this pattern on the MongoDB documentation page "Trees in MongoDB" in the
|
||||||
|
`Full Tree in Single Document <http://www.mongodb.org/display/DOCS/Trees+in+MongoDB#TreesinMongoDB-FullTreeinSingleDocument>`_ section.
|
||||||
|
|
||||||
|
Parent Reference
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class Category
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
private $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceOne(targetDocument="Category")
|
||||||
|
* @Index
|
||||||
|
*/
|
||||||
|
private $parent;
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
Query for children by a specific parent id:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$children = $dm->createQueryBuilder('Category')
|
||||||
|
->field('parent.id')->equals('theid')
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
You can read more about this pattern on the MongoDB documentation page "Trees in MongoDB" in the
|
||||||
|
`Parent Links <https://docs.mongodb.com/manual/tutorial/model-tree-structures/#model-tree-structures-with-parent-references>`_ section.
|
||||||
|
|
||||||
|
Child Reference
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class Category
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
private $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceMany(targetDocument="Category")
|
||||||
|
* @Index
|
||||||
|
*/
|
||||||
|
private $children = array();
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
Query for immediate children of a category:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$category = $dm->createQueryBuilder('Category')
|
||||||
|
->field('id')->equals('theid')
|
||||||
|
->getQuery()
|
||||||
|
->getSingleResult();
|
||||||
|
|
||||||
|
$children = $category->getChildren();
|
||||||
|
|
||||||
|
Query for immediate parent of a category:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$parent = $dm->createQueryBuilder('Category')
|
||||||
|
->field('children.id')->equals('theid')
|
||||||
|
->getQuery()
|
||||||
|
->getSingleResult();
|
||||||
|
|
||||||
|
You can read more about this pattern on the MongoDB documentation page "Trees in MongoDB" in the
|
||||||
|
`Child Links <https://docs.mongodb.com/manual/tutorial/model-tree-structures/#model-tree-structures-with-child-references>`_ section.
|
||||||
|
|
||||||
|
Array of Ancestors
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @MappedSuperclass */
|
||||||
|
class BaseCategory
|
||||||
|
{
|
||||||
|
/** @Field(type="string") */
|
||||||
|
private $name;
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class Category extends BaseCategory
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceMany(targetDocument="Category")
|
||||||
|
* @Index
|
||||||
|
*/
|
||||||
|
private $ancestors = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ReferenceOne(targetDocument="Category")
|
||||||
|
* @Index
|
||||||
|
*/
|
||||||
|
private $parent;
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @EmbeddedDocument */
|
||||||
|
class SubCategory extends BaseCategory
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Query for all descendants of a category:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$categories = $dm->createQueryBuilder('Category')
|
||||||
|
->field('ancestors.id')->equals('theid')
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
Query for all ancestors of a category:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$category = $dm->createQuery('Category')
|
||||||
|
->field('id')->equals('theid')
|
||||||
|
->getQuery()
|
||||||
|
->getSingleResult();
|
||||||
|
|
||||||
|
$ancestors = $category->getAncestors();
|
||||||
|
|
||||||
|
You can read more about this pattern on the MongoDB documentation page "Trees in MongoDB" in the
|
||||||
|
`Array of Ancestors <https://docs.mongodb.com/manual/tutorial/model-tree-structures/#model-tree-structures-with-an-array-of-ancestors>`_ section.
|
||||||
|
|
||||||
|
Materialized Paths
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/** @Document */
|
||||||
|
class Category
|
||||||
|
{
|
||||||
|
/** @Id */
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
private $name;
|
||||||
|
|
||||||
|
/** @Field(type="string") */
|
||||||
|
private $path;
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
Query for the entire tree:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$categories = $dm->createQuery('Category')
|
||||||
|
->sort('path', 'asc')
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
Query for the node 'b' and all its descendants:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$categories = $dm->createQuery('Category')
|
||||||
|
->field('path')->equals('/^a,b,/')
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
You can read more about this pattern on the MongoDB documentation page "Trees in MongoDB" in the
|
||||||
|
`Materialized Paths (Full Path in Each Node) <https://docs.mongodb.com/manual/tutorial/model-tree-structures/#model-tree-structures-with-materialized-paths>`_ section.
|
|
@ -0,0 +1,34 @@
|
||||||
|
Upserting Documents
|
||||||
|
===================
|
||||||
|
|
||||||
|
Upserting documents in the MongoDB ODM is easy. All you really have to do
|
||||||
|
is specify an ID ahead of time and Doctrine will perform an ``update`` operation
|
||||||
|
with the ``upsert`` flag internally instead of a ``batchInsert``.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$article = new Article();
|
||||||
|
$article->setId($articleId);
|
||||||
|
$article->incrementNumViews();
|
||||||
|
$dm->persist($article);
|
||||||
|
$dm->flush();
|
||||||
|
|
||||||
|
The above would result in an operation like the following:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$articleCollection->update(
|
||||||
|
array('_id' => new MongoId($articleId)),
|
||||||
|
array('$inc' => array('numViews' => 1)),
|
||||||
|
array('upsert' => true, 'safe' => true)
|
||||||
|
);
|
||||||
|
|
||||||
|
The extra benefit is the fact that you don't have to fetch the ``$article`` in order
|
||||||
|
to append some new data to the document or change something. All you need is the
|
||||||
|
identifier.
|
|
@ -0,0 +1,573 @@
|
||||||
|
Working with Objects
|
||||||
|
====================
|
||||||
|
|
||||||
|
Understanding
|
||||||
|
-------------
|
||||||
|
|
||||||
|
In this chapter we will help you understand the ``DocumentManager``
|
||||||
|
and the ``UnitOfWork``. A Unit of Work is similar to an
|
||||||
|
object-level transaction. A new Unit of Work is implicitly started
|
||||||
|
when a DocumentManager is initially created or after
|
||||||
|
``DocumentManager#flush()`` has been invoked. A Unit of Work is
|
||||||
|
committed (and a new one started) by invoking
|
||||||
|
``DocumentManager#flush()``.
|
||||||
|
|
||||||
|
A Unit of Work can be manually closed by calling
|
||||||
|
``DocumentManager#close()``. Any changes to objects within this
|
||||||
|
Unit of Work that have not yet been persisted are lost.
|
||||||
|
|
||||||
|
The size of a Unit of Work
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The size of a Unit of Work mainly refers to the number of managed
|
||||||
|
documents at a particular point in time.
|
||||||
|
|
||||||
|
The cost of flush()
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
How costly a flush operation is in terms of performance mainly
|
||||||
|
depends on the size. You can get the size of your Unit of Work as
|
||||||
|
follows:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$uowSize = $dm->getUnitOfWork()->size();
|
||||||
|
|
||||||
|
The size represents the number of managed documents in the Unit of
|
||||||
|
Work. This size affects the performance of flush() operations due
|
||||||
|
to change tracking and, of course, memory consumption, so you may
|
||||||
|
want to check it from time to time during development.
|
||||||
|
|
||||||
|
.. caution::
|
||||||
|
|
||||||
|
Do not invoke ``flush`` after every change to a
|
||||||
|
document or every single invocation of persist/remove/merge/...
|
||||||
|
This is an anti-pattern and unnecessarily reduces the performance
|
||||||
|
of your application. Instead, form units of work that operate on
|
||||||
|
your objects and call ``flush`` when you are done. While serving a
|
||||||
|
single HTTP request there should be usually no need for invoking
|
||||||
|
``flush`` more than 0-2 times.
|
||||||
|
|
||||||
|
Direct access to a Unit of Work
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
You can get direct access to the Unit of Work by calling
|
||||||
|
``DocumentManager#getUnitOfWork()``. This will return the
|
||||||
|
UnitOfWork instance the DocumentManager is currently using.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$uow = $dm->getUnitOfWork();
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Directly manipulating a UnitOfWork is not recommended.
|
||||||
|
When working directly with the UnitOfWork API, respect methods
|
||||||
|
marked as INTERNAL by not using them and carefully read the API
|
||||||
|
documentation.
|
||||||
|
|
||||||
|
Persisting documents
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
A document can be made persistent by passing it to the
|
||||||
|
``DocumentManager#persist($document)`` method. By applying the
|
||||||
|
persist operation on some document, that document becomes MANAGED,
|
||||||
|
which means that its persistence is from now on managed by an
|
||||||
|
DocumentManager. As a result the persistent state of such a
|
||||||
|
document will subsequently be properly synchronized with the
|
||||||
|
database when ``DocumentManager#flush()`` is invoked.
|
||||||
|
|
||||||
|
.. caution::
|
||||||
|
|
||||||
|
Invoking the ``persist`` method on a document does NOT
|
||||||
|
cause an immediate insert to be issued on the database. Doctrine
|
||||||
|
applies a strategy called "transactional write-behind", which means
|
||||||
|
that it will delay most operations until
|
||||||
|
``DocumentManager#flush()`` is invoked which will then issue all
|
||||||
|
necessary queries to synchronize your objects with the database in
|
||||||
|
the most efficient way.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$user = new User();
|
||||||
|
$user->setUsername('jwage');
|
||||||
|
$user->setPassword('changeme');
|
||||||
|
$dm->persist($user);
|
||||||
|
$dm->flush();
|
||||||
|
|
||||||
|
.. caution::
|
||||||
|
|
||||||
|
The document identifier is generated during ``persist`` if not previously
|
||||||
|
specified. Users cannot rely on a document identifier being available during
|
||||||
|
the ``prePersist`` event.
|
||||||
|
|
||||||
|
The semantics of the persist operation, applied on a document X,
|
||||||
|
are as follows:
|
||||||
|
|
||||||
|
-
|
||||||
|
If X is a new document, it becomes managed. The document X will be
|
||||||
|
entered into the database as a result of the flush operation.
|
||||||
|
-
|
||||||
|
If X is a preexisting managed document, it is ignored by the
|
||||||
|
persist operation. However, the persist operation is cascaded to
|
||||||
|
documents referenced by X, if the relationships from X to these
|
||||||
|
other documents are mapped with cascade=PERSIST or cascade=ALL.
|
||||||
|
- If X is a removed document, it becomes managed.
|
||||||
|
- If X is a detached document, the behavior is undefined.
|
||||||
|
|
||||||
|
.. caution::
|
||||||
|
|
||||||
|
Do not pass detached documents to the persist operation.
|
||||||
|
|
||||||
|
.. _flush_options:
|
||||||
|
|
||||||
|
Flush Options
|
||||||
|
-------------
|
||||||
|
|
||||||
|
When committing your documents you can specify an array of options to the
|
||||||
|
``flush`` method. With it you can send options to the underlying database
|
||||||
|
like ``safe``, ``fsync``, etc.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$user = $dm->getRepository('User')->find($userId);
|
||||||
|
// ...
|
||||||
|
$user->setPassword('changeme');
|
||||||
|
$dm->flush(null, array('safe' => true, 'fsync' => true));
|
||||||
|
|
||||||
|
You can configure the default flush options on your ``Configuration`` object
|
||||||
|
if you want to set them globally for all flushes.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$config->setDefaultCommitOptions(array(
|
||||||
|
'safe' => true,
|
||||||
|
'fsync' => true
|
||||||
|
));
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Safe is set to true by default for all writes when using the ODM.
|
||||||
|
|
||||||
|
Removing documents
|
||||||
|
------------------
|
||||||
|
|
||||||
|
A document can be removed from persistent storage by passing it to
|
||||||
|
the ``DocumentManager#remove($document)`` method. By applying the
|
||||||
|
``remove`` operation on some document, that document becomes
|
||||||
|
REMOVED, which means that its persistent state will be deleted once
|
||||||
|
``DocumentManager#flush()`` is invoked. The in-memory state of a
|
||||||
|
document is unaffected by the ``remove`` operation.
|
||||||
|
|
||||||
|
.. caution::
|
||||||
|
|
||||||
|
Just like ``persist``, invoking ``remove`` on a
|
||||||
|
document does NOT cause an immediate query to be issued on the
|
||||||
|
database. The document will be removed on the next invocation of
|
||||||
|
``DocumentManager#flush()`` that involves that document.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$dm->remove($user);
|
||||||
|
$dm->flush();
|
||||||
|
|
||||||
|
The semantics of the remove operation, applied to a document X are
|
||||||
|
as follows:
|
||||||
|
|
||||||
|
-
|
||||||
|
If X is a new document, it is ignored by the remove operation.
|
||||||
|
However, the remove operation is cascaded to documents referenced
|
||||||
|
by X, if the relationship from X to these other documents is mapped
|
||||||
|
with cascade=REMOVE or cascade=ALL.
|
||||||
|
-
|
||||||
|
If X is a managed document, the remove operation causes it to
|
||||||
|
become removed. The remove operation is cascaded to documents
|
||||||
|
referenced by X, if the relationships from X to these other
|
||||||
|
documents is mapped with cascade=REMOVE or cascade=ALL.
|
||||||
|
-
|
||||||
|
If X is a detached document, an InvalidArgumentException will be
|
||||||
|
thrown.
|
||||||
|
-
|
||||||
|
If X is a removed document, it is ignored by the remove operation.
|
||||||
|
-
|
||||||
|
A removed document X will be removed from the database as a result
|
||||||
|
of the flush operation.
|
||||||
|
|
||||||
|
Detaching documents
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
A document is detached from a DocumentManager and thus no longer
|
||||||
|
managed by invoking the ``DocumentManager#detach($document)``
|
||||||
|
method on it or by cascading the detach operation to it. Changes
|
||||||
|
made to the detached document, if any (including removal of the
|
||||||
|
document), will not be synchronized to the database after the
|
||||||
|
document has been detached.
|
||||||
|
|
||||||
|
Doctrine will not hold on to any references to a detached
|
||||||
|
document.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$dm->detach($document);
|
||||||
|
|
||||||
|
The semantics of the detach operation, applied to a document X are
|
||||||
|
as follows:
|
||||||
|
|
||||||
|
|
||||||
|
-
|
||||||
|
If X is a managed document, the detach operation causes it to
|
||||||
|
become detached. The detach operation is cascaded to documents
|
||||||
|
referenced by X, if the relationships from X to these other
|
||||||
|
documents is mapped with cascade=DETACH or cascade=ALL. Documents
|
||||||
|
which previously referenced X will continue to reference X.
|
||||||
|
-
|
||||||
|
If X is a new or detached document, it is ignored by the detach
|
||||||
|
operation.
|
||||||
|
-
|
||||||
|
If X is a removed document, the detach operation is cascaded to
|
||||||
|
documents referenced by X, if the relationships from X to these
|
||||||
|
other documents is mapped with cascade=DETACH or
|
||||||
|
cascade=ALL/Documents which previously referenced X will continue
|
||||||
|
to reference X.
|
||||||
|
|
||||||
|
There are several situations in which a document is detached
|
||||||
|
automatically without invoking the ``detach`` method:
|
||||||
|
|
||||||
|
|
||||||
|
-
|
||||||
|
When ``DocumentManager#clear()`` is invoked, all documents that are
|
||||||
|
currently managed by the DocumentManager instance become detached.
|
||||||
|
-
|
||||||
|
When serializing a document. The document retrieved upon subsequent
|
||||||
|
unserialization will be detached (This is the case for all
|
||||||
|
documents that are serialized and stored in some cache).
|
||||||
|
|
||||||
|
The ``detach`` operation is usually not as frequently needed and
|
||||||
|
used as ``persist`` and ``remove``.
|
||||||
|
|
||||||
|
Merging documents
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Merging documents refers to the merging of (usually detached)
|
||||||
|
documents into the context of a DocumentManager so that they
|
||||||
|
become managed again. To merge the state of a document into an
|
||||||
|
DocumentManager use the ``DocumentManager#merge($document)``
|
||||||
|
method. The state of the passed document will be merged into a
|
||||||
|
managed copy of this document and this copy will subsequently be
|
||||||
|
returned.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$detachedDocument = unserialize($serializedDocument); // some detached document
|
||||||
|
$document = $dm->merge($detachedDocument);
|
||||||
|
// $document now refers to the fully managed copy returned by the merge operation.
|
||||||
|
// The DocumentManager $dm now manages the persistence of $document as usual.
|
||||||
|
|
||||||
|
The semantics of the merge operation, applied to a document X, are
|
||||||
|
as follows:
|
||||||
|
|
||||||
|
-
|
||||||
|
If X is a detached document, the state of X is copied onto a
|
||||||
|
pre-existing managed document instance X' of the same iddocument or
|
||||||
|
a new managed copy X' of X is created.
|
||||||
|
-
|
||||||
|
If X is a new document instance, an InvalidArgumentException will
|
||||||
|
be thrown.
|
||||||
|
-
|
||||||
|
If X is a removed document instance, an InvalidArgumentException
|
||||||
|
will be thrown.
|
||||||
|
-
|
||||||
|
If X is a managed document, it is ignored by the merge operation,
|
||||||
|
however, the merge operation is cascaded to documents referenced by
|
||||||
|
relationships from X if these relationships have been mapped with
|
||||||
|
the cascade element value MERGE or ALL.
|
||||||
|
-
|
||||||
|
For all documents Y referenced by relationships from X having the
|
||||||
|
cascade element value MERGE or ALL, Y is merged recursively as Y'.
|
||||||
|
For all such Y referenced by X, X' is set to reference Y'. (Note
|
||||||
|
that if X is managed then X is the same object as X'.)
|
||||||
|
-
|
||||||
|
If X is a document merged to X', with a reference to another
|
||||||
|
document Y, where cascade=MERGE or cascade=ALL is not specified,
|
||||||
|
then navigation of the same association from X' yields a reference
|
||||||
|
to a managed object Y' with the same persistent iddocument as Y.
|
||||||
|
|
||||||
|
The ``merge`` operation is usually not as frequently needed and
|
||||||
|
used as ``persist`` and ``remove``. The most common scenario for
|
||||||
|
the ``merge`` operation is to reattach documents to an
|
||||||
|
DocumentManager that come from some cache (and are therefore
|
||||||
|
detached) and you want to modify and persist such a document.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you load some detached documents from a cache and you
|
||||||
|
do not need to persist or delete them or otherwise make use of them
|
||||||
|
without the need for persistence services there is no need to use
|
||||||
|
``merge``. I.e. you can simply pass detached objects from a cache
|
||||||
|
directly to the view.
|
||||||
|
|
||||||
|
References
|
||||||
|
----------
|
||||||
|
|
||||||
|
References between documents and embedded documents are represented
|
||||||
|
just like in regular object-oriented PHP, with references to other
|
||||||
|
objects or collections of objects.
|
||||||
|
|
||||||
|
Establishing References
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Establishing a reference to another document is straight forward:
|
||||||
|
|
||||||
|
Here is an example where we add a new comment to an article:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$comment = new Comment();
|
||||||
|
// ...
|
||||||
|
|
||||||
|
$article->getComments()->add($comment);
|
||||||
|
|
||||||
|
Or you can set a single reference:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$address = new Address();
|
||||||
|
// ...
|
||||||
|
|
||||||
|
$user->setAddress($address);
|
||||||
|
|
||||||
|
Removing References
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Removing an association between two documents is similarly
|
||||||
|
straight-forward. There are two strategies to do so, by key and by
|
||||||
|
element. Here are some examples:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$article->getComments()->removeElement($comment);
|
||||||
|
$article->getComments()->remove($ithComment);
|
||||||
|
|
||||||
|
Or you can remove a single reference:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$user->setAddress(null);
|
||||||
|
|
||||||
|
When working with collections, keep in mind that a Collection is
|
||||||
|
essentially an ordered map (just like a PHP array). That is why the
|
||||||
|
``remove`` operation accepts an index/key. ``removeElement`` is a
|
||||||
|
separate method that has O(n) complexity, where n is the size of
|
||||||
|
the map.
|
||||||
|
|
||||||
|
Transitive persistence
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Persisting, removing, detaching and merging individual documents
|
||||||
|
can become pretty cumbersome, especially when a larger object graph
|
||||||
|
with collections is involved. Therefore Doctrine provides a
|
||||||
|
mechanism for transitive persistence through cascading of these
|
||||||
|
operations. Each reference to another document or a collection of
|
||||||
|
documents can be configured to automatically cascade certain
|
||||||
|
operations. By default, no operations are cascaded.
|
||||||
|
|
||||||
|
The following cascade options exist:
|
||||||
|
|
||||||
|
|
||||||
|
-
|
||||||
|
persist : Cascades persist operations to the associated documents.
|
||||||
|
- remove : Cascades remove operations to the associated documents.
|
||||||
|
- merge : Cascades merge operations to the associated documents.
|
||||||
|
- detach : Cascades detach operations to the associated documents.
|
||||||
|
-
|
||||||
|
all : Cascades persist, remove, merge and detach operations to
|
||||||
|
associated documents.
|
||||||
|
|
||||||
|
The following example shows an association to a number of
|
||||||
|
addresses. If persist() or remove() is invoked on any User
|
||||||
|
document, it will be cascaded to all associated Address documents
|
||||||
|
in the $addresses collection.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
//...
|
||||||
|
/**
|
||||||
|
* @ReferenceMany(targetDocument="Address", cascade={"persist", "remove"})
|
||||||
|
*/
|
||||||
|
private $addresses;
|
||||||
|
//...
|
||||||
|
}
|
||||||
|
|
||||||
|
Even though automatic cascading is convenient it should be used
|
||||||
|
with care. Do not blindly apply cascade=all to all associations as
|
||||||
|
it will unnecessarily degrade the performance of your application.
|
||||||
|
|
||||||
|
Querying
|
||||||
|
--------
|
||||||
|
|
||||||
|
Doctrine provides the following ways, in increasing level of power
|
||||||
|
and flexibility, to query for persistent objects. You should always
|
||||||
|
start with the simplest one that suits your needs.
|
||||||
|
|
||||||
|
By Primary Key
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The most basic way to query for a persistent object is by its
|
||||||
|
identifier / primary key using the
|
||||||
|
``DocumentManager#find($documentName, $id)`` method. Here is an
|
||||||
|
example:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$user = $dm->find('User', $id);
|
||||||
|
|
||||||
|
The return value is either the found document instance or null if
|
||||||
|
no instance could be found with the given identifier.
|
||||||
|
|
||||||
|
Essentially, ``DocumentManager#find()`` is just a shortcut for the
|
||||||
|
following:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$user = $dm->getRepository('User')->find($id);
|
||||||
|
|
||||||
|
``DocumentManager#getRepository($documentName)`` returns a
|
||||||
|
repository object which provides many ways to retrieve documents of
|
||||||
|
the specified type. By default, the repository instance is of type
|
||||||
|
``Doctrine\ODM\MongoDB\DocumentRepository``. You can also use
|
||||||
|
custom repository classes.
|
||||||
|
|
||||||
|
By Simple Conditions
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
To query for one or more documents based on several conditions that
|
||||||
|
form a logical conjunction, use the ``findBy`` and ``findOneBy``
|
||||||
|
methods on a repository as follows:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// All users that are 20 years old
|
||||||
|
$users = $dm->getRepository('User')->findBy(array('age' => 20));
|
||||||
|
|
||||||
|
// All users that are 20 years old and have a surname of 'Miller'
|
||||||
|
$users = $dm->getRepository('User')->findBy(array('age' => 20, 'surname' => 'Miller'));
|
||||||
|
|
||||||
|
// A single user by its nickname
|
||||||
|
$user = $dm->getRepository('User')->findOneBy(array('nickname' => 'romanb'));
|
||||||
|
|
||||||
|
A DocumentRepository also provides a mechanism for more concise
|
||||||
|
calls through its use of ``__call``. Thus, the following two
|
||||||
|
examples are equivalent:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// A single user by its nickname
|
||||||
|
$user = $dm->getRepository('User')->findOneBy(array('nickname' => 'romanb'));
|
||||||
|
|
||||||
|
// A single user by its nickname (__call magic)
|
||||||
|
$user = $dm->getRepository('User')->findOneByNickname('romanb');
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
You can learn more about Repositories in a :ref:`dedicated chapter <document_repositories>`.
|
||||||
|
|
||||||
|
By Lazy Loading
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Whenever you have a managed document instance at hand, you can
|
||||||
|
traverse and use any associations of that document as if they were
|
||||||
|
in-memory already. Doctrine will automatically load the associated
|
||||||
|
objects on demand through the concept of lazy-loading.
|
||||||
|
|
||||||
|
By Query Builder Objects
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The most powerful and flexible method to query for persistent
|
||||||
|
objects is the Query\Builder object. The Query\Builder object enables you to query
|
||||||
|
for persistent objects with a fluent object oriented interface.
|
||||||
|
|
||||||
|
You can create a query using
|
||||||
|
``DocumentManager#createQueryBuilder($documentName = null)``. Here is a
|
||||||
|
simple example:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// All users with an age between 20 and 30 (inclusive).
|
||||||
|
$qb = $dm->createQueryBuilder('User')
|
||||||
|
->field('age')->range(20, 30);
|
||||||
|
$q = $qb->getQuery()
|
||||||
|
$users = $q->execute();
|
||||||
|
|
||||||
|
By Reference
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
To query documents with a ReferenceOne association to another document, use the ``references($document)`` expression:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$group = $dm->find('Group', $id);
|
||||||
|
$usersWithGroup = $dm->createQueryBuilder('User')
|
||||||
|
->field('group')->references($group)
|
||||||
|
->getQuery()->execute();
|
||||||
|
|
||||||
|
To find documents with a ReferenceMany association that includes a certain document, use the ``includesReferenceTo($document)`` expression:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$users = $dm->createQueryBuilder('User')
|
||||||
|
->field('groups')->includesReferenceTo($group)
|
||||||
|
->getQuery()->execute();
|
|
@ -0,0 +1,188 @@
|
||||||
|
XML Mapping
|
||||||
|
===========
|
||||||
|
|
||||||
|
The XML mapping driver enables you to provide the ODM metadata in
|
||||||
|
form of XML documents.
|
||||||
|
|
||||||
|
The XML driver is backed by an XML Schema document that describes
|
||||||
|
the structure of a mapping document. The most recent version of the
|
||||||
|
XML Schema document is available online at
|
||||||
|
`http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd <http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd>`_.
|
||||||
|
The most convenient way to work with XML mapping files is to use an
|
||||||
|
IDE/editor that can provide code-completion based on such an XML
|
||||||
|
Schema document. The following is an outline of a XML mapping
|
||||||
|
document with the proper xmlns/xsi setup for the latest code in
|
||||||
|
trunk.
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you do not want to use latest XML Schema document please use link like
|
||||||
|
`http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping-1.0.0-BETA12.xsd <http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping-1.0.0-BETA12.xsd>`_.
|
||||||
|
You can change ``1.0.0-BETA12`` part of the URL to
|
||||||
|
`any other ODM version <https://github.com/doctrine/mongodb-odm/releases>`_.
|
||||||
|
|
||||||
|
The XML mapping document of a class is loaded on-demand the first
|
||||||
|
time it is requested and subsequently stored in the metadata cache.
|
||||||
|
In order to work, this requires certain conventions:
|
||||||
|
|
||||||
|
|
||||||
|
-
|
||||||
|
Each document/mapped superclass must get its own dedicated XML
|
||||||
|
mapping document.
|
||||||
|
-
|
||||||
|
The name of the mapping document must consist of the fully
|
||||||
|
qualified name of the class, where namespace separators are
|
||||||
|
replaced by dots (.).
|
||||||
|
-
|
||||||
|
All mapping documents should get the extension ".dcm.xml" to
|
||||||
|
identify it as a Doctrine mapping file. This is more of a
|
||||||
|
convention and you are not forced to do this. You can change the
|
||||||
|
file extension easily enough.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$driver->setFileExtension('.xml');
|
||||||
|
|
||||||
|
It is recommended to put all XML mapping documents in a single
|
||||||
|
folder but you can spread the documents over several folders if you
|
||||||
|
want to. In order to tell the XmlDriver where to look for your
|
||||||
|
mapping documents, supply an array of paths as the first argument
|
||||||
|
of the constructor, like this:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// $config instanceof Doctrine\ODM\MongoDB\Configuration
|
||||||
|
$driver = new XmlDriver(array('/path/to/files'));
|
||||||
|
$config->setMetadataDriverImpl($driver);
|
||||||
|
|
||||||
|
Simplified XML Driver
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The Symfony project sponsored a driver that simplifies usage of the XML Driver.
|
||||||
|
The changes between the original driver are:
|
||||||
|
|
||||||
|
1. File Extension is .mongodb-odm.xml
|
||||||
|
2. Filenames are shortened, "MyProject\Documents\User" will become User.mongodb-odm.xml
|
||||||
|
3. You can add a global file and add multiple documents in this file.
|
||||||
|
|
||||||
|
Configuration of this client works a little bit different:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$namespaces = array(
|
||||||
|
'MyProject\Documents' => '/path/to/files1',
|
||||||
|
'OtherProject\Documents' => '/path/to/files2'
|
||||||
|
);
|
||||||
|
$driver = new \Doctrine\ODM\MongoDB\Mapping\Driver\SimplifiedXmlDriver($namespaces);
|
||||||
|
$driver->setGlobalBasename('global'); // global.mongodb-odm.xml
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
As a quick start, here is a small example document that makes use
|
||||||
|
of several common elements:
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
// Documents.User.dcm.xml
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
||||||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
||||||
|
|
||||||
|
<document name="Documents\User" db="documents" collection="users">
|
||||||
|
<field fieldName="id" id="true" />
|
||||||
|
<field fieldName="username" name="login" type="string" />
|
||||||
|
<field fieldName="email" type="string" unique="true" order="desc" />
|
||||||
|
<field fieldName="createdAt" type="date" />
|
||||||
|
<indexes>
|
||||||
|
<index unique="true" dropDups="true">
|
||||||
|
<key name="username" order="desc">
|
||||||
|
<option name="safe" value="true" />
|
||||||
|
</index>
|
||||||
|
</indexes>
|
||||||
|
<embed-one target-document="Documents\Address" field="address" />
|
||||||
|
<reference-one target-document="Documents\Profile" field="profile">
|
||||||
|
<cascade>
|
||||||
|
<all />
|
||||||
|
</cascade>
|
||||||
|
</reference-one>
|
||||||
|
<embed-many target-document="Documents\Phonenumber" field="phonenumbers" />
|
||||||
|
<reference-many target-document="Documents\Group" field="groups">
|
||||||
|
<cascade>
|
||||||
|
<all />
|
||||||
|
</cascade>
|
||||||
|
</reference-many>
|
||||||
|
<reference-one target-document="Documents\Account" field="account">
|
||||||
|
<cascade>
|
||||||
|
<all />
|
||||||
|
</cascade>
|
||||||
|
</reference-one>
|
||||||
|
</document>
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
Be aware that class-names specified in the XML files should be fully qualified.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
``field-name`` is the name of **property in your object** while ``name`` specifies
|
||||||
|
name of the field **in the database**. Specifying latter is optional and defaults to
|
||||||
|
``field-name`` if not set explicitly.
|
||||||
|
|
||||||
|
Reference
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. _xml_reference_lock:
|
||||||
|
|
||||||
|
Lock
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
The field with the ``lock`` attribute will be used to store lock information for :ref:`pessimistic locking <transactions_and_concurrency_pessimistic_locking>`.
|
||||||
|
This is only compatible with the ``int`` field type, and cannot be combined with ``id="true"``.
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<doctrine-mongo-mapping>
|
||||||
|
<field fieldName="lock" lock="true" type="int" />
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
.. _xml_reference_version:
|
||||||
|
|
||||||
|
Version
|
||||||
|
^^^^^^^
|
||||||
|
|
||||||
|
The field with the ``version`` attribute will be used to store version information for :ref:`optimistic locking <transactions_and_concurrency_optimistic_locking>`.
|
||||||
|
This is only compatible with ``int`` and ``date`` field types, and cannot be combined with ``id="true"``.
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<doctrine-mongo-mapping>
|
||||||
|
<field fieldName="version" version="true" type="int" />
|
||||||
|
</doctrine-mongo-mapping>
|
||||||
|
|
||||||
|
By default, Doctrine ODM updates :ref:`embed-many <embed_many>` and
|
||||||
|
:ref:`reference-many <reference_many>` collections in separate write operations,
|
||||||
|
which do not bump the document version. Users employing document versioning are
|
||||||
|
encouraged to use the :ref:`atomicSet <atomic_set>` or
|
||||||
|
:ref:`atomicSetArray <atomic_set_array>` strategies for such collections, which
|
||||||
|
will ensure that collections are updated in the same write operation as the
|
||||||
|
versioned parent document.
|
|
@ -0,0 +1,211 @@
|
||||||
|
YAML Mapping
|
||||||
|
============
|
||||||
|
|
||||||
|
The YAML mapping driver enables you to provide the ODM metadata in
|
||||||
|
form of YAML documents.
|
||||||
|
|
||||||
|
The YAML mapping document of a class is loaded on-demand the first
|
||||||
|
time it is requested and subsequently stored in the metadata cache.
|
||||||
|
In order to work, this requires certain conventions:
|
||||||
|
|
||||||
|
-
|
||||||
|
Each document/mapped superclass must get its own dedicated YAML
|
||||||
|
mapping document.
|
||||||
|
-
|
||||||
|
The name of the mapping document must consist of the fully
|
||||||
|
qualified name of the class, where namespace separators are
|
||||||
|
replaced by dots (.).
|
||||||
|
-
|
||||||
|
All mapping documents should get the extension ".dcm.yml" to
|
||||||
|
identify it as a Doctrine mapping file. This is more of a
|
||||||
|
convention and you are not forced to do this. You can change the
|
||||||
|
file extension easily enough.
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$driver->setFileExtension('.yml');
|
||||||
|
|
||||||
|
It is recommended to put all YAML mapping documents in a single
|
||||||
|
folder but you can spread the documents over several folders if you
|
||||||
|
want to. In order to tell the YamlDriver where to look for your
|
||||||
|
mapping documents, supply an array of paths as the first argument
|
||||||
|
of the constructor, like this:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// $config instanceof Doctrine\ODM\MongoDB\Configuration
|
||||||
|
$driver = new YamlDriver(array('/path/to/files'));
|
||||||
|
$config->setMetadataDriverImpl($driver);
|
||||||
|
|
||||||
|
Simplified YAML Driver
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The Symfony project sponsored a driver that simplifies usage of the YAML Driver.
|
||||||
|
The changes between the original driver are:
|
||||||
|
|
||||||
|
1. File Extension is .mongodb-odm.yml
|
||||||
|
2. Filenames are shortened, "MyProject\\Documents\\User" will become User.mongodb-odm.yml
|
||||||
|
3. You can add a global file and add multiple documents in this file.
|
||||||
|
|
||||||
|
Configuration of this client works a little bit different:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$namespaces = array(
|
||||||
|
'/path/to/files1' => 'MyProject\Documents',
|
||||||
|
'/path/to/files2' => 'OtherProject\Documents'
|
||||||
|
);
|
||||||
|
$driver = new \Doctrine\ODM\MongoDB\Mapping\Driver\SimplifiedYamlDriver($namespaces);
|
||||||
|
$driver->setGlobalBasename('global'); // global.mongodb-odm.yml
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
As a quick start, here is a small example document that makes use
|
||||||
|
of several common elements:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
# Documents.User.dcm.yml
|
||||||
|
|
||||||
|
Documents\User:
|
||||||
|
db: documents
|
||||||
|
collection: user
|
||||||
|
fields:
|
||||||
|
id:
|
||||||
|
id: true
|
||||||
|
username:
|
||||||
|
name: login
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
unique:
|
||||||
|
order: desc
|
||||||
|
createdAt:
|
||||||
|
type: date
|
||||||
|
indexes:
|
||||||
|
index1:
|
||||||
|
keys:
|
||||||
|
username: desc
|
||||||
|
options:
|
||||||
|
unique: true
|
||||||
|
dropDups: true
|
||||||
|
safe: true
|
||||||
|
embedOne:
|
||||||
|
address:
|
||||||
|
targetDocument: Documents\Address
|
||||||
|
embedMany:
|
||||||
|
phonenumbers:
|
||||||
|
targetDocument: Documents\Phonenumber
|
||||||
|
referenceOne:
|
||||||
|
profile:
|
||||||
|
targetDocument: Documents\Profile
|
||||||
|
cascade: all
|
||||||
|
account:
|
||||||
|
targetDocument: Documents\Account
|
||||||
|
cascade: all
|
||||||
|
referenceMany:
|
||||||
|
groups:
|
||||||
|
targetDocument: Documents\Group
|
||||||
|
cascade: all
|
||||||
|
|
||||||
|
# Alternative syntax for the exact same example
|
||||||
|
# (allows custom key name for embedded document and reference).
|
||||||
|
Documents\User:
|
||||||
|
db: documents
|
||||||
|
collection: user
|
||||||
|
fields:
|
||||||
|
id:
|
||||||
|
id: true
|
||||||
|
username:
|
||||||
|
name: login
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
unique:
|
||||||
|
order: desc
|
||||||
|
createdAt:
|
||||||
|
type: date
|
||||||
|
address:
|
||||||
|
embedded: true
|
||||||
|
type: one
|
||||||
|
targetDocument: Documents\Address
|
||||||
|
phonenumbers:
|
||||||
|
embedded: true
|
||||||
|
type: many
|
||||||
|
targetDocument: Documents\Phonenumber
|
||||||
|
profile:
|
||||||
|
reference: true
|
||||||
|
type: one
|
||||||
|
targetDocument: Documents\Profile
|
||||||
|
cascade: all
|
||||||
|
account:
|
||||||
|
reference: true
|
||||||
|
type: one
|
||||||
|
targetDocument: Documents\Account
|
||||||
|
cascade: all
|
||||||
|
groups:
|
||||||
|
reference: true
|
||||||
|
type: many
|
||||||
|
targetDocument: Documents\Group
|
||||||
|
cascade: all
|
||||||
|
indexes:
|
||||||
|
index1:
|
||||||
|
keys:
|
||||||
|
username: desc
|
||||||
|
options:
|
||||||
|
unique: true
|
||||||
|
dropDups: true
|
||||||
|
safe: true
|
||||||
|
|
||||||
|
Be aware that class-names specified in the YAML files should be fully qualified.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The ``name`` property is an optional setting to change name of the field
|
||||||
|
**in the database**. Specifying it is optional and defaults to the name
|
||||||
|
of mapped field.
|
||||||
|
|
||||||
|
Reference
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. _yml_reference_lock:
|
||||||
|
|
||||||
|
Lock
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
The field with the ``lock`` property will be used to store lock information for :ref:`pessimistic locking <transactions_and_concurrency_pessimistic_locking>`.
|
||||||
|
This is only compatible with the ``int`` field type, and cannot be combined with ``id: true``.
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
lock:
|
||||||
|
type: int
|
||||||
|
lock: true
|
||||||
|
|
||||||
|
.. _yml_reference_version:
|
||||||
|
|
||||||
|
Version
|
||||||
|
^^^^^^^
|
||||||
|
|
||||||
|
The field with the ``version`` property will be used to store version information for :ref:`optimistic locking <transactions_and_concurrency_optimistic_locking>`.
|
||||||
|
This is only compatible with ``int`` and ``date`` field types, and cannot be combined with ``id: true``.
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
version:
|
||||||
|
type: int
|
||||||
|
version: true
|
||||||
|
|
||||||
|
By default, Doctrine ODM updates :ref:`embed-many <embed_many>` and
|
||||||
|
:ref:`reference-many <reference_many>` collections in separate write operations,
|
||||||
|
which do not bump the document version. Users employing document versioning are
|
||||||
|
encouraged to use the :ref:`atomicSet <atomic_set>` or
|
||||||
|
:ref:`atomicSetArray <atomic_set_array>` strategies for such collections, which
|
||||||
|
will ensure that collections are updated in the same write operation as the
|
||||||
|
versioned parent document.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user