Last active
February 10, 2025 22:47
-
-
Save kmuenkel/a35ad658e30f5c80f35b7669e5b58492 to your computer and use it in GitHub Desktop.
Entity to array
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 Tests; | |
use ArrayAccess; | |
use BackedEnum; | |
use DateTimeInterface; | |
use RecursiveArrayIterator; | |
use RecursiveIteratorIterator; | |
use ReflectionClass; | |
use ReflectionProperty; | |
use stdClass; | |
readonly class DataTransformer | |
{ | |
public static function toArray(object $entity, int $depth = 0): array | |
{ | |
return ($self = function (mixed $value, int $depth, int $level = 0) use (&$self): mixed { | |
return match (true) { | |
$value instanceof DateTimeInterface => $value->format($value::ATOM), | |
$value instanceof BackedEnum => $value->value, | |
$value instanceof stdClass => $level >= $depth ? '{...}' : array_map(fn (mixed $item): mixed => $self($item, $depth, $level), (array) $value), | |
is_array($value) || $value instanceof ArrayAccess => $level >= $depth ? '{...}' : array_map(fn (mixed $item): mixed => $self($item, $depth, $level), iterator_to_array($value)), | |
is_string($value) || (is_object($value) && in_array('__toString', get_class_methods($value))) => json_decode($value = (string) $value) ?: $value, | |
is_object($value) => $level >= $depth ? '{...}' : array_filter(array_combine( | |
array_map(fn (ReflectionProperty $property): string => $property->name, $properties = (new ReflectionClass($value))->getProperties()), | |
array_map(fn (ReflectionProperty $property): mixed => $property->isInitialized($value) && !str_starts_with($property->class, 'Proxies\\__CG__\\') ? $self($property->getValue($value), $depth, $level + 1) : '{{_UNDEFINED}}', $properties) | |
), fn (mixed $value): bool => $value != '{{_UNDEFINED}}'), | |
default => $value | |
}; | |
})($entity, max($depth ?: 20, 1)); | |
} | |
public static function toDot(mixed $array, int $depth = 0): array | |
{ | |
$array = is_array($array) ? $array : self::toArray($array, $depth); | |
$iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($array), RecursiveIteratorIterator::SELF_FIRST); | |
$path = $flatArray = []; | |
foreach ($iterator as $key => $value) { | |
$path[$iterator->getDepth()] = $key; | |
!is_array($value) && $flatArray[implode('.', array_slice($path, 0, $iterator->getDepth() + 1))] = $value; | |
} | |
return $flatArray; | |
} | |
} |
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 Tests; | |
use DateTimeInterface; | |
use Doctrine\Common\Collections\Collection; | |
use Doctrine\ORM\Mapping\ClassMetadata; | |
use Doctrine\ORM\Mapping\Driver\AttributeDriver; | |
use Doctrine\Persistence\Mapping\RuntimeReflectionService; | |
use ReflectionClass; | |
use ReflectionProperty; | |
use stdClass; | |
readonly class EntityToArray | |
{ | |
public static function get(object $entity, int $depth = 0): array | |
{ | |
$entityMetaData = new ClassMetadata($entity::class); | |
(new AttributeDriver([]))->loadMetadataForClass($entity::class, $entityMetaData); | |
$entityMetaData->wakeupReflection(new RuntimeReflectionService()); | |
$fields = array_keys($entityMetaData->fieldMappings); | |
$values = array_map(fn (string $field) => self::stringify($entityMetaData->getFieldValue($entity, $field), max($depth, 10)), $fields); | |
return array_combine($fields, $values); | |
} | |
private static function stringify(mixed $value, int $depth, int $level = 0) | |
{ | |
return match (true) { | |
$value instanceof DateTimeInterface => $value->format($value::ATOM), | |
is_object($value) && in_array('__toString', get_class_methods($value)) => json_decode($value = (string) $value) ?: $value, | |
$value instanceof EntityInterface => ($level > $depth) ? '{{_MAX_DEPTH_REACHED}}' : self::get($value, $depth), | |
$value instanceof DataTypeInterface => array_combine( | |
array_map(fn (ReflectionProperty $property): string => $property->name, $properties = (new ReflectionClass($value))->getProperties()), | |
array_map(fn (ReflectionProperty $property) => self::stringify($property->getValue($value), $depth, $level + 1), $properties) | |
), | |
$value instanceof Collection => ($level > $depth) ? '{{_MAX_DEPTH_REACHED}}' : array_map(fn ($item) => $this->stringify($item, $depth, $level + 1), $value->getValues()), | |
$value instanceof stdClass => json_decode(json_encode($value)), | |
is_object($value) => (array) $value, | |
is_string($value) && json_decode($value) => json_decode($value), | |
default => $value | |
}; | |
} | |
} |
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 | |
/** | |
* @throws \Psr\Container\ContainerExceptionInterface | |
* @throws \Psr\Container\NotFoundExceptionInterface | |
*/ | |
$self = function entityToArray( | |
\Chess\WebBundle\Entity\EntityInterface|array $entity, | |
\Chess\WebBundle\Doctrine\ORM\EntityRepository|\Doctrine\ORM\EntityManager|string $manager = null, | |
\Psr\Container\ContainerInterface $container = null | |
) use(&$self): array { | |
if (!$entity) { | |
return []; | |
} | |
if (is_array($entity)) { | |
return array_map(fn ($item) => $self($item, $depth, $manager, $container), $entity); | |
} | |
if (!$container) { | |
require_once __DIR__ . '/../app/AppKernel.php'; | |
$kernel = new AppKernel('test', true); | |
$kernel->boot(); | |
$container = $kernel->getContainer(); | |
} | |
$entityManagerName = is_string($manager) ? $manager | |
: ($manager instanceof \Doctrine\ORM\EntityRepository | |
? ($manager->getConnection()?->getParams()['name'] ?: 'default') : 'default'); | |
/** @var \Doctrine\ORM\EntityManager $entityManager */ | |
$entityManager = $manager instanceof \Doctrine\ORM\EntityManager ? $manager | |
: $container->get('doctrine.orm.' . $entityManagerName . '_entity_manager'); | |
if ($entityManagerName == 'default' && null === $manager) { | |
try { | |
$entityManager->getRepository(get_class($entity)); | |
} catch (\Doctrine\Persistence\Mapping\MappingException) { | |
$entityManagers = []; | |
/** @var \Doctrine\ORM\EntityManagerInterface $entityManager */ | |
foreach ($container->get('doctrine')->getManagers() as $entityManager) { | |
try { | |
$entityManager->getRepository(get_class($entity)); | |
$entityManagers[] = $entityManager; | |
break; | |
} catch (\Doctrine\Persistence\Mapping\MappingException) { | |
} | |
} | |
if (count($entityManagers) != 1) { | |
throw new RuntimeException('Unable to identify a single EntityManager.'); | |
} | |
$entityManager = current($entityManagers); | |
} | |
} | |
$values = ($entityToArray = function ( | |
?Chess\WebBundle\Entity\EntityInterface $entity, | |
Doctrine\ORM\Mapping\ClassMetadataFactory $entityManager, | |
$depth | |
) use (&$entityToArray): array { | |
static $records = []; | |
if (in_array($recordId = $entity::class . '@' . $entity->getId(), $records)) { | |
return [$recordId => '{{RECURSION DETECTED}}']; | |
} | |
$records[] = $recordId; | |
$entityMetaData = $entityManager->getMetadataFor($entity::class); | |
$properties = array_keys($entityMetaData->getReflectionProperties()); | |
$values = array_map(function (string $property) use ($entityToArray, $entityMetaData, $entity, $entityManager, $depth) { | |
$value = $entityMetaData->getFieldValue($entity, $property); | |
return ($normalize = function ($value) use (&$normalize, $entityToArray, $entityManager, $depth) { | |
static $level = 0; | |
++$level; | |
if ($level > 5) { | |
throw new RuntimeException('Recursion detected'); | |
} | |
if ($value instanceof DateTimeImmutable || $value instanceof DateTime) { | |
$value = $value->format($value::ATOM); | |
} elseif (is_object($value) && in_array('__toString', get_class_methods($value))) { | |
$value = (string) $value; | |
$value = json_decode($value, true) ?: $value; | |
} elseif ($value instanceof \Chess\WebBundle\Entity\EntityInterface) { | |
$value = ($depth && $level >= $depth) ? '_DEPTH_REACHED' : $entityToArray($value, $entityManager, $depth); | |
} elseif ($value instanceof \Chess\Common\JsonData\DataTypeInterface) { | |
$properties = (new ReflectionClass($value))->getProperties(); | |
$values = array_map(fn (ReflectionProperty $property) => $normalize($property->getValue($value)), $properties); | |
$names = array_map(fn (ReflectionProperty $property): string => $property->name, $properties); | |
$value = array_combine($names, $values); | |
} elseif ($value instanceof \Doctrine\ORM\PersistentCollection) { | |
$value = array_map(fn ($collectionValue) => $normalize($collectionValue), $value->getValues()); | |
} elseif ($value instanceof stdClass) { | |
$value = json_decode(json_encode($value)); | |
} elseif (is_object($value)) { | |
$value = (array) $value; | |
} elseif (is_string($value) && ($decoded = json_decode($value, true))) { | |
$value = $decoded; | |
} | |
--$level; | |
return $value; | |
})($value); | |
}, $properties); | |
return array_combine($properties, $values); | |
})($entity, $entityManager->getMetadataFactory(), $depth); | |
return $values; | |
}; |
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 Tests; | |
use ArrayAccess; | |
use BackedEnum; | |
use DateTimeInterface; | |
use ReflectionClass; | |
use ReflectionProperty; | |
readonly class ToArray | |
{ | |
public static function get(object $entity, int $depth = 0): array | |
{ | |
return ($self = function (mixed $value, int $depth, int $level = 0) use (&$self): mixed { | |
return match (true) { | |
$value instanceof DateTimeInterface => $value->format($value::ATOM), | |
$value instanceof BackedEnum => $value->value, | |
is_array($value) || $value instanceof ArrayAccess => $level >= $depth ? '{...}' : array_map(fn (mixed $item): mixed => $self($item, $depth, $level), iterator_to_array($value)), | |
is_string($value) || (is_object($value) && in_array('__toString', get_class_methods($value))) => json_decode($value = (string) $value) ?: $value, | |
is_object($value) => $level >= $depth ? '{...}' : array_filter(array_combine( | |
array_map(fn (ReflectionProperty $property): string => $property->name, $properties = (new ReflectionClass($value))->getProperties()), | |
array_map(fn (ReflectionProperty $property): mixed => $property->isInitialized($value) ? $self($property->getValue($value), $depth, $level + 1) : '{{_UNDEFINED}}', $properties) | |
), fn (mixed $value): bool => $value != '{{_UNDEFINED}}'), | |
default => $value | |
}; | |
})($entity, max($depth ?: 10, 1)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment