浏览代码

added an event system

Johannes Schmitt 12 年之前
父节点
当前提交
47a404eb63

+ 56 - 0
DependencyInjection/Compiler/RegisterEventListenersAndSubscribersPass.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace JMS\SerializerBundle\DependencyInjection\Compiler;
+
+use JMS\SerializerBundle\EventDispatcher\EventDispatcher;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+
+class RegisterEventListenersAndSubscribersPass implements CompilerPassInterface
+{
+    public function process(ContainerBuilder $container)
+    {
+        $listeners = array();
+        foreach ($container->findTaggedServiceIds('jms_serializer.event_listener') as $id => $tags) {
+            foreach ($tags as $attributes) {
+                if ( ! isset($attributes['event'])) {
+                    throw new \RuntimeException(sprintf('The tag "jms_serializer.event_listener" of service "%s" requires an attribute named "event".', $id));
+                }
+
+                $class = isset($attributes['class']) ? $attributes['class'] : null;
+                $format = isset($attributes['format']) ? $attributes['format'] : null;
+                $method = isset($attributes['method']) ? $attributes['method'] : EventDispatcher::getDefaultMethodName($attributes['event']);
+                $priority = isset($attributes['priority']) ? (integer) $attributes['priority'] : 0;
+
+                $listeners[$priority][] = array($attributes['event'], array($id, $method), $class, $format);
+            }
+        }
+
+        foreach ($container->findTaggedServiceIds('jms_serializer.event_subscriber') as $id => $tags) {
+            $subscriberClass = $container->getDefinition($id)->getClass();
+            if ( ! is_subclass_of($subscriberClass, 'JMS\SerializerBundle\EventDispatcher\EventSubscriberInterface')) {
+                throw new \RuntimeException(sprintf('The service "%s" (class: %s) does not implement the EventSubscriberInterface.', $id, $subscriberClass));
+            }
+
+            foreach (call_user_func($subscriberClass, 'getSubscribedEvents') as $eventData) {
+                if ( ! isset($eventData['event'])) {
+                    throw new \RuntimeException(sprintf('The service "%s" (class: %s) must return an event for each subscribed event.', $id, $subscriberClass));
+                }
+
+                $class = isset($eventData['class']) ? $eventData['class'] : null;
+                $format = isset($eventData['format']) ? $eventData['format'] : null;
+                $method = isset($eventData['method']) ? $eventData['method'] : EventDispatcher::getDefaultMethodName($eventData['event']);
+                $priority = isset($attributes['priority']) ? (integer) $attributes['priority'] : 0;
+
+                $listeners[$priority][] = array($eventData['event'], array($id, $method), $class, $format);
+            }
+        }
+
+        if ($listeners) {
+            ksort($listeners);
+
+            $container->getDefinition('jms_serializer.event_dispatcher')
+                ->addMethodCall('setListeners', array(call_user_func_array('array_merge', $listeners)));
+        }
+    }
+}

+ 2 - 2
DependencyInjection/Compiler/SetVisitorsPass.php

@@ -61,8 +61,8 @@ class SetVisitorsPass implements CompilerPassInterface
         foreach ($container->findTaggedServiceIds('jms_serializer.serializer') as $id => $attr) {
             $container
                 ->getDefinition($id)
-                ->replaceArgument(1, $serializationVisitors)
-                ->replaceArgument(2, $deserializationVisitors)
+                ->replaceArgument(2, $serializationVisitors)
+                ->replaceArgument(3, $deserializationVisitors)
             ;
         }
     }

+ 37 - 0
EventDispatcher/Event.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace JMS\SerializerBundle\EventDispatcher;
+
+use JMS\SerializerBundle\Serializer\VisitorInterface;
+
+use JMS\SerializerBundle\Metadata\ClassMetadata;
+
+class Event
+{
+    private $object;
+    private $visitor;
+    private $classMetadata;
+    private $preventDefault = false;
+
+    public function __construct(VisitorInterface $visitor, $object, ClassMetadata $classMetadata)
+    {
+        $this->visitor = $visitor;
+        $this->object = $object;
+        $this->classMetadata = $classMetadata;
+    }
+
+    public function getVisitor()
+    {
+        return $this->visitor;
+    }
+
+    public function getObject()
+    {
+        return $this->object;
+    }
+
+    public function getClassMetadata()
+    {
+        return $this->classMetadata;
+    }
+}

+ 104 - 0
EventDispatcher/EventDispatcher.php

