diff --git a/composer.json b/composer.json index dae429559..833ce59c5 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 9971bfebf..f087ef644 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/config/services.php b/config/services.php index 20f56ca9d..f64ae38a7 100644 --- a/config/services.php +++ b/config/services.php @@ -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 ), diff --git a/src/Entity/ApiKey.php b/src/Entity/ApiKey.php index b3a0aaf35..0cdb29c86 100644 --- a/src/Entity/ApiKey.php +++ b/src/Entity/ApiKey.php @@ -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; diff --git a/src/Entity/Podcast.php b/src/Entity/Podcast.php index 30f1d0924..7c1b40e81 100644 --- a/src/Entity/Podcast.php +++ b/src/Entity/Podcast.php @@ -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; diff --git a/src/Entity/Station.php b/src/Entity/Station.php index 9e84e9c3f..0d8ae2f06 100644 --- a/src/Entity/Station.php +++ b/src/Entity/Station.php @@ -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; diff --git a/src/Entity/StationMedia.php b/src/Entity/StationMedia.php index 04af160be..aabd87853 100644 --- a/src/Entity/StationMedia.php +++ b/src/Entity/StationMedia.php @@ -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; diff --git a/src/Entity/StationPlaylist.php b/src/Entity/StationPlaylist.php index 674db9a1f..f559c8ded 100644 --- a/src/Entity/StationPlaylist.php +++ b/src/Entity/StationPlaylist.php @@ -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; diff --git a/src/Entity/StationStreamer.php b/src/Entity/StationStreamer.php index 7574a7a5b..8d3db13d8 100644 --- a/src/Entity/StationStreamer.php +++ b/src/Entity/StationStreamer.php @@ -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; diff --git a/src/Entity/User.php b/src/Entity/User.php index 8cb568b11..b3b4b00f4 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -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; diff --git a/src/Normalizer/Attributes/DeepNormalize.php b/src/Normalizer/Attributes/DeepNormalize.php deleted file mode 100644 index de5046d0b..000000000 --- a/src/Normalizer/Attributes/DeepNormalize.php +++ /dev/null @@ -1,23 +0,0 @@ -deepNormalize = $value; - } - - public function getDeepNormalize(): bool - { - return $this->deepNormalize; - } -} diff --git a/src/Normalizer/DoctrineEntityNormalizer.php b/src/Normalizer/DoctrineEntityNormalizer.php deleted file mode 100644 index 91202ac31..000000000 --- a/src/Normalizer/DoctrineEntityNormalizer.php +++ /dev/null @@ -1,392 +0,0 @@ -inflector = InflectorFactory::create()->build(); - } - - /** - * Replicates the "toArray" functionality previously present in Doctrine 1. - * - * @return array|string|int|float|bool|ArrayObject|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 $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 $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 $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 $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 $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 $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]; - } -} diff --git a/src/Normalizer/Exception/NoGetterAvailableException.php b/src/Normalizer/Exception/NoGetterAvailableException.php deleted file mode 100644 index 0f581c5a0..000000000 --- a/src/Normalizer/Exception/NoGetterAvailableException.php +++ /dev/null @@ -1,11 +0,0 @@ -