Switch back to common Doctrine Entity Normalizer library.

This commit is contained in:
Buster Neece 2024-03-20 12:55:01 -05:00
parent 838095eee6
commit df401b5468
No known key found for this signature in database
13 changed files with 68 additions and 435 deletions

View File

@ -27,6 +27,7 @@
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"azuracast/doctrine-entity-normalizer": "^3.0",
"azuracast/nowplaying": "dev-main",
"beberlei/doctrineextensions": "^1.5",
"br33f/php-ga4-mp": "^0.1.2",

60
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "201fa8d6f4ca06afe1f45c39845a3603",
"content-hash": "5bca0c641ba21645d05ab830394898ae",
"packages": [
{
"name": "aws/aws-crt-php",
@ -155,6 +155,64 @@
},
"time": "2024-03-15T18:14:42+00:00"
},
{
"name": "azuracast/doctrine-entity-normalizer",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/AzuraCast/doctrine-entity-normalizer.git",
"reference": "81219dc6777c36f16b26e891be64bb57fefbc4ad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/AzuraCast/doctrine-entity-normalizer/zipball/81219dc6777c36f16b26e891be64bb57fefbc4ad",
"reference": "81219dc6777c36f16b26e891be64bb57fefbc4ad",
"shasum": ""
},
"require": {
"doctrine/collections": ">1",
"doctrine/inflector": "^2",
"doctrine/orm": "^3",
"doctrine/persistence": "^2|^3",
"php": ">=8.2",
"symfony/serializer": "^7"
},
"require-dev": {
"php-parallel-lint/php-console-highlighter": "^1",
"php-parallel-lint/php-parallel-lint": "^1.3",
"phpstan/phpstan": "^1",
"roave/security-advisories": "dev-master"
},
"type": "library",
"autoload": {
"psr-4": {
"Azura\\Normalizer\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Buster Neece",
"email": "buster@busterneece.com",
"homepage": "https://busterneece.com/"
}
],
"description": "An implementation of the Symfony Serializer with custom support for Doctrine 3 ORM entities.",
"support": {
"issues": "https://github.com/AzuraCast/doctrine-entity-normalizer/issues",
"source": "https://github.com/AzuraCast/doctrine-entity-normalizer/tree/3.0.0"
},
"funding": [
{
"url": "https://github.com/SlvrEagle23",
"type": "github"
}
],
"time": "2024-03-20T14:58:35+00:00"
},
{
"name": "azuracast/nowplaying",
"version": "dev-main",

View File

@ -279,7 +279,7 @@ return [
$normalizers = [
new Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer(),
new Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer(),
new App\Normalizer\DoctrineEntityNormalizer(
new Azura\Normalizer\DoctrineEntityNormalizer(
$em,
classMetadataFactory: $classMetaFactory
),

View File

@ -6,8 +6,8 @@ namespace App\Entity;
use App\Entity\Interfaces\EntityGroupsInterface;
use App\Entity\Interfaces\IdentifiableEntityInterface;
use App\Normalizer\Attributes\DeepNormalize;
use App\Security\SplitToken;
use Azura\Normalizer\Attributes\DeepNormalize;
use Doctrine\ORM\Mapping as ORM;
use Stringable;
use Symfony\Component\Serializer\Annotation as Serializer;

View File

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Entity;
use App\Entity\Enums\PodcastSources;
use App\Normalizer\Attributes\DeepNormalize;
use Azura\Normalizer\Attributes\DeepNormalize;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

View File

@ -9,11 +9,11 @@ use App\Entity\Enums\StorageLocationTypes;
use App\Entity\Interfaces\EntityGroupsInterface;
use App\Entity\Interfaces\IdentifiableEntityInterface;
use App\Environment;
use App\Normalizer\Attributes\DeepNormalize;
use App\Radio\Enums\BackendAdapters;
use App\Radio\Enums\FrontendAdapters;
use App\Utilities\File;
use App\Validator\Constraints as AppAssert;
use Azura\Normalizer\Attributes\DeepNormalize;
use DateTimeZone;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

View File

@ -7,10 +7,10 @@ namespace App\Entity;
use App\Flysystem\StationFilesystems;
use App\Media\Metadata;
use App\Media\MetadataInterface;
use App\Normalizer\Attributes\DeepNormalize;
use App\OpenApi;
use App\Utilities\Time;
use App\Utilities\Types;
use Azura\Normalizer\Attributes\DeepNormalize;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

View File

@ -8,8 +8,8 @@ use App\Entity\Enums\PlaylistOrders;
use App\Entity\Enums\PlaylistRemoteTypes;
use App\Entity\Enums\PlaylistSources;
use App\Entity\Enums\PlaylistTypes;
use App\Normalizer\Attributes\DeepNormalize;
use App\Utilities\File;
use Azura\Normalizer\Attributes\DeepNormalize;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

View File

@ -4,9 +4,9 @@ declare(strict_types=1);
namespace App\Entity;
use App\Normalizer\Attributes\DeepNormalize;
use App\OpenApi;
use App\Validator\Constraints\UniqueEntity;
use Azura\Normalizer\Attributes\DeepNormalize;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

View File

@ -7,10 +7,10 @@ namespace App\Entity;
use App\Auth;
use App\Entity\Interfaces\EntityGroupsInterface;
use App\Entity\Interfaces\IdentifiableEntityInterface;
use App\Normalizer\Attributes\DeepNormalize;
use App\OpenApi;
use App\Utilities\Strings;
use App\Validator\Constraints\UniqueEntity;
use Azura\Normalizer\Attributes\DeepNormalize;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

View File

@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Normalizer\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)]
final class DeepNormalize
{
private bool $deepNormalize;
public function __construct(bool $value)
{
$this->deepNormalize = $value;
}
public function getDeepNormalize(): bool
{
return $this->deepNormalize;
}
}

View File

@ -1,392 +0,0 @@
<?php
namespace App\Normalizer;
use App\Doctrine\ReloadableEntityManagerInterface;
use App\Normalizer\Attributes\DeepNormalize;
use App\Normalizer\Exception\NoGetterAvailableException;
use ArrayObject;
use Doctrine\Common\Collections\Collection;
use Doctrine\Inflector\Inflector;
use Doctrine\Inflector\InflectorFactory;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use InvalidArgumentException;
use ReflectionClass;
use ReflectionException;
use ReflectionProperty;
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
final class DoctrineEntityNormalizer extends AbstractObjectNormalizer
{
private const string CLASS_METADATA = 'class_metadata';
private const string ASSOCIATION_MAPPINGS = 'association_mappings';
private readonly Inflector $inflector;
public function __construct(
private readonly ReloadableEntityManagerInterface $em,
ClassMetadataFactoryInterface $classMetadataFactory = null,
array $defaultContext = []
) {
$defaultContext[AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES] = true;
parent::__construct(
classMetadataFactory: $classMetadataFactory,
defaultContext: $defaultContext
);
$this->inflector = InflectorFactory::create()->build();
}
/**
* Replicates the "toArray" functionality previously present in Doctrine 1.
*
* @return array|string|int|float|bool|ArrayObject<int, mixed>|null
*/
public function normalize(
mixed $object,
?string $format = null,
array $context = []
): array|string|int|float|bool|ArrayObject|null {
if (!is_object($object)) {
throw new InvalidArgumentException('Cannot normalize non-object.');
}
$context = $this->addDoctrineContext($object::class, $context);
return parent::normalize($object, $format, $context);
}
/**
* Replicates the "fromArray" functionality previously present in Doctrine 1.
*
* @template T as object
* @param mixed $data
* @param class-string<T> $type
* @param string|null $format
* @param array $context
* @return T
*/
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): object
{
$context = $this->addDoctrineContext($type, $context);
return parent::denormalize($data, $type, $format, $context);
}
/**
* @param class-string<object> $className
* @param array $context
* @return array
*/
private function addDoctrineContext(
string $className,
array $context
): array {
$context[self::CLASS_METADATA] = $this->em->getClassMetadata($className);
$context[self::ASSOCIATION_MAPPINGS] = [];
if ($context[self::CLASS_METADATA]->associationMappings) {
foreach ($context[self::CLASS_METADATA]->associationMappings as $mappingName => $mappingInfo) {
$entity = $mappingInfo['targetEntity'];
if (isset($mappingInfo['joinTable'])) {
$context[self::ASSOCIATION_MAPPINGS][$mappingInfo['fieldName']] = [
'type' => 'many',
'entity' => $entity,
'is_owning_side' => ($mappingInfo['isOwningSide'] == 1),
];
} elseif (isset($mappingInfo['joinColumns'])) {
foreach ($mappingInfo['joinColumns'] as $col) {
$colName = $col['name'];
$colName = $context[self::CLASS_METADATA]->fieldNames[$colName] ?? $colName;
$context[self::ASSOCIATION_MAPPINGS][$mappingName] = [
'name' => $colName,
'type' => 'one',
'entity' => $entity,
];
}
}
}
}
return $context;
}
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
{
return $this->isEntity($data);
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return $this->isEntity($type);
}
/**
* @param object|class-string<object> $classOrObject
* @param array $context
* @param bool $attributesAsString
* @return string[]|AttributeMetadataInterface[]|bool
*/
protected function getAllowedAttributes(
$classOrObject,
array $context,
bool $attributesAsString = false
): array|bool {
$groups = $this->getGroups($context);
if (empty($groups)) {
return false;
}
return parent::getAllowedAttributes($classOrObject, $context, $attributesAsString);
}
protected function extractAttributes(object $object, string $format = null, array $context = []): array
{
$rawProps = (new ReflectionClass($object))->getProperties(
ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED
);
$props = [];
foreach ($rawProps as $rawProp) {
$props[] = $rawProp->getName();
}
return array_filter(
$props,
fn($attribute) => $this->isAllowedAttribute($object, $attribute, $format, $context)
);
}
/**
* @param object|class-string<object> $classOrObject
* @param string $attribute
* @param string|null $format
* @param array $context
* @return bool
* @throws ReflectionException
*/
protected function isAllowedAttribute(
object|string $classOrObject,
string $attribute,
string $format = null,
array $context = []
): bool {
if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) {
return false;
}
$reflectionClass = new ReflectionClass($classOrObject);
if (!$reflectionClass->hasProperty($attribute)) {
return false;
}
if (isset($context[self::CLASS_METADATA]->associationMappings[$attribute])) {
if (!$this->supportsDeepNormalization($reflectionClass, $attribute)) {
return false;
}
}
return $this->hasGetter($reflectionClass, $attribute);
}
/**
* @param ReflectionClass<object> $reflectionClass
* @param string $attribute
* @return bool
*/
private function hasGetter(ReflectionClass $reflectionClass, string $attribute): bool
{
// Default to "getStatus", "getConfig", etc...
$getterMethod = $this->getMethodName($attribute, 'get');
if ($reflectionClass->hasMethod($getterMethod)) {
return true;
}
$rawMethod = $this->getMethodName($attribute);
return $reflectionClass->hasMethod($rawMethod);
}
protected function getAttributeValue(
object $object,
string $attribute,
string $format = null,
array $context = []
): mixed {
if (isset($context[self::CLASS_METADATA]->associationMappings[$attribute])) {
if (!$this->supportsDeepNormalization(new ReflectionClass($object), $attribute)) {
throw new NoGetterAvailableException(
sprintf(
'Deep normalization disabled for property %s.',
$attribute
)
);
}
}
$value = $this->getProperty($object, $attribute);
if ($value instanceof Collection) {
$value = $value->getValues();
}
return $value;
}
/**
* @param ReflectionClass<object> $reflectionClass
* @param string $attribute
* @return bool
* @throws ReflectionException
*/
private function supportsDeepNormalization(ReflectionClass $reflectionClass, string $attribute): bool
{
$deepNormalizeAttrs = $reflectionClass->getProperty($attribute)->getAttributes(
DeepNormalize::class
);
if (empty($deepNormalizeAttrs)) {
return false;
}
/** @var DeepNormalize $deepNormalize */
$deepNormalize = current($deepNormalizeAttrs)->newInstance();
return $deepNormalize->getDeepNormalize();
}
private function getProperty(object $entity, string $key): mixed
{
// Default to "getStatus", "getConfig", etc...
$getterMethod = $this->getMethodName($key, 'get');
if (method_exists($entity, $getterMethod)) {
return $entity->{$getterMethod}();
}
// but also allow "isEnabled" instead of "getIsEnabled"
$rawMethod = $this->getMethodName($key);
if (method_exists($entity, $rawMethod)) {
return $entity->{$rawMethod}();
}
throw new NoGetterAvailableException(sprintf('No getter is available for property %s.', $key));
}
/**
* Converts "getvar_name_blah" to "getVarNameBlah".
*/
private function getMethodName(string $var, string $prefix = ''): string
{
return $this->inflector->camelize(($prefix ? $prefix . '_' : '') . $var);
}
/**
* @param object $object
* @param string $attribute
* @param mixed $value
* @param string|null $format
* @param array $context
*/
protected function setAttributeValue(
object $object,
string $attribute,
mixed $value,
?string $format = null,
array $context = []
): void {
if (isset($context[self::ASSOCIATION_MAPPINGS][$attribute])) {
// Handle a mapping to another entity.
$mapping = $context[self::ASSOCIATION_MAPPINGS][$attribute];
if ('one' === $mapping['type']) {
if (empty($value)) {
$this->setProperty($object, $attribute, null);
} else {
/** @var class-string $entity */
$entity = $mapping['entity'];
if (($fieldItem = $this->em->find($entity, $value)) instanceof $entity) {
$this->setProperty($object, $attribute, $fieldItem);
}
}
} elseif ($mapping['is_owning_side']) {
$collection = $this->getProperty($object, $attribute);
if ($collection instanceof Collection) {
$collection->clear();
if ($value) {
foreach ((array)$value as $fieldId) {
/** @var class-string $entity */
$entity = $mapping['entity'];
$fieldItem = $this->em->find($entity, $fieldId);
if ($fieldItem instanceof $entity) {
$collection->add($fieldItem);
}
}
}
}
}
} else {
$methodName = $this->getMethodName($attribute, 'set');
$reflClass = new ReflectionClass($object);
if (!$reflClass->hasMethod($methodName)) {
return;
}
// If setter parameter is a special class, normalize to it.
$methodParams = $reflClass->getMethod($methodName)->getParameters();
$parameter = $methodParams[0];
if (null === $value && $parameter->allowsNull()) {
$value = null;
} else {
$value = $this->denormalizeParameter(
$reflClass,
$parameter,
$attribute,
$value,
$this->createChildContext($context, $attribute, $format),
$format
);
}
$this->setProperty($object, $attribute, $value);
}
}
private function setProperty(
object $entity,
string $attribute,
mixed $value
): void {
$methodName = $this->getMethodName($attribute, 'set');
if (!method_exists($entity, $methodName)) {
return;
}
$entity->$methodName($value);
}
private function isEntity(mixed $class): bool
{
if (is_object($class)) {
$class = DefaultProxyClassNameResolver::getClass($class);
}
if (!is_string($class) || !class_exists($class)) {
return false;
}
return !$this->em->getMetadataFactory()->isTransient($class);
}
public function getSupportedTypes(?string $format): array
{
return ['object' => true];
}
}

View File

@ -1,11 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Normalizer\Exception;
use Exception;
final class NoGetterAvailableException extends Exception
{
}