@@ -0,0 +1,104 @@
+<?php
+
+namespace JMS\SerializerBundle\EventDispatcher;
+
+use JMS\SerializerBundle\EventDispatcher\EventDispatcherInterface;
+use JMS\SerializerBundle\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Light-weight event dispatcher.
+ *
+ * This implementation focuses primarily on performance, and dispatching
+ * events for certain classes. It is not a general purpose event dispatcher.
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+class EventDispatcher implements EventDispatcherInterface
+{
+    private $listeners = array();
+    private $classListeners = array();
+
+    public static function getDefaultMethodName($eventName)
+    {
+        return 'on'.str_replace(array('_', '.'), '', $eventName);
+    }
+
+    /**
+     * Sets the listeners.
+     *
+     * @param array $listeners
+     */
+    public function setListeners(array $listeners)
+    {
+        $this->listeners = $listeners;
+        $this->classListeners = array();
+    }
+
+    public function addListener($eventName, $callable, $class = null, $format = null)
+    {
+        $this->listeners[$eventName][] = array($callable, null === $class ? null : strtolower($class), $format);
+        unset($this->classListeners[$eventName]);
+    }
+
+    public function addSubscriber(EventSubscriberInterface $subscriber)
+    {
+        foreach ($subscriber->getSubscribedEvents() as $eventData) {
+            if ( ! isset($eventData['event'])) {
+                throw new \InvalidArgumentException(sprintf('Each event must have a "event" key.'));
+            }
+
+            $method = isset($eventData['method']) ? $eventData['method'] : self::getDefaultMethodName($eventData['event']);
+            $class = isset($eventData['class']) ? strtolower($eventData['class']) : null;
+            $format = isset($eventData['format']) ? $eventData['format'] : null;
+            $this->listeners[$eventData['event']][] = array(array($subscriber, $method), $class, $format);
+            unset($this->classListeners[$eventData['event']]);
+        }
+    }
+
+    public function hasListeners($eventName, $class, $format)
+    {
+        if ( ! isset($this->listeners[$eventName])) {
+            return false;
+        }
+
+        $loweredClass = strtolower($class);
+        if ( ! isset($this->classListeners[$eventName][$loweredClass][$format])) {
+            $this->classListeners[$eventName][$loweredClass][$format] = $this->initializeListeners($eventName, $loweredClass, $format);
+        }
+
+        return !!$this->classListeners[$eventName][$loweredClass][$format];
+    }
+
+    public function dispatch($eventName, $class, $format, Event $event)
+    {
+        if ( ! isset($this->listeners[$eventName])) {
+            return;
+        }
+
+        $loweredClass = strtolower($class);
+        if ( ! isset($this->classListeners[$eventName][$loweredClass][$format])) {
+            $this->classListeners[$eventName][$loweredClass][$format] = $this->initializeListeners($eventName, $loweredClass, $format);
+        }
+
+        foreach ($this->classListeners[$eventName][$loweredClass][$format] as $listener) {
+            call_user_func($listener, $event);
+        }
+    }
+
+    protected function initializeListeners($eventName, $loweredClass, $format)
+    {
+        $listeners = array();
+        foreach ($this->listeners[$eventName] as $listener) {
+            if (null !== $listener[1] && $loweredClass !== $listener[1]) {
+                continue;
+            }
+            if (null !== $listener[2] && $format !== $listener[2]) {
+                continue;
+            }
+
+            $listeners[] = $listener[0];
+        }
+
+        return $listeners;
+    }
+}

+ 47 - 0
EventDispatcher/EventDispatcherInterface.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace JMS\SerializerBundle\EventDispatcher;
+
+interface EventDispatcherInterface
+{
+    /**
+     * Returns whether there are listeners.
+     *
+     * @param string $eventName
+     * @param string $class
+     * @param string $format
+     *
+     * @return boolean
+     */
+    public function hasListeners($eventName, $class, $format);
+
+    /**
+     * Dispatches an event.
+     *
+     * The listeners/subscribers are called in the same order in which they
+     * were added to the dispatcher.
+     *
+     * @param string $eventName
+     * @param string $class
+     * @param string $format
+     * @param Event $event
+     */
+    public function dispatch($eventName, $class, $format, Event $event);
+
+    /**
+     * Adds a listener.
+     *
+     * @param string $eventName
+     * @param callable $callable
+     * @param string|null $class
+     * @param string|null $format
+     */
+    public function addListener($eventName, $callable, $class = null, $format = null);
+
+    /**
+     * Adds a subscribers.
+     *
+     * @param EventSubscriberInterface $subscriber
+     */
+    public function addSubscriber(EventSubscriberInterface $subscriber);
+}

+ 22 - 0
EventDispatcher/EventSubscriberInterface.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace JMS\SerializerBundle\EventDispatcher;
+
+interface EventSubscriberInterface
+{
+    /**
+     * Returns the events to which this class has subscribed.
+     *
+     * Return format:
+     *     array(
+     *         array('event' => 'the-event-name', 'method' => 'onEventName', 'class' => 'some-class', 'format' => 'json'),
+     *         array(...),
+     *     )
+     *
+     * The class may be omitted if the class wants to subscribe to events of all classes.
+     * Same goes for the format key.
+     *
+     * @return array
+     */
+    public static function getSubscribedEvents();
+}

+ 34 - 0
EventDispatcher/LazyEventDispatcher.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace JMS\SerializerBundle\EventDispatcher;
+
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class LazyEventDispatcher extends EventDispatcher
+{
+    private $container;
+
+    public function __construct(ContainerInterface $container)
+    {
+        $this->container = $container;
+    }
+
+    protected function initializeListeners($eventName, $loweredClass, $format)
+    {
+        $listeners = parent::initializeListeners($eventName, $loweredClass, $format);
+
+        foreach ($listeners as &$listener) {
+            if ( ! is_array($listener) || ! is_string($listener[0])) {
+                continue;
+            }
+
+            if ( ! $this->container->has($listener[0])) {
+                continue;
+            }
+
+            $listener[0] = $this->container->get($listener[0]);
+        }
+
+        return $listeners;
+    }
+}

+ 2 - 0
JMSSerializerBundle.php

@@ -18,6 +18,7 @@
 
 namespace JMS\SerializerBundle;
 
+use JMS\SerializerBundle\DependencyInjection\Compiler\RegisterEventListenersAndSubscribersPass;
 use JMS\SerializerBundle\DependencyInjection\Factory\FormErrorFactory;
 use JMS\SerializerBundle\DependencyInjection\Factory\DateTimeFactory;
 use JMS\SerializerBundle\DependencyInjection\Factory\ConstraintViolationFactory;
@@ -35,6 +36,7 @@ class JMSSerializerBundle extends Bundle
     public function build(ContainerBuilder $builder)
     {
         $builder->addCompilerPass(new SetVisitorsPass());
+        $builder->addCompilerPass(new RegisterEventListenersAndSubscribersPass());
 
         $ext = $builder->getExtension('jms_serializer');
         $ext->addHandlerFactory(new ObjectBasedFactory());

+ 7 - 0
Resources/config/services.xml

@@ -16,6 +16,8 @@
         <parameter key="jms_serializer.metadata.metadata_factory.class">Metadata\MetadataFactory</parameter>
         <parameter key="jms_serializer.metadata.cache.file_cache.class">Metadata\Cache\FileCache</parameter>
 
+        <parameter key="jms_serializer.event_dispatcher.class">JMS\SerializerBundle\EventDispatcher\LazyEventDispatcher</parameter>
+
         <parameter key="jms_serializer.camel_case_naming_strategy.class">JMS\SerializerBundle\Serializer\Naming\CamelCaseNamingStrategy</parameter>
         <parameter key="jms_serializer.serialized_name_annotation_strategy.class">JMS\SerializerBundle\Serializer\Naming\SerializedNameAnnotationStrategy</parameter>
         <parameter key="jms_serializer.cache_naming_strategy.class">JMS\SerializerBundle\Serializer\Naming\CacheNamingStrategy</parameter>
@@ -46,6 +48,10 @@
     </parameters>
 
     <services>
+        <service id="jms_serializer.event_dispatcher" class="%jms_serializer.event_dispatcher.class%" public="false">
+            <argument type="service" id="service_container" />
+        </service>
+    
         <!-- Metadata Drivers -->
         <service id="jms_serializer.metadata.file_locator" class="%jms_serializer.metadata.file_locator.class%" public="false">
             <argument type="collection" /><!-- Namespace Prefixes mapping to Directories -->
@@ -115,6 +121,7 @@
         <!-- Serializer -->
         <service id="jms_serializer.serializer" class="%jms_serializer.serializer.class%" public="false">
             <argument type="service" id="jms_serializer.metadata_factory" />
+            <argument type="service" id="jms_serializer.event_dispatcher" />
             <argument type="collection" /><!-- Serialization Visitors -->
             <argument type="collection" /><!-- Deserialization Visitors -->
             <call method="setContainer">

+ 16 - 0
Serializer/GenericSerializationVisitor.php

@@ -146,6 +146,22 @@ abstract class GenericSerializationVisitor extends AbstractSerializationVisitor
         }
     }
 
+    /**
+     * Allows you to add additional data to the current object/root element.
+     *
+     * @param string $key
+     * @param scalar|array $value This value must either be a regular scalar, or an array.
+     *                            It must not contain any objects anymore.
+     */
+    public function addData($key, $value)
+    {
+        if (isset($this->data[$key])) {
+            throw new \InvalidArgumentException(sprintf('There is already data for "%s".', $key));
+        }
+
+        $this->data[$key] = $value;
+    }
+
     public function visitPropertyUsingCustomHandler(PropertyMetadata $metadata, $object)
     {
         // TODO

+ 44 - 11
Serializer/GraphNavigator.php

@@ -18,6 +18,8 @@
 
 namespace JMS\SerializerBundle\Serializer;
 
+use JMS\SerializerBundle\EventDispatcher\Event;
+use JMS\SerializerBundle\EventDispatcher\EventDispatcherInterface;
 use JMS\SerializerBundle\Metadata\ClassMetadata;
 use Metadata\MetadataFactoryInterface;
 use JMS\SerializerBundle\Exception\InvalidArgumentException;
@@ -29,14 +31,18 @@ final class GraphNavigator
     const DIRECTION_DESERIALIZATION = 2;
 
     private $direction;
-    private $exclusionStrategy;
+    private $dispatcher;
     private $metadataFactory;
+    private $format;
+    private $exclusionStrategy;
     private $visiting;
 
-    public function __construct($direction, MetadataFactoryInterface $metadataFactory, ExclusionStrategyInterface $exclusionStrategy = null)
+    public function __construct($direction, MetadataFactoryInterface $metadataFactory, $format, ExclusionStrategyInterface $exclusionStrategy = null, EventDispatcherInterface $dispatcher = null)
     {
         $this->direction = $direction;
+        $this->dispatcher = $dispatcher;
         $this->metadataFactory = $metadataFactory;
+        $this->format = $format;
         $this->exclusionStrategy = $exclusionStrategy;
         $this->visiting = new \SplObjectStorage();
     }
@@ -66,13 +72,8 @@ final class GraphNavigator
         } else if ('array' === $type || ('a' === $type[0] && 0 === strpos($type, 'array<'))) {
             return $visitor->visitArray($data, $type);
         } else if ('resource' === $type) {
-            $path = array();
-            foreach ($this->visiting as $obj) {
-                $path[] = get_class($obj);
-            }
-
             $msg = 'Resources are not supported in serialized data.';
-            if ($path) {
+            if (null !== $path = $this->getCurrentPath()) {
                 $msg .= ' Path: '.implode(' -> ', $path);
             }
 
@@ -110,12 +111,16 @@ final class GraphNavigator
                 foreach ($metadata->preSerializeMethods as $method) {
                     $method->invoke($data);
                 }
+
+                if (null !== $this->dispatcher && $this->dispatcher->hasListeners('serializer.pre_serialize', $type, $this->format)) {
+                    $this->dispatcher->dispatch('serializer.pre_serialize', $type, $this->format, new Event($visitor, $data, $metadata));
+                }
             }
 
             // check if traversable
             if (self::DIRECTION_SERIALIZATION === $this->direction && $data instanceof \Traversable) {
                 $rs = $visitor->visitTraversable($data, $type);
-                $this->afterVisitingObject($metadata, $data, self::DIRECTION_SERIALIZATION === $this->direction);
+                $this->afterVisitingObject($visitor, $metadata, $data, self::DIRECTION_SERIALIZATION === $this->direction);
 
                 return $rs;
             }
@@ -136,8 +141,14 @@ final class GraphNavigator
                 }
             }
 
+            if (self::DIRECTION_SERIALIZATION === $this->direction) {
+                $this->afterVisitingObject($visitor, $metadata, $data);
+
+                return $visitor->endVisitingObject($metadata, $data, $type);
+            }
+
             $rs = $visitor->endVisitingObject($metadata, $data, $type);
-            $this->afterVisitingObject($metadata, self::DIRECTION_SERIALIZATION === $this->direction ? $data : $rs);
+            $this->afterVisitingObject($visitor, $metadata, $rs);
 
             return $rs;
         }
@@ -154,7 +165,21 @@ final class GraphNavigator
         $this->visiting->detach($object);
     }
 
-    private function afterVisitingObject(ClassMetadata $metadata, $object)
+    private function getCurrentPath()
+    {
+        $path = array();
+        foreach ($this->visiting as $obj) {
+            $path[] = get_class($obj);
+        }
+
+        if ( ! $path) {
+            return null;
+        }
+
+        return implode(' -> ', $path);
+    }
+
+    private function afterVisitingObject(VisitorInterface $visitor, ClassMetadata $metadata, $object)
     {
         if (self::DIRECTION_SERIALIZATION === $this->direction) {
             $this->visiting->detach($object);
@@ -163,11 +188,19 @@ final class GraphNavigator
                 $method->invoke($object);
             }
 
+            if (null !== $this->dispatcher && $this->dispatcher->hasListeners('serializer.post_serialize', $metadata->name, $this->format)) {
+                $this->dispatcher->dispatch('serializer.post_serialize', $metadata->name, $this->format, new Event($visitor, $object, $metadata));
+            }
+
             return;
         }
 
         foreach ($metadata->postDeserializeMethods as $method) {
             $method->invoke($object);
         }
+
+        if (null !== $this->dispatcher && $this->dispatcher->hasListeners('serializer.post_deserialize', $metadata->name, $this->format)) {
+            $this->dispatcher->dispatch('serializer.post_deserialize', $metadata->name, $this->format, new Event($visitor, $object, $metadata));
+        }
     }
 }

+ 8 - 4
Serializer/Serializer.php

@@ -18,6 +18,8 @@
 
 namespace JMS\SerializerBundle\Serializer;
 
+use JMS\SerializerBundle\EventDispatcher\EventDispatcherInterface;
+
 use JMS\SerializerBundle\Exception\UnsupportedFormatException;
 use Metadata\MetadataFactoryInterface;
 use JMS\SerializerBundle\Exception\InvalidArgumentException;
@@ -28,13 +30,15 @@ use JMS\SerializerBundle\Serializer\Exclusion\ExclusionStrategyInterface;
 class Serializer implements SerializerInterface
 {
     private $factory;
+    private $dispatcher;
     private $serializationVisitors;
     private $deserializationVisitors;
     private $exclusionStrategy;
 
-    public function __construct(MetadataFactoryInterface $factory, array $serializationVisitors = array(), array $deserializationVisitors = array())
+    public function __construct(MetadataFactoryInterface $factory, EventDispatcherInterface $dispatcher = null, array $serializationVisitors = array(), array $deserializationVisitors = array())
     {
         $this->factory = $factory;
+        $this->dispatcher = $dispatcher;
         $this->serializationVisitors = $serializationVisitors;
         $this->deserializationVisitors = $deserializationVisitors;
     }
@@ -54,7 +58,7 @@ class Serializer implements SerializerInterface
 
         $this->exclusionStrategy = new VersionExclusionStrategy($version);
     }
-    
+
     public function setGroups($groups)
     {
         if (!$groups) {
@@ -69,7 +73,7 @@ class Serializer implements SerializerInterface
     public function serialize($data, $format)
     {
         $visitor = $this->getSerializationVisitor($format);
-        $visitor->setNavigator($navigator = new GraphNavigator(GraphNavigator::DIRECTION_SERIALIZATION, $this->factory, $this->exclusionStrategy));
+        $visitor->setNavigator($navigator = new GraphNavigator(GraphNavigator::DIRECTION_SERIALIZATION, $this->factory, $format, $this->exclusionStrategy, $this->dispatcher));
         $navigator->accept($visitor->prepare($data), null, $visitor);
 
         return $visitor->getResult();
@@ -78,7 +82,7 @@ class Serializer implements SerializerInterface
     public function deserialize($data, $type, $format)
     {
         $visitor = $this->getDeserializationVisitor($format);
-        $visitor->setNavigator($navigator = new GraphNavigator(GraphNavigator::DIRECTION_DESERIALIZATION, $this->factory, $this->exclusionStrategy));
+        $visitor->setNavigator($navigator = new GraphNavigator(GraphNavigator::DIRECTION_DESERIALIZATION, $this->factory, $format, $this->exclusionStrategy, $this->dispatcher));
         $navigator->accept($visitor->prepare($data), $type, $visitor);
 
         return $visitor->getResult();

+ 126 - 0
Tests/EventDispatcher/EventDispatcherTest.php

@@ -0,0 +1,126 @@
+<?php
+
+namespace JMS\SerializerBundle\Tests\EventDispatcher;
+
+use JMS\SerializerBundle\EventDispatcher\EventSubscriberInterface;
+
+use JMS\SerializerBundle\Metadata\ClassMetadata;
+use JMS\SerializerBundle\EventDispatcher\Event;
+use JMS\SerializerBundle\EventDispatcher\EventDispatcher;
+
+class EventDispatcherTest extends \PHPUnit_Framework_TestCase
+{
+    private $dispatcher;
+    private $event;
+
+    public function testHasListeners()
+    {
+        $this->assertFalse($this->dispatcher->hasListeners('foo', 'Foo', 'json'));
+        $this->dispatcher->addListener('foo', function() { });
+        $this->assertTrue($this->dispatcher->hasListeners('foo', 'Foo', 'json'));
+
+        $this->assertFalse($this->dispatcher->hasListeners('bar', 'Bar', 'json'));
+        $this->dispatcher->addListener('bar', function() { }, 'Foo');
+        $this->assertFalse($this->dispatcher->hasListeners('bar', 'Bar', 'json'));
+        $this->dispatcher->addListener('bar', function() { }, 'Bar', 'xml');
+        $this->assertFalse($this->dispatcher->hasListeners('bar', 'Bar', 'json'));
+        $this->dispatcher->addListener('bar', function() { }, null, 'json');
+        $this->assertTrue($this->dispatcher->hasListeners('bar', 'Baz', 'json'));
+        $this->assertTrue($this->dispatcher->hasListeners('bar', 'Bar', 'json'));
+
+        $this->assertFalse($this->dispatcher->hasListeners('baz', 'Bar', 'xml'));
+        $this->dispatcher->addListener('baz', function() { }, 'Bar');
+        $this->assertTrue($this->dispatcher->hasListeners('baz', 'Bar', 'xml'));
+        $this->assertTrue($this->dispatcher->hasListeners('baz', 'bAr', 'xml'));
+    }
+
+    public function testDispatch()
+    {
+        $a = new MockListener();
+        $this->dispatcher->addListener('foo', array($a, 'foo'));
+        $this->dispatch('bar');
+        $a->_verify('Listener is not called for other event.');
+
+        $b = new MockListener();
+        $this->dispatcher->addListener('pre', array($b, 'bar'), 'Bar');
+        $this->dispatcher->addListener('pre', array($b, 'foo'), 'Foo');
+        $this->dispatcher->addListener('pre', array($b, 'all'));
+
+        $b->bar($this->event);
+        $b->all($this->event);
+        $b->foo($this->event);
+        $b->all($this->event);
+        $b->_replay();
+        $this->dispatch('pre', 'Bar');
+        $this->dispatch('pre', 'Foo');
+        $b->_verify();
+    }
+
+    public function testAddSubscriber()
+    {
+        $subscriber = new MockSubscriber();
+        $subscriber::$events = array(
+            array('event' => 'foo.bar_baz', 'format' => 'foo'),
+            array('event' => 'bar', 'method' => 'bar', 'class' => 'foo'),
+        );
+
+        $this->dispatcher->addSubscriber($subscriber);
+        $this->assertAttributeEquals(array(
+            'foo.bar_baz' => array(
+                array(array($subscriber, 'onfoobarbaz'), null, 'foo'),
+            ),
+            'bar' => array(
+                array(array($subscriber, 'bar'), 'foo', null),
+            ),
+        ), 'listeners', $this->dispatcher);
+    }
+
+    protected function setUp()
+    {
+        $this->dispatcher = new EventDispatcher();
+        $this->event = new Event($this->getMock('JMS\SerializerBundle\Serializer\VisitorInterface'), new \stdClass(), new ClassMetadata('stdClass'));
+    }
+
+    private function dispatch($eventName, $class = 'Foo', $format = 'json', Event $event = null)
+    {
+        $this->dispatcher->dispatch($eventName, $class, $format, $event ?: $this->event);
+    }
+}
+
+class MockSubscriber implements EventSubscriberInterface
+{
+    public static $events = array();
+
+    public static function getSubscribedEvents()
+    {
+        return self::$events;
+    }
+}
+
+class MockListener
+{
+    private $expected = array();
+    private $actual = array();
+    private $wasReplayed = false;
+
+    public function __call($method, array $args = array())
+    {
+        if ( ! $this->wasReplayed) {
+            $this->expected[] = array($method, $args);
+
+            return;
+        }
+
+        $this->actual[] = array($method, $args);
+    }
+
+    public function _replay()
+    {
+        $this->wasReplayed = true;
+    }
+
+    public function _verify($message = null)
+    {
+        \PHPUnit_Framework_Assert::assertSame($this->expected, $this->actual, $message);
+    }
+}

+ 10 - 1
Tests/Serializer/BaseSerializationTest.php

@@ -18,6 +18,8 @@
 
 namespace JMS\SerializerBundle\Tests\Serializer;
 
+use JMS\SerializerBundle\EventDispatcher\EventDispatcher;
+
 use Doctrine\Common\Annotations\AnnotationReader;
 use Doctrine\Common\Collections\ArrayCollection;
 use JMS\SerializerBundle\Metadata\Driver\AnnotationDriver;
@@ -70,6 +72,8 @@ use Symfony\Component\Yaml\Inline;
 
 abstract class BaseSerializationTest extends \PHPUnit_Framework_TestCase
 {
+    protected $dispatcher;
+
     public function testString()
     {
         $this->assertEquals($this->getContent('string'), $this->serialize('foo'));
@@ -476,6 +480,11 @@ abstract class BaseSerializationTest extends \PHPUnit_Framework_TestCase
         return $this->getSerializer()->deserialize($content, $type, $this->getFormat());
     }
 
+    protected function setUp()
+    {
+        $this->dispatcher = new EventDispatcher();
+    }
+
     protected function getSerializer()
     {
         $factory = new MetadataFactory(new AnnotationDriver(new AnnotationReader()));
@@ -495,7 +504,7 @@ abstract class BaseSerializationTest extends \PHPUnit_Framework_TestCase
             'xml'  => new XmlDeserializationVisitor($namingStrategy, $customDeserializationHandlers, $objectConstructor),
         );
 
-        return new Serializer($factory, $serializationVisitors, $deserializationVisitors);
+        return new Serializer($factory, $this->dispatcher, $serializationVisitors, $deserializationVisitors);
     }
 
     protected function getSerializationHandlers()

+ 7 - 3
Tests/Serializer/GraphNavigatorTest.php

@@ -2,6 +2,8 @@
 
 namespace JMS\SerializerBundle\Tests\Serializer;
 
+use JMS\SerializerBundle\EventDispatcher\EventDispatcher;
+
 use Doctrine\Common\Annotations\AnnotationReader;
 use JMS\SerializerBundle\Metadata\Driver\AnnotationDriver;
 use JMS\SerializerBundle\Serializer\GraphNavigator;
@@ -10,6 +12,7 @@ use Metadata\MetadataFactory;
 class GraphNavigatorTest extends \PHPUnit_Framework_TestCase
 {
     private $metadataFactory;
+    private $dispatcher;
     private $navigator;
     private $visitor;
 
@@ -35,7 +38,7 @@ class GraphNavigatorTest extends \PHPUnit_Framework_TestCase
             ->method('shouldSkipProperty')
             ->with($metadata->propertyMetadata['foo'], $object);
 
-        $this->navigator = new GraphNavigator(GraphNavigator::DIRECTION_SERIALIZATION, $this->metadataFactory, $exclusionStrategy);
+        $this->navigator = new GraphNavigator(GraphNavigator::DIRECTION_SERIALIZATION, $this->metadataFactory, $this->dispatcher, $exclusionStrategy);
         $this->navigator->accept($object, null, $this->visitor);
     }
 
@@ -53,16 +56,17 @@ class GraphNavigatorTest extends \PHPUnit_Framework_TestCase
             ->method('shouldSkipProperty')
             ->with($metadata->propertyMetadata['foo'], null);
 
-        $this->navigator = new GraphNavigator(GraphNavigator::DIRECTION_DESERIALIZATION, $this->metadataFactory, $exclusionStrategy);
+        $this->navigator = new GraphNavigator(GraphNavigator::DIRECTION_DESERIALIZATION, $this->metadataFactory, $this->dispatcher, $exclusionStrategy);
         $this->navigator->accept('random', $class, $this->visitor);
     }
 
     protected function setUp()
     {
         $this->visitor = $this->getMock('JMS\SerializerBundle\Serializer\VisitorInterface');
+        $this->dispatcher = new EventDispatcher();
 
         $this->metadataFactory = new MetadataFactory(new AnnotationDriver(new AnnotationReader()));
-        $this->navigator = new GraphNavigator(GraphNavigator::DIRECTION_SERIALIZATION, $this->metadataFactory);
+        $this->navigator = new GraphNavigator(GraphNavigator::DIRECTION_SERIALIZATION, $this->metadataFactory, $this->dispatcher);
     }
 }
 

+ 39 - 0
Tests/Serializer/JsonSerializationTest.php

@@ -18,6 +18,14 @@
 
 namespace JMS\SerializerBundle\Tests\Serializer;
 
+use JMS\SerializerBundle\EventDispatcher\EventSubscriberInterface;
+
+use JMS\SerializerBundle\EventDispatcher\Event;
+
+use JMS\SerializerBundle\Tests\Fixtures\Author;
+
+use JMS\SerializerBundle\Tests\Fixtures\AuthorList;
+
 use JMS\SerializerBundle\Exception\RuntimeException;
 use JMS\SerializerBundle\Tests\Fixtures\SimpleObject;
 
@@ -77,8 +85,39 @@ class JsonSerializationTest extends BaseSerializationTest
         return $outputs[$key];
     }
 
+    public function testAddLinksToOutput()
+    {
+        $this->dispatcher->addSubscriber(new LinkAddingSubscriber());
+
+        $list = new AuthorList();
+        $list->add(new Author('foo'));
+        $list->add(new Author('bar'));
+
+        $this->assertEquals('[{"full_name":"foo","_links":{"details":"http:\/\/foo.bar\/details\/foo","comments":"http:\/\/foo.bar\/details\/foo\/comments"}},{"full_name":"bar","_links":{"details":"http:\/\/foo.bar\/details\/bar","comments":"http:\/\/foo.bar\/details\/bar\/comments"}}]', $this->serialize($list));
+    }
+
     protected function getFormat()
     {
         return 'json';
     }
 }
+
+class LinkAddingSubscriber implements EventSubscriberInterface
+{
+    public function onPostSerialize(Event $event)
+    {
+        $author = $event->getObject();
+
+        $event->getVisitor()->addData('_links', array(
+            'details' => 'http://foo.bar/details/'.$author->getName(),
+            'comments' => 'http://foo.bar/details/'.$author->getName().'/comments',
+        ));
+    }
+
+    public static function getSubscribedEvents()
+    {
+        return array(
+            array('event' => 'serializer.post_serialize', 'method' => 'onPostSerialize', 'format' => 'json', 'class' => 'JMS\SerializerBundle\Tests\Fixtures\Author'),
+        );
+    }
+}

+ 3 - 1
Tests/Serializer/XmlSerializationTest.php

@@ -18,6 +18,8 @@
 
 namespace JMS\SerializerBundle\Tests\Serializer;
 
+use JMS\SerializerBundle\EventDispatcher\EventDispatcher;
+
 use Metadata\MetadataFactory;
 use Doctrine\Common\Annotations\AnnotationReader;
 use JMS\SerializerBundle\Tests\Fixtures\InvalidUsageOfXmlValue;
@@ -104,7 +106,7 @@ class XmlSerializationTest extends BaseSerializationTest
             '<!DOCTYPE authorized SYSTEM "http://authorized_url.dtd">',
             '<!DOCTYPE author [<!ENTITY foo SYSTEM "php://filter/read=convert.base64-encode/resource='.basename(__FILE__).'">]>'));
 
-        $serializer = new Serializer(new MetadataFactory(new AnnotationDriver(new AnnotationReader())), array(), array('xml' => $xmlVisitor));
+        $serializer = new Serializer(new MetadataFactory(new AnnotationDriver(new AnnotationReader())), new EventDispatcher(), array(), array('xml' => $xmlVisitor));
 
         $serializer->deserialize('<?xml version="1.0"?>
             <!DOCTYPE authorized SYSTEM "http://authorized_url.dtd">

+ 2 - 0
Tests/Serializer/YamlSerializationTest.php

@@ -16,6 +16,8 @@
  * limitations under the License.
  */
 
+use JMS\SerializerBundle\EventDispatcher\EventSubscriberInterface;
+
 namespace JMS\SerializerBundle\Tests\Serializer;
 
 use JMS\SerializerBundle\Exception\RuntimeException;