Created
January 31, 2020 16:55
-
-
Save jamesthomasonjr/6f6e17a9442f63fda431984253adb394 to your computer and use it in GitHub Desktop.
Incomplete Symfony Serializer Normalizer for Doctrine Objects
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Something\Somewhere; | |
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as ODMClassMetadata; | |
use Doctrine\ORM\Mapping\ClassMetadata as ORMClassMetadata; | |
use Doctrine\Common\Persistence\Mapping\ClassMetadata; | |
use Doctrine\Common\Persistence\ObjectManager; | |
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; | |
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; | |
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; | |
use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait; | |
use Symfony\Component\Serializer\SerializerAwareInterface; | |
use Symfony\Component\Serializer\SerializerAwareTrait; | |
class DoctrineObjectNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface | |
{ | |
use ObjectToPopulateTrait; | |
use SerializerAwareTrait; | |
/** | |
* @var ObjectManager | |
*/ | |
private $om; | |
/** | |
* @var array<string, array<mixed, true>> | |
*/ | |
private $objectCache; | |
/** | |
* @var bool | |
*/ | |
private $initialized = false; | |
public function __construct(ObjectManager $om) | |
{ | |
$this->om = $om; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function normalize($object, string $format = null, array $context = []) | |
{ | |
$metadata = $this->om->getClassMetadata(\get_class($object)); | |
if (class_exists(ORMClassMetadata::class) && $metadata instanceof ORMClassMetadata) { | |
return $this->normalizeORM($object, $metadata, $format, $context); | |
} | |
if (class_exists(ODMClassMetadata::class) && $metadata instanceof ODMClassMetadata) { | |
return $this->normalizeODM($object, $metadata, $format, $context); | |
} | |
return $this->normalizeDefault($object, $metadata, $format, $context); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function supportsNormalization($data, string $format = null) | |
{ | |
if (!$this->initialized) { | |
$this->initialize(); | |
} | |
return $this->om->getMetadataFactory()->hasMetadataFor(\get_class($data)); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function denormalize($data, string $type, string $format = null, array $context = []) | |
{ | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function supportsDenormalization($data, string $type, string $format = null) | |
{ | |
if (!$this->initialized) { | |
$this->initialize(); | |
} | |
return $this->om->getMetadataFactory()->hasMetadataFor($type); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function hasCacheableSupportsMethod() | |
{ | |
return __CLASS__ === \get_class($this); | |
} | |
/** | |
* This pre-loads the metadata for all classes known to the MetadataFactory. | |
* This is necessary because its hasMetadataFor() method only looks for already loaded classes. | |
* This could be removed if the factory's driver was public or it implemented an interface to check against the driver's known classes. | |
* | |
* @return void | |
*/ | |
private function initialize() | |
{ | |
$this->om->getMetadataFactory()->getAllMetadata(); | |
$this->initialized = true; | |
} | |
/** | |
* Normalize a Doctrine object that is a relational entity | |
*/ | |
private function normalizeORM(object $object, ORMClassMetadata $metadata, string $format = null, array $context = []) | |
{ | |
$data = []; | |
$fields = $metadata->fieldMappings; | |
$associations = $metadata->associationMappings; | |
$discriminatorColumn = $metadata->discriminatorColumn; | |
$discriminatorValue = $metadata->discriminatorValue; | |
if ($discriminatorColumn !== null && $discriminatorValue !== null) { | |
$key = $discriminatorColumn['name']; | |
$type = $discriminatorColumn['type']; | |
$value = $discriminatorValue; | |
\settype($value, $type); | |
$data[$key] = $value; | |
} | |
foreach ($fields as $field) { | |
$objectKey = $field['fieldName']; | |
$arrayKey = $field['columnName']; | |
$fieldType = $field['type']; | |
[$key, $value] = $this->getFieldValue($object, $metadata, $objectKey, $arrayKey, $fieldType); | |
if ($key !== null) { | |
$data[$key] = $value; | |
} | |
} | |
foreach ($associations as $association) { | |
$objectKey = $association['fieldName']; | |
$arrayKey = implode('.', $association['joinColumnFieldNames']); | |
[$key, $value] = $this->getAssociationValue($object, $metadata, $objectKey, $arrayKey, $format); | |
if ($key !== null) { | |
$data[$key] = $value; | |
} | |
} | |
} | |
/** | |
* Normalize a Doctrine object that is a MongoDB document | |
*/ | |
private function normalizeODM(object $object, ODMClassMetadata $metadata, string $format = null, array $context = []) | |
{ | |
return $this->normalizeDefault($object, $metadata, $format, $context); | |
} | |
/** | |
* Normalize a Doctrine object of unknown origin | |
*/ | |
private function normalizeDefault(object $object, ClassMetadata $metadata, string $format = null, array $context = []) | |
{ | |
$fields = $metadata->getFieldNames(); | |
$associations = $metadata->getAssociationNames(); | |
foreach ($fields as $field) { | |
$objectKey = $arrayKey = $field; | |
$fieldType = $metadata->getTypeOfField($field); | |
[$key, $value] = $this->getFieldValue($object, $metadata, $objectKey, $arrayKey, $fieldType); | |
if ($key !== null) { | |
$data[$key] = $value; | |
} | |
} | |
foreach ($associations as $association) { | |
[$key, $value] = $this->getAssociationValue($object, $metadata, $objectKey, $arrayKey, $fieldType); | |
if ($key !== null) { | |
$data[$key] = $value; | |
} | |
} | |
} | |
/** | |
* Gets the column name and properly typed value for a field. | |
* This does not enforce unique/nullable/length constraints, though it could. | |
*/ | |
private function getFieldValue(object $object, ClassMetadata $metadata, string $objectKey, string $arrayKey, string $fieldType) | |
{ | |
try { | |
$reflectionProperty = $this->getReflectionProperty($object, $metadata, $objectKey); | |
} catch (\ReflectionException $reflectionException) { | |
return [null, null]; | |
} | |
// Override visibility | |
if (!$reflectionProperty->isPublic()) { | |
$reflectionProperty->setAccessible(true); | |
} | |
$value = $reflectionProperty->getValue($object); | |
\settype($value, $fieldType); | |
return [$arrayKey, $value]; | |
} | |
private function getAssociationValue(object $object, ClassMetadata $metadata, string $objectKey, string $arrayKey, string $format = null, array $context = []) | |
{ | |
try { | |
$reflectionProperty = $this->getReflectionProperty($object, $metadata, $objectKey); | |
} catch (\ReflectionException $reflectionException) { | |
return [null, null]; | |
} | |
// Override visibility | |
if (!$reflectionProperty->isPublic()) { | |
$reflectionProperty->setAccessible(true); | |
} | |
$value = $reflectionProperty->getValue($object); | |
$normalized = $this->normalizer->normalize($value, $format, $context); | |
return [$arrayKey, $normalized]; | |
} | |
/** | |
* @param string|object $classOrObject | |
* | |
* @throws \ReflectionException | |
*/ | |
private function getReflectionProperty($classOrObject, ClassMetadata $metadata, string $attribute): \ReflectionProperty | |
{ | |
$reflectionClass = new \ReflectionClass($classOrObject); | |
while (true) { | |
try { | |
return $reflectionClass->getProperty($attribute); | |
} catch (\ReflectionException $e) { | |
if (!$reflectionClass = $reflectionClass->getParentClass()) { | |
throw $e; | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment