Move Doctrine EntityNormalizer and batch utilities to external libraries.
This commit is contained in:
parent
6c5f852b97
commit
fc8a2aea08
|
@ -23,6 +23,8 @@
|
|||
"ext-simplexml": "*",
|
||||
"ext-xml": "*",
|
||||
"ext-xmlwriter": "*",
|
||||
"azuracast/doctrine-batch-utilities": "dev-main",
|
||||
"azuracast/doctrine-entity-normalizer": "dev-main",
|
||||
"azuracast/flysystem-v2-extensions": "dev-main",
|
||||
"azuracast/metadata-manager": "dev-main",
|
||||
"azuracast/nowplaying": "dev-main",
|
||||
|
|
|
@ -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": "16e701ab7337c8e0d43e7d0f8303bfde",
|
||||
"content-hash": "d80ce9685b9f9e5e6fa60092463e277e",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
|
@ -147,6 +147,120 @@
|
|||
},
|
||||
"time": "2021-11-02T19:37:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "azuracast/doctrine-batch-utilities",
|
||||
"version": "dev-main",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/AzuraCast/doctrine-batch-utilities.git",
|
||||
"reference": "87041ef806e346812095a86a1d0e7cf88112a784"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/AzuraCast/doctrine-batch-utilities/zipball/87041ef806e346812095a86a1d0e7cf88112a784",
|
||||
"reference": "87041ef806e346812095a86a1d0e7cf88112a784",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/orm": "^2.10|^3.0",
|
||||
"php": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-parallel-lint/php-console-highlighter": "^0.5.0",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.3",
|
||||
"phpstan/phpstan": "^0.12",
|
||||
"roave/security-advisories": "dev-master"
|
||||
},
|
||||
"default-branch": true,
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Azura\\DoctrineBatchUtils\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Buster 'Silver Eagle' Neece",
|
||||
"email": "buster@busterneece.com",
|
||||
"homepage": "https://dashdev.net/"
|
||||
}
|
||||
],
|
||||
"description": "A customized fork of ocramius/DoctrineBatchUtils.",
|
||||
"support": {
|
||||
"issues": "https://github.com/AzuraCast/doctrine-batch-utilities/issues",
|
||||
"source": "https://github.com/AzuraCast/doctrine-batch-utilities/tree/main"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/SlvrEagle23",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2021-11-07T07:03:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "azuracast/doctrine-entity-normalizer",
|
||||
"version": "dev-main",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/AzuraCast/doctrine-entity-normalizer.git",
|
||||
"reference": "66d1787971b62cd6c7a1b29f134a449e3c8a95c6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/AzuraCast/doctrine-entity-normalizer/zipball/66d1787971b62cd6c7a1b29f134a449e3c8a95c6",
|
||||
"reference": "66d1787971b62cd6c7a1b29f134a449e3c8a95c6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/collections": ">1",
|
||||
"doctrine/inflector": "^2",
|
||||
"doctrine/orm": "^2",
|
||||
"doctrine/persistence": "^2",
|
||||
"php": "^8.0",
|
||||
"symfony/serializer": "^5"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-parallel-lint/php-console-highlighter": "^0.5.0",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.3",
|
||||
"phpstan/phpstan": "^0.12",
|
||||
"roave/security-advisories": "dev-master"
|
||||
},
|
||||
"default-branch": true,
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Azura\\Normalizer\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Buster 'Silver Eagle' Neece",
|
||||
"email": "buster@busterneece.com",
|
||||
"homepage": "https://dashdev.net/"
|
||||
}
|
||||
],
|
||||
"description": "An implementation of the Symfony Serializer with custom support for Doctrine 2 ORM entities.",
|
||||
"support": {
|
||||
"issues": "https://github.com/AzuraCast/doctrine-entity-normalizer/issues",
|
||||
"source": "https://github.com/AzuraCast/doctrine-entity-normalizer/tree/main"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/SlvrEagle23",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2021-11-07T07:49:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "azuracast/flysystem-v2-extensions",
|
||||
"version": "dev-main",
|
||||
|
@ -14189,6 +14303,8 @@
|
|||
"aliases": [],
|
||||
"minimum-stability": "dev",
|
||||
"stability-flags": {
|
||||
"azuracast/doctrine-batch-utilities": 20,
|
||||
"azuracast/doctrine-entity-normalizer": 20,
|
||||
"azuracast/flysystem-v2-extensions": 20,
|
||||
"azuracast/metadata-manager": 20,
|
||||
"azuracast/nowplaying": 20,
|
||||
|
|
|
@ -312,7 +312,7 @@ return [
|
|||
|
||||
$normalizers = [
|
||||
new Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer(),
|
||||
new App\Normalizer\DoctrineEntityNormalizer($em, $classMetaFactory),
|
||||
new Azura\Normalizer\DoctrineEntityNormalizer($em, $classMetaFactory),
|
||||
new Symfony\Component\Serializer\Normalizer\ObjectNormalizer($classMetaFactory),
|
||||
];
|
||||
$encoders = [
|
||||
|
|
|
@ -5,11 +5,11 @@ declare(strict_types=1);
|
|||
namespace App\Controller\Api\Stations;
|
||||
|
||||
use App;
|
||||
use App\Doctrine\ReadOnlyBatchIteratorAggregate;
|
||||
use App\Entity;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Utilities\Csv;
|
||||
use Azura\DoctrineBatchUtils\ReadOnlyBatchIteratorAggregate;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use OpenApi\Annotations as OA;
|
||||
|
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Controller\Api\Stations;
|
||||
|
||||
use App\Doctrine\ReadOnlyBatchIteratorAggregate;
|
||||
use App\Entity;
|
||||
use App\Environment;
|
||||
use App\Http\Response;
|
||||
|
@ -12,6 +11,7 @@ use App\Http\ServerRequest;
|
|||
use App\Locale;
|
||||
use App\Service\DeviceDetector;
|
||||
use App\Service\IpGeolocation;
|
||||
use Azura\DoctrineBatchUtils\ReadOnlyBatchIteratorAggregate;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use GuzzleHttp\Psr7\Stream;
|
||||
|
|
|
@ -4,13 +4,13 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Controller\Api\Stations\OnDemand;
|
||||
|
||||
use App\Doctrine\ReadOnlyBatchIteratorAggregate;
|
||||
use App\Entity;
|
||||
use App\Http\Response;
|
||||
use App\Http\RouterInterface;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Paginator;
|
||||
use App\Utilities;
|
||||
use Azura\DoctrineBatchUtils\ReadOnlyBatchIteratorAggregate;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Doctrine;
|
||||
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use IteratorAggregate;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
* @implements \IteratorAggregate<TKey, TValue>
|
||||
*/
|
||||
abstract class AbstractBatchIteratorAggregate implements IteratorAggregate
|
||||
{
|
||||
protected iterable $resultSet;
|
||||
|
||||
protected EntityManagerInterface $entityManager;
|
||||
|
||||
protected int $batchSize;
|
||||
|
||||
protected bool $clearMemoryWithFlush = true;
|
||||
|
||||
public static function fromQuery(
|
||||
AbstractQuery $query,
|
||||
int $batchSize
|
||||
): static {
|
||||
return new static($query->toIterable(), $query->getEntityManager(), $batchSize);
|
||||
}
|
||||
|
||||
public static function fromArrayResult(
|
||||
array $results,
|
||||
EntityManagerInterface $entityManager,
|
||||
int $batchSize
|
||||
): static {
|
||||
return new static($results, $entityManager, $batchSize);
|
||||
}
|
||||
|
||||
public static function fromTraversableResult(
|
||||
Traversable $results,
|
||||
EntityManagerInterface $entityManager,
|
||||
int $batchSize
|
||||
): static {
|
||||
return new static($results, $entityManager, $batchSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* BatchIteratorAggregate constructor (private by design: use a named constructor instead).
|
||||
*
|
||||
* @param iterable<TKey, TValue> $resultSet
|
||||
*/
|
||||
final protected function __construct(
|
||||
iterable $resultSet,
|
||||
EntityManagerInterface $entityManager,
|
||||
int $batchSize
|
||||
) {
|
||||
$this->resultSet = $resultSet;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->batchSize = $batchSize;
|
||||
}
|
||||
|
||||
public function setBatchSize(int $batchSize): void
|
||||
{
|
||||
$this->batchSize = $batchSize;
|
||||
}
|
||||
|
||||
public function setClearMemoryWithFlush(bool $clearMemoryWithFlush): void
|
||||
{
|
||||
$this->clearMemoryWithFlush = $clearMemoryWithFlush;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Traversable<TKey, TValue>
|
||||
*/
|
||||
abstract public function getIterator(): iterable;
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Doctrine;
|
||||
|
||||
final class ReadOnlyBatchIteratorAggregate extends AbstractBatchIteratorAggregate
|
||||
{
|
||||
/** @inheritDoc */
|
||||
public function getIterator(): iterable
|
||||
{
|
||||
$iteration = 0;
|
||||
foreach ($this->resultSet as $key => $value) {
|
||||
++$iteration;
|
||||
yield $key => $value;
|
||||
|
||||
$this->entityManager->detach($value);
|
||||
$this->clearBatch($iteration);
|
||||
}
|
||||
|
||||
$this->clearMemory();
|
||||
}
|
||||
|
||||
private function clearBatch(int $iteration): void
|
||||
{
|
||||
if ($iteration % $this->batchSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->clearMemory();
|
||||
}
|
||||
|
||||
private function clearMemory(): void
|
||||
{
|
||||
if ($this->clearMemoryWithFlush) {
|
||||
gc_collect_cycles();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Doctrine;
|
||||
|
||||
use Closure;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
use function get_class;
|
||||
use function is_array;
|
||||
use function is_object;
|
||||
use function key;
|
||||
|
||||
final class ReadWriteBatchIteratorAggregate extends AbstractBatchIteratorAggregate
|
||||
{
|
||||
private ?Closure $customFetchFunction = null;
|
||||
|
||||
public function setCustomFetchFunction(?callable $customFetchFunction = null): void
|
||||
{
|
||||
$this->customFetchFunction = (null === $customFetchFunction)
|
||||
? null
|
||||
: Closure::fromCallable($customFetchFunction);
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function getIterator(): iterable
|
||||
{
|
||||
$iteration = 0;
|
||||
$resultSet = $this->resultSet;
|
||||
|
||||
$this->entityManager->beginTransaction();
|
||||
|
||||
try {
|
||||
foreach ($resultSet as $key => $value) {
|
||||
++$iteration;
|
||||
|
||||
yield $key => $this->getObjectFromValue($value);
|
||||
|
||||
$this->flushAndClearBatch($iteration);
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
$this->entityManager->rollback();
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$this->flushAndClearEntityManager();
|
||||
$this->entityManager->commit();
|
||||
}
|
||||
|
||||
private function getObjectFromValue(mixed $value): mixed
|
||||
{
|
||||
if ($this->customFetchFunction instanceof Closure) {
|
||||
return ($this->customFetchFunction)($value, $this->entityManager);
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$firstKey = key($value);
|
||||
if (
|
||||
$firstKey !== null && is_object(
|
||||
$value[$firstKey]
|
||||
) && $value === [$firstKey => $value[$firstKey]]
|
||||
) {
|
||||
return $this->reFetchObject($value[$firstKey]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_object($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return $this->reFetchObject($value);
|
||||
}
|
||||
|
||||
private function reFetchObject(object $object): object
|
||||
{
|
||||
$metadata = $this->entityManager->getClassMetadata(get_class($object));
|
||||
|
||||
/** @psalm-var class-string $classname */
|
||||
$classname = $metadata->getName();
|
||||
$freshValue = $this->entityManager->find($classname, $metadata->getIdentifierValues($object));
|
||||
|
||||
if (!$freshValue) {
|
||||
throw new RuntimeException(
|
||||
sprintf(
|
||||
'Requested batch item %s#%s (of type %s) with identifier "%s" could not be found',
|
||||
get_class($object),
|
||||
spl_object_hash($object),
|
||||
$metadata->getName(),
|
||||
json_encode($metadata->getIdentifierValues($object), JSON_THROW_ON_ERROR)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $freshValue;
|
||||
}
|
||||
|
||||
private function flushAndClearBatch(int $iteration): void
|
||||
{
|
||||
if ($iteration % $this->batchSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->flushAndClearEntityManager();
|
||||
}
|
||||
|
||||
private function flushAndClearEntityManager(): void
|
||||
{
|
||||
$this->entityManager->flush();
|
||||
$this->entityManager->clear();
|
||||
|
||||
if ($this->clearMemoryWithFlush) {
|
||||
gc_collect_cycles();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,10 +5,11 @@ declare(strict_types=1);
|
|||
namespace App\Doctrine;
|
||||
|
||||
use App\Environment;
|
||||
use App\Normalizer\DoctrineEntityNormalizer;
|
||||
use Azura\Normalizer\DoctrineEntityNormalizer;
|
||||
use Closure;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
|
||||
/**
|
||||
|
@ -122,7 +123,7 @@ class Repository
|
|||
get_class($entity),
|
||||
null,
|
||||
[
|
||||
DoctrineEntityNormalizer::OBJECT_TO_POPULATE => $entity,
|
||||
AbstractNormalizer::OBJECT_TO_POPULATE => $entity,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -7,10 +7,10 @@ namespace App\Entity;
|
|||
use App\Entity\Interfaces\EntityGroupsInterface;
|
||||
use App\Entity\Interfaces\IdentifiableEntityInterface;
|
||||
use App\Environment;
|
||||
use App\Normalizer\Attributes\DeepNormalize;
|
||||
use App\Radio\Adapters;
|
||||
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;
|
||||
|
|
|
@ -8,10 +8,10 @@ use App\Entity\Interfaces\IdentifiableEntityInterface;
|
|||
use App\Entity\Interfaces\PathAwareInterface;
|
||||
use App\Entity\Interfaces\ProcessableMediaInterface;
|
||||
use App\Entity\Interfaces\SongInterface;
|
||||
use App\Normalizer\Attributes\DeepNormalize;
|
||||
use App\Utilities\Time;
|
||||
use Azura\MetadataManager\Metadata;
|
||||
use Azura\MetadataManager\MetadataInterface;
|
||||
use Azura\Normalizer\Attributes\DeepNormalize;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Entity;
|
||||
|
||||
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;
|
||||
|
|
|
@ -4,8 +4,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Normalizer\Attributes\DeepNormalize;
|
||||
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,9 +7,9 @@ namespace App\Entity;
|
|||
use App\Auth;
|
||||
use App\Entity\Interfaces\EntityGroupsInterface;
|
||||
use App\Entity\Interfaces\IdentifiableEntityInterface;
|
||||
use App\Normalizer\Attributes\DeepNormalize;
|
||||
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,21 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exception;
|
||||
|
||||
use App\Exception;
|
||||
use Psr\Log\LogLevel;
|
||||
use Throwable;
|
||||
|
||||
class NoGetterAvailableException extends Exception
|
||||
{
|
||||
public function __construct(
|
||||
string $message = 'No getter available for this variable.',
|
||||
int $code = 0,
|
||||
Throwable $previous = null,
|
||||
string $loggerLevel = LogLevel::INFO
|
||||
) {
|
||||
parent::__construct($message, $code, $previous, $loggerLevel);
|
||||
}
|
||||
}
|
|
@ -4,9 +4,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Media;
|
||||
|
||||
use App\Doctrine\ReadWriteBatchIteratorAggregate;
|
||||
use App\Entity;
|
||||
use App\Utilities\File;
|
||||
use Azura\DoctrineBatchUtils\ReadWriteBatchIteratorAggregate;
|
||||
use Azura\Files\ExtendedFilesystemInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Throwable;
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Normalizer\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)]
|
||||
class DeepNormalize
|
||||
{
|
||||
private bool $deepNormalize;
|
||||
|
||||
public function __construct(bool $value)
|
||||
{
|
||||
$this->deepNormalize = $value;
|
||||
}
|
||||
|
||||
public function getDeepNormalize(): bool
|
||||
{
|
||||
return $this->deepNormalize;
|
||||
}
|
||||
}
|
|
@ -1,376 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Normalizer;
|
||||
|
||||
use App\Exception\NoGetterAvailableException;
|
||||
use App\Normalizer\Attributes\DeepNormalize;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Inflector\Inflector;
|
||||
use Doctrine\Inflector\InflectorFactory;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\Persistence\Proxy;
|
||||
use ProxyManager\Proxy\GhostObjectInterface;
|
||||
use ReflectionClass;
|
||||
use ReflectionProperty;
|
||||
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
|
||||
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||
|
||||
use function is_array;
|
||||
|
||||
class DoctrineEntityNormalizer extends AbstractNormalizer implements NormalizerAwareInterface
|
||||
{
|
||||
use NormalizerAwareTrait;
|
||||
|
||||
public const NORMALIZE_TO_IDENTIFIERS = 'form_mode';
|
||||
|
||||
public const CLASS_METADATA = 'class_metadata';
|
||||
public const ASSOCIATION_MAPPINGS = 'association_mappings';
|
||||
|
||||
protected Inflector $inflector;
|
||||
|
||||
public function __construct(
|
||||
protected EntityManagerInterface $em,
|
||||
ClassMetadataFactoryInterface $classMetadataFactory = null,
|
||||
NameConverterInterface $nameConverter = null,
|
||||
array $defaultContext = []
|
||||
) {
|
||||
$defaultContext[self::ALLOW_EXTRA_ATTRIBUTES] = false;
|
||||
parent::__construct($classMetadataFactory, $nameConverter, $defaultContext);
|
||||
|
||||
$this->inflector = InflectorFactory::create()->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replicates the "toArray" functionality previously present in Doctrine 1.
|
||||
*/
|
||||
public function normalize(mixed $object, ?string $format = null, array $context = []): mixed
|
||||
{
|
||||
if ($this->isCircularReference($object, $context)) {
|
||||
return $this->handleCircularReference($object, $format, $context);
|
||||
}
|
||||
|
||||
$context[self::CLASS_METADATA] = $this->em->getClassMetadata((string)get_class($object));
|
||||
|
||||
$props = $this->getAllowedAttributes($object, $context);
|
||||
|
||||
$return_arr = [];
|
||||
if ($props) {
|
||||
foreach ($props as $property) {
|
||||
$attribute = $property->getName();
|
||||
|
||||
try {
|
||||
$value = $this->getAttributeValue($object, $attribute, $format, $context);
|
||||
|
||||
/** @var callable|null $callback */
|
||||
$callback = $context[self::CALLBACKS][$attribute]
|
||||
?? $this->defaultContext[self::CALLBACKS][$attribute]
|
||||
?? null;
|
||||
|
||||
if ($callback) {
|
||||
$value = $callback($value, $object, $attribute, $format, $context);
|
||||
}
|
||||
|
||||
$return_arr[$attribute] = $value;
|
||||
} catch (NoGetterAvailableException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $return_arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($data, string $type, string $format = null, array $context = []): object
|
||||
{
|
||||
/** @var T $object */
|
||||
$object = $this->instantiateObject($data, $type, $context, new ReflectionClass($type), false, $format);
|
||||
|
||||
$type = get_class($object);
|
||||
|
||||
$context[self::CLASS_METADATA] = $this->em->getMetadataFactory()->getMetadataFor($type);
|
||||
$context[self::ASSOCIATION_MAPPINGS] = [];
|
||||
|
||||
if ($context[self::CLASS_METADATA]->associationMappings) {
|
||||
foreach ($context[self::CLASS_METADATA]->associationMappings as $mapping_name => $mapping_info) {
|
||||
$entity = $mapping_info['targetEntity'];
|
||||
|
||||
if (isset($mapping_info['joinTable'])) {
|
||||
$context[self::ASSOCIATION_MAPPINGS][$mapping_info['fieldName']] = [
|
||||
'type' => 'many',
|
||||
'entity' => $entity,
|
||||
'is_owning_side' => ($mapping_info['isOwningSide'] == 1),
|
||||
];
|
||||
} elseif (isset($mapping_info['joinColumns'])) {
|
||||
foreach ($mapping_info['joinColumns'] as $col) {
|
||||
$col_name = $col['name'];
|
||||
$col_name = $context[self::CLASS_METADATA]->fieldNames[$col_name] ?? $col_name;
|
||||
|
||||
$context[self::ASSOCIATION_MAPPINGS][$mapping_name] = [
|
||||
'name' => $col_name,
|
||||
'type' => 'one',
|
||||
'entity' => $entity,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ((array)$data as $attribute => $value) {
|
||||
/** @var callable|null $callback */
|
||||
$callback = $context[self::CALLBACKS][$attribute]
|
||||
?? $this->defaultContext[self::CALLBACKS][$attribute]
|
||||
?? null;
|
||||
|
||||
if ($callback) {
|
||||
$value = $callback($value, $object, $attribute, $format, $context);
|
||||
}
|
||||
|
||||
$this->setAttributeValue($object, $attribute, $value, $format, $context);
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function supportsNormalization($data, string $format = null): bool
|
||||
{
|
||||
return $this->isEntity($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function supportsDenormalization($data, $type, string $format = null): bool
|
||||
{
|
||||
return $this->isEntity($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object|class-string $classOrObject
|
||||
* @param array $context
|
||||
* @param bool $attributesAsString
|
||||
*
|
||||
*/
|
||||
protected function getAllowedAttributes(
|
||||
$classOrObject,
|
||||
array $context,
|
||||
bool $attributesAsString = false
|
||||
): array|false {
|
||||
$meta = $this->classMetadataFactory?->getMetadataFor($classOrObject)?->getAttributesMetadata();
|
||||
if (null === $meta) {
|
||||
throw new \RuntimeException('Class metadata factory not specified.');
|
||||
}
|
||||
|
||||
$props_raw = (new ReflectionClass($classOrObject))->getProperties(
|
||||
ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED
|
||||
);
|
||||
$props = [];
|
||||
foreach ($props_raw as $prop_raw) {
|
||||
$props[$prop_raw->getName()] = $prop_raw;
|
||||
}
|
||||
|
||||
$props = array_intersect_key($meta, $props);
|
||||
|
||||
$tmpGroups = $context[self::GROUPS] ?? $this->defaultContext[self::GROUPS] ?? null;
|
||||
$groups = (is_array($tmpGroups) || is_scalar($tmpGroups)) ? (array)$tmpGroups : false;
|
||||
|
||||
$allowedAttributes = [];
|
||||
foreach ($props as $attributeMetadata) {
|
||||
$name = $attributeMetadata->getName();
|
||||
|
||||
if (
|
||||
(false === $groups || array_intersect($attributeMetadata->getGroups(), $groups)) &&
|
||||
$this->isAllowedAttribute($classOrObject, $name, null, $context)
|
||||
) {
|
||||
$allowedAttributes[] = $attributesAsString ? $name : $attributeMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
return $allowedAttributes;
|
||||
}
|
||||
|
||||
protected function getAttributeValue(
|
||||
object $object,
|
||||
string $prop_name,
|
||||
string $format = null,
|
||||
array $context = []
|
||||
): mixed {
|
||||
$form_mode = $context[self::NORMALIZE_TO_IDENTIFIERS] ?? false;
|
||||
|
||||
if (isset($context[self::CLASS_METADATA]->associationMappings[$prop_name])) {
|
||||
$deepNormalizeAttrs = (new ReflectionClass($object))->getProperty($prop_name)->getAttributes(
|
||||
DeepNormalize::class
|
||||
);
|
||||
if (!empty($deepNormalizeAttrs)) {
|
||||
/** @var DeepNormalize $deepNormalize */
|
||||
$deepNormalize = current($deepNormalizeAttrs)->newInstance();
|
||||
|
||||
$deep = $deepNormalize->getDeepNormalize();
|
||||
} else {
|
||||
$deep = false;
|
||||
}
|
||||
|
||||
if (!$deep) {
|
||||
throw new NoGetterAvailableException(
|
||||
sprintf(
|
||||
'Deep normalization disabled for property %s.',
|
||||
$prop_name
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$prop_val = $this->getProperty($object, $prop_name);
|
||||
|
||||
if ($prop_val instanceof Collection) {
|
||||
$return_val = [];
|
||||
if (count($prop_val) > 0) {
|
||||
foreach ($prop_val as $val_obj) {
|
||||
if ($form_mode) {
|
||||
$id_field = $this->em->getClassMetadata((string)get_class($val_obj))->identifier;
|
||||
|
||||
if ($id_field && count($id_field) === 1) {
|
||||
$return_val[] = $this->getProperty($val_obj, $id_field[0]);
|
||||
}
|
||||
} else {
|
||||
$return_val[] = $this->normalizer->normalize($val_obj, $format, $context);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $return_val;
|
||||
}
|
||||
|
||||
return $this->normalizer->normalize($prop_val, $format, $context);
|
||||
}
|
||||
|
||||
$value = $this->getProperty($object, $prop_name);
|
||||
if ($value instanceof Collection) {
|
||||
$value = $value->toArray();
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $entity
|
||||
* @param string $key
|
||||
*
|
||||
*/
|
||||
protected function getProperty(object $entity, string $key): mixed
|
||||
{
|
||||
// Default to "getStatus", "getConfig", etc...
|
||||
$getter_method = $this->getMethodName($key, 'get');
|
||||
if (method_exists($entity, $getter_method)) {
|
||||
return $entity->{$getter_method}();
|
||||
}
|
||||
|
||||
// but also allow "isEnabled" instead of "getIsEnabled"
|
||||
$raw_method = $this->getMethodName($key);
|
||||
if (method_exists($entity, $raw_method)) {
|
||||
return $entity->{$raw_method}();
|
||||
}
|
||||
|
||||
throw new NoGetterAvailableException(sprintf('No getter is available for property %s.', $key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts "getvar_name_blah" to "getVarNameBlah".
|
||||
*
|
||||
* @param string $var
|
||||
* @param string $prefix
|
||||
*/
|
||||
protected function getMethodName(string $var, string $prefix = ''): string
|
||||
{
|
||||
return $this->inflector->camelize(($prefix ? $prefix . '_' : '') . $var);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $object
|
||||
* @param string $field
|
||||
* @param mixed $value
|
||||
* @param string|null $format
|
||||
* @param array $context
|
||||
*/
|
||||
protected function setAttributeValue(
|
||||
object $object,
|
||||
string $field,
|
||||
mixed $value,
|
||||
?string $format = null,
|
||||
array $context = []
|
||||
): void {
|
||||
if (isset($context[self::ASSOCIATION_MAPPINGS][$field])) {
|
||||
// Handle a mapping to another entity.
|
||||
$mapping = $context[self::ASSOCIATION_MAPPINGS][$field];
|
||||
|
||||
if ('one' === $mapping['type']) {
|
||||
if (empty($value)) {
|
||||
$this->setProperty($object, $field, null);
|
||||
} else {
|
||||
/** @var class-string $entity */
|
||||
$entity = $mapping['entity'];
|
||||
if (($field_item = $this->em->find($entity, $value)) instanceof $entity) {
|
||||
$this->setProperty($object, $field, $field_item);
|
||||
}
|
||||
}
|
||||
} elseif ($mapping['is_owning_side']) {
|
||||
$collection = $this->getProperty($object, $field);
|
||||
|
||||
if ($collection instanceof Collection) {
|
||||
$collection->clear();
|
||||
|
||||
if ($value) {
|
||||
foreach ((array)$value as $field_id) {
|
||||
/** @var class-string $entity */
|
||||
$entity = $mapping['entity'];
|
||||
|
||||
$field_item = $this->em->find($entity, $field_id);
|
||||
if ($field_item instanceof $entity) {
|
||||
$collection->add($field_item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->setProperty($object, $field, $value);
|
||||
}
|
||||
}
|
||||
|
||||
protected function setProperty(object $entity, string $key, mixed $value): void
|
||||
{
|
||||
$method_name = $this->getMethodName($key, 'set');
|
||||
if (!method_exists($entity, $method_name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entity->$method_name($value);
|
||||
}
|
||||
|
||||
protected function isEntity(mixed $class): bool
|
||||
{
|
||||
if (is_object($class)) {
|
||||
$class = ($class instanceof Proxy || $class instanceof GhostObjectInterface)
|
||||
? get_parent_class($class)
|
||||
: get_class($class);
|
||||
}
|
||||
|
||||
if (!is_string($class) || !class_exists($class)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !$this->em->getMetadataFactory()->isTransient($class);
|
||||
}
|
||||
}
|
|
@ -4,9 +4,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Sync\Task;
|
||||
|
||||
use App\Doctrine\ReadWriteBatchIteratorAggregate;
|
||||
use App\Doctrine\ReloadableEntityManagerInterface;
|
||||
use App\Entity;
|
||||
use Azura\DoctrineBatchUtils\ReadWriteBatchIteratorAggregate;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
abstract class AbstractTask
|
||||
|
|
|
@ -4,11 +4,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Sync\Task;
|
||||
|
||||
use App\Doctrine\ReadOnlyBatchIteratorAggregate;
|
||||
use App\Doctrine\ReloadableEntityManagerInterface;
|
||||
use App\Entity;
|
||||
use App\Exception;
|
||||
use App\Radio\Adapters;
|
||||
use Azura\DoctrineBatchUtils\ReadOnlyBatchIteratorAggregate;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
|
|
Loading…
Reference in New Issue