Skip to content

Instantly share code, notes, and snippets.

@immutef
Created April 30, 2011 09:48
Show Gist options
  • Save immutef/949563 to your computer and use it in GitHub Desktop.
Save immutef/949563 to your computer and use it in GitHub Desktop.
Normalizers
<?php
namespace Turtle\ApiBundle\Serializer\Normalizer;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer,
Symfony\Component\Serializer\SerializerInterface;
class DateTimeNormalizer extends AbstractNormalizer
{
/**
* @var string
*/
protected $format;
/**
* Constructor.
*
* @param string $format (optional)
* @return void
*/
public function __construct($format = \DateTime::RFC3339)
{
$this->format = $format;
}
/**
* {@inheritDoc}
*/
public function normalize($object, $format, $properties = null)
{
return $object->format($this->format);
}
/**
* {@inheritDoc}
*/
public function denormalize($data, $class, $format = null)
{
return $class::createFromFormat($this->format, $data);
}
/**
* {@inheritDoc}
*/
public function supports(\ReflectionClass $class, $format = null)
{
if ('DateTime' == $class->getName()) {
return true;
}
return false;
}
}
<?php
namespace Turtle\ApiBundle\Serializer\Normalizer;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer,
Symfony\Component\Serializer\SerializerInterface;
class DoctrineCollectionNormalizer extends AbstractNormalizer
{
/**
* {@inheritDoc}
*/
public function normalize($object, $format, $properties = null)
{
$attributes = array();
foreach ($object as $attributeName => $attributeValue) {
$attributes[$attributeName] = $this->serializer->normalizeObject($attributeValue, $format, $properties);
}
return $attributes;
}
/**
* {@inheritDoc}
*/
public function denormalize($data, $class, $format = null)
{
throw new \BadMethodCallException('Denormalization of a Doctrine Collection is not possible.');
}
/**
* {@inheritDoc}
*/
public function supports(\ReflectionClass $class, $format = null)
{
if ($class->implementsInterface('Doctrine\\Common\\Collections\\Collection')) {
return true;
}
return false;
}
}
<?php
namespace Turtle\ApiBundle\Entity;
use Doctrine\Common\NotifyPropertyChanged,
Doctrine\Common\PropertyChangedListener;
class Entity implements NotifyPropertyChanged, \ArrayAccess
{
/**
* @var array
*/
private $_listeners = array();
/**
* {@inheritDoc}
*/
public function addPropertyChangedListener(PropertyChangedListener $listener) {
$this->_listeners[] = $listener;
}
/**
* @param string $propName
* @param mixed $oldValue
* @param mixed $newValue
*/
protected function _onPropertyChanged($propName, $oldValue, $newValue) {
if (count($this->_listeners) > 0) {
foreach ($this->_listeners as $listener) {
$listener->propertyChanged($this, $propName, $oldValue, $newValue);
}
}
}
/**
* Retrieve value of a property.
*
* @param string $field
* @return mixed
*/
public function get($field)
{
return $this->offsetGet($field);
}
/**
* Set value of a property.
*
* @param string $field
* @param mixed $value
* @return void
*/
public function set($field, $value)
{
$this->offsetSet($field, $value);
}
/**
* Exists if a property is set.
*
* @param string $field
* @return boolean
*/
public function has($field)
{
return $this->offsetExists($field);
}
/**
* {@inheritDoc}
*/
public function __call($method, array $arguments = array())
{
$match = null;
if (!preg_match('/^(get|set|has)_([a-zA-Z0-9_]+)$/', preg_replace('/([A-Z])/', '_$1', $method), $match)) {
throw new \BadMethodCallException(sprintf('Method "%s" does not exist in "%s" entity.', $method, get_class($this)));
}
list(, $type, $field) = $match;
$field = strtolower($field);
if ('get' == $type) {
return $this->offsetGet($field);
}
if ('has' == $type) {
return $this->offsetExists($field);
}
if (!count($arguments)) {
$arguments = array(null);
}
$this->offsetSet($field, current($arguments));
}
/**
* {@inheritDoc}
*/
public function __get($field)
{
return $this->offsetGet($field);
}
/**
* {@inheritDoc}
*/
public function __set($field, $value)
{
$this->offsetSet($field, $value);
}
/**
* {@inheritDoc}
*/
public function __isset($field)
{
return $this->offsetExists($field);
}
/**
* {@inheritDoc}
*/
public function __unset($field)
{
return $this->offsetUnset($field);
}
/**
* {@inheritDoc}
*/
public function offsetGet($field)
{
$method = 'get'.$this->normalizeMethod($field);
if ($this->isMethodAccessible($method)) {
return $this->$method();
}
if ($this->isPropertyAccessible($field)) {
return $this->$field;
}
if ($this->isIdField($field)) {
return $this->offsetGet($this->normalizeIdField($field));
}
throw new \BadMethodCallException(sprintf('Field "%s" does not exist in "%s" entity.', $field, get_class($this)));
}
/**
* {@inheritDoc}
*/
public function offsetSet($field, $value)
{
$method = 'set'.$this->normalizeMethod($field);
if ($this->isMethodAccessible($method)) {
return $this->$method($value);
}
if ($this->isIdField($field)) {
return $this->offsetSet($this->normalizeIdField($field), $value);
}
if (!$this->isPropertyAccessible($field)) {
throw new \BadMethodCallException(sprintf('Field "%s" does not exist in "%s" entity.', $field, get_class($this)));
}
if ($value != $this->$field) {
$this->_onPropertyChanged($field, $this->$field, $value);
$this->$field = $value;
}
}
/**
* {@inheritDoc}
*/
public function offsetExists($field)
{
$method = 'has'.$this->normalizeMethod($field);
if ($this->isMethodAccessible($method)) {
return $this->$method();
}
$method = 'get'.$this->normalizeMethod($field);
if (method_exists($this, $method)) {
return null !== $this->$method();
}
if ($this->isPropertyAccessible($field)) {
return null !== $this->$field;
}
if ($this->isIdField($field)) {
return $this->offsetExists($this->normalizeIdField($field));
}
throw new \BadMethodCallException(sprintf('Field "%s" does not exist in "%s" entity.', $field, get_class($this)));
}
/**
* {@inheritDoc}
*/
public function offsetUnset($field)
{
$this->offsetSet($field, null);
}
/**
* @param string $field
* @return string
*/
private function normalizeField($field)
{
return strtr($field, array('_' => '', '.' => '_'));
}
/**
* @param string $field
* @return string
*/
private function normalizeIdField($field)
{
return rtrim(substr($this->normalizeField($field), 0, -2), '_');
}
/**
* @param string $field
* @return string
*/
private function normalizeMethod($field)
{
return ucfirst($this->normalizeField($field));
}
/**
* @param string $field
* @return boolean
*/
private function isIdField($field)
{
return 'id' == substr(strtolower($field), -2);
}
/**
* @param string $method
* @return boolean
*/
private function isMethodAccessible($method)
{
if (!method_exists($this, $method)) {
return false;
}
$reflMethod = new \ReflectionMethod($this, $method);
if (!$reflMethod->isPublic()) {
return false;
}
return true;
}
/**
* @param string $property
* @return boolean
*/
private function isPropertyAccessible($property)
{
if ('_' == substr($property, 0, 1) || !property_exists($this, $property)) {
return false;
}
$reflProp = new \ReflectionProperty($this, $property);
if ($reflProp->isPrivate() || $reflProp->isStatic()) {
return false;
}
return true;
}
}
<?php
namespace Turtle\ApiBundle\Serializer\Normalizer;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer,
Symfony\Component\Serializer\SerializerInterface,
Symfony\Bundle\DoctrineBundle\Registry;
class EntityNormalizer extends AbstractNormalizer
{
/**
* @var Symfony\Bundle\DoctrineBundle\Registry $registry
*/
private $registry;
/**
* Constructor.
*
* @param Symfony\Bundle\DoctrineBundle\Registry $registry
* @return void
*/
public function __construct(Registry $registry)
{
$this->registry = $registry;
}
/**
* {@inheritDoc}
*/
public function normalize($object, $format, $properties = null)
{
$propertyMap = (null === $properties) ? null : array_map('strtolower', $properties);
$metadata = $this->getMetadata(get_class($object));
$properties = $metadata->getReflectionProperties();
$attributes = array();
foreach ($properties as $property) {
$attributeName = $property->getName();
if ($this->isPropertyAccessible($property) && (null === $propertyMap || in_array($attributeName, $propertyMap))) {
$attributeValue = $object->get($attributeName);
if ($this->serializer->isStructuredType($attributeValue)) {
if ($metadata->isSingleValuedAssociation($attributeName)) {
$associcationProperties = $this->getMetadata(get_class($attributeValue))->getIdentifierFieldNames();
$attributeValue = $this->serializer->normalizeObject($attributeValue, $format, $associcationProperties);
} else if ($metadata->isCollectionValuedAssociation($attributeName)) {
$associationMapping = $metadata->getAssociationMapping($attributeName);
$associcationProperties = $this->getMetadata($associationMapping['targetEntity'])->getIdentifierFieldNames();
$attributeValue = $this->serializer->normalizeObject($attributeValue, $format, $associcationProperties);
} else {
$attributeValue = $this->serializer->normalize($attributeValue, $format);
}
}
$attributes[$attributeName] = $attributeValue;
}
}
return $attributes;
}
/**
* {@inheritDoc}
*/
public function denormalize($data, $class, $format = null)
{
$metadata = $this->getMetadata($class);
$reflectionClass = $metadata->getReflectionClass();
$constructor = $reflectionClass->getConstructor();
if ($constructor) {
$constructorParameters = $constructor->getParameters();
$parameters = array();
foreach ($constructorParameters as $constructorParameter) {
$parameterName = strtolower($constructorParameter->getName());
if (isset($data[$parameterName])) {
$parameters[] = $data[$parameterName];
unset($data[$parameterName]);
} else if (!$constructorParameter->isOptional()) {
throw new \RuntimeException(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires parameter "%s" to be present.', $class, $parameterName));
}
}
$object = $reflectionClass->newInstanceArgs($parameters);
} else {
$object = new $class;
}
foreach ($data as $attributeName => $attributeValue) {
$object->set($attributeName, $attributeValue);
}
return $object;
}
/**
* {@inheritDoc}
*/
public function supports(\ReflectionClass $class, $format = null)
{
if ($class->isSubclassOf('Turtle\\ApiBundle\\Entity\\Entity')) {
return true;
}
return false;
}
/**
* @return Doctrine\ORM\Mapping\ClassMetadata
*/
private function getMetadata($class)
{
return $this->registry
->getEntityManager()
->getMetadataFactory()
->getMetadataFor($class);
}
/**
* @param \ReflectionProperty $reflProp
* @return boolean
*/
private function isPropertyAccessible(\ReflectionProperty $reflProp)
{
if ($reflProp->isPrivate() || $reflProp->isStatic() || '_' == substr($reflProp->getName(), 0, 1)) {
return false;
}
return true;
}
}
<response>
<match>
<id>550</id>
<!-- participants: Collection Valued Association -->
<participants>
<id>21</id>
</participants>
<participants>
<id>22</id>
</participants>
<is_played>0</is_played>
<created_at>2011-05-04T00:08:43+02:00</created_at>
<updated_at>2011-05-04T00:08:43+02:00</updated_at>
</match>
</response>
<response>
<participant>
<id>21</id>
<match><!-- match: Single Valued Association -->
<id>550</id>
</match>
<created_at>2011-05-04T00:12:30+02:00</created_at>
<updated_at/>
</participant>
<participant>
<id>22</id>
<match><!-- match: Single Valued Association -->
<id>550</id>
</match>
<created_at>2011-05-04T00:12:30+02:00</created_at>
<updated_at/>
</participant>
</response>
<?php
namespace Versus\RankedMatchBundle\Entity;
use Turtle\ApiBundle\Entity\Entity;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @orm:Entity(repositoryClass="Versus\RankedMatchBundle\Entity\MatchRepository")
* @orm:Table(name="matches")
* @orm:HasLifecycleCallbacks
*/
class Match extends Entity
{
/**
* @var integer
* @orm:Id
* @orm:Column(type="bigint")
* @orm:GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @var \Doctrine\Common\Collections\ArrayCollection
* @orm:OneToMany(targetEntity="Participant", mappedBy="match")
*/
protected $participants;
/**
* @var boolean
* @orm:Column(type="boolean")
*/
protected $is_played;
/**
* @var \DateTime
* @gedmo:Timestampable(on="create")
* @orm:Column(type="datetime")
*/
protected $created_at;
/**
* @var \DateTime
* @gedmo:Timestampable(on="update")
* @orm:Column(type="datetime", nullable=true)
*/
protected $updated_at;
/**
* Constructor.
*
* @return void
*/
public function __construct()
{
$this->is_played = false;
$this->created_at = new \DateTime('now');
$this->participants = new ArrayCollection();
}
}
<?php
namespace Versus\RankedMatchBundle\Entity;
use Turtle\ApiBundle\Entity\Entity;
/**
* @orm:Entity(repositoryClass="Versus\RankedMatchBundle\Entity\ParticipantRepository")
* @orm:Table(name="participants")
* @orm:HasLifecycleCallbacks
*/
class Participant extends Entity
{
/**
* @var integer
* @orm:Id
* @orm:Column(type="bigint")
* @orm:GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @var Versus\RankedMatchBundle\Entity\Match
* @orm:ManyToOne(targetEntity="Match", inversedBy="participants")
* @orm:JoinColumn(referencedColumnName="id", nullable=false)
*/
protected $match;
/**
* @var \DateTime
* @gedmo:Timestampable(on="create")
* @orm:Column(type="datetime")
*/
protected $created_at;
/**
* @var \DateTime
* @gedmo:Timestampable(on="update")
* @orm:Column(type="datetime", nullable=true)
*/
protected $updated_at;
/**
* Constructor.
*
* @return void
*/
public function __construct()
{
$this->created_at = new \DateTime('now');
}
}
@HelloGrayson
Copy link

Any chance you could help me update these to Symfony2 BETA5 compatible?

@immutef
Copy link
Author

immutef commented Jun 22, 2011

I'm not using these Normalizers anymore because I switched to Propel2 for Entity/ActiveRecord generation and use the GetSetMethodNormalizer. No more magic methods ;-)

@HelloGrayson
Copy link

Oh so with propel all your entities return data structures simple enough for even GetSetMethodNormalizer to understand? Isn't the Symfony 2 support sub-par in comparison to Doctrine support? Thanks

@immutef
Copy link
Author

immutef commented Jun 23, 2011

No, but it generates generic getter and setter methods which could be used by this Normalizer. My "magic" Entity just used PHP's magic methods to access properties via methods/array access, thus the GetSetNormalizer simply did not work properly. With Fabien's SensioGeneratorBundle it's just a lot easier to generate Entities without Propel2 and use 'em with RestBundle/SerializerBundle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment