Skip to content

Instantly share code, notes, and snippets.

@jamesthomasonjr
Created January 31, 2020 16:55
Show Gist options
  • Save jamesthomasonjr/6f6e17a9442f63fda431984253adb394 to your computer and use it in GitHub Desktop.
Save jamesthomasonjr/6f6e17a9442f63fda431984253adb394 to your computer and use it in GitHub Desktop.
Incomplete Symfony Serializer Normalizer for Doctrine Objects
<?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