# Mapping extension for Doctrine2
**Mapping** extension makes it easy to map additional metadata for event listeners.
It supports **Yaml**, **Xml** and **Annotation** drivers which will be chosen depending on
currently used mapping driver for your domain objects. **Mapping** extension also
provides abstraction layer of **EventArgs** to make it possible to use single listener
for different object managers like **ODM** and **ORM**.
Features:
- Mapping drivers for annotation and yaml
- Conventional extension points for metadata extraction and object manager abstraction
[blog_reference]: http://gediminasm.org/article/mapping-extension-for-doctrine2 "Mapping extension for Doctrine2 makes it easy to create extensions based on annotation, xml, yaml mapping drivers"
[blog_test]: http://gediminasm.org/test "Test extensions on this blog"
- Public [Mapping repository](http://github.com/l3pp4rd/DoctrineExtensions "Mapping extension on Github") is available on github
- Last update date: **2012-01-02**
This article will cover the basic installation and usage of **Mapping** extension
Content:
- [Including](#including-extension) the extension
- [Creating](#create-extension) an extension
- Defining [annotations](#annotations)
- Creating [listener](#create-listener)
- Attaching our [listener](#attach-listener) to the event manager
- [Entity](#entity-mapping) with some fields to encode
- Addapting listener to support [different](#different-managers) object managers
- [Customizing](#event-adapter-customize) event adapter for specific functions
## Setup and autoloading
Read the [documentation](http://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/annotations.md#em-setup)
or check the [example code](http://github.com/l3pp4rd/DoctrineExtensions/tree/master/example)
on how to setup and use the extensions in most optimized way.
## Tutorial on creation of mapped extension
First, lets asume we will use **Extension** namespace for our additional
extension library. You should create an **Extension** directory in your library
or vendor directory. After some changes your project might look like:
```
project
...
bootstrap.php
vendor
Extension
...
...
```
Now you can use any namespace autoloader class and register this namespace. We
will use Doctrine\Common\ClassLoader for instance:
``` php
register();
```
Now lets create some files which are necessary for our extension:
```
project
...
bootstrap.php
vendor
Extension
Encoder
Mapping
Driver
Annotation.php
Annotations.php
EncoderListener.php
...
```
**Note:** that extension will look for mapping in **ExtensionNamespace/Mapping**
directory. And **Driver** directory should be named as Driver. These are the conventions
of **Mapping** extension.
That is all we will need for now. As you may noticed we will create an encoding
listener which could encode your fields by specified annotations. In real life it
may not be useful since object will not know how to match the value.
## Now lets define available annotations and setup drivers
Edit **Annotations.php** file:
``` php
getReflectionClass();
// check only property annotations
foreach ($class->getProperties() as $property) {
// skip inherited properties
if ($meta->isMappedSuperclass && !$property->isPrivate() ||
$meta->isInheritedField($property->name) ||
isset($meta->associationMappings[$property->name]['inherited'])
) {
continue;
}
// now lets check if property has our annotation
if ($encode = $reader->getPropertyAnnotation($property, 'Extension\Encoder\Mapping\Encode')) {
$field = $property->getName();
// check if field is mapped
if (!$meta->hasField($field)) {
throw new \Exception("Field is not mapped as object property");
}
// allow encoding only strings
if (!in_array($encode->type, array('sha1', 'md5'))) {
throw new \Exception("Invalid encoding type supplied");
}
// validate encoding type
$mapping = $meta->getFieldMapping($field);
if ($mapping['type'] != 'string') {
throw new \Exception("Only strings can be encoded");
}
// store the metadata
$config['encode'][$field] = array(
'type' => $encode->type,
'secret' => $encode->secret
);
}
}
}
}
```
## Finally, lets create the listener
**Note:** this version of listener will support only ORM Entities
``` php
loadMetadataForObjectClass(
$args->getEntityManager(),
$args->getClassMetadata()
);
}
public function onFlush(EventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
// check all pending updates
foreach ($uow->getScheduledEntityUpdates() as $object) {
$meta = $em->getClassMetadata(get_class($object));
// if it has our metadata lets encode the properties
if ($config = $this->getConfiguration($em, $meta->name)) {
$this->encode($em, $object, $config);
}
}
// check all pending insertions
foreach ($uow->getScheduledEntityInsertions() as $object) {
$meta = $em->getClassMetadata(get_class($object));
// if it has our metadata lets encode the properties
if ($config = $this->getConfiguration($em, $meta->name)) {
$this->encode($em, $object, $config);
}
// recalculate changeset
$em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $object);
}
}
protected function getNamespace()
{
// mapper must know the namespace of extension
return __NAMESPACE__;
}
private function encode($em, $object, $config)
{
$meta = $em->getClassMetadata(get_class($object));
foreach ($config['encode'] as $field => $options) {
$value = $meta->getReflectionProperty($field)->getValue($object);
$method = $options['type'];
$encoded = $method($options['secret'].$value);
$meta->getReflectionProperty($field)->setValue($object, $encoded);
}
// recalculate changeset
$em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $object);
}
}
```
Our **Encoder** extension is ready, now if we want to test it, we need
to attach our **EncoderListener** to the EventManager and create an entity
with some fields to encode.
### Attaching the EncoderListener
``` php
addEventSubscriber($encoderListener);
// now this event manager should be passed to entity manager constructor
```
### Create an entity with some fields to encode
``` php
name = $name;
}
public function getName()
{
return $this->name;
}
public function setPassword($password)
{
$this->password = $password;
}
public function getPassword()
{
return $this->password;
}
}
```
If you will try to create a new **User** you will get encoded fields in database.
## Adapting listener to support other object managers
Now the event adapter comes into play, lets slightly modify our listener:
``` php
getEventAdapter($args);
// this will check for our metadata
$this->loadMetadataForObjectClass(
$ea->getObjectManager(),
$args->getClassMetadata()
);
}
public function onFlush(EventArgs $args)
{
$ea = $this->getEventAdapter($args);
$om = $ea->getObjectManager();
$uow = $om->getUnitOfWork();
// check all pending updates
foreach ($ea->getScheduledObjectUpdates($uow) as $object) {
$meta = $om->getClassMetadata(get_class($object));
// if it has our metadata lets encode the properties
if ($config = $this->getConfiguration($om, $meta->name)) {
$this->encode($ea, $object, $config);
}
}
// check all pending insertions
foreach ($ea->getScheduledObjectInsertions($uow) as $object) {
$meta = $om->getClassMetadata(get_class($object));
// if it has our metadata lets encode the properties
if ($config = $this->getConfiguration($om, $meta->name)) {
$this->encode($ea, $object, $config);
}
// recalculate changeset
$ea->recomputeSingleObjectChangeSet($uow, $meta, $object);
}
}
protected function getNamespace()
{
// mapper must know the namespace of extension
return __NAMESPACE__;
}
private function encode(EventAdapterInterface $ea, $object, $config)
{
$om = $ea->getObjectManager();
$meta = $om->getClassMetadata(get_class($object));
$uow = $om->getUnitOfWork();
foreach ($config['encode'] as $field => $options) {
$value = $meta->getReflectionProperty($field)->getValue($object);
$method = $options['type'];
$encoded = $method($options['secret'].$value);
$meta->getReflectionProperty($field)->setValue($object, $encoded);
}
// recalculate changeset
$ea->recomputeSingleObjectChangeSet($uow, $meta, $object);
}
}
```
**Note:** event adapter uses **EventArgs** to recognize with which manager
we are dealing with. It also uses event arguments to retrieve manager and transforms
the method call in its way. You can extend the event adapter in order to add some
specific methods for each manager.
Thats it, now it will work on ORM and ODM object managers.
## Customizing event adapter for specific functions
In most cases event listener will need specific functionality which will differ
for every object manager. For instance, a query to load users will differ. The
example bellow will illustrate how to handle such situations. You will need to
extend default ORM and ODM event adapters to implement specific functions which
will be available through the event adapter. First we will need to follow the
mapping convention to use those extension points.
### Extending default event adapters
Update your directory structure:
```
project
...
bootstrap.php
vendor
Extension
Encoder
Mapping
Driver
Annotation.php
Event
Adapter
ORM.php
ODM.php
Annotations.php
EncoderListener.php
...
```
Now **Mapping** extension will automatically create event adapter instances
from the extended ones.
Create extended ORM event adapter:
``` php