Skip to content

Instantly share code, notes, and snippets.

@kmuenkel
Last active February 10, 2025 22:47
Show Gist options
  • Save kmuenkel/a35ad658e30f5c80f35b7669e5b58492 to your computer and use it in GitHub Desktop.
Save kmuenkel/a35ad658e30f5c80f35b7669e5b58492 to your computer and use it in GitHub Desktop.
Entity to array
<?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;
}
}
<?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
};
}
}
<?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;
};
<?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