Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP Navigator context #3

Merged
merged 2 commits into from
Dec 7, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/JMS/Serializer/Exclusion/ExclusionStrategyInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\NavigatorContext;

/**
* Interface for exclusion strategies.
Expand All @@ -32,19 +33,19 @@ interface ExclusionStrategyInterface
* Whether the class should be skipped.
*
* @param ClassMetadata $metadata
* @param object|null $object instance, provided during serialization but not deserialization
* @param NavigatorContext $navigatorContext
*
* @return boolean
*/
public function shouldSkipClass(ClassMetadata $metadata, $object = null);
public function shouldSkipClass(ClassMetadata $metadata, NavigatorContext $navigatorContext);

/**
* Whether the property should be skipped.
*
* @param PropertyMetadata $property
* @param object|null $object instance, provided during serialization but not deserialization
* @param NavigatorContext $navigatorContext
*
* @return boolean
*/
public function shouldSkipProperty(PropertyMetadata $property, $object = null);
public function shouldSkipProperty(PropertyMetadata $property, NavigatorContext $navigatorContext);
}
8 changes: 6 additions & 2 deletions src/JMS/Serializer/Exclusion/GroupsExclusionStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\NavigatorContext;

class GroupsExclusionStrategy implements ExclusionStrategyInterface
{
Expand All @@ -38,15 +39,18 @@ public function __construct(array $groups)
}
}

public function shouldSkipClass(ClassMetadata $metadata, $object = null)
/**
* {@inheritDoc}
*/
public function shouldSkipClass(ClassMetadata $metadata, NavigatorContext $navigatorContext)
{
return false;
}

