Switch back to common Doctrine Entity Normalizer library.
This commit is contained in:
parent
838095eee6
commit
df401b5468
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Normalizer\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class NoGetterAvailableException extends Exception
|
||||
{
|
||||
}
|
Loading…
Reference in New Issue