Initial commit

This commit is contained in:
Will Bradley 2017-12-01 19:35:11 -08:00
commit 897a334bf6
201 changed files with 56590 additions and 0 deletions

View 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>

View 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

View File

@ -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

View File

@ -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!");
}
}

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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>`

View 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>`

View File

@ -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'
}

View File

@ -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.

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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

View File

@ -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');

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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);

View File

@ -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);

View File

@ -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.

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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.

View File

@ -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).

View File

@ -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();

View File

@ -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();

View File

@ -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.

View File

@ -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/

View File

@ -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'));

View File

@ -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

View File

@ -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";
}

View File

@ -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/

View File

@ -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.

View File

@ -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);

View File

@ -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>

View File

@ -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.

View File

@ -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();
}
}

View File

@ -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/

View File

@ -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();

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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();

View File

@ -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.

View File

@ -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