Trying a new generation script

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