/**
* {@inheritDoc}
*/
public function shouldSkipProperty(PropertyMetadata $property, $object = null)
public function shouldSkipProperty(PropertyMetadata $property, NavigatorContext $navigatorContext)
{
if ( ! $property->groups) {
return ! isset($this->groups[self::DEFAULT_GROUP]);
Expand Down
8 changes: 6 additions & 2 deletions src/JMS/Serializer/Exclusion/VersionExclusionStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\NavigatorContext;

class VersionExclusionStrategy implements ExclusionStrategyInterface
{
Expand All @@ -30,15 +31,18 @@ public function __construct($version)
$this->version = $version;
}

public function shouldSkipClass(ClassMetadata $metadata, $object = null)
/**
* {@inheritDoc}
*/
public function shouldSkipClass(ClassMetadata $metadata, NavigatorContext $navigatorContext)
{
return false;
}

/**
* {@inheritDoc}
*/
public function shouldSkipProperty(PropertyMetadata $property, $object = null)
public function shouldSkipProperty(PropertyMetadata $property, NavigatorContext $navigatorContext)
{
if ((null !== $version = $property->sinceVersion) && version_compare($this->version, $version, '<')) {
return true;
Expand Down
73 changes: 25 additions & 48 deletions src/JMS/Serializer/GraphNavigator.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,12 @@ final class GraphNavigator
const DIRECTION_SERIALIZATION = 1;
const DIRECTION_DESERIALIZATION = 2;

private $direction;
private $context;
private $dispatcher;
private $metadataFactory;
private $format;
private $handlerRegistry;
private $objectConstructor;
private $exclusionStrategy;
private $customHandlers = array();
private $visiting;

/**
* Parses a direction string to one of the direction constants.
Expand All @@ -74,14 +71,12 @@ public static function parseDirection($dirStr)

public function __construct($direction, MetadataFactoryInterface $metadataFactory, $format, HandlerRegistryInterface $handlerRegistry, ObjectConstructorInterface $objectConstructor, ExclusionStrategyInterface $exclusionStrategy = null, EventDispatcherInterface $dispatcher = null)
{
$this->direction = $direction;
$this->context = new NavigatorContext($direction, $format);
$this->dispatcher = $dispatcher;
$this->metadataFactory = $metadataFactory;
$this->format = $format;
$this->handlerRegistry = $handlerRegistry;
$this->objectConstructor = $objectConstructor;
$this->exclusionStrategy = $exclusionStrategy;
$this->visiting = new \SplObjectStorage();
}

/**
Expand All @@ -98,9 +93,9 @@ public function accept($data, array $type = null, VisitorInterface $visitor)
// If the type was not given, we infer the most specific type from the
// input data in serialization mode.
if (null === $type) {
if (self::DIRECTION_DESERIALIZATION === $this->direction) {
if (!$this->context->isSerializing()) {
$msg = 'The type must be given for all properties when deserializing.';
if (null !== $path = $this->getCurrentPath()) {
if (null !== $path = $this->context->getPath()) {
$msg .= ' Path: '.$path;
}

Expand All @@ -116,7 +111,7 @@ public function accept($data, array $type = null, VisitorInterface $visitor)
}
// If the data is null, we have to force the type to null regardless of the input in order to
// guarantee correct handling of null values, and not have any internal auto-casting behavior.
else if (self::DIRECTION_SERIALIZATION === $this->direction && null === $data) {
else if ($this->context->isSerializing() && null === $data) {
$type = array('name' => 'NULL', 'params' => array());
}

Expand All @@ -142,49 +137,45 @@ public function accept($data, array $type = null, VisitorInterface $visitor)

case 'resource':
$msg = 'Resources are not supported in serialized data.';
if (null !== $path = $this->getCurrentPath()) {
if (null !== $path = $this->context->getPath()) {
$msg .= ' Path: '.$path;
}

throw new \RuntimeException($msg);

default:
$isSerializing = self::DIRECTION_SERIALIZATION === $this->direction;
$isSerializing = $this->context->isSerializing();

if ($isSerializing && null !== $data) {
if ($this->visiting->contains($data)) {
if ($this->context->isVisiting($data)) {
return null;
}
$this->visiting->attach($data);
$this->context->startVisiting($data);
}

// First, try whether a custom handler exists for the given type. This is done
// before loading metadata because the type name might not be a class, but
// could also simply be an artifical type.
if (null !== $handler = $this->handlerRegistry->getHandler($this->direction, $type['name'], $this->format)) {
if (null !== $handler = $this->handlerRegistry->getHandler($this->context->getDirection(), $type['name'], $this->context->getFormat())) {
$rs = call_user_func($handler, $visitor, $data, $type);

if ($isSerializing) {
$this->visiting->detach($data);
}
$this->context->stopVisiting($data);

return $rs;
}

// Trigger pre-serialization callbacks, and listeners if they exist.
if ($isSerializing) {
if (null !== $this->dispatcher && $this->dispatcher->hasListeners('serializer.pre_serialize', $type['name'], $this->format)) {
$this->dispatcher->dispatch('serializer.pre_serialize', $type['name'], $this->format, $event = new PreSerializeEvent($visitor, $data, $type));
if (null !== $this->dispatcher && $this->dispatcher->hasListeners('serializer.pre_serialize', $type['name'], $this->context->getFormat())) {
$this->dispatcher->dispatch('serializer.pre_serialize', $type['name'], $this->context->getFormat(), $event = new PreSerializeEvent($visitor, $data, $type));
$type = $event->getType();
}
}

// Load metadata, and check whether this class should be excluded.
$metadata = $this->metadataFactory->getMetadataForClass($type['name']);
if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipClass($metadata, $isSerializing ? $data : null)) {
if ($isSerializing) {
$this->visiting->detach($data);
}
if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipClass($metadata, $this->context)) {
$this->context->stopVisiting($data);

return null;
}
Expand All @@ -200,16 +191,16 @@ public function accept($data, array $type = null, VisitorInterface $visitor)
$object = $this->objectConstructor->construct($visitor, $metadata, $data, $type);
}

if (isset($metadata->handlerCallbacks[$this->direction][$this->format])) {
$rs = $object->{$metadata->handlerCallbacks[$this->direction][$this->format]}($visitor, $isSerializing ? null : $data);
if (isset($metadata->handlerCallbacks[$this->context->getDirection()][$this->context->getFormat()])) {
$rs = $object->{$metadata->handlerCallbacks[$this->context->getDirection()][$this->context->getFormat()]}($visitor, $isSerializing ? null : $data);
$this->afterVisitingObject($visitor, $metadata, $object, $type);

return $isSerializing ? $rs : $object;
}

$visitor->startVisitingObject($metadata, $object, $type);
foreach ($metadata->propertyMetadata as $propertyMetadata) {
if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipProperty($propertyMetadata, $isSerializing ? $data : null)) {
if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) {
continue;
}

Expand Down Expand Up @@ -250,34 +241,20 @@ public function detachObject($object)
throw new InvalidArgumentException(sprintf('Expected an object to detach, given "%s".', gettype($object)));
}

$this->visiting->detach($object);
}

private function getCurrentPath()
{
$path = array();
foreach ($this->visiting as $obj) {
$path[] = get_class($obj);
}

if ( ! $path) {
return null;
}

return implode(' -> ', $path);
$this->context->stopVisiting($object);
}

private function afterVisitingObject(VisitorInterface $visitor, ClassMetadata $metadata, $object, array $type)
{
if (self::DIRECTION_SERIALIZATION === $this->direction) {
$this->visiting->detach($object);
if ($this->context->isSerializing()) {
$this->context->stopVisiting($object);

foreach ($metadata->postSerializeMethods as $method) {
$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, $type));
if (null !== $this->dispatcher && $this->dispatcher->hasListeners('serializer.post_serialize', $metadata->name, $this->context->getFormat())) {
$this->dispatcher->dispatch('serializer.post_serialize', $metadata->name, $this->context->getFormat(), new Event($visitor, $object, $type));
}

return;
Expand All @@ -287,8 +264,8 @@ private function afterVisitingObject(VisitorInterface $visitor, ClassMetadata $m
$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, $type));
if (null !== $this->dispatcher && $this->dispatcher->hasListeners('serializer.post_deserialize', $metadata->name, $this->context->getFormat())) {
$this->dispatcher->dispatch('serializer.post_deserialize', $metadata->name, $this->context->getFormat(), new Event($visitor, $object, $type));
}
}
}
87 changes: 87 additions & 0 deletions src/JMS/Serializer/NavigatorContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

namespace JMS\Serializer;

class NavigatorContext
{
private $direction;
private $format;
private $visitingSet;
private $visitingStack;

public function __construct($direction, $format)
{
$this->direction = $direction;
$this->format = $format;
$this->visitingSet = new \SplObjectStorage();
$this->visitingStack = new \SplStack();
}

public function getDirection()
{
return $this->direction;
}

public function isSerializing()
{
return $this->direction === GraphNavigator::DIRECTION_SERIALIZATION;
}

public function getFormat()
{
return $this->format;
}

public function startVisiting($object)
{
if (!$this->isSerializing()) {
return;
}

$this->visitingSet->attach($object);
$this->visitingStack->push($object);
}

public function stopVisiting($object)
{
if (!$this->isSerializing()) {
return;
}

$this->visitingSet->detach($object);
$poppedObject = $this->visitingStack->pop();

if ($object !== $poppedObject) {
throw new \RuntimeException('NavigatorContext visitingStack not working well');
}
}

public function isVisiting($object)
{
return $this->visitingSet->contains($object);
}

public function getPath()
{
$path = array();
foreach ($this->visitingStack as $obj) {
$path[] = get_class($obj);
}

if ( ! $path) {
return null;
}

return implode(' -> ', $path);
}

public function getDepth()
{
return $this->visitingStack->count();
}

public function getObject()
{
return !$this->visitingStack->isEmpty() ? $this->visitingStack->top() : null;
}
}
31 changes: 31 additions & 0 deletions tests/JMS/Serializer/Tests/Fixtures/Node.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/*
* Copyright 2011 Johannes M. Schmitt <[email protected]>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace JMS\Serializer\Tests\Fixtures;

use JMS\Serializer\Annotation as Serializer;

class Node
{
public $children;

public function __construct($children = array())
{
$this->children = $children;
}
}
Loading