welcome back to dyb-tech
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\ArgumentResolver;
|
||||
|
||||
use Doctrine\DBAL\Types\ConversionException;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
|
||||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Yields the entity matching the criteria provided in the route.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
final class EntityValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ManagerRegistry $registry,
|
||||
private ?ExpressionLanguage $expressionLanguage = null,
|
||||
private MapEntity $defaults = new MapEntity(),
|
||||
) {
|
||||
}
|
||||
|
||||
public function resolve(Request $request, ArgumentMetadata $argument): array
|
||||
{
|
||||
if (\is_object($request->attributes->get($argument->getName()))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$options = $argument->getAttributes(MapEntity::class, ArgumentMetadata::IS_INSTANCEOF);
|
||||
$options = ($options[0] ?? $this->defaults)->withDefaults($this->defaults, $argument->getType());
|
||||
|
||||
if (!$options->class || $options->disabled) {
|
||||
return [];
|
||||
}
|
||||
if (!$manager = $this->getManager($options->objectManager, $options->class)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$message = '';
|
||||
if (null !== $options->expr) {
|
||||
if (null === $object = $this->findViaExpression($manager, $request, $options)) {
|
||||
$message = sprintf(' The expression "%s" returned null.', $options->expr);
|
||||
}
|
||||
// find by identifier?
|
||||
} elseif (false === $object = $this->find($manager, $request, $options, $argument->getName())) {
|
||||
// find by criteria
|
||||
if (!$criteria = $this->getCriteria($request, $options, $manager)) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
$object = $manager->getRepository($options->class)->findOneBy($criteria);
|
||||
} catch (NoResultException|ConversionException) {
|
||||
$object = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $object && !$argument->isNullable()) {
|
||||
throw new NotFoundHttpException(sprintf('"%s" object not found by "%s".', $options->class, self::class).$message);
|
||||
}
|
||||
|
||||
return [$object];
|
||||
}
|
||||
|
||||
private function getManager(?string $name, string $class): ?ObjectManager
|
||||
{
|
||||
if (null === $name) {
|
||||
return $this->registry->getManagerForClass($class);
|
||||
}
|
||||
|
||||
try {
|
||||
$manager = $this->registry->getManager($name);
|
||||
} catch (\InvalidArgumentException) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $manager->getMetadataFactory()->isTransient($class) ? null : $manager;
|
||||
}
|
||||
|
||||
private function find(ObjectManager $manager, Request $request, MapEntity $options, string $name): false|object|null
|
||||
{
|
||||
if ($options->mapping || $options->exclude) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$id = $this->getIdentifier($request, $options, $name);
|
||||
if (false === $id || null === $id) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
if ($options->evictCache && $manager instanceof EntityManagerInterface) {
|
||||
$cacheProvider = $manager->getCache();
|
||||
if ($cacheProvider && $cacheProvider->containsEntity($options->class, $id)) {
|
||||
$cacheProvider->evictEntity($options->class, $id);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return $manager->getRepository($options->class)->find($id);
|
||||
} catch (NoResultException|ConversionException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function getIdentifier(Request $request, MapEntity $options, string $name): mixed
|
||||
{
|
||||
if (\is_array($options->id)) {
|
||||
$id = [];
|
||||
foreach ($options->id as $field) {
|
||||
// Convert "%s_uuid" to "foobar_uuid"
|
||||
if (str_contains($field, '%s')) {
|
||||
$field = sprintf($field, $name);
|
||||
}
|
||||
|
||||
$id[$field] = $request->attributes->get($field);
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
if (null !== $options->id) {
|
||||
$name = $options->id;
|
||||
}
|
||||
|
||||
if ($request->attributes->has($name)) {
|
||||
return $request->attributes->get($name) ?? ($options->stripNull ? false : null);
|
||||
}
|
||||
|
||||
if (!$options->id && $request->attributes->has('id')) {
|
||||
return $request->attributes->get('id') ?? ($options->stripNull ? false : null);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getCriteria(Request $request, MapEntity $options, ObjectManager $manager): array
|
||||
{
|
||||
if (null === $mapping = $options->mapping) {
|
||||
$mapping = $request->attributes->keys();
|
||||
}
|
||||
|
||||
if ($mapping && \is_array($mapping) && array_is_list($mapping)) {
|
||||
$mapping = array_combine($mapping, $mapping);
|
||||
}
|
||||
|
||||
foreach ($options->exclude as $exclude) {
|
||||
unset($mapping[$exclude]);
|
||||
}
|
||||
|
||||
if (!$mapping) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// if a specific id has been defined in the options and there is no corresponding attribute
|
||||
// return false in order to avoid a fallback to the id which might be of another object
|
||||
if (\is_string($options->id) && null === $request->attributes->get($options->id)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$criteria = [];
|
||||
$metadata = $manager->getClassMetadata($options->class);
|
||||
|
||||
foreach ($mapping as $attribute => $field) {
|
||||
if (!$metadata->hasField($field) && (!$metadata->hasAssociation($field) || !$metadata->isSingleValuedAssociation($field))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$criteria[$field] = $request->attributes->get($attribute);
|
||||
}
|
||||
|
||||
if ($options->stripNull) {
|
||||
$criteria = array_filter($criteria, static fn ($value) => null !== $value);
|
||||
}
|
||||
|
||||
return $criteria;
|
||||
}
|
||||
|
||||
private function findViaExpression(ObjectManager $manager, Request $request, MapEntity $options): ?object
|
||||
{
|
||||
if (!$this->expressionLanguage) {
|
||||
throw new \LogicException(sprintf('You cannot use the "%s" if the ExpressionLanguage component is not available. Try running "composer require symfony/expression-language".', __CLASS__));
|
||||
}
|
||||
|
||||
$repository = $manager->getRepository($options->class);
|
||||
$variables = array_merge($request->attributes->all(), ['repository' => $repository]);
|
||||
|
||||
try {
|
||||
return $this->expressionLanguage->evaluate($options->expr, $variables);
|
||||
} catch (NoResultException|ConversionException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Attribute;
|
||||
|
||||
use Symfony\Bridge\Doctrine\ArgumentResolver\EntityValueResolver;
|
||||
use Symfony\Component\HttpKernel\Attribute\ValueResolver;
|
||||
|
||||
/**
|
||||
* Indicates that a controller argument should receive an Entity.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_PARAMETER)]
|
||||
class MapEntity extends ValueResolver
|
||||
{
|
||||
public function __construct(
|
||||
public ?string $class = null,
|
||||
public ?string $objectManager = null,
|
||||
public ?string $expr = null,
|
||||
public ?array $mapping = null,
|
||||
public ?array $exclude = null,
|
||||
public ?bool $stripNull = null,
|
||||
public array|string|null $id = null,
|
||||
public ?bool $evictCache = null,
|
||||
bool $disabled = false,
|
||||
string $resolver = EntityValueResolver::class,
|
||||
) {
|
||||
parent::__construct($resolver, $disabled);
|
||||
}
|
||||
|
||||
public function withDefaults(self $defaults, ?string $class): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->class ??= class_exists($class ?? '') ? $class : null;
|
||||
$clone->objectManager ??= $defaults->objectManager;
|
||||
$clone->expr ??= $defaults->expr;
|
||||
$clone->mapping ??= $defaults->mapping;
|
||||
$clone->exclude ??= $defaults->exclude ?? [];
|
||||
$clone->stripNull ??= $defaults->stripNull ?? false;
|
||||
$clone->id ??= $defaults->id;
|
||||
$clone->evictCache ??= $defaults->evictCache ?? false;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
6.3
|
||||
---
|
||||
|
||||
* Deprecate passing Doctrine subscribers to `ContainerAwareEventManager` class, use listeners instead
|
||||
* Add `AbstractSchemaListener`, `LockStoreSchemaListener` and `PdoSessionHandlerSchemaListener`
|
||||
* Deprecate `DoctrineDbalCacheAdapterSchemaSubscriber` in favor of `DoctrineDbalCacheAdapterSchemaListener`
|
||||
* Deprecate `MessengerTransportDoctrineSchemaSubscriber` in favor of `MessengerTransportDoctrineSchemaListener`
|
||||
* Deprecate `RememberMeTokenProviderDoctrineSchemaSubscriber` in favor of `RememberMeTokenProviderDoctrineSchemaListener`
|
||||
* Add optional parameter `$isSameDatabase` to `DoctrineTokenProvider::configureSchema()`
|
||||
|
||||
6.2
|
||||
---
|
||||
|
||||
* Add `#[MapEntity]` with its corresponding `EntityValueResolver`
|
||||
* Add `NAME` constant to `UlidType` and `UuidType`
|
||||
|
||||
6.0
|
||||
---
|
||||
|
||||
* Remove `DoctrineTestHelper` and `TestRepositoryFactory`
|
||||
|
||||
5.4
|
||||
---
|
||||
|
||||
* Add `DoctrineOpenTransactionLoggerMiddleware` to log when a transaction has been left open
|
||||
* Deprecate `PdoCacheAdapterDoctrineSchemaSubscriber` and add `DoctrineDbalCacheAdapterSchemaSubscriber` instead
|
||||
* `UniqueEntity` constraint retrieves a maximum of two entities if the default repository method is used.
|
||||
* Add support for the newer bundle structure to `AbstractDoctrineExtension::loadMappingInformation()`
|
||||
* Add argument `$bundleDir` to `AbstractDoctrineExtension::getMappingDriverBundleConfigDefaults()`
|
||||
* Add argument `$bundleDir` to `AbstractDoctrineExtension::getMappingResourceConfigDirectory()`
|
||||
|
||||
5.3
|
||||
---
|
||||
|
||||
* Deprecate `UserLoaderInterface::loadUserByUsername()` in favor of `UserLoaderInterface::loadUserByIdentifier()
|
||||
* Deprecate `DoctrineTestHelper` and `TestRepositoryFactory`
|
||||
* [BC BREAK] Remove `UuidV*Generator` classes
|
||||
* Add `UuidGenerator`
|
||||
* Add support for the new security-core `TokenVerifierInterface` in `DoctrineTokenProvider`, fixing parallel requests handling in remember-me
|
||||
|
||||
5.2.0
|
||||
-----
|
||||
|
||||
* added support for symfony/uid as `UlidType` and `UuidType` as Doctrine types
|
||||
* added `UlidGenerator`, `UuidV1Generator`, `UuidV4Generator` and `UuidV6Generator`
|
||||
|
||||
5.0.0
|
||||
-----
|
||||
|
||||
* the `getMetadataDriverClass()` method is abstract and must be implemented by class extending `AbstractDoctrineExtension`
|
||||
* passing an `IdReader` to the `DoctrineChoiceLoader` when the query cannot be optimized with single id field, throws an exception; pass `null` instead
|
||||
* not explicitly passing an instance of `IdReader` to `DoctrineChoiceLoader` when it can optimize single id field, will not apply any optimization
|
||||
* `DoctrineExtractor` now requires an `EntityManagerInterface` on instantiation
|
||||
|
||||
4.4.0
|
||||
-----
|
||||
|
||||
* [BC BREAK] using null as `$classValidatorRegexp` value in `DoctrineLoader::__construct` will not enable auto-mapping for all classes anymore, use `'{.*}'` instead.
|
||||
* added `DoctrineClearEntityManagerWorkerSubscriber`
|
||||
* deprecated `RegistryInterface`, use `Doctrine\Persistence\ManagerRegistry`
|
||||
* added support for invokable event listeners
|
||||
* added `getMetadataDriverClass` method to deprecate class parameters in service configuration files
|
||||
|
||||
4.3.0
|
||||
-----
|
||||
|
||||
* changed guessing of DECIMAL to set the `input` option of `NumberType` to string
|
||||
* deprecated not passing an `IdReader` to the `DoctrineChoiceLoader` when query can be optimized with a single id field
|
||||
* deprecated passing an `IdReader` to the `DoctrineChoiceLoader` when entities have a composite id
|
||||
* added two Messenger middleware: `DoctrinePingConnectionMiddleware` and `DoctrineCloseConnectionMiddleware`
|
||||
|
||||
4.2.0
|
||||
-----
|
||||
|
||||
* deprecated injecting `ClassMetadataFactory` in `DoctrineExtractor`,
|
||||
an instance of `EntityManagerInterface` should be injected instead
|
||||
* added support for `simple_array` type
|
||||
* the `DoctrineTransactionMiddlewareFactory` class has been removed
|
||||
|
||||
4.1.0
|
||||
-----
|
||||
|
||||
* added support for datetime immutable types in form type guesser
|
||||
|
||||
4.0.0
|
||||
-----
|
||||
|
||||
* the first constructor argument of the `DoctrineChoiceLoader` class must be
|
||||
an `ObjectManager` implementation
|
||||
* removed the `MergeDoctrineCollectionListener::onBind()` method
|
||||
* trying to reset a non-lazy manager service using the `ManagerRegistry::resetService()`
|
||||
method throws an exception
|
||||
* removed the `DoctrineParserCache` class
|
||||
|
||||
3.4.0
|
||||
-----
|
||||
|
||||
* added support for doctrine/dbal v2.6 types
|
||||
* added cause of UniqueEntity constraint violation
|
||||
* deprecated `DbalSessionHandler` and `DbalSessionHandlerSchema` in favor of
|
||||
`Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler`
|
||||
|
||||
3.1.0
|
||||
-----
|
||||
|
||||
* added "{{ value }}" message placeholder to UniqueEntityValidator
|
||||
* deprecated `MergeDoctrineCollectionListener::onBind` in favor of
|
||||
`MergeDoctrineCollectionListener::onSubmit`
|
||||
* deprecated passing `ChoiceListFactoryInterface` as first argument of
|
||||
`DoctrineChoiceLoader`'s constructor
|
||||
|
||||
3.0.0
|
||||
-----
|
||||
|
||||
* removed `EntityChoiceList`
|
||||
* removed `$manager` (2nd) and `$class` (3th) arguments of `ORMQueryBuilderLoader`
|
||||
* removed passing a query builder closure to `ORMQueryBuilderLoader`
|
||||
* removed `loader` and `property` options of the `DoctrineType`
|
||||
|
||||
2.8.0
|
||||
-----
|
||||
|
||||
* deprecated using the entity provider with a Doctrine repository implementing UserProviderInterface
|
||||
* added UserLoaderInterface for loading users through Doctrine.
|
||||
|
||||
2.7.0
|
||||
-----
|
||||
|
||||
* added DoctrineChoiceLoader
|
||||
* deprecated EntityChoiceList
|
||||
* deprecated passing a query builder closure to ORMQueryBuilderLoader
|
||||
* deprecated $manager and $em arguments of ORMQueryBuilderLoader
|
||||
* added optional arguments $propertyAccessor and $choiceListFactory to DoctrineOrmExtension constructor
|
||||
* deprecated "loader" and "property" options of DoctrineType
|
||||
|
||||
2.4.0
|
||||
-----
|
||||
|
||||
* deprecated DoctrineOrmTestCase class
|
||||
|
||||
2.2.0
|
||||
-----
|
||||
|
||||
* added an optional PropertyAccessorInterface parameter to DoctrineType,
|
||||
EntityType and EntityChoiceList
|
||||
|
||||
2.1.0
|
||||
-----
|
||||
|
||||
* added a default implementation of the ManagerRegistry
|
||||
* added a session storage for Doctrine DBAL
|
||||
* DoctrineOrmTypeGuesser now guesses "collection" for array Doctrine type
|
||||
* DoctrineType now caches its choice lists in order to improve performance
|
||||
* DoctrineType now uses ManagerRegistry::getManagerForClass() if the option "em" is not set
|
||||
* UniqueEntity validation constraint now accepts a "repositoryMethod" option that will be used to check for uniqueness instead of the default "findBy"
|
||||
* [BC BREAK] the DbalLogger::log() visibility has been changed from public to
|
||||
protected
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\CacheWarmer;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
|
||||
|
||||
/**
|
||||
* The proxy generator cache warmer generates all entity proxies.
|
||||
*
|
||||
* In the process of generating proxies the cache for all the metadata is primed also,
|
||||
* since this information is necessary to build the proxies in the first place.
|
||||
*
|
||||
* @author Benjamin Eberlei <kontakt@beberlei.de>
|
||||
*/
|
||||
class ProxyCacheWarmer implements CacheWarmerInterface
|
||||
{
|
||||
private ManagerRegistry $registry;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* This cache warmer is not optional, without proxies fatal error occurs!
|
||||
*/
|
||||
public function isOptional(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[] A list of files to preload on PHP 7.4+
|
||||
*/
|
||||
public function warmUp(string $cacheDir): array
|
||||
{
|
||||
$files = [];
|
||||
foreach ($this->registry->getManagers() as $em) {
|
||||
// we need the directory no matter the proxy cache generation strategy
|
||||
if (!is_dir($proxyCacheDir = $em->getConfiguration()->getProxyDir())) {
|
||||
if (false === @mkdir($proxyCacheDir, 0777, true) && !is_dir($proxyCacheDir)) {
|
||||
throw new \RuntimeException(sprintf('Unable to create the Doctrine Proxy directory "%s".', $proxyCacheDir));
|
||||
}
|
||||
} elseif (!is_writable($proxyCacheDir)) {
|
||||
throw new \RuntimeException(sprintf('The Doctrine Proxy directory "%s" is not writeable for the current system user.', $proxyCacheDir));
|
||||
}
|
||||
|
||||
// if proxies are autogenerated we don't need to generate them in the cache warmer
|
||||
if ($em->getConfiguration()->getAutoGenerateProxyClasses()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$classes = $em->getMetadataFactory()->getAllMetadata();
|
||||
|
||||
$em->getProxyFactory()->generateProxyClasses($classes);
|
||||
|
||||
foreach (scandir($proxyCacheDir) as $file) {
|
||||
if (!is_dir($file = $proxyCacheDir.'/'.$file)) {
|
||||
$files[] = $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine;
|
||||
|
||||
use Doctrine\Common\EventArgs;
|
||||
use Doctrine\Common\EventManager;
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Allows lazy loading of listener and subscriber services.
|
||||
*
|
||||
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||
*/
|
||||
class ContainerAwareEventManager extends EventManager
|
||||
{
|
||||
/**
|
||||
* Map of registered listeners.
|
||||
*
|
||||
* <event> => <listeners>
|
||||
*/
|
||||
private array $listeners = [];
|
||||
private array $initialized = [];
|
||||
private bool $initializedSubscribers = false;
|
||||
private array $initializedHashMapping = [];
|
||||
private array $methods = [];
|
||||
private ContainerInterface $container;
|
||||
|
||||
/**
|
||||
* @param list<array{string[], string|object}> $listeners List of [events, listener] tuples
|
||||
*/
|
||||
public function __construct(ContainerInterface $container, array $listeners = [])
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->listeners = $listeners;
|
||||
}
|
||||
|
||||
public function dispatchEvent($eventName, ?EventArgs $eventArgs = null): void
|
||||
{
|
||||
if (!$this->initializedSubscribers) {
|
||||
$this->initializeSubscribers();
|
||||
}
|
||||
if (!isset($this->listeners[$eventName])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$eventArgs ??= EventArgs::getEmptyInstance();
|
||||
|
||||
if (!isset($this->initialized[$eventName])) {
|
||||
$this->initializeListeners($eventName);
|
||||
}
|
||||
|
||||
foreach ($this->listeners[$eventName] as $hash => $listener) {
|
||||
$listener->{$this->methods[$eventName][$hash]}($eventArgs);
|
||||
}
|
||||
}
|
||||
|
||||
public function getListeners($event = null): array
|
||||
{
|
||||
if (null === $event) {
|
||||
trigger_deprecation('symfony/doctrine-bridge', '6.2', 'Calling "%s()" without an event name is deprecated. Call "getAllListeners()" instead.', __METHOD__);
|
||||
|
||||
return $this->getAllListeners();
|
||||
}
|
||||
if (!$this->initializedSubscribers) {
|
||||
$this->initializeSubscribers();
|
||||
}
|
||||
if (!isset($this->initialized[$event])) {
|
||||
$this->initializeListeners($event);
|
||||
}
|
||||
|
||||
return $this->listeners[$event];
|
||||
}
|
||||
|
||||
public function getAllListeners(): array
|
||||
{
|
||||
if (!$this->initializedSubscribers) {
|
||||
$this->initializeSubscribers();
|
||||
}
|
||||
|
||||
foreach ($this->listeners as $event => $listeners) {
|
||||
if (!isset($this->initialized[$event])) {
|
||||
$this->initializeListeners($event);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->listeners;
|
||||
}
|
||||
|
||||
public function hasListeners($event): bool
|
||||
{
|
||||
if (!$this->initializedSubscribers) {
|
||||
$this->initializeSubscribers();
|
||||
}
|
||||
|
||||
return isset($this->listeners[$event]) && $this->listeners[$event];
|
||||
}
|
||||
|
||||
public function addEventListener($events, $listener): void
|
||||
{
|
||||
if (!$this->initializedSubscribers) {
|
||||
$this->initializeSubscribers();
|
||||
}
|
||||
|
||||
$hash = $this->getHash($listener);
|
||||
|
||||
foreach ((array) $events as $event) {
|
||||
// Overrides listener if a previous one was associated already
|
||||
// Prevents duplicate listeners on same event (same instance only)
|
||||
$this->listeners[$event][$hash] = $listener;
|
||||
|
||||
if (\is_string($listener)) {
|
||||
unset($this->initialized[$event]);
|
||||
unset($this->initializedHashMapping[$event][$hash]);
|
||||
} else {
|
||||
$this->methods[$event][$hash] = $this->getMethod($listener, $event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function removeEventListener($events, $listener): void
|
||||
{
|
||||
if (!$this->initializedSubscribers) {
|
||||
$this->initializeSubscribers();
|
||||
}
|
||||
|
||||
$hash = $this->getHash($listener);
|
||||
|
||||
foreach ((array) $events as $event) {
|
||||
if (isset($this->initializedHashMapping[$event][$hash])) {
|
||||
$hash = $this->initializedHashMapping[$event][$hash];
|
||||
unset($this->initializedHashMapping[$event][$hash]);
|
||||
}
|
||||
|
||||
// Check if we actually have this listener associated
|
||||
if (isset($this->listeners[$event][$hash])) {
|
||||
unset($this->listeners[$event][$hash]);
|
||||
}
|
||||
|
||||
if (isset($this->methods[$event][$hash])) {
|
||||
unset($this->methods[$event][$hash]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function addEventSubscriber(EventSubscriber $subscriber): void
|
||||
{
|
||||
if (!$this->initializedSubscribers) {
|
||||
$this->initializeSubscribers();
|
||||
}
|
||||
|
||||
parent::addEventSubscriber($subscriber);
|
||||
}
|
||||
|
||||
public function removeEventSubscriber(EventSubscriber $subscriber): void
|
||||
{
|
||||
if (!$this->initializedSubscribers) {
|
||||
$this->initializeSubscribers();
|
||||
}
|
||||
|
||||
parent::removeEventSubscriber($subscriber);
|
||||
}
|
||||
|
||||
private function initializeListeners(string $eventName): void
|
||||
{
|
||||
$this->initialized[$eventName] = true;
|
||||
|
||||
// We'll refill the whole array in order to keep the same order
|
||||
$listeners = [];
|
||||
foreach ($this->listeners[$eventName] as $hash => $listener) {
|
||||
if (\is_string($listener)) {
|
||||
$listener = $this->container->get($listener);
|
||||
$newHash = $this->getHash($listener);
|
||||
|
||||
$this->initializedHashMapping[$eventName][$hash] = $newHash;
|
||||
|
||||
$listeners[$newHash] = $listener;
|
||||
|
||||
$this->methods[$eventName][$newHash] = $this->getMethod($listener, $eventName);
|
||||
} else {
|
||||
$listeners[$hash] = $listener;
|
||||
}
|
||||
}
|
||||
|
||||
$this->listeners[$eventName] = $listeners;
|
||||
}
|
||||
|
||||
private function initializeSubscribers(): void
|
||||
{
|
||||
$this->initializedSubscribers = true;
|
||||
$listeners = $this->listeners;
|
||||
$this->listeners = [];
|
||||
foreach ($listeners as $listener) {
|
||||
if (\is_array($listener)) {
|
||||
$this->addEventListener(...$listener);
|
||||
continue;
|
||||
}
|
||||
if (\is_string($listener)) {
|
||||
$listener = $this->container->get($listener);
|
||||
}
|
||||
// throw new \InvalidArgumentException(sprintf('Using Doctrine subscriber "%s" is not allowed. Register it as a listener instead, using e.g. the #[AsDoctrineListener] or #[AsDocumentListener] attribute.', \is_object($listener) ? $listener::class : $listener));
|
||||
trigger_deprecation('symfony/doctrine-bridge', '6.3', 'Registering "%s" as a Doctrine subscriber is deprecated. Register it as a listener instead, using e.g. the #[AsDoctrineListener] or #[AsDocumentListener] attribute.', \is_object($listener) ? get_debug_type($listener) : $listener);
|
||||
parent::addEventSubscriber($listener);
|
||||
}
|
||||
}
|
||||
|
||||
private function getHash(string|object $listener): string
|
||||
{
|
||||
if (\is_string($listener)) {
|
||||
return '_service_'.$listener;
|
||||
}
|
||||
|
||||
return spl_object_hash($listener);
|
||||
}
|
||||
|
||||
private function getMethod(object $listener, string $event): string
|
||||
{
|
||||
if (!method_exists($listener, $event) && method_exists($listener, '__invoke')) {
|
||||
return '__invoke';
|
||||
}
|
||||
|
||||
return $event;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\DataCollector;
|
||||
|
||||
use Doctrine\DBAL\Logging\DebugStack;
|
||||
use Doctrine\DBAL\Types\ConversionException;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Symfony\Bridge\Doctrine\Middleware\Debug\DebugDataHolder;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
|
||||
use Symfony\Component\VarDumper\Caster\Caster;
|
||||
use Symfony\Component\VarDumper\Cloner\Stub;
|
||||
|
||||
/**
|
||||
* DoctrineDataCollector.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class DoctrineDataCollector extends DataCollector
|
||||
{
|
||||
private array $connections;
|
||||
private array $managers;
|
||||
|
||||
/**
|
||||
* @var array<string, DebugStack>
|
||||
*/
|
||||
private array $loggers = [];
|
||||
|
||||
public function __construct(
|
||||
private ManagerRegistry $registry,
|
||||
private ?DebugDataHolder $debugDataHolder = null,
|
||||
) {
|
||||
$this->connections = $registry->getConnectionNames();
|
||||
$this->managers = $registry->getManagerNames();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the stack logger for a connection.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addLogger(string $name, DebugStack $logger)
|
||||
{
|
||||
$this->loggers[$name] = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function collect(Request $request, Response $response, ?\Throwable $exception = null)
|
||||
{
|
||||
$this->data = [
|
||||
'queries' => $this->collectQueries(),
|
||||
'connections' => $this->connections,
|
||||
'managers' => $this->managers,
|
||||
];
|
||||
}
|
||||
|
||||
private function collectQueries(): array
|
||||
{
|
||||
$queries = [];
|
||||
|
||||
if (null !== $this->debugDataHolder) {
|
||||
foreach ($this->debugDataHolder->getData() as $name => $data) {
|
||||
$queries[$name] = $this->sanitizeQueries($name, $data);
|
||||
}
|
||||
|
||||
return $queries;
|
||||
}
|
||||
|
||||
foreach ($this->loggers as $name => $logger) {
|
||||
$queries[$name] = $this->sanitizeQueries($name, $logger->queries);
|
||||
}
|
||||
|
||||
return $queries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
$this->data = [];
|
||||
|
||||
if (null !== $this->debugDataHolder) {
|
||||
$this->debugDataHolder->reset();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->loggers as $logger) {
|
||||
$logger->queries = [];
|
||||
$logger->currentQuery = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public function getManagers()
|
||||
{
|
||||
return $this->data['managers'];
|
||||
}
|
||||
|
||||
public function getConnections()
|
||||
{
|
||||
return $this->data['connections'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getQueryCount()
|
||||
{
|
||||
return array_sum(array_map('count', $this->data['queries']));
|
||||
}
|
||||
|
||||
public function getQueries()
|
||||
{
|
||||
return $this->data['queries'];
|
||||
}
|
||||
|
||||
public function getTime()
|
||||
{
|
||||
$time = 0;
|
||||
foreach ($this->data['queries'] as $queries) {
|
||||
foreach ($queries as $query) {
|
||||
$time += $query['executionMS'];
|
||||
}
|
||||
}
|
||||
|
||||
return $time;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'db';
|
||||
}
|
||||
|
||||
protected function getCasters(): array
|
||||
{
|
||||
return parent::getCasters() + [
|
||||
ObjectParameter::class => static function (ObjectParameter $o, array $a, Stub $s): array {
|
||||
$s->class = $o->getClass();
|
||||
$s->value = $o->getObject();
|
||||
|
||||
$r = new \ReflectionClass($o->getClass());
|
||||
if ($f = $r->getFileName()) {
|
||||
$s->attr['file'] = $f;
|
||||
$s->attr['line'] = $r->getStartLine();
|
||||
} else {
|
||||
unset($s->attr['file']);
|
||||
unset($s->attr['line']);
|
||||
}
|
||||
|
||||
if ($error = $o->getError()) {
|
||||
return [Caster::PREFIX_VIRTUAL.'⚠' => $error->getMessage()];
|
||||
}
|
||||
|
||||
if ($o->isStringable()) {
|
||||
return [Caster::PREFIX_VIRTUAL.'__toString()' => (string) $o->getObject()];
|
||||
}
|
||||
|
||||
return [Caster::PREFIX_VIRTUAL.'⚠' => sprintf('Object of class "%s" could not be converted to string.', $o->getClass())];
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private function sanitizeQueries(string $connectionName, array $queries): array
|
||||
{
|
||||
foreach ($queries as $i => $query) {
|
||||
$queries[$i] = $this->sanitizeQuery($connectionName, $query);
|
||||
}
|
||||
|
||||
return $queries;
|
||||
}
|
||||
|
||||
private function sanitizeQuery(string $connectionName, array $query): array
|
||||
{
|
||||
$query['explainable'] = true;
|
||||
$query['runnable'] = true;
|
||||
$query['params'] ??= [];
|
||||
if (!\is_array($query['params'])) {
|
||||
$query['params'] = [$query['params']];
|
||||
}
|
||||
if (!\is_array($query['types'])) {
|
||||
$query['types'] = [];
|
||||
}
|
||||
foreach ($query['params'] as $j => $param) {
|
||||
$e = null;
|
||||
if (isset($query['types'][$j])) {
|
||||
// Transform the param according to the type
|
||||
$type = $query['types'][$j];
|
||||
if (\is_string($type)) {
|
||||
$type = Type::getType($type);
|
||||
}
|
||||
if ($type instanceof Type) {
|
||||
$query['types'][$j] = $type->getBindingType();
|
||||
try {
|
||||
$param = $type->convertToDatabaseValue($param, $this->registry->getConnection($connectionName)->getDatabasePlatform());
|
||||
} catch (\TypeError $e) {
|
||||
} catch (ConversionException $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[$query['params'][$j], $explainable, $runnable] = $this->sanitizeParam($param, $e);
|
||||
if (!$explainable) {
|
||||
$query['explainable'] = false;
|
||||
}
|
||||
|
||||
if (!$runnable) {
|
||||
$query['runnable'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
$query['params'] = $this->cloneVar($query['params']);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a param.
|
||||
*
|
||||
* The return value is an array with the sanitized value and a boolean
|
||||
* indicating if the original value was kept (allowing to use the sanitized
|
||||
* value to explain the query).
|
||||
*/
|
||||
private function sanitizeParam(mixed $var, ?\Throwable $error): array
|
||||
{
|
||||
if (\is_object($var)) {
|
||||
return [$o = new ObjectParameter($var, $error), false, $o->isStringable() && !$error];
|
||||
}
|
||||
|
||||
if ($error) {
|
||||
return ['⚠ '.$error->getMessage(), false, false];
|
||||
}
|
||||
|
||||
if (\is_array($var)) {
|
||||
$a = [];
|
||||
$explainable = $runnable = true;
|
||||
foreach ($var as $k => $v) {
|
||||
[$value, $e, $r] = $this->sanitizeParam($v, null);
|
||||
$explainable = $explainable && $e;
|
||||
$runnable = $runnable && $r;
|
||||
$a[$k] = $value;
|
||||
}
|
||||
|
||||
return [$a, $explainable, $runnable];
|
||||
}
|
||||
|
||||
if (\is_resource($var)) {
|
||||
return [sprintf('/* Resource(%s) */', get_resource_type($var)), false, false];
|
||||
}
|
||||
|
||||
return [$var, true, true];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\DataCollector;
|
||||
|
||||
final class ObjectParameter
|
||||
{
|
||||
private object $object;
|
||||
private ?\Throwable $error;
|
||||
private bool $stringable;
|
||||
private string $class;
|
||||
|
||||
public function __construct(object $object, ?\Throwable $error)
|
||||
{
|
||||
$this->object = $object;
|
||||
$this->error = $error;
|
||||
$this->stringable = \is_callable([$object, '__toString']);
|
||||
$this->class = $object::class;
|
||||
}
|
||||
|
||||
public function getObject(): object
|
||||
{
|
||||
return $this->object;
|
||||
}
|
||||
|
||||
public function getError(): ?\Throwable
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
public function isStringable(): bool
|
||||
{
|
||||
return $this->stringable;
|
||||
}
|
||||
|
||||
public function getClass(): string
|
||||
{
|
||||
return $this->class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\DataFixtures;
|
||||
|
||||
use Doctrine\Common\DataFixtures\FixtureInterface;
|
||||
use Doctrine\Common\DataFixtures\Loader;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Doctrine data fixtures loader that injects the service container into
|
||||
* fixture objects that implement ContainerAwareInterface.
|
||||
*
|
||||
* Note: Use of this class requires the Doctrine data fixtures extension, which
|
||||
* is a suggested dependency for Symfony.
|
||||
*/
|
||||
class ContainerAwareLoader extends Loader
|
||||
{
|
||||
private ContainerInterface $container;
|
||||
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function addFixture(FixtureInterface $fixture)
|
||||
{
|
||||
if ($fixture instanceof ContainerAwareInterface) {
|
||||
$fixture->setContainer($this->container);
|
||||
}
|
||||
|
||||
parent::addFixture($fixture);
|
||||
}
|
||||
}
|
||||
+488
@@ -0,0 +1,488 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\Resource\GlobResource;
|
||||
use Symfony\Component\DependencyInjection\Alias;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
||||
|
||||
/**
|
||||
* This abstract classes groups common code that Doctrine Object Manager extensions (ORM, MongoDB, CouchDB) need.
|
||||
*
|
||||
* @author Benjamin Eberlei <kontakt@beberlei.de>
|
||||
*/
|
||||
abstract class AbstractDoctrineExtension extends Extension
|
||||
{
|
||||
/**
|
||||
* Used inside metadata driver method to simplify aggregation of data.
|
||||
*/
|
||||
protected $aliasMap = [];
|
||||
|
||||
/**
|
||||
* Used inside metadata driver method to simplify aggregation of data.
|
||||
*/
|
||||
protected $drivers = [];
|
||||
|
||||
/**
|
||||
* @param array $objectManager A configured object manager
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function loadMappingInformation(array $objectManager, ContainerBuilder $container)
|
||||
{
|
||||
if ($objectManager['auto_mapping']) {
|
||||
// automatically register bundle mappings
|
||||
foreach (array_keys($container->getParameter('kernel.bundles')) as $bundle) {
|
||||
if (!isset($objectManager['mappings'][$bundle])) {
|
||||
$objectManager['mappings'][$bundle] = [
|
||||
'mapping' => true,
|
||||
'is_bundle' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($objectManager['mappings'] as $mappingName => $mappingConfig) {
|
||||
if (null !== $mappingConfig && false === $mappingConfig['mapping']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mappingConfig = array_replace([
|
||||
'dir' => false,
|
||||
'type' => false,
|
||||
'prefix' => false,
|
||||
], (array) $mappingConfig);
|
||||
|
||||
$mappingConfig['dir'] = $container->getParameterBag()->resolveValue($mappingConfig['dir']);
|
||||
// a bundle configuration is detected by realizing that the specified dir is not absolute and existing
|
||||
if (!isset($mappingConfig['is_bundle'])) {
|
||||
$mappingConfig['is_bundle'] = !is_dir($mappingConfig['dir']);
|
||||
}
|
||||
|
||||
if ($mappingConfig['is_bundle']) {
|
||||
$bundle = null;
|
||||
$bundleMetadata = null;
|
||||
foreach ($container->getParameter('kernel.bundles') as $name => $class) {
|
||||
if ($mappingName === $name) {
|
||||
$bundle = new \ReflectionClass($class);
|
||||
$bundleMetadata = $container->getParameter('kernel.bundles_metadata')[$name];
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $bundle) {
|
||||
throw new \InvalidArgumentException(sprintf('Bundle "%s" does not exist or it is not enabled.', $mappingName));
|
||||
}
|
||||
|
||||
$mappingConfig = $this->getMappingDriverBundleConfigDefaults($mappingConfig, $bundle, $container, $bundleMetadata['path']);
|
||||
if (!$mappingConfig) {
|
||||
continue;
|
||||
}
|
||||
} elseif (!$mappingConfig['type']) {
|
||||
$mappingConfig['type'] = $this->detectMappingType($mappingConfig['dir'], $container);
|
||||
}
|
||||
|
||||
$this->assertValidMappingConfiguration($mappingConfig, $objectManager['name']);
|
||||
$this->setMappingDriverConfig($mappingConfig, $mappingName);
|
||||
$this->setMappingDriverAlias($mappingConfig, $mappingName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the alias for this mapping driver.
|
||||
*
|
||||
* Aliases can be used in the Query languages of all the Doctrine object managers to simplify writing tasks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function setMappingDriverAlias(array $mappingConfig, string $mappingName)
|
||||
{
|
||||
if (isset($mappingConfig['alias'])) {
|
||||
$this->aliasMap[$mappingConfig['alias']] = $mappingConfig['prefix'];
|
||||
} else {
|
||||
$this->aliasMap[$mappingName] = $mappingConfig['prefix'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the mapping driver configuration for later use with the object managers metadata driver chain.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function setMappingDriverConfig(array $mappingConfig, string $mappingName)
|
||||
{
|
||||
$mappingDirectory = $mappingConfig['dir'];
|
||||
if (!is_dir($mappingDirectory)) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid Doctrine mapping path given. Cannot load Doctrine mapping/bundle named "%s".', $mappingName));
|
||||
}
|
||||
|
||||
$this->drivers[$mappingConfig['type']][$mappingConfig['prefix']] = realpath($mappingDirectory) ?: $mappingDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is a bundle controlled mapping all the missing information can be autodetected by this method.
|
||||
*
|
||||
* Returns false when autodetection failed, an array of the completed information otherwise.
|
||||
*/
|
||||
protected function getMappingDriverBundleConfigDefaults(array $bundleConfig, \ReflectionClass $bundle, ContainerBuilder $container, ?string $bundleDir = null): array|false
|
||||
{
|
||||
$bundleClassDir = \dirname($bundle->getFileName());
|
||||
$bundleDir ??= $bundleClassDir;
|
||||
|
||||
if (!$bundleConfig['type']) {
|
||||
$bundleConfig['type'] = $this->detectMetadataDriver($bundleDir, $container);
|
||||
|
||||
if (!$bundleConfig['type'] && $bundleDir !== $bundleClassDir) {
|
||||
$bundleConfig['type'] = $this->detectMetadataDriver($bundleClassDir, $container);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$bundleConfig['type']) {
|
||||
// skip this bundle, no mapping information was found.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$bundleConfig['dir']) {
|
||||
if (\in_array($bundleConfig['type'], ['annotation', 'staticphp', 'attribute'])) {
|
||||
$bundleConfig['dir'] = $bundleClassDir.'/'.$this->getMappingObjectDefaultName();
|
||||
} else {
|
||||
$bundleConfig['dir'] = $bundleDir.'/'.$this->getMappingResourceConfigDirectory($bundleDir);
|
||||
}
|
||||
} else {
|
||||
$bundleConfig['dir'] = $bundleDir.'/'.$bundleConfig['dir'];
|
||||
}
|
||||
|
||||
if (!$bundleConfig['prefix']) {
|
||||
$bundleConfig['prefix'] = $bundle->getNamespaceName().'\\'.$this->getMappingObjectDefaultName();
|
||||
}
|
||||
|
||||
return $bundleConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all the collected mapping information with the object manager by registering the appropriate mapping drivers.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerMappingDrivers(array $objectManager, ContainerBuilder $container)
|
||||
{
|
||||
// configure metadata driver for each bundle based on the type of mapping files found
|
||||
if ($container->hasDefinition($this->getObjectManagerElementName($objectManager['name'].'_metadata_driver'))) {
|
||||
$chainDriverDef = $container->getDefinition($this->getObjectManagerElementName($objectManager['name'].'_metadata_driver'));
|
||||
} else {
|
||||
$chainDriverDef = new Definition($this->getMetadataDriverClass('driver_chain'));
|
||||
$chainDriverDef->setPublic(false);
|
||||
}
|
||||
|
||||
foreach ($this->drivers as $driverType => $driverPaths) {
|
||||
$mappingService = $this->getObjectManagerElementName($objectManager['name'].'_'.$driverType.'_metadata_driver');
|
||||
if ($container->hasDefinition($mappingService)) {
|
||||
$mappingDriverDef = $container->getDefinition($mappingService);
|
||||
$args = $mappingDriverDef->getArguments();
|
||||
if ('annotation' == $driverType) {
|
||||
$args[1] = array_merge(array_values($driverPaths), $args[1]);
|
||||
} else {
|
||||
$args[0] = array_merge(array_values($driverPaths), $args[0]);
|
||||
}
|
||||
$mappingDriverDef->setArguments($args);
|
||||
} elseif ('attribute' === $driverType) {
|
||||
$mappingDriverDef = new Definition($this->getMetadataDriverClass($driverType), [
|
||||
array_values($driverPaths),
|
||||
]);
|
||||
} elseif ('annotation' == $driverType) {
|
||||
$mappingDriverDef = new Definition($this->getMetadataDriverClass($driverType), [
|
||||
new Reference($this->getObjectManagerElementName('metadata.annotation_reader')),
|
||||
array_values($driverPaths),
|
||||
]);
|
||||
} else {
|
||||
$mappingDriverDef = new Definition($this->getMetadataDriverClass($driverType), [
|
||||
array_values($driverPaths),
|
||||
]);
|
||||
}
|
||||
$mappingDriverDef->setPublic(false);
|
||||
if (str_contains($mappingDriverDef->getClass(), 'yml') || str_contains($mappingDriverDef->getClass(), 'xml')) {
|
||||
$mappingDriverDef->setArguments([array_flip($driverPaths)]);
|
||||
$mappingDriverDef->addMethodCall('setGlobalBasename', ['mapping']);
|
||||
}
|
||||
|
||||
$container->setDefinition($mappingService, $mappingDriverDef);
|
||||
|
||||
foreach ($driverPaths as $prefix => $driverPath) {
|
||||
$chainDriverDef->addMethodCall('addDriver', [new Reference($mappingService), $prefix]);
|
||||
}
|
||||
}
|
||||
|
||||
$container->setDefinition($this->getObjectManagerElementName($objectManager['name'].'_metadata_driver'), $chainDriverDef);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assertion if the specified mapping information is valid.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function assertValidMappingConfiguration(array $mappingConfig, string $objectManagerName)
|
||||
{
|
||||
if (!$mappingConfig['type'] || !$mappingConfig['dir'] || !$mappingConfig['prefix']) {
|
||||
throw new \InvalidArgumentException(sprintf('Mapping definitions for Doctrine manager "%s" require at least the "type", "dir" and "prefix" options.', $objectManagerName));
|
||||
}
|
||||
|
||||
if (!is_dir($mappingConfig['dir'])) {
|
||||
throw new \InvalidArgumentException(sprintf('Specified non-existing directory "%s" as Doctrine mapping source.', $mappingConfig['dir']));
|
||||
}
|
||||
|
||||
if (!\in_array($mappingConfig['type'], ['xml', 'yml', 'annotation', 'php', 'staticphp', 'attribute'])) {
|
||||
throw new \InvalidArgumentException(sprintf('Can only configure "xml", "yml", "annotation", "php", "staticphp" or "attribute" through the DoctrineBundle. Use your own bundle to configure other metadata drivers. You can register them by adding a new driver to the "%s" service definition.', $this->getObjectManagerElementName($objectManagerName.'_metadata_driver')));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects what metadata driver to use for the supplied directory.
|
||||
*/
|
||||
protected function detectMetadataDriver(string $dir, ContainerBuilder $container): ?string
|
||||
{
|
||||
$configPath = $this->getMappingResourceConfigDirectory($dir);
|
||||
$extension = $this->getMappingResourceExtension();
|
||||
|
||||
if (glob($dir.'/'.$configPath.'/*.'.$extension.'.xml', \GLOB_NOSORT)) {
|
||||
$driver = 'xml';
|
||||
} elseif (glob($dir.'/'.$configPath.'/*.'.$extension.'.yml', \GLOB_NOSORT)) {
|
||||
$driver = 'yml';
|
||||
} elseif (glob($dir.'/'.$configPath.'/*.'.$extension.'.php', \GLOB_NOSORT)) {
|
||||
$driver = 'php';
|
||||
} else {
|
||||
// add the closest existing directory as a resource
|
||||
$resource = $dir.'/'.$configPath;
|
||||
while (!is_dir($resource)) {
|
||||
$resource = \dirname($resource);
|
||||
}
|
||||
$container->fileExists($resource, false);
|
||||
|
||||
if ($container->fileExists($discoveryPath = $dir.'/'.$this->getMappingObjectDefaultName(), false)) {
|
||||
return $this->detectMappingType($discoveryPath, $container);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
$container->fileExists($dir.'/'.$configPath, false);
|
||||
|
||||
return $driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects what mapping type to use for the supplied directory.
|
||||
*
|
||||
* @return string A mapping type 'attribute' or 'annotation'
|
||||
*/
|
||||
private function detectMappingType(string $directory, ContainerBuilder $container): string
|
||||
{
|
||||
$type = 'attribute';
|
||||
|
||||
$glob = new GlobResource($directory, '*', true);
|
||||
$container->addResource($glob);
|
||||
|
||||
$quotedMappingObjectName = preg_quote($this->getMappingObjectDefaultName(), '/');
|
||||
|
||||
foreach ($glob as $file) {
|
||||
$content = file_get_contents($file);
|
||||
|
||||
if (
|
||||
preg_match('/^#\[.*'.$quotedMappingObjectName.'\b/m', $content)
|
||||
|| preg_match('/^#\[.*Embeddable\b/m', $content)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
if (
|
||||
preg_match('/^(?: \*|\/\*\*) @.*'.$quotedMappingObjectName.'\b/m', $content)
|
||||
|| preg_match('/^(?: \*|\/\*\*) @.*Embeddable\b/m', $content)
|
||||
) {
|
||||
$type = 'annotation';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a configured object manager metadata, query or result cache driver.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \InvalidArgumentException in case of unknown driver type
|
||||
*/
|
||||
protected function loadObjectManagerCacheDriver(array $objectManager, ContainerBuilder $container, string $cacheName)
|
||||
{
|
||||
$this->loadCacheDriver($cacheName, $objectManager['name'], $objectManager[$cacheName.'_driver'], $container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a cache driver.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function loadCacheDriver(string $cacheName, string $objectManagerName, array $cacheDriver, ContainerBuilder $container): string
|
||||
{
|
||||
$cacheDriverServiceId = $this->getObjectManagerElementName($objectManagerName.'_'.$cacheName);
|
||||
|
||||
switch ($cacheDriver['type']) {
|
||||
case 'service':
|
||||
$container->setAlias($cacheDriverServiceId, new Alias($cacheDriver['id'], false));
|
||||
|
||||
return $cacheDriverServiceId;
|
||||
case 'memcached':
|
||||
$memcachedClass = !empty($cacheDriver['class']) ? $cacheDriver['class'] : '%'.$this->getObjectManagerElementName('cache.memcached.class').'%';
|
||||
$memcachedInstanceClass = !empty($cacheDriver['instance_class']) ? $cacheDriver['instance_class'] : '%'.$this->getObjectManagerElementName('cache.memcached_instance.class').'%';
|
||||
$memcachedHost = !empty($cacheDriver['host']) ? $cacheDriver['host'] : '%'.$this->getObjectManagerElementName('cache.memcached_host').'%';
|
||||
$memcachedPort = !empty($cacheDriver['port']) ? $cacheDriver['port'] : '%'.$this->getObjectManagerElementName('cache.memcached_port').'%';
|
||||
$cacheDef = new Definition($memcachedClass);
|
||||
$memcachedInstance = new Definition($memcachedInstanceClass);
|
||||
$memcachedInstance->addMethodCall('addServer', [
|
||||
$memcachedHost, $memcachedPort,
|
||||
]);
|
||||
$container->setDefinition($this->getObjectManagerElementName(sprintf('%s_memcached_instance', $objectManagerName)), $memcachedInstance);
|
||||
$cacheDef->addMethodCall('setMemcached', [new Reference($this->getObjectManagerElementName(sprintf('%s_memcached_instance', $objectManagerName)))]);
|
||||
break;
|
||||
case 'redis':
|
||||
$redisClass = !empty($cacheDriver['class']) ? $cacheDriver['class'] : '%'.$this->getObjectManagerElementName('cache.redis.class').'%';
|
||||
$redisInstanceClass = !empty($cacheDriver['instance_class']) ? $cacheDriver['instance_class'] : '%'.$this->getObjectManagerElementName('cache.redis_instance.class').'%';
|
||||
$redisHost = !empty($cacheDriver['host']) ? $cacheDriver['host'] : '%'.$this->getObjectManagerElementName('cache.redis_host').'%';
|
||||
$redisPort = !empty($cacheDriver['port']) ? $cacheDriver['port'] : '%'.$this->getObjectManagerElementName('cache.redis_port').'%';
|
||||
$cacheDef = new Definition($redisClass);
|
||||
$redisInstance = new Definition($redisInstanceClass);
|
||||
$redisInstance->addMethodCall('connect', [
|
||||
$redisHost, $redisPort,
|
||||
]);
|
||||
$container->setDefinition($this->getObjectManagerElementName(sprintf('%s_redis_instance', $objectManagerName)), $redisInstance);
|
||||
$cacheDef->addMethodCall('setRedis', [new Reference($this->getObjectManagerElementName(sprintf('%s_redis_instance', $objectManagerName)))]);
|
||||
break;
|
||||
case 'apc':
|
||||
case 'apcu':
|
||||
case 'array':
|
||||
case 'xcache':
|
||||
case 'wincache':
|
||||
case 'zenddata':
|
||||
$cacheDef = new Definition('%'.$this->getObjectManagerElementName(sprintf('cache.%s.class', $cacheDriver['type'])).'%');
|
||||
break;
|
||||
default:
|
||||
throw new \InvalidArgumentException(sprintf('"%s" is an unrecognized Doctrine cache driver.', $cacheDriver['type']));
|
||||
}
|
||||
|
||||
$cacheDef->setPublic(false);
|
||||
|
||||
if (!isset($cacheDriver['namespace'])) {
|
||||
// generate a unique namespace for the given application
|
||||
if ($container->hasParameter('cache.prefix.seed')) {
|
||||
$seed = $container->getParameterBag()->resolveValue($container->getParameter('cache.prefix.seed'));
|
||||
} else {
|
||||
$seed = '_'.$container->getParameter('kernel.project_dir');
|
||||
$seed .= '.'.$container->getParameter('kernel.container_class');
|
||||
}
|
||||
|
||||
$namespace = 'sf_'.$this->getMappingResourceExtension().'_'.$objectManagerName.'_'.ContainerBuilder::hash($seed);
|
||||
|
||||
$cacheDriver['namespace'] = $namespace;
|
||||
}
|
||||
|
||||
$cacheDef->addMethodCall('setNamespace', [$cacheDriver['namespace']]);
|
||||
|
||||
$container->setDefinition($cacheDriverServiceId, $cacheDef);
|
||||
|
||||
return $cacheDriverServiceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a modified version of $managerConfigs.
|
||||
*
|
||||
* The manager called $autoMappedManager will map all bundles that are not mapped by other managers.
|
||||
*/
|
||||
protected function fixManagersAutoMappings(array $managerConfigs, array $bundles): array
|
||||
{
|
||||
if ($autoMappedManager = $this->validateAutoMapping($managerConfigs)) {
|
||||
foreach (array_keys($bundles) as $bundle) {
|
||||
foreach ($managerConfigs as $manager) {
|
||||
if (isset($manager['mappings'][$bundle])) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
$managerConfigs[$autoMappedManager]['mappings'][$bundle] = [
|
||||
'mapping' => true,
|
||||
'is_bundle' => true,
|
||||
];
|
||||
}
|
||||
$managerConfigs[$autoMappedManager]['auto_mapping'] = false;
|
||||
}
|
||||
|
||||
return $managerConfigs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefixes the relative dependency injection container path with the object manager prefix.
|
||||
*
|
||||
* @example $name is 'entity_manager' then the result would be 'doctrine.orm.entity_manager'
|
||||
*/
|
||||
abstract protected function getObjectManagerElementName(string $name): string;
|
||||
|
||||
/**
|
||||
* Noun that describes the mapped objects such as Entity or Document.
|
||||
*
|
||||
* Will be used for autodetection of persistent objects directory.
|
||||
*/
|
||||
abstract protected function getMappingObjectDefaultName(): string;
|
||||
|
||||
/**
|
||||
* Relative path from the bundle root to the directory where mapping files reside.
|
||||
*/
|
||||
abstract protected function getMappingResourceConfigDirectory(?string $bundleDir = null): string;
|
||||
|
||||
/**
|
||||
* Extension used by the mapping files.
|
||||
*/
|
||||
abstract protected function getMappingResourceExtension(): string;
|
||||
|
||||
/**
|
||||
* The class name used by the various mapping drivers.
|
||||
*/
|
||||
abstract protected function getMetadataDriverClass(string $driverType): string;
|
||||
|
||||
/**
|
||||
* Search for a manager that is declared as 'auto_mapping' = true.
|
||||
*
|
||||
* @throws \LogicException
|
||||
*/
|
||||
private function validateAutoMapping(array $managerConfigs): ?string
|
||||
{
|
||||
$autoMappedManager = null;
|
||||
foreach ($managerConfigs as $name => $manager) {
|
||||
if (!$manager['auto_mapping']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null !== $autoMappedManager) {
|
||||
throw new \LogicException(sprintf('You cannot enable "auto_mapping" on more than one manager at the same time (found in "%s" and "%s"").', $autoMappedManager, $name));
|
||||
}
|
||||
|
||||
$autoMappedManager = $name;
|
||||
}
|
||||
|
||||
return $autoMappedManager;
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
/**
|
||||
* Registers additional validators.
|
||||
*
|
||||
* @author Benjamin Eberlei <kontakt@beberlei.de>
|
||||
*/
|
||||
class DoctrineValidationPass implements CompilerPassInterface
|
||||
{
|
||||
private string $managerType;
|
||||
|
||||
public function __construct(string $managerType)
|
||||
{
|
||||
$this->managerType = $managerType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
$this->updateValidatorMappingFiles($container, 'xml', 'xml');
|
||||
$this->updateValidatorMappingFiles($container, 'yaml', 'yml');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the validation mapping files for the format and extends them with
|
||||
* files matching a doctrine search pattern (Resources/config/validation.orm.xml).
|
||||
*/
|
||||
private function updateValidatorMappingFiles(ContainerBuilder $container, string $mapping, string $extension): void
|
||||
{
|
||||
if (!$container->hasParameter('validator.mapping.loader.'.$mapping.'_files_loader.mapping_files')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = $container->getParameter('validator.mapping.loader.'.$mapping.'_files_loader.mapping_files');
|
||||
$validationPath = '/config/validation.'.$this->managerType.'.'.$extension;
|
||||
|
||||
foreach ($container->getParameter('kernel.bundles_metadata') as $bundle) {
|
||||
if ($container->fileExists($file = $bundle['path'].'/Resources'.$validationPath) || $container->fileExists($file = $bundle['path'].$validationPath)) {
|
||||
$files[] = $file;
|
||||
}
|
||||
}
|
||||
|
||||
$container->setParameter('validator.mapping.loader.'.$mapping.'_files_loader.mapping_files', $files);
|
||||
}
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass;
|
||||
|
||||
use Symfony\Bridge\Doctrine\ContainerAwareEventManager;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
/**
|
||||
* Registers event listeners and subscribers to the available doctrine connections.
|
||||
*
|
||||
* @author Jeremy Mikola <jmikola@gmail.com>
|
||||
* @author Alexander <iam.asm89@gmail.com>
|
||||
* @author David Maicher <mail@dmaicher.de>
|
||||
*/
|
||||
class RegisterEventListenersAndSubscribersPass implements CompilerPassInterface
|
||||
{
|
||||
private string $connectionsParameter;
|
||||
private array $connections;
|
||||
|
||||
/**
|
||||
* @var array<string, Definition>
|
||||
*/
|
||||
private array $eventManagers = [];
|
||||
|
||||
private string $managerTemplate;
|
||||
private string $tagPrefix;
|
||||
|
||||
/**
|
||||
* @param string $managerTemplate sprintf() template for generating the event
|
||||
* manager's service ID for a connection name
|
||||
* @param string $tagPrefix Tag prefix for listeners and subscribers
|
||||
*/
|
||||
public function __construct(string $connectionsParameter, string $managerTemplate, string $tagPrefix)
|
||||
{
|
||||
$this->connectionsParameter = $connectionsParameter;
|
||||
$this->managerTemplate = $managerTemplate;
|
||||
$this->tagPrefix = $tagPrefix;
|
||||
}
|
||||
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
if (!$container->hasParameter($this->connectionsParameter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->connections = $container->getParameter($this->connectionsParameter);
|
||||
$listenerRefs = $this->addTaggedServices($container);
|
||||
|
||||
// replace service container argument of event managers with smaller service locator
|
||||
// so services can even remain private
|
||||
foreach ($listenerRefs as $connection => $refs) {
|
||||
$this->getEventManagerDef($container, $connection)
|
||||
->replaceArgument(0, ServiceLocatorTagPass::register($container, $refs));
|
||||
}
|
||||
}
|
||||
|
||||
private function addTaggedServices(ContainerBuilder $container): array
|
||||
{
|
||||
$listenerTag = $this->tagPrefix.'.event_listener';
|
||||
$subscriberTag = $this->tagPrefix.'.event_subscriber';
|
||||
$listenerRefs = [];
|
||||
$taggedServices = $this->findAndSortTags($subscriberTag, $listenerTag, $container);
|
||||
|
||||
$managerDefs = [];
|
||||
foreach ($taggedServices as $taggedSubscriber) {
|
||||
[$tagName, $id, $tag] = $taggedSubscriber;
|
||||
$connections = isset($tag['connection'])
|
||||
? [$container->getParameterBag()->resolveValue($tag['connection'])]
|
||||
: array_keys($this->connections);
|
||||
if ($listenerTag === $tagName && !isset($tag['event'])) {
|
||||
throw new InvalidArgumentException(sprintf('Doctrine event listener "%s" must specify the "event" attribute.', $id));
|
||||
}
|
||||
foreach ($connections as $con) {
|
||||
if (!isset($this->connections[$con])) {
|
||||
throw new RuntimeException(sprintf('The Doctrine connection "%s" referenced in service "%s" does not exist. Available connections names: "%s".', $con, $id, implode('", "', array_keys($this->connections))));
|
||||
}
|
||||
|
||||
if (!isset($managerDefs[$con])) {
|
||||
$managerDef = $parentDef = $this->getEventManagerDef($container, $con);
|
||||
while (!$parentDef->getClass() && $parentDef instanceof ChildDefinition) {
|
||||
$parentDef = $container->findDefinition($parentDef->getParent());
|
||||
}
|
||||
$managerClass = $container->getParameterBag()->resolveValue($parentDef->getClass());
|
||||
$managerDefs[$con] = [$managerDef, $managerClass];
|
||||
} else {
|
||||
[$managerDef, $managerClass] = $managerDefs[$con];
|
||||
}
|
||||
|
||||
if (ContainerAwareEventManager::class === $managerClass) {
|
||||
$refs = $managerDef->getArguments()[1] ?? [];
|
||||
$listenerRefs[$con][$id] = new Reference($id);
|
||||
if ($subscriberTag === $tagName) {
|
||||
trigger_deprecation('symfony/doctrine-bridge', '6.3', 'Registering "%s" as a Doctrine subscriber is deprecated. Register it as a listener instead, using e.g. the #[%s] attribute.', $id, str_starts_with($this->tagPrefix, 'doctrine_mongodb') ? 'AsDocumentListener' : 'AsDoctrineListener');
|
||||
$refs[] = $id;
|
||||
} else {
|
||||
$refs[] = [[$tag['event']], $id];
|
||||
}
|
||||
$managerDef->setArgument(1, $refs);
|
||||
} else {
|
||||
if ($subscriberTag === $tagName) {
|
||||
$managerDef->addMethodCall('addEventSubscriber', [new Reference($id)]);
|
||||
} else {
|
||||
$managerDef->addMethodCall('addEventListener', [[$tag['event']], new Reference($id)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $listenerRefs;
|
||||
}
|
||||
|
||||
private function getEventManagerDef(ContainerBuilder $container, string $name): Definition
|
||||
{
|
||||
if (!isset($this->eventManagers[$name])) {
|
||||
$this->eventManagers[$name] = $container->getDefinition(sprintf($this->managerTemplate, $name));
|
||||
}
|
||||
|
||||
return $this->eventManagers[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and orders all service tags with the given name by their priority.
|
||||
*
|
||||
* The order of additions must be respected for services having the same priority,
|
||||
* and knowing that the \SplPriorityQueue class does not respect the FIFO method,
|
||||
* we should not use this class.
|
||||
*
|
||||
* @see https://bugs.php.net/53710
|
||||
* @see https://bugs.php.net/60926
|
||||
*/
|
||||
private function findAndSortTags(string $subscriberTag, string $listenerTag, ContainerBuilder $container): array
|
||||
{
|
||||
$sortedTags = [];
|
||||
$taggedIds = [
|
||||
$subscriberTag => $container->findTaggedServiceIds($subscriberTag, true),
|
||||
$listenerTag => $container->findTaggedServiceIds($listenerTag, true),
|
||||
];
|
||||
$taggedIds[$subscriberTag] = array_diff_key($taggedIds[$subscriberTag], $taggedIds[$listenerTag]);
|
||||
|
||||
foreach ($taggedIds as $tagName => $serviceIds) {
|
||||
foreach ($serviceIds as $serviceId => $tags) {
|
||||
foreach ($tags as $attributes) {
|
||||
$priority = $attributes['priority'] ?? 0;
|
||||
$sortedTags[$priority][] = [$tagName, $serviceId, $attributes];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
krsort($sortedTags);
|
||||
|
||||
return array_merge(...$sortedTags);
|
||||
}
|
||||
}
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
/**
|
||||
* Base class for the doctrine bundles to provide a compiler pass class that
|
||||
* helps to register doctrine mappings.
|
||||
*
|
||||
* The compiler pass is meant to register the mappings with the metadata
|
||||
* chain driver corresponding to one of the object managers.
|
||||
*
|
||||
* For concrete implementations, see the RegisterXyMappingsPass classes
|
||||
* in the DoctrineBundle resp.
|
||||
* DoctrineMongodbBundle, DoctrineCouchdbBundle and DoctrinePhpcrBundle.
|
||||
*
|
||||
* @author David Buchmann <david@liip.ch>
|
||||
*/
|
||||
abstract class RegisterMappingsPass implements CompilerPassInterface
|
||||
{
|
||||
/**
|
||||
* DI object for the driver to use, either a service definition for a
|
||||
* private service or a reference for a public service.
|
||||
*
|
||||
* @var Definition|Reference
|
||||
*/
|
||||
protected $driver;
|
||||
|
||||
/**
|
||||
* List of namespaces handled by the driver.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $namespaces;
|
||||
|
||||
/**
|
||||
* List of potential container parameters that hold the object manager name
|
||||
* to register the mappings with the correct metadata driver, for example
|
||||
* ['acme.manager', 'doctrine.default_entity_manager'].
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $managerParameters;
|
||||
|
||||
/**
|
||||
* Naming pattern of the metadata chain driver service ids, for example
|
||||
* 'doctrine.orm.%s_metadata_driver'.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $driverPattern;
|
||||
|
||||
/**
|
||||
* A name for a parameter in the container. If set, this compiler pass will
|
||||
* only do anything if the parameter is present. (But regardless of the
|
||||
* value of that parameter.
|
||||
*
|
||||
* @var string|false
|
||||
*/
|
||||
protected $enabledParameter;
|
||||
|
||||
/**
|
||||
* Naming pattern for the configuration service id, for example
|
||||
* 'doctrine.orm.%s_configuration'.
|
||||
*/
|
||||
private string $configurationPattern;
|
||||
|
||||
/**
|
||||
* Method name to call on the configuration service. This depends on the
|
||||
* Doctrine implementation. For example addEntityNamespace.
|
||||
*/
|
||||
private string $registerAliasMethodName;
|
||||
|
||||
/**
|
||||
* Map of alias to namespace.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private array $aliasMap;
|
||||
|
||||
/**
|
||||
* The $managerParameters is an ordered list of container parameters that could provide the
|
||||
* name of the manager to register these namespaces and alias on. The first non-empty name
|
||||
* is used, the others skipped.
|
||||
*
|
||||
* The $aliasMap parameter can be used to define bundle namespace shortcuts like the
|
||||
* DoctrineBundle provides automatically for objects in the default Entity/Document folder.
|
||||
*
|
||||
* @param Definition|Reference $driver Driver DI definition or reference
|
||||
* @param string[] $namespaces List of namespaces handled by $driver
|
||||
* @param string[] $managerParameters list of container parameters that could
|
||||
* hold the manager name
|
||||
* @param string $driverPattern Pattern for the metadata driver service name
|
||||
* @param string|false $enabledParameter Service container parameter that must be
|
||||
* present to enable the mapping. Set to false
|
||||
* to not do any check, optional.
|
||||
* @param string $configurationPattern Pattern for the Configuration service name
|
||||
* @param string $registerAliasMethodName Name of Configuration class method to
|
||||
* register alias
|
||||
* @param string[] $aliasMap Map of alias to namespace
|
||||
*/
|
||||
public function __construct(Definition|Reference $driver, array $namespaces, array $managerParameters, string $driverPattern, string|false $enabledParameter = false, string $configurationPattern = '', string $registerAliasMethodName = '', array $aliasMap = [])
|
||||
{
|
||||
$this->driver = $driver;
|
||||
$this->namespaces = $namespaces;
|
||||
$this->managerParameters = $managerParameters;
|
||||
$this->driverPattern = $driverPattern;
|
||||
$this->enabledParameter = $enabledParameter;
|
||||
if (\count($aliasMap) && (!$configurationPattern || !$registerAliasMethodName)) {
|
||||
throw new \InvalidArgumentException('configurationPattern and registerAliasMethodName are required to register namespace alias.');
|
||||
}
|
||||
$this->configurationPattern = $configurationPattern;
|
||||
$this->registerAliasMethodName = $registerAliasMethodName;
|
||||
$this->aliasMap = $aliasMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register mappings and alias with the metadata drivers.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
if (!$this->enabled($container)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mappingDriverDef = $this->getDriver($container);
|
||||
$chainDriverDefService = $this->getChainDriverServiceName($container);
|
||||
// Definition for a Doctrine\Persistence\Mapping\Driver\MappingDriverChain
|
||||
$chainDriverDef = $container->getDefinition($chainDriverDefService);
|
||||
foreach ($this->namespaces as $namespace) {
|
||||
$chainDriverDef->addMethodCall('addDriver', [$mappingDriverDef, $namespace]);
|
||||
}
|
||||
|
||||
if (!\count($this->aliasMap)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$configurationServiceName = $this->getConfigurationServiceName($container);
|
||||
// Definition of the Doctrine\...\Configuration class specific to the Doctrine flavour.
|
||||
$configurationServiceDefinition = $container->getDefinition($configurationServiceName);
|
||||
foreach ($this->aliasMap as $alias => $namespace) {
|
||||
$configurationServiceDefinition->addMethodCall($this->registerAliasMethodName, [$alias, $namespace]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the service name of the metadata chain driver that the mappings
|
||||
* should be registered with.
|
||||
*
|
||||
* @throws InvalidArgumentException if non of the managerParameters has a
|
||||
* non-empty value
|
||||
*/
|
||||
protected function getChainDriverServiceName(ContainerBuilder $container): string
|
||||
{
|
||||
return sprintf($this->driverPattern, $this->getManagerName($container));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the service definition for the metadata driver.
|
||||
*
|
||||
* @param ContainerBuilder $container Passed on in case an extending class
|
||||
* needs access to the container
|
||||
*/
|
||||
protected function getDriver(ContainerBuilder $container): Definition|Reference
|
||||
{
|
||||
return $this->driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the service name from the pattern and the configured manager name.
|
||||
*
|
||||
* @throws InvalidArgumentException if none of the managerParameters has a
|
||||
* non-empty value
|
||||
*/
|
||||
private function getConfigurationServiceName(ContainerBuilder $container): string
|
||||
{
|
||||
return sprintf($this->configurationPattern, $this->getManagerName($container));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the manager name.
|
||||
*
|
||||
* The default implementation loops over the managerParameters and returns
|
||||
* the first non-empty parameter.
|
||||
*
|
||||
* @throws InvalidArgumentException if none of the managerParameters is found in the container
|
||||
*/
|
||||
private function getManagerName(ContainerBuilder $container): string
|
||||
{
|
||||
foreach ($this->managerParameters as $param) {
|
||||
if ($container->hasParameter($param)) {
|
||||
$name = $container->getParameter($param);
|
||||
if ($name) {
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('Could not find the manager name parameter in the container. Tried the following parameter names: "%s".', implode('", "', $this->managerParameters)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether this mapping should be activated or not. This allows
|
||||
* to take this decision with the container builder available.
|
||||
*
|
||||
* This default implementation checks if the class has the enabledParameter
|
||||
* configured and if so if that parameter is present in the container.
|
||||
*/
|
||||
protected function enabled(ContainerBuilder $container): bool
|
||||
{
|
||||
return !$this->enabledParameter || $container->hasParameter($this->enabledParameter);
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass;
|
||||
|
||||
use Symfony\Bridge\Doctrine\Types\UlidType;
|
||||
use Symfony\Bridge\Doctrine\Types\UuidType;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\Uid\AbstractUid;
|
||||
|
||||
final class RegisterUidTypePass implements CompilerPassInterface
|
||||
{
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
if (!class_exists(AbstractUid::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$container->hasParameter('doctrine.dbal.connection_factory.types')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$typeDefinition = $container->getParameter('doctrine.dbal.connection_factory.types');
|
||||
|
||||
if (!isset($typeDefinition['uuid'])) {
|
||||
$typeDefinition['uuid'] = ['class' => UuidType::class];
|
||||
}
|
||||
|
||||
if (!isset($typeDefinition['ulid'])) {
|
||||
$typeDefinition['ulid'] = ['class' => UlidType::class];
|
||||
}
|
||||
|
||||
$container->setParameter('doctrine.dbal.connection_factory.types', $typeDefinition);
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\DependencyInjection\Security\UserProvider;
|
||||
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface;
|
||||
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
/**
|
||||
* EntityFactory creates services for Doctrine user provider.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Christophe Coevoet <stof@notk.org>
|
||||
*/
|
||||
class EntityFactory implements UserProviderFactoryInterface
|
||||
{
|
||||
private string $key;
|
||||
private string $providerId;
|
||||
|
||||
public function __construct(string $key, string $providerId)
|
||||
{
|
||||
$this->key = $key;
|
||||
$this->providerId = $providerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function create(ContainerBuilder $container, string $id, array $config)
|
||||
{
|
||||
$container
|
||||
->setDefinition($id, new ChildDefinition($this->providerId))
|
||||
->addArgument($config['class'])
|
||||
->addArgument($config['property'])
|
||||
->addArgument($config['manager_name'])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getKey()
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function addConfiguration(NodeDefinition $node)
|
||||
{
|
||||
$node
|
||||
->children()
|
||||
->scalarNode('class')
|
||||
->isRequired()
|
||||
->info('The full entity class name of your user class.')
|
||||
->cannotBeEmpty()
|
||||
->end()
|
||||
->scalarNode('property')->defaultNull()->end()
|
||||
->scalarNode('manager_name')->defaultNull()->end()
|
||||
->end()
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Form\ChoiceList;
|
||||
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Symfony\Component\Form\ChoiceList\Loader\AbstractChoiceLoader;
|
||||
use Symfony\Component\Form\Exception\LogicException;
|
||||
|
||||
/**
|
||||
* Loads choices using a Doctrine object manager.
|
||||
*
|
||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||
*/
|
||||
class DoctrineChoiceLoader extends AbstractChoiceLoader
|
||||
{
|
||||
private ObjectManager $manager;
|
||||
private string $class;
|
||||
private ?IdReader $idReader;
|
||||
private ?EntityLoaderInterface $objectLoader;
|
||||
|
||||
/**
|
||||
* Creates a new choice loader.
|
||||
*
|
||||
* Optionally, an implementation of {@link EntityLoaderInterface} can be
|
||||
* passed which optimizes the object loading for one of the Doctrine
|
||||
* mapper implementations.
|
||||
*
|
||||
* @param string $class The class name of the loaded objects
|
||||
*/
|
||||
public function __construct(ObjectManager $manager, string $class, ?IdReader $idReader = null, ?EntityLoaderInterface $objectLoader = null)
|
||||
{
|
||||
$classMetadata = $manager->getClassMetadata($class);
|
||||
|
||||
if ($idReader && !$idReader->isSingleId()) {
|
||||
throw new \InvalidArgumentException(sprintf('The "$idReader" argument of "%s" must be null when the query cannot be optimized because of composite id fields.', __METHOD__));
|
||||
}
|
||||
|
||||
$this->manager = $manager;
|
||||
$this->class = $classMetadata->getName();
|
||||
$this->idReader = $idReader;
|
||||
$this->objectLoader = $objectLoader;
|
||||
}
|
||||
|
||||
protected function loadChoices(): iterable
|
||||
{
|
||||
return $this->objectLoader
|
||||
? $this->objectLoader->getEntities()
|
||||
: $this->manager->getRepository($this->class)->findAll();
|
||||
}
|
||||
|
||||
protected function doLoadValuesForChoices(array $choices): array
|
||||
{
|
||||
// Optimize performance for single-field identifiers. We already
|
||||
// know that the IDs are used as values
|
||||
// Attention: This optimization does not check choices for existence
|
||||
if ($this->idReader) {
|
||||
throw new LogicException('Not defining the IdReader explicitly as a value callback when the query can be optimized is not supported.');
|
||||
}
|
||||
|
||||
return parent::doLoadValuesForChoices($choices);
|
||||
}
|
||||
|
||||
protected function doLoadChoicesForValues(array $values, ?callable $value): array
|
||||
{
|
||||
if ($this->idReader && null === $value) {
|
||||
throw new LogicException('Not defining the IdReader explicitly as a value callback when the query can be optimized is not supported.');
|
||||
}
|
||||
|
||||
$idReader = null;
|
||||
if (\is_array($value) && $value[0] instanceof IdReader) {
|
||||
$idReader = $value[0];
|
||||
} elseif ($value instanceof \Closure && ($rThis = (new \ReflectionFunction($value))->getClosureThis()) instanceof IdReader) {
|
||||
$idReader = $rThis;
|
||||
}
|
||||
|
||||
// Optimize performance in case we have an object loader and
|
||||
// a single-field identifier
|
||||
if ($idReader && $this->objectLoader) {
|
||||
$objects = [];
|
||||
$objectsById = [];
|
||||
|
||||
// Maintain order and indices from the given $values
|
||||
// An alternative approach to the following loop is to add the
|
||||
// "INDEX BY" clause to the Doctrine query in the loader,
|
||||
// but I'm not sure whether that's doable in a generic fashion.
|
||||
foreach ($this->objectLoader->getEntitiesByIds($idReader->getIdField(), $values) as $object) {
|
||||
$objectsById[$idReader->getIdValue($object)] = $object;
|
||||
}
|
||||
|
||||
foreach ($values as $i => $id) {
|
||||
if (isset($objectsById[$id])) {
|
||||
$objects[$i] = $objectsById[$id];
|
||||
}
|
||||
}
|
||||
|
||||
return $objects;
|
||||
}
|
||||
|
||||
return parent::doLoadChoicesForValues($values, $value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Form\ChoiceList;
|
||||
|
||||
/**
|
||||
* Custom loader for entities in the choice list.
|
||||
*
|
||||
* @author Benjamin Eberlei <kontakt@beberlei.de>
|
||||
*/
|
||||
interface EntityLoaderInterface
|
||||
{
|
||||
/**
|
||||
* Returns an array of entities that are valid choices in the corresponding choice list.
|
||||
*/
|
||||
public function getEntities(): array;
|
||||
|
||||
/**
|
||||
* Returns an array of entities matching the given identifiers.
|
||||
*/
|
||||
public function getEntitiesByIds(string $identifier, array $values): array;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Form\ChoiceList;
|
||||
|
||||
use Doctrine\Persistence\Mapping\ClassMetadata;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Symfony\Component\Form\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* A utility for reading object IDs.
|
||||
*
|
||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class IdReader
|
||||
{
|
||||
private ObjectManager $om;
|
||||
private ClassMetadata $classMetadata;
|
||||
private bool $singleId;
|
||||
private bool $intId;
|
||||
private string $idField;
|
||||
private ?self $associationIdReader = null;
|
||||
|
||||
public function __construct(ObjectManager $om, ClassMetadata $classMetadata)
|
||||
{
|
||||
$ids = $classMetadata->getIdentifierFieldNames();
|
||||
$idType = $classMetadata->getTypeOfField(current($ids));
|
||||
|
||||
$this->om = $om;
|
||||
$this->classMetadata = $classMetadata;
|
||||
$this->singleId = 1 === \count($ids);
|
||||
$this->intId = $this->singleId && \in_array($idType, ['integer', 'smallint', 'bigint']);
|
||||
$this->idField = current($ids);
|
||||
|
||||
// single field association are resolved, since the schema column could be an int
|
||||
if ($this->singleId && $classMetadata->hasAssociation($this->idField)) {
|
||||
$this->associationIdReader = new self($om, $om->getClassMetadata(
|
||||
$classMetadata->getAssociationTargetClass($this->idField)
|
||||
));
|
||||
|
||||
$this->singleId = $this->associationIdReader->isSingleId();
|
||||
$this->intId = $this->associationIdReader->isIntId();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the class has a single-column ID.
|
||||
*/
|
||||
public function isSingleId(): bool
|
||||
{
|
||||
return $this->singleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the class has a single-column integer ID.
|
||||
*/
|
||||
public function isIntId(): bool
|
||||
{
|
||||
return $this->intId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID value for an object.
|
||||
*
|
||||
* This method assumes that the object has a single-column ID.
|
||||
*/
|
||||
public function getIdValue(?object $object = null): string
|
||||
{
|
||||
if (!$object) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->om->contains($object)) {
|
||||
throw new RuntimeException(sprintf('Entity of type "%s" passed to the choice field must be managed. Maybe you forget to persist it in the entity manager?', get_debug_type($object)));
|
||||
}
|
||||
|
||||
$this->om->initializeObject($object);
|
||||
|
||||
$idValue = current($this->classMetadata->getIdentifierValues($object));
|
||||
|
||||
if ($this->associationIdReader) {
|
||||
$idValue = $this->associationIdReader->getIdValue($idValue);
|
||||
}
|
||||
|
||||
return (string) $idValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the ID field.
|
||||
*
|
||||
* This method assumes that the object has a single-column ID.
|
||||
*/
|
||||
public function getIdField(): string
|
||||
{
|
||||
return $this->idField;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Form\ChoiceList;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Types\ConversionException;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\Exception\TransformationFailedException;
|
||||
|
||||
/**
|
||||
* Loads entities using a {@link QueryBuilder} instance.
|
||||
*
|
||||
* @author Benjamin Eberlei <kontakt@beberlei.de>
|
||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||
*/
|
||||
class ORMQueryBuilderLoader implements EntityLoaderInterface
|
||||
{
|
||||
/**
|
||||
* Contains the query builder that builds the query for fetching the
|
||||
* entities.
|
||||
*
|
||||
* This property should only be accessed through queryBuilder.
|
||||
*/
|
||||
private QueryBuilder $queryBuilder;
|
||||
|
||||
public function __construct(QueryBuilder $queryBuilder)
|
||||
{
|
||||
$this->queryBuilder = $queryBuilder;
|
||||
}
|
||||
|
||||
public function getEntities(): array
|
||||
{
|
||||
return $this->queryBuilder->getQuery()->execute();
|
||||
}
|
||||
|
||||
public function getEntitiesByIds(string $identifier, array $values): array
|
||||
{
|
||||
if (null !== $this->queryBuilder->getMaxResults() || 0 < (int) $this->queryBuilder->getFirstResult()) {
|
||||
// an offset or a limit would apply on results including the where clause with submitted id values
|
||||
// that could make invalid choices valid
|
||||
$choices = [];
|
||||
$metadata = $this->queryBuilder->getEntityManager()->getClassMetadata(current($this->queryBuilder->getRootEntities()));
|
||||
|
||||
foreach ($this->getEntities() as $entity) {
|
||||
if (\in_array((string) current($metadata->getIdentifierValues($entity)), $values, true)) {
|
||||
$choices[] = $entity;
|
||||
}
|
||||
}
|
||||
|
||||
return $choices;
|
||||
}
|
||||
|
||||
$qb = clone $this->queryBuilder;
|
||||
$alias = current($qb->getRootAliases());
|
||||
$parameter = 'ORMQueryBuilderLoader_getEntitiesByIds_'.$identifier;
|
||||
$parameter = str_replace('.', '_', $parameter);
|
||||
$where = $qb->expr()->in($alias.'.'.$identifier, ':'.$parameter);
|
||||
|
||||
// Guess type
|
||||
$entity = current($qb->getRootEntities());
|
||||
$metadata = $qb->getEntityManager()->getClassMetadata($entity);
|
||||
if (\in_array($type = $metadata->getTypeOfField($identifier), ['integer', 'bigint', 'smallint'])) {
|
||||
$parameterType = class_exists(ArrayParameterType::class) ? ArrayParameterType::INTEGER : Connection::PARAM_INT_ARRAY;
|
||||
|
||||
// Filter out non-integer values (e.g. ""). If we don't, some
|
||||
// databases such as PostgreSQL fail.
|
||||
$values = array_values(array_filter($values, fn ($v) => (string) $v === (string) (int) $v || ctype_digit($v)));
|
||||
} elseif (\in_array($type, ['ulid', 'uuid', 'guid'])) {
|
||||
$parameterType = class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING : Connection::PARAM_STR_ARRAY;
|
||||
|
||||
// Like above, but we just filter out empty strings.
|
||||
$values = array_values(array_filter($values, fn ($v) => '' !== (string) $v));
|
||||
|
||||
// Convert values into right type
|
||||
if (Type::hasType($type)) {
|
||||
$doctrineType = Type::getType($type);
|
||||
$platform = $qb->getEntityManager()->getConnection()->getDatabasePlatform();
|
||||
foreach ($values as &$value) {
|
||||
try {
|
||||
$value = $doctrineType->convertToDatabaseValue($value, $platform);
|
||||
} catch (ConversionException $e) {
|
||||
throw new TransformationFailedException(sprintf('Failed to transform "%s" into "%s".', $value, $type), 0, $e);
|
||||
}
|
||||
}
|
||||
unset($value);
|
||||
}
|
||||
} else {
|
||||
$parameterType = class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING : Connection::PARAM_STR_ARRAY;
|
||||
}
|
||||
if (!$values) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $qb->andWhere($where)
|
||||
->getQuery()
|
||||
->setParameter($parameter, $values, $parameterType)
|
||||
->getResult();
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Form\DataTransformer;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
use Symfony\Component\Form\Exception\TransformationFailedException;
|
||||
|
||||
/**
|
||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||
*
|
||||
* @implements DataTransformerInterface<Collection, array>
|
||||
*/
|
||||
class CollectionToArrayTransformer implements DataTransformerInterface
|
||||
{
|
||||
/**
|
||||
* Transforms a collection into an array.
|
||||
*
|
||||
* @throws TransformationFailedException
|
||||
*/
|
||||
public function transform(mixed $collection): mixed
|
||||
{
|
||||
if (null === $collection) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// For cases when the collection getter returns $collection->toArray()
|
||||
// in order to prevent modifications of the returned collection
|
||||
if (\is_array($collection)) {
|
||||
return $collection;
|
||||
}
|
||||
|
||||
if (!$collection instanceof Collection) {
|
||||
throw new TransformationFailedException('Expected a Doctrine\Common\Collections\Collection object.');
|
||||
}
|
||||
|
||||
return $collection->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms an array into a collection.
|
||||
*/
|
||||
public function reverseTransform(mixed $array): Collection
|
||||
{
|
||||
if ('' === $array || null === $array) {
|
||||
$array = [];
|
||||
} else {
|
||||
$array = (array) $array;
|
||||
}
|
||||
|
||||
return new ArrayCollection($array);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Form;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractExtension;
|
||||
use Symfony\Component\Form\FormTypeGuesserInterface;
|
||||
|
||||
class DoctrineOrmExtension extends AbstractExtension
|
||||
{
|
||||
protected $registry;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
protected function loadTypes(): array
|
||||
{
|
||||
return [
|
||||
new EntityType($this->registry),
|
||||
];
|
||||
}
|
||||
|
||||
protected function loadTypeGuesser(): ?FormTypeGuesserInterface
|
||||
{
|
||||
return new DoctrineOrmTypeGuesser($this->registry);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Form;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping\ClassMetadataInfo;
|
||||
use Doctrine\ORM\Mapping\MappingException as LegacyMappingException;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Doctrine\Persistence\Mapping\MappingException;
|
||||
use Doctrine\Persistence\Proxy;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateIntervalType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TimeType;
|
||||
use Symfony\Component\Form\FormTypeGuesserInterface;
|
||||
use Symfony\Component\Form\Guess\Guess;
|
||||
use Symfony\Component\Form\Guess\TypeGuess;
|
||||
use Symfony\Component\Form\Guess\ValueGuess;
|
||||
|
||||
class DoctrineOrmTypeGuesser implements FormTypeGuesserInterface
|
||||
{
|
||||
protected $registry;
|
||||
|
||||
private array $cache = [];
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
public function guessType(string $class, string $property): ?TypeGuess
|
||||
{
|
||||
if (!$ret = $this->getMetadata($class)) {
|
||||
return new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE);
|
||||
}
|
||||
|
||||
[$metadata, $name] = $ret;
|
||||
|
||||
if ($metadata->hasAssociation($property)) {
|
||||
$multiple = $metadata->isCollectionValuedAssociation($property);
|
||||
$mapping = $metadata->getAssociationMapping($property);
|
||||
|
||||
return new TypeGuess(EntityType::class, ['em' => $name, 'class' => $mapping['targetEntity'], 'multiple' => $multiple], Guess::HIGH_CONFIDENCE);
|
||||
}
|
||||
|
||||
return match ($metadata->getTypeOfField($property)) {
|
||||
'array', // DBAL < 4
|
||||
Types::SIMPLE_ARRAY => new TypeGuess(CollectionType::class, [], Guess::MEDIUM_CONFIDENCE),
|
||||
Types::BOOLEAN => new TypeGuess(CheckboxType::class, [], Guess::HIGH_CONFIDENCE),
|
||||
Types::DATETIME_MUTABLE,
|
||||
Types::DATETIMETZ_MUTABLE,
|
||||
'vardatetime' => new TypeGuess(DateTimeType::class, [], Guess::HIGH_CONFIDENCE),
|
||||
Types::DATETIME_IMMUTABLE,
|
||||
Types::DATETIMETZ_IMMUTABLE => new TypeGuess(DateTimeType::class, ['input' => 'datetime_immutable'], Guess::HIGH_CONFIDENCE),
|
||||
Types::DATEINTERVAL => new TypeGuess(DateIntervalType::class, [], Guess::HIGH_CONFIDENCE),
|
||||
Types::DATE_MUTABLE => new TypeGuess(DateType::class, [], Guess::HIGH_CONFIDENCE),
|
||||
Types::DATE_IMMUTABLE => new TypeGuess(DateType::class, ['input' => 'datetime_immutable'], Guess::HIGH_CONFIDENCE),
|
||||
Types::TIME_MUTABLE => new TypeGuess(TimeType::class, [], Guess::HIGH_CONFIDENCE),
|
||||
Types::TIME_IMMUTABLE => new TypeGuess(TimeType::class, ['input' => 'datetime_immutable'], Guess::HIGH_CONFIDENCE),
|
||||
Types::DECIMAL => new TypeGuess(NumberType::class, ['input' => 'string'], Guess::MEDIUM_CONFIDENCE),
|
||||
Types::FLOAT => new TypeGuess(NumberType::class, [], Guess::MEDIUM_CONFIDENCE),
|
||||
Types::INTEGER,
|
||||
Types::BIGINT,
|
||||
Types::SMALLINT => new TypeGuess(IntegerType::class, [], Guess::MEDIUM_CONFIDENCE),
|
||||
Types::STRING => new TypeGuess(TextType::class, [], Guess::MEDIUM_CONFIDENCE),
|
||||
Types::TEXT => new TypeGuess(TextareaType::class, [], Guess::MEDIUM_CONFIDENCE),
|
||||
default => new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE),
|
||||
};
|
||||
}
|
||||
|
||||
public function guessRequired(string $class, string $property): ?ValueGuess
|
||||
{
|
||||
$classMetadatas = $this->getMetadata($class);
|
||||
|
||||
if (!$classMetadatas) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var ClassMetadataInfo $classMetadata */
|
||||
$classMetadata = $classMetadatas[0];
|
||||
|
||||
// Check whether the field exists and is nullable or not
|
||||
if (isset($classMetadata->fieldMappings[$property])) {
|
||||
if (!$classMetadata->isNullable($property) && Types::BOOLEAN !== $classMetadata->getTypeOfField($property)) {
|
||||
return new ValueGuess(true, Guess::HIGH_CONFIDENCE);
|
||||
}
|
||||
|
||||
return new ValueGuess(false, Guess::MEDIUM_CONFIDENCE);
|
||||
}
|
||||
|
||||
// Check whether the association exists, is a to-one association and its
|
||||
// join column is nullable or not
|
||||
if ($classMetadata->isAssociationWithSingleJoinColumn($property)) {
|
||||
$mapping = $classMetadata->getAssociationMapping($property);
|
||||
|
||||
if (!isset($mapping['joinColumns'][0]['nullable'])) {
|
||||
// The "nullable" option defaults to true, in that case the
|
||||
// field should not be required.
|
||||
return new ValueGuess(false, Guess::HIGH_CONFIDENCE);
|
||||
}
|
||||
|
||||
return new ValueGuess(!$mapping['joinColumns'][0]['nullable'], Guess::HIGH_CONFIDENCE);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function guessMaxLength(string $class, string $property): ?ValueGuess
|
||||
{
|
||||
$ret = $this->getMetadata($class);
|
||||
if ($ret && isset($ret[0]->fieldMappings[$property]) && !$ret[0]->hasAssociation($property)) {
|
||||
$mapping = $ret[0]->getFieldMapping($property);
|
||||
|
||||
if (isset($mapping['length'])) {
|
||||
return new ValueGuess($mapping['length'], Guess::HIGH_CONFIDENCE);
|
||||
}
|
||||
|
||||
if (\in_array($ret[0]->getTypeOfField($property), [Types::DECIMAL, Types::FLOAT])) {
|
||||
return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function guessPattern(string $class, string $property): ?ValueGuess
|
||||
{
|
||||
$ret = $this->getMetadata($class);
|
||||
if ($ret && isset($ret[0]->fieldMappings[$property]) && !$ret[0]->hasAssociation($property)) {
|
||||
if (\in_array($ret[0]->getTypeOfField($property), [Types::DECIMAL, Types::FLOAT])) {
|
||||
return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getMetadata(string $class)
|
||||
{
|
||||
// normalize class name
|
||||
$class = self::getRealClass(ltrim($class, '\\'));
|
||||
|
||||
if (\array_key_exists($class, $this->cache)) {
|
||||
return $this->cache[$class];
|
||||
}
|
||||
|
||||
$this->cache[$class] = null;
|
||||
foreach ($this->registry->getManagers() as $name => $em) {
|
||||
try {
|
||||
return $this->cache[$class] = [$em->getClassMetadata($class), $name];
|
||||
} catch (MappingException) {
|
||||
// not an entity or mapped super class
|
||||
} catch (LegacyMappingException) {
|
||||
// not an entity or mapped super class, using Doctrine ORM 2.2
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function getRealClass(string $class): string
|
||||
{
|
||||
if (false === $pos = strrpos($class, '\\'.Proxy::MARKER.'\\')) {
|
||||
return $class;
|
||||
}
|
||||
|
||||
return substr($class, $pos + Proxy::MARKER_LENGTH + 2);
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Form\EventListener;
|
||||
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
|
||||
/**
|
||||
* Merge changes from the request to a Doctrine\Common\Collections\Collection instance.
|
||||
*
|
||||
* This works with ORM, MongoDB and CouchDB instances of the collection interface.
|
||||
*
|
||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||
*
|
||||
* @see Collection
|
||||
*/
|
||||
class MergeDoctrineCollectionListener implements EventSubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
// Higher priority than core MergeCollectionListener so that this one
|
||||
// is called before
|
||||
return [
|
||||
FormEvents::SUBMIT => [
|
||||
['onSubmit', 5],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function onSubmit(FormEvent $event)
|
||||
{
|
||||
$collection = $event->getForm()->getData();
|
||||
$data = $event->getData();
|
||||
|
||||
// If all items were removed, call clear which has a higher
|
||||
// performance on persistent collections
|
||||
if ($collection instanceof Collection && 0 === \count($data)) {
|
||||
$collection->clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Form\Type;
|
||||
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader;
|
||||
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface;
|
||||
use Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader;
|
||||
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
|
||||
use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\ChoiceList\ChoiceList;
|
||||
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
|
||||
use Symfony\Component\Form\Exception\RuntimeException;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
abstract class DoctrineType extends AbstractType implements ResetInterface
|
||||
{
|
||||
/**
|
||||
* @var ManagerRegistry
|
||||
*/
|
||||
protected $registry;
|
||||
|
||||
/**
|
||||
* @var IdReader[]
|
||||
*/
|
||||
private array $idReaders = [];
|
||||
|
||||
/**
|
||||
* @var EntityLoaderInterface[]
|
||||
*/
|
||||
private array $entityLoaders = [];
|
||||
|
||||
/**
|
||||
* Creates the label for a choice.
|
||||
*
|
||||
* For backwards compatibility, objects are cast to strings by default.
|
||||
*
|
||||
* @internal This method is public to be usable as callback. It should not
|
||||
* be used in user code.
|
||||
*/
|
||||
public static function createChoiceLabel(object $choice): string
|
||||
{
|
||||
return (string) $choice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the field name for a choice.
|
||||
*
|
||||
* This method is used to generate field names if the underlying object has
|
||||
* a single-column integer ID. In that case, the value of the field is
|
||||
* the ID of the object. That ID is also used as field name.
|
||||
*
|
||||
* @param string $value The choice value. Corresponds to the object's ID here.
|
||||
*
|
||||
* @internal This method is public to be usable as callback. It should not
|
||||
* be used in user code.
|
||||
*/
|
||||
public static function createChoiceName(object $choice, int|string $key, string $value): string
|
||||
{
|
||||
return str_replace('-', '_', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets important parts from QueryBuilder that will allow to cache its results.
|
||||
* For instance in ORM two query builders with an equal SQL string and
|
||||
* equal parameters are considered to be equal.
|
||||
*
|
||||
* @param object $queryBuilder A query builder, type declaration is not present here as there
|
||||
* is no common base class for the different implementations
|
||||
*
|
||||
* @internal This method is public to be usable as callback. It should not
|
||||
* be used in user code.
|
||||
*/
|
||||
public function getQueryBuilderPartsForCachingHash(object $queryBuilder): ?array
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
if ($options['multiple'] && interface_exists(Collection::class)) {
|
||||
$builder
|
||||
->addEventSubscriber(new MergeDoctrineCollectionListener())
|
||||
->addViewTransformer(new CollectionToArrayTransformer(), true)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$choiceLoader = function (Options $options) {
|
||||
// Unless the choices are given explicitly, load them on demand
|
||||
if (null === $options['choices']) {
|
||||
// If there is no QueryBuilder we can safely cache
|
||||
$vary = [$options['em'], $options['class']];
|
||||
|
||||
// also if concrete Type can return important QueryBuilder parts to generate
|
||||
// hash key we go for it as well, otherwise fallback on the instance
|
||||
if ($options['query_builder']) {
|
||||
$vary[] = $this->getQueryBuilderPartsForCachingHash($options['query_builder']) ?? $options['query_builder'];
|
||||
}
|
||||
|
||||
return ChoiceList::loader($this, new DoctrineChoiceLoader(
|
||||
$options['em'],
|
||||
$options['class'],
|
||||
$options['id_reader'],
|
||||
$this->getCachedEntityLoader(
|
||||
$options['em'],
|
||||
$options['query_builder'] ?? $options['em']->getRepository($options['class'])->createQueryBuilder('e'),
|
||||
$options['class'],
|
||||
$vary
|
||||
)
|
||||
), $vary);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
$choiceName = function (Options $options) {
|
||||
// If the object has a single-column, numeric ID, use that ID as
|
||||
// field name. We can only use numeric IDs as names, as we cannot
|
||||
// guarantee that a non-numeric ID contains a valid form name
|
||||
if ($options['id_reader'] instanceof IdReader && $options['id_reader']->isIntId()) {
|
||||
return ChoiceList::fieldName($this, [__CLASS__, 'createChoiceName']);
|
||||
}
|
||||
|
||||
// Otherwise, an incrementing integer is used as name automatically
|
||||
return null;
|
||||
};
|
||||
|
||||
// The choices are always indexed by ID (see "choices" normalizer
|
||||
// and DoctrineChoiceLoader), unless the ID is composite. Then they
|
||||
// are indexed by an incrementing integer.
|
||||
// Use the ID/incrementing integer as choice value.
|
||||
$choiceValue = function (Options $options) {
|
||||
// If the entity has a single-column ID, use that ID as value
|
||||
if ($options['id_reader'] instanceof IdReader && $options['id_reader']->isSingleId()) {
|
||||
return ChoiceList::value($this, $options['id_reader']->getIdValue(...), $options['id_reader']);
|
||||
}
|
||||
|
||||
// Otherwise, an incrementing integer is used as value automatically
|
||||
return null;
|
||||
};
|
||||
|
||||
$emNormalizer = function (Options $options, $em) {
|
||||
if (null !== $em) {
|
||||
if ($em instanceof ObjectManager) {
|
||||
return $em;
|
||||
}
|
||||
|
||||
return $this->registry->getManager($em);
|
||||
}
|
||||
|
||||
$em = $this->registry->getManagerForClass($options['class']);
|
||||
|
||||
if (null === $em) {
|
||||
throw new RuntimeException(sprintf('Class "%s" seems not to be a managed Doctrine entity. Did you forget to map it?', $options['class']));
|
||||
}
|
||||
|
||||
return $em;
|
||||
};
|
||||
|
||||
// Invoke the query builder closure so that we can cache choice lists
|
||||
// for equal query builders
|
||||
$queryBuilderNormalizer = function (Options $options, $queryBuilder) {
|
||||
if (\is_callable($queryBuilder)) {
|
||||
$queryBuilder = $queryBuilder($options['em']->getRepository($options['class']));
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
};
|
||||
|
||||
// Set the "id_reader" option via the normalizer. This option is not
|
||||
// supposed to be set by the user.
|
||||
// The ID reader is a utility that is needed to read the object IDs
|
||||
// when generating the field values. The callback generating the
|
||||
// field values has no access to the object manager or the class
|
||||
// of the field, so we store that information in the reader.
|
||||
// The reader is cached so that two choice lists for the same class
|
||||
// (and hence with the same reader) can successfully be cached.
|
||||
$idReaderNormalizer = fn (Options $options) => $this->getCachedIdReader($options['em'], $options['class']);
|
||||
|
||||
$resolver->setDefaults([
|
||||
'em' => null,
|
||||
'query_builder' => null,
|
||||
'choices' => null,
|
||||
'choice_loader' => $choiceLoader,
|
||||
'choice_label' => ChoiceList::label($this, [__CLASS__, 'createChoiceLabel']),
|
||||
'choice_name' => $choiceName,
|
||||
'choice_value' => $choiceValue,
|
||||
'id_reader' => null, // internal
|
||||
'choice_translation_domain' => false,
|
||||
]);
|
||||
|
||||
$resolver->setRequired(['class']);
|
||||
|
||||
$resolver->setNormalizer('em', $emNormalizer);
|
||||
$resolver->setNormalizer('query_builder', $queryBuilderNormalizer);
|
||||
$resolver->setNormalizer('id_reader', $idReaderNormalizer);
|
||||
|
||||
$resolver->setAllowedTypes('em', ['null', 'string', ObjectManager::class]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the default loader object.
|
||||
*/
|
||||
abstract public function getLoader(ObjectManager $manager, object $queryBuilder, string $class): EntityLoaderInterface;
|
||||
|
||||
public function getParent(): string
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
$this->idReaders = [];
|
||||
$this->entityLoaders = [];
|
||||
}
|
||||
|
||||
private function getCachedIdReader(ObjectManager $manager, string $class): ?IdReader
|
||||
{
|
||||
$hash = CachingFactoryDecorator::generateHash([$manager, $class]);
|
||||
|
||||
if (isset($this->idReaders[$hash])) {
|
||||
return $this->idReaders[$hash];
|
||||
}
|
||||
|
||||
$idReader = new IdReader($manager, $manager->getClassMetadata($class));
|
||||
|
||||
// don't cache the instance for composite ids that cannot be optimized
|
||||
return $this->idReaders[$hash] = $idReader->isSingleId() ? $idReader : null;
|
||||
}
|
||||
|
||||
private function getCachedEntityLoader(ObjectManager $manager, object $queryBuilder, string $class, array $vary): EntityLoaderInterface
|
||||
{
|
||||
$hash = CachingFactoryDecorator::generateHash($vary);
|
||||
|
||||
return $this->entityLoaders[$hash] ??= $this->getLoader($manager, $queryBuilder, $class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Form\Type;
|
||||
|
||||
use Doctrine\ORM\Query\Parameter;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader;
|
||||
use Symfony\Component\Form\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class EntityType extends DoctrineType
|
||||
{
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
parent::configureOptions($resolver);
|
||||
|
||||
// Invoke the query builder closure so that we can cache choice lists
|
||||
// for equal query builders
|
||||
$queryBuilderNormalizer = function (Options $options, $queryBuilder) {
|
||||
if (\is_callable($queryBuilder)) {
|
||||
$queryBuilder = $queryBuilder($options['em']->getRepository($options['class']));
|
||||
|
||||
if (null !== $queryBuilder && !$queryBuilder instanceof QueryBuilder) {
|
||||
throw new UnexpectedTypeException($queryBuilder, QueryBuilder::class);
|
||||
}
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
};
|
||||
|
||||
$resolver->setNormalizer('query_builder', $queryBuilderNormalizer);
|
||||
$resolver->setAllowedTypes('query_builder', ['null', 'callable', QueryBuilder::class]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the default loader object.
|
||||
*
|
||||
* @param QueryBuilder $queryBuilder
|
||||
*/
|
||||
public function getLoader(ObjectManager $manager, object $queryBuilder, string $class): ORMQueryBuilderLoader
|
||||
{
|
||||
if (!$queryBuilder instanceof QueryBuilder) {
|
||||
throw new \TypeError(sprintf('Expected an instance of "%s", but got "%s".', QueryBuilder::class, get_debug_type($queryBuilder)));
|
||||
}
|
||||
|
||||
return new ORMQueryBuilderLoader($queryBuilder);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'entity';
|
||||
}
|
||||
|
||||
/**
|
||||
* We consider two query builders with an equal SQL string and
|
||||
* equal parameters to be equal.
|
||||
*
|
||||
* @param QueryBuilder $queryBuilder
|
||||
*
|
||||
* @internal This method is public to be usable as callback. It should not
|
||||
* be used in user code.
|
||||
*/
|
||||
public function getQueryBuilderPartsForCachingHash(object $queryBuilder): ?array
|
||||
{
|
||||
if (!$queryBuilder instanceof QueryBuilder) {
|
||||
throw new \TypeError(sprintf('Expected an instance of "%s", but got "%s".', QueryBuilder::class, get_debug_type($queryBuilder)));
|
||||
}
|
||||
|
||||
return [
|
||||
$queryBuilder->getQuery()->getSQL(),
|
||||
array_map($this->parameterToArray(...), $queryBuilder->getParameters()->toArray()),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a query parameter to an array.
|
||||
*/
|
||||
private function parameterToArray(Parameter $parameter): array
|
||||
{
|
||||
return [$parameter->getName(), $parameter->getType(), $parameter->getValue()];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\IdGenerator;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Id\AbstractIdGenerator;
|
||||
use Symfony\Component\Uid\Factory\UlidFactory;
|
||||
use Symfony\Component\Uid\Ulid;
|
||||
|
||||
final class UlidGenerator extends AbstractIdGenerator
|
||||
{
|
||||
private ?UlidFactory $factory;
|
||||
|
||||
public function __construct(?UlidFactory $factory = null)
|
||||
{
|
||||
$this->factory = $factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* doctrine/orm < 2.11 BC layer.
|
||||
*/
|
||||
public function generate(EntityManager $em, $entity): Ulid
|
||||
{
|
||||
return $this->generateId($em, $entity);
|
||||
}
|
||||
|
||||
public function generateId(EntityManagerInterface $em, $entity): Ulid
|
||||
{
|
||||
if ($this->factory) {
|
||||
return $this->factory->create();
|
||||
}
|
||||
|
||||
return new Ulid();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\IdGenerator;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Id\AbstractIdGenerator;
|
||||
use Symfony\Component\Uid\Factory\NameBasedUuidFactory;
|
||||
use Symfony\Component\Uid\Factory\RandomBasedUuidFactory;
|
||||
use Symfony\Component\Uid\Factory\TimeBasedUuidFactory;
|
||||
use Symfony\Component\Uid\Factory\UuidFactory;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
final class UuidGenerator extends AbstractIdGenerator
|
||||
{
|
||||
private UuidFactory $protoFactory;
|
||||
private UuidFactory|NameBasedUuidFactory|RandomBasedUuidFactory|TimeBasedUuidFactory $factory;
|
||||
private ?string $entityGetter = null;
|
||||
|
||||
public function __construct(?UuidFactory $factory = null)
|
||||
{
|
||||
$this->protoFactory = $this->factory = $factory ?? new UuidFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* doctrine/orm < 2.11 BC layer.
|
||||
*/
|
||||
public function generate(EntityManager $em, $entity): Uuid
|
||||
{
|
||||
return $this->generateId($em, $entity);
|
||||
}
|
||||
|
||||
public function generateId(EntityManagerInterface $em, $entity): Uuid
|
||||
{
|
||||
if (null !== $this->entityGetter) {
|
||||
if (\is_callable([$entity, $this->entityGetter])) {
|
||||
return $this->factory->create($entity->{$this->entityGetter}());
|
||||
}
|
||||
|
||||
return $this->factory->create($entity->{$this->entityGetter});
|
||||
}
|
||||
|
||||
return $this->factory->create();
|
||||
}
|
||||
|
||||
public function nameBased(string $entityGetter, Uuid|string|null $namespace = null): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->factory = $clone->protoFactory->nameBased($namespace);
|
||||
$clone->entityGetter = $entityGetter;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function randomBased(): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->factory = $clone->protoFactory->randomBased();
|
||||
$clone->entityGetter = null;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function timeBased(Uuid|string|null $node = null): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->factory = $clone->protoFactory->timeBased($node);
|
||||
$clone->entityGetter = null;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2004-present Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Logger;
|
||||
|
||||
use Doctrine\DBAL\Logging\SQLLogger;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class DbalLogger implements SQLLogger
|
||||
{
|
||||
public const MAX_STRING_LENGTH = 32;
|
||||
public const BINARY_DATA_VALUE = '(binary value)';
|
||||
|
||||
protected $logger;
|
||||
protected $stopwatch;
|
||||
|
||||
public function __construct(?LoggerInterface $logger = null, ?Stopwatch $stopwatch = null)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
$this->stopwatch = $stopwatch;
|
||||
}
|
||||
|
||||
public function startQuery($sql, ?array $params = null, ?array $types = null): void
|
||||
{
|
||||
$this->stopwatch?->start('doctrine', 'doctrine');
|
||||
|
||||
if (null !== $this->logger) {
|
||||
$this->log($sql, null === $params ? [] : $this->normalizeParams($params));
|
||||
}
|
||||
}
|
||||
|
||||
public function stopQuery(): void
|
||||
{
|
||||
$this->stopwatch?->stop('doctrine');
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function log(string $message, array $params)
|
||||
{
|
||||
$this->logger->debug($message, $params);
|
||||
}
|
||||
|
||||
private function normalizeParams(array $params): array
|
||||
{
|
||||
foreach ($params as $index => $param) {
|
||||
// normalize recursively
|
||||
if (\is_array($param)) {
|
||||
$params[$index] = $this->normalizeParams($param);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!\is_string($params[$index])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// non utf-8 strings break json encoding
|
||||
if (!preg_match('//u', $params[$index])) {
|
||||
$params[$index] = self::BINARY_DATA_VALUE;
|
||||
continue;
|
||||
}
|
||||
|
||||
// detect if the too long string must be shorten
|
||||
if (self::MAX_STRING_LENGTH < mb_strlen($params[$index], 'UTF-8')) {
|
||||
$params[$index] = mb_substr($params[$index], 0, self::MAX_STRING_LENGTH - 6, 'UTF-8').' [...]';
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine;
|
||||
|
||||
use Doctrine\Persistence\AbstractManagerRegistry;
|
||||
use ProxyManager\Proxy\GhostObjectInterface;
|
||||
use ProxyManager\Proxy\LazyLoadingInterface;
|
||||
use Symfony\Component\DependencyInjection\Container;
|
||||
use Symfony\Component\VarExporter\LazyObjectInterface;
|
||||
|
||||
/**
|
||||
* References Doctrine connections and entity/document managers.
|
||||
*
|
||||
* @author Lukas Kahwe Smith <smith@pooteeweet.org>
|
||||
*/
|
||||
abstract class ManagerRegistry extends AbstractManagerRegistry
|
||||
{
|
||||
/**
|
||||
* @var Container
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
protected function getService($name): object
|
||||
{
|
||||
return $this->container->get($name);
|
||||
}
|
||||
|
||||
protected function resetService($name): void
|
||||
{
|
||||
if (!$this->container->initialized($name)) {
|
||||
return;
|
||||
}
|
||||
$manager = $this->container->get($name);
|
||||
|
||||
if ($manager instanceof LazyObjectInterface) {
|
||||
if (!$manager->resetLazyObject()) {
|
||||
throw new \LogicException(sprintf('Resetting a non-lazy manager service is not supported. Declare the "%s" service as lazy.', $name));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
if (!$manager instanceof LazyLoadingInterface) {
|
||||
throw new \LogicException(sprintf('Resetting a non-lazy manager service is not supported. Declare the "%s" service as lazy.', $name));
|
||||
}
|
||||
if ($manager instanceof GhostObjectInterface) {
|
||||
throw new \LogicException('Resetting a lazy-ghost-object manager service is not supported.');
|
||||
}
|
||||
$manager->setProxyInitializer(\Closure::bind(
|
||||
function (&$wrappedInstance, LazyLoadingInterface $manager) use ($name) {
|
||||
if (isset($this->aliases[$name])) {
|
||||
$name = $this->aliases[$name];
|
||||
}
|
||||
if (isset($this->fileMap[$name])) {
|
||||
$wrappedInstance = $this->load($this->fileMap[$name], false);
|
||||
} else {
|
||||
$wrappedInstance = $this->{$this->methodMap[$name]}(false);
|
||||
}
|
||||
|
||||
$manager->setProxyInitializer(null);
|
||||
|
||||
return true;
|
||||
},
|
||||
$this->container,
|
||||
Container::class
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Messenger;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
||||
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
|
||||
use Symfony\Component\Messenger\Middleware\StackInterface;
|
||||
|
||||
/**
|
||||
* @author Konstantin Myakshin <molodchick@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract class AbstractDoctrineMiddleware implements MiddlewareInterface
|
||||
{
|
||||
protected $managerRegistry;
|
||||
protected $entityManagerName;
|
||||
|
||||
public function __construct(ManagerRegistry $managerRegistry, ?string $entityManagerName = null)
|
||||
{
|
||||
$this->managerRegistry = $managerRegistry;
|
||||
$this->entityManagerName = $entityManagerName;
|
||||
}
|
||||
|
||||
final public function handle(Envelope $envelope, StackInterface $stack): Envelope
|
||||
{
|
||||
try {
|
||||
$entityManager = $this->managerRegistry->getManager($this->entityManagerName);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
throw new UnrecoverableMessageHandlingException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
return $this->handleForManager($entityManager, $envelope, $stack);
|
||||
}
|
||||
|
||||
abstract protected function handleForManager(EntityManagerInterface $entityManager, Envelope $envelope, StackInterface $stack): Envelope;
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Messenger;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
|
||||
|
||||
/**
|
||||
* Clears entity managers between messages being handled to avoid outdated data.
|
||||
*
|
||||
* @author Ryan Weaver <ryan@symfonycasts.com>
|
||||
*/
|
||||
class DoctrineClearEntityManagerWorkerSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private ManagerRegistry $managerRegistry;
|
||||
|
||||
public function __construct(ManagerRegistry $managerRegistry)
|
||||
{
|
||||
$this->managerRegistry = $managerRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function onWorkerMessageHandled()
|
||||
{
|
||||
$this->clearEntityManagers();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function onWorkerMessageFailed()
|
||||
{
|
||||
$this->clearEntityManagers();
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
WorkerMessageHandledEvent::class => 'onWorkerMessageHandled',
|
||||
WorkerMessageFailedEvent::class => 'onWorkerMessageFailed',
|
||||
];
|
||||
}
|
||||
|
||||
private function clearEntityManagers(): void
|
||||
{
|
||||
foreach ($this->managerRegistry->getManagers() as $manager) {
|
||||
$manager->clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Messenger;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Middleware\StackInterface;
|
||||
use Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp;
|
||||
|
||||
/**
|
||||
* Closes connection and therefore saves number of connections.
|
||||
*
|
||||
* @author Fuong <insidestyles@gmail.com>
|
||||
*/
|
||||
class DoctrineCloseConnectionMiddleware extends AbstractDoctrineMiddleware
|
||||
{
|
||||
protected function handleForManager(EntityManagerInterface $entityManager, Envelope $envelope, StackInterface $stack): Envelope
|
||||
{
|
||||
try {
|
||||
$connection = $entityManager->getConnection();
|
||||
|
||||
return $stack->next()->handle($envelope, $stack);
|
||||
} finally {
|
||||
if (null !== $envelope->last(ConsumedByWorkerStamp::class)) {
|
||||
$connection->close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Messenger;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Middleware\StackInterface;
|
||||
|
||||
/**
|
||||
* Middleware to log when transaction has been left open.
|
||||
*
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class DoctrineOpenTransactionLoggerMiddleware extends AbstractDoctrineMiddleware
|
||||
{
|
||||
private $logger;
|
||||
/** @var bool */
|
||||
private $isHandling = false;
|
||||
|
||||
public function __construct(ManagerRegistry $managerRegistry, ?string $entityManagerName = null, ?LoggerInterface $logger = null)
|
||||
{
|
||||
parent::__construct($managerRegistry, $entityManagerName);
|
||||
|
||||
$this->logger = $logger ?? new NullLogger();
|
||||
}
|
||||
|
||||
protected function handleForManager(EntityManagerInterface $entityManager, Envelope $envelope, StackInterface $stack): Envelope
|
||||
{
|
||||
if ($this->isHandling) {
|
||||
return $stack->next()->handle($envelope, $stack);
|
||||
}
|
||||
|
||||
$this->isHandling = true;
|
||||
|
||||
try {
|
||||
return $stack->next()->handle($envelope, $stack);
|
||||
} finally {
|
||||
if ($entityManager->getConnection()->isTransactionActive()) {
|
||||
$this->logger->error('A handler opened a transaction but did not close it.', [
|
||||
'message' => $envelope->getMessage(),
|
||||
]);
|
||||
}
|
||||
$this->isHandling = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Messenger;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Exception as DBALException;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Middleware\StackInterface;
|
||||
use Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp;
|
||||
|
||||
/**
|
||||
* Checks whether the connection is still open or reconnects otherwise.
|
||||
*
|
||||
* @author Fuong <insidestyles@gmail.com>
|
||||
*/
|
||||
class DoctrinePingConnectionMiddleware extends AbstractDoctrineMiddleware
|
||||
{
|
||||
protected function handleForManager(EntityManagerInterface $entityManager, Envelope $envelope, StackInterface $stack): Envelope
|
||||
{
|
||||
if (null !== $envelope->last(ConsumedByWorkerStamp::class)) {
|
||||
$this->pingConnection($entityManager);
|
||||
}
|
||||
|
||||
return $stack->next()->handle($envelope, $stack);
|
||||
}
|
||||
|
||||
private function pingConnection(EntityManagerInterface $entityManager): void
|
||||
{
|
||||
$connection = $entityManager->getConnection();
|
||||
|
||||
try {
|
||||
$this->executeDummySql($connection);
|
||||
} catch (DBALException) {
|
||||
$connection->close();
|
||||
// Attempt to reestablish the lazy connection by sending another query.
|
||||
$this->executeDummySql($connection);
|
||||
}
|
||||
|
||||
if (!$entityManager->isOpen()) {
|
||||
$this->managerRegistry->resetManager($this->entityManagerName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DBALException
|
||||
*/
|
||||
private function executeDummySql(Connection $connection): void
|
||||
{
|
||||
$connection->executeQuery($connection->getDatabasePlatform()->getDummySelectSQL());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Messenger;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Exception\HandlerFailedException;
|
||||
use Symfony\Component\Messenger\Middleware\StackInterface;
|
||||
use Symfony\Component\Messenger\Stamp\HandledStamp;
|
||||
|
||||
/**
|
||||
* Wraps all handlers in a single doctrine transaction.
|
||||
*
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
*/
|
||||
class DoctrineTransactionMiddleware extends AbstractDoctrineMiddleware
|
||||
{
|
||||
protected function handleForManager(EntityManagerInterface $entityManager, Envelope $envelope, StackInterface $stack): Envelope
|
||||
{
|
||||
$entityManager->getConnection()->beginTransaction();
|
||||
try {
|
||||
$envelope = $stack->next()->handle($envelope, $stack);
|
||||
$entityManager->flush();
|
||||
$entityManager->getConnection()->commit();
|
||||
|
||||
return $envelope;
|
||||
} catch (\Throwable $exception) {
|
||||
$entityManager->getConnection()->rollBack();
|
||||
|
||||
if ($exception instanceof HandlerFailedException) {
|
||||
// Remove all HandledStamp from the envelope so the retry will execute all handlers again.
|
||||
// When a handler fails, the queries of allegedly successful previous handlers just got rolled back.
|
||||
throw new HandlerFailedException($exception->getEnvelope()->withoutAll(HandledStamp::class), $exception->getNestedExceptions());
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Middleware\Debug;
|
||||
|
||||
use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
|
||||
use Doctrine\DBAL\Driver\Middleware\AbstractConnectionMiddleware;
|
||||
use Doctrine\DBAL\Driver\Result;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
|
||||
/**
|
||||
* @author Laurent VOULLEMIER <laurent.voullemier@gmail.com>
|
||||
* @author Alexander M. Turek <me@derrabus.de>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Connection extends AbstractConnectionMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
ConnectionInterface $connection,
|
||||
private DebugDataHolder $debugDataHolder,
|
||||
private ?Stopwatch $stopwatch,
|
||||
private string $connectionName,
|
||||
) {
|
||||
parent::__construct($connection);
|
||||
}
|
||||
|
||||
public function prepare(string $sql): Statement
|
||||
{
|
||||
return new Statement(
|
||||
parent::prepare($sql),
|
||||
$this->debugDataHolder,
|
||||
$this->connectionName,
|
||||
$sql,
|
||||
$this->stopwatch,
|
||||
);
|
||||
}
|
||||
|
||||
public function query(string $sql): Result
|
||||
{
|
||||
$this->debugDataHolder->addQuery($this->connectionName, $query = new Query($sql));
|
||||
|
||||
$this->stopwatch?->start('doctrine', 'doctrine');
|
||||
$query->start();
|
||||
|
||||
try {
|
||||
return parent::query($sql);
|
||||
} finally {
|
||||
$query->stop();
|
||||
$this->stopwatch?->stop('doctrine');
|
||||
}
|
||||
}
|
||||
|
||||
public function exec(string $sql): int
|
||||
{
|
||||
$this->debugDataHolder->addQuery($this->connectionName, $query = new Query($sql));
|
||||
|
||||
$this->stopwatch?->start('doctrine', 'doctrine');
|
||||
$query->start();
|
||||
|
||||
try {
|
||||
$affectedRows = parent::exec($sql);
|
||||
} finally {
|
||||
$query->stop();
|
||||
$this->stopwatch?->stop('doctrine');
|
||||
}
|
||||
|
||||
return $affectedRows;
|
||||
}
|
||||
|
||||
public function beginTransaction(): void
|
||||
{
|
||||
$query = new Query('"START TRANSACTION"');
|
||||
$this->debugDataHolder->addQuery($this->connectionName, $query);
|
||||
|
||||
$this->stopwatch?->start('doctrine', 'doctrine');
|
||||
$query->start();
|
||||
|
||||
try {
|
||||
parent::beginTransaction();
|
||||
} finally {
|
||||
$query->stop();
|
||||
$this->stopwatch?->stop('doctrine');
|
||||
}
|
||||
}
|
||||
|
||||
public function commit(): void
|
||||
{
|
||||
$query = new Query('"COMMIT"');
|
||||
$this->debugDataHolder->addQuery($this->connectionName, $query);
|
||||
|
||||
$this->stopwatch?->start('doctrine', 'doctrine');
|
||||
$query->start();
|
||||
|
||||
try {
|
||||
parent::commit();
|
||||
} finally {
|
||||
$query->stop();
|
||||
$this->stopwatch?->stop('doctrine');
|
||||
}
|
||||
}
|
||||
|
||||
public function rollBack(): void
|
||||
{
|
||||
$query = new Query('"ROLLBACK"');
|
||||
$this->debugDataHolder->addQuery($this->connectionName, $query);
|
||||
|
||||
$this->stopwatch?->start('doctrine', 'doctrine');
|
||||
$query->start();
|
||||
|
||||
try {
|
||||
parent::rollBack();
|
||||
} finally {
|
||||
$query->stop();
|
||||
$this->stopwatch?->stop('doctrine');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Middleware\Debug\DBAL3;
|
||||
|
||||
use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
|
||||
use Doctrine\DBAL\Driver\Middleware\AbstractConnectionMiddleware;
|
||||
use Doctrine\DBAL\Driver\Result;
|
||||
use Symfony\Bridge\Doctrine\Middleware\Debug\DebugDataHolder;
|
||||
use Symfony\Bridge\Doctrine\Middleware\Debug\Query;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
|
||||
/**
|
||||
* @author Laurent VOULLEMIER <laurent.voullemier@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Connection extends AbstractConnectionMiddleware
|
||||
{
|
||||
private int $nestingLevel = 0;
|
||||
|
||||
public function __construct(
|
||||
ConnectionInterface $connection,
|
||||
private DebugDataHolder $debugDataHolder,
|
||||
private ?Stopwatch $stopwatch,
|
||||
private string $connectionName,
|
||||
) {
|
||||
parent::__construct($connection);
|
||||
}
|
||||
|
||||
public function prepare(string $sql): Statement
|
||||
{
|
||||
return new Statement(
|
||||
parent::prepare($sql),
|
||||
$this->debugDataHolder,
|
||||
$this->connectionName,
|
||||
$sql,
|
||||
$this->stopwatch,
|
||||
);
|
||||
}
|
||||
|
||||
public function query(string $sql): Result
|
||||
{
|
||||
$this->debugDataHolder->addQuery($this->connectionName, $query = new Query($sql));
|
||||
|
||||
$this->stopwatch?->start('doctrine', 'doctrine');
|
||||
$query->start();
|
||||
|
||||
try {
|
||||
return parent::query($sql);
|
||||
} finally {
|
||||
$query->stop();
|
||||
$this->stopwatch?->stop('doctrine');
|
||||
}
|
||||
}
|
||||
|
||||
public function exec(string $sql): int
|
||||
{
|
||||
$this->debugDataHolder->addQuery($this->connectionName, $query = new Query($sql));
|
||||
|
||||
$this->stopwatch?->start('doctrine', 'doctrine');
|
||||
$query->start();
|
||||
|
||||
try {
|
||||
return parent::exec($sql);
|
||||
} finally {
|
||||
$query->stop();
|
||||
$this->stopwatch?->stop('doctrine');
|
||||
}
|
||||
}
|
||||
|
||||
public function beginTransaction(): bool
|
||||
{
|
||||
$query = null;
|
||||
if (1 === ++$this->nestingLevel) {
|
||||
$this->debugDataHolder->addQuery($this->connectionName, $query = new Query('"START TRANSACTION"'));
|
||||
}
|
||||
|
||||
$this->stopwatch?->start('doctrine', 'doctrine');
|
||||
$query?->start();
|
||||
|
||||
try {
|
||||
return parent::beginTransaction();
|
||||
} finally {
|
||||
$query?->stop();
|
||||
$this->stopwatch?->stop('doctrine');
|
||||
}
|
||||
}
|
||||
|
||||
public function commit(): bool
|
||||
{
|
||||
$query = null;
|
||||
if (1 === $this->nestingLevel--) {
|
||||
$this->debugDataHolder->addQuery($this->connectionName, $query = new Query('"COMMIT"'));
|
||||
}
|
||||
|
||||
$this->stopwatch?->start('doctrine', 'doctrine');
|
||||
$query?->start();
|
||||
|
||||
try {
|
||||
return parent::commit();
|
||||
} finally {
|
||||
$query?->stop();
|
||||
$this->stopwatch?->stop('doctrine');
|
||||
}
|
||||
}
|
||||
|
||||
public function rollBack(): bool
|
||||
{
|
||||
$query = null;
|
||||
if (1 === $this->nestingLevel--) {
|
||||
$this->debugDataHolder->addQuery($this->connectionName, $query = new Query('"ROLLBACK"'));
|
||||
}
|
||||
|
||||
$this->stopwatch?->start('doctrine', 'doctrine');
|
||||
$query?->start();
|
||||
|
||||
try {
|
||||
return parent::rollBack();
|
||||
} finally {
|
||||
$query?->stop();
|
||||
$this->stopwatch?->stop('doctrine');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Middleware\Debug\DBAL3;
|
||||
|
||||
use Doctrine\DBAL\Driver\Middleware\AbstractStatementMiddleware;
|
||||
use Doctrine\DBAL\Driver\Result as ResultInterface;
|
||||
use Doctrine\DBAL\Driver\Statement as StatementInterface;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Symfony\Bridge\Doctrine\Middleware\Debug\DebugDataHolder;
|
||||
use Symfony\Bridge\Doctrine\Middleware\Debug\Query;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
|
||||
/**
|
||||
* @author Laurent VOULLEMIER <laurent.voullemier@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Statement extends AbstractStatementMiddleware
|
||||
{
|
||||
private Query $query;
|
||||
|
||||
public function __construct(
|
||||
StatementInterface $statement,
|
||||
private DebugDataHolder $debugDataHolder,
|
||||
private string $connectionName,
|
||||
string $sql,
|
||||
private ?Stopwatch $stopwatch = null,
|
||||
) {
|
||||
$this->query = new Query($sql);
|
||||
|
||||
parent::__construct($statement);
|
||||
}
|
||||
|
||||
public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool
|
||||
{
|
||||
$this->query->setParam($param, $variable, $type);
|
||||
|
||||
return parent::bindParam($param, $variable, $type, ...\array_slice(\func_get_args(), 3));
|
||||
}
|
||||
|
||||
public function bindValue($param, $value, $type = ParameterType::STRING): bool
|
||||
{
|
||||
$this->query->setValue($param, $value, $type);
|
||||
|
||||
return parent::bindValue($param, $value, $type);
|
||||
}
|
||||
|
||||
public function execute($params = null): ResultInterface
|
||||
{
|
||||
if (null !== $params) {
|
||||
$this->query->setValues($params);
|
||||
}
|
||||
|
||||
// clone to prevent variables by reference to change
|
||||
$this->debugDataHolder->addQuery($this->connectionName, $query = clone $this->query);
|
||||
|
||||
$this->stopwatch?->start('doctrine', 'doctrine');
|
||||
$query->start();
|
||||
|
||||
try {
|
||||
return parent::execute($params);
|
||||
} finally {
|
||||
$query->stop();
|
||||
$this->stopwatch?->stop('doctrine');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Middleware\Debug;
|
||||
|
||||
/**
|
||||
* @author Laurent VOULLEMIER <laurent.voullemier@gmail.com>
|
||||
*/
|
||||
class DebugDataHolder
|
||||
{
|
||||
private array $data = [];
|
||||
|
||||
public function addQuery(string $connectionName, Query $query): void
|
||||
{
|
||||
$this->data[$connectionName][] = [
|
||||
'sql' => $query->getSql(),
|
||||
'params' => $query->getParams(),
|
||||
'types' => $query->getTypes(),
|
||||
'executionMS' => $query->getDuration(...), // stop() may not be called at this point
|
||||
];
|
||||
}
|
||||
|
||||
public function getData(): array
|
||||
{
|
||||
foreach ($this->data as $connectionName => $dataForConn) {
|
||||
foreach ($dataForConn as $idx => $data) {
|
||||
if (\is_callable($data['executionMS'])) {
|
||||
$this->data[$connectionName][$idx]['executionMS'] = $data['executionMS']();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->data = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Middleware\Debug;
|
||||
|
||||
use Doctrine\DBAL\Driver as DriverInterface;
|
||||
use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
|
||||
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
|
||||
/**
|
||||
* @author Laurent VOULLEMIER <laurent.voullemier@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Driver extends AbstractDriverMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
DriverInterface $driver,
|
||||
private readonly DebugDataHolder $debugDataHolder,
|
||||
private readonly ?Stopwatch $stopwatch,
|
||||
private readonly string $connectionName,
|
||||
) {
|
||||
parent::__construct($driver);
|
||||
}
|
||||
|
||||
public function connect(array $params): ConnectionInterface
|
||||
{
|
||||
$connection = parent::connect($params);
|
||||
|
||||
if ('void' !== (string) (new \ReflectionMethod(DriverInterface\Connection::class, 'commit'))->getReturnType()) {
|
||||
return new DBAL3\Connection(
|
||||
$connection,
|
||||
$this->debugDataHolder,
|
||||
$this->stopwatch,
|
||||
$this->connectionName
|
||||
);
|
||||
}
|
||||
|
||||
return new Connection(
|
||||
$connection,
|
||||
$this->debugDataHolder,
|
||||
$this->stopwatch,
|
||||
$this->connectionName
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Middleware\Debug;
|
||||
|
||||
use Doctrine\DBAL\Driver as DriverInterface;
|
||||
use Doctrine\DBAL\Driver\Middleware as MiddlewareInterface;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
|
||||
/**
|
||||
* Middleware to collect debug data.
|
||||
*
|
||||
* @author Laurent VOULLEMIER <laurent.voullemier@gmail.com>
|
||||
*/
|
||||
final class Middleware implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(
|
||||
private DebugDataHolder $debugDataHolder,
|
||||
private ?Stopwatch $stopwatch,
|
||||
private string $connectionName = 'default',
|
||||
) {
|
||||
}
|
||||
|
||||
public function wrap(DriverInterface $driver): DriverInterface
|
||||
{
|
||||
return new Driver($driver, $this->debugDataHolder, $this->stopwatch, $this->connectionName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Middleware\Debug;
|
||||
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
|
||||
/**
|
||||
* @author Laurent VOULLEMIER <laurent.voullemier@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class Query
|
||||
{
|
||||
private array $params = [];
|
||||
|
||||
/** @var array<ParameterType|int> */
|
||||
private array $types = [];
|
||||
|
||||
private ?float $start = null;
|
||||
private ?float $duration = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $sql,
|
||||
) {
|
||||
}
|
||||
|
||||
public function start(): void
|
||||
{
|
||||
$this->start = microtime(true);
|
||||
}
|
||||
|
||||
public function stop(): void
|
||||
{
|
||||
if (null !== $this->start) {
|
||||
$this->duration = microtime(true) - $this->start;
|
||||
}
|
||||
}
|
||||
|
||||
public function setParam(string|int $param, mixed &$variable, ParameterType|int $type): void
|
||||
{
|
||||
// Numeric indexes start at 0 in profiler
|
||||
$idx = \is_int($param) ? $param - 1 : $param;
|
||||
|
||||
$this->params[$idx] = &$variable;
|
||||
$this->types[$idx] = $type;
|
||||
}
|
||||
|
||||
public function setValue(string|int $param, mixed $value, ParameterType|int $type): void
|
||||
{
|
||||
// Numeric indexes start at 0 in profiler
|
||||
$idx = \is_int($param) ? $param - 1 : $param;
|
||||
|
||||
$this->params[$idx] = $value;
|
||||
$this->types[$idx] = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string|int, string|int|float> $values
|
||||
*/
|
||||
public function setValues(array $values): void
|
||||
{
|
||||
foreach ($values as $param => $value) {
|
||||
$this->setValue($param, $value, ParameterType::STRING);
|
||||
}
|
||||
}
|
||||
|
||||
public function getSql(): string
|
||||
{
|
||||
return $this->sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string|int|float}>
|
||||
*/
|
||||
public function getParams(): array
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int|ParameterType>
|
||||
*/
|
||||
public function getTypes(): array
|
||||
{
|
||||
return $this->types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query duration in seconds.
|
||||
*/
|
||||
public function getDuration(): ?float
|
||||
{
|
||||
return $this->duration;
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$copy = [];
|
||||
foreach ($this->params as $param => $valueOrVariable) {
|
||||
$copy[$param] = $valueOrVariable;
|
||||
}
|
||||
$this->params = $copy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Middleware\Debug;
|
||||
|
||||
use Doctrine\DBAL\Driver\Middleware\AbstractStatementMiddleware;
|
||||
use Doctrine\DBAL\Driver\Result as ResultInterface;
|
||||
use Doctrine\DBAL\Driver\Statement as StatementInterface;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
|
||||
/**
|
||||
* @author Laurent VOULLEMIER <laurent.voullemier@gmail.com>
|
||||
* @author Alexander M. Turek <me@derrabus.de>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Statement extends AbstractStatementMiddleware
|
||||
{
|
||||
private Query $query;
|
||||
|
||||
public function __construct(
|
||||
StatementInterface $statement,
|
||||
private DebugDataHolder $debugDataHolder,
|
||||
private string $connectionName,
|
||||
string $sql,
|
||||
private ?Stopwatch $stopwatch = null,
|
||||
) {
|
||||
parent::__construct($statement);
|
||||
|
||||
$this->query = new Query($sql);
|
||||
}
|
||||
|
||||
public function bindValue(int|string $param, mixed $value, ParameterType $type): void
|
||||
{
|
||||
$this->query->setValue($param, $value, $type);
|
||||
|
||||
parent::bindValue($param, $value, $type);
|
||||
}
|
||||
|
||||
public function execute(): ResultInterface
|
||||
{
|
||||
// clone to prevent variables by reference to change
|
||||
$this->debugDataHolder->addQuery($this->connectionName, $query = clone $this->query);
|
||||
|
||||
$this->stopwatch?->start('doctrine', 'doctrine');
|
||||
$query->start();
|
||||
|
||||
try {
|
||||
return parent::execute();
|
||||
} finally {
|
||||
$query->stop();
|
||||
$this->stopwatch?->stop('doctrine');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\PropertyInfo;
|
||||
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\AssociationMapping;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\MappingException as OrmMappingException;
|
||||
use Doctrine\Persistence\Mapping\MappingException;
|
||||
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
|
||||
use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
|
||||
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
/**
|
||||
* Extracts data using Doctrine ORM and ODM metadata.
|
||||
*
|
||||
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||
*/
|
||||
class DoctrineExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function getProperties(string $class, array $context = []): ?array
|
||||
{
|
||||
if (null === $metadata = $this->getMetadata($class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$properties = array_merge($metadata->getFieldNames(), $metadata->getAssociationNames());
|
||||
|
||||
if ($metadata instanceof ClassMetadata && $metadata->embeddedClasses) {
|
||||
$properties = array_filter($properties, fn ($property) => !str_contains($property, '.'));
|
||||
|
||||
$properties = array_merge($properties, array_keys($metadata->embeddedClasses));
|
||||
}
|
||||
|
||||
return $properties;
|
||||
}
|
||||
|
||||
public function getTypes(string $class, string $property, array $context = []): ?array
|
||||
{
|
||||
if (null === $metadata = $this->getMetadata($class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($metadata->hasAssociation($property)) {
|
||||
$class = $metadata->getAssociationTargetClass($property);
|
||||
|
||||
if ($metadata->isSingleValuedAssociation($property)) {
|
||||
if ($metadata instanceof ClassMetadata) {
|
||||
$associationMapping = $metadata->getAssociationMapping($property);
|
||||
|
||||
$nullable = $this->isAssociationNullable($associationMapping);
|
||||
} else {
|
||||
$nullable = false;
|
||||
}
|
||||
|
||||
return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $class)];
|
||||
}
|
||||
|
||||
$collectionKeyType = Type::BUILTIN_TYPE_INT;
|
||||
|
||||
if ($metadata instanceof ClassMetadata) {
|
||||
$associationMapping = $metadata->getAssociationMapping($property);
|
||||
|
||||
if (isset($associationMapping['indexBy'])) {
|
||||
$subMetadata = $this->entityManager->getClassMetadata($associationMapping['targetEntity']);
|
||||
|
||||
// Check if indexBy value is a property
|
||||
$fieldName = $associationMapping['indexBy'];
|
||||
if (null === ($typeOfField = $subMetadata->getTypeOfField($fieldName))) {
|
||||
$fieldName = $subMetadata->getFieldForColumn($associationMapping['indexBy']);
|
||||
// Not a property, maybe a column name?
|
||||
if (null === ($typeOfField = $subMetadata->getTypeOfField($fieldName))) {
|
||||
// Maybe the column name is the association join column?
|
||||
$associationMapping = $subMetadata->getAssociationMapping($fieldName);
|
||||
|
||||
$indexProperty = $subMetadata->getSingleAssociationReferencedJoinColumnName($fieldName);
|
||||
$subMetadata = $this->entityManager->getClassMetadata($associationMapping['targetEntity']);
|
||||
|
||||
// Not a property, maybe a column name?
|
||||
if (null === ($typeOfField = $subMetadata->getTypeOfField($indexProperty))) {
|
||||
$fieldName = $subMetadata->getFieldForColumn($indexProperty);
|
||||
$typeOfField = $subMetadata->getTypeOfField($fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$collectionKeyType = $this->getPhpType($typeOfField)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [new Type(
|
||||
Type::BUILTIN_TYPE_OBJECT,
|
||||
false,
|
||||
Collection::class,
|
||||
true,
|
||||
new Type($collectionKeyType),
|
||||
new Type(Type::BUILTIN_TYPE_OBJECT, false, $class)
|
||||
)];
|
||||
}
|
||||
|
||||
if ($metadata instanceof ClassMetadata && isset($metadata->embeddedClasses[$property])) {
|
||||
return [new Type(Type::BUILTIN_TYPE_OBJECT, false, $metadata->embeddedClasses[$property]['class'])];
|
||||
}
|
||||
|
||||
if ($metadata->hasField($property)) {
|
||||
$typeOfField = $metadata->getTypeOfField($property);
|
||||
|
||||
if (!$builtinType = $this->getPhpType($typeOfField)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$nullable = $metadata instanceof ClassMetadata && $metadata->isNullable($property);
|
||||
$enumType = null;
|
||||
if (null !== $enumClass = $metadata->getFieldMapping($property)['enumType'] ?? null) {
|
||||
$enumType = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $enumClass);
|
||||
}
|
||||
|
||||
switch ($builtinType) {
|
||||
case Type::BUILTIN_TYPE_OBJECT:
|
||||
switch ($typeOfField) {
|
||||
case Types::DATE_MUTABLE:
|
||||
case Types::DATETIME_MUTABLE:
|
||||
case Types::DATETIMETZ_MUTABLE:
|
||||
case 'vardatetime':
|
||||
case Types::TIME_MUTABLE:
|
||||
return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTime')];
|
||||
|
||||
case Types::DATE_IMMUTABLE:
|
||||
case Types::DATETIME_IMMUTABLE:
|
||||
case Types::DATETIMETZ_IMMUTABLE:
|
||||
case Types::TIME_IMMUTABLE:
|
||||
return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTimeImmutable')];
|
||||
|
||||
case Types::DATEINTERVAL:
|
||||
return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateInterval')];
|
||||
}
|
||||
|
||||
break;
|
||||
case Type::BUILTIN_TYPE_ARRAY:
|
||||
switch ($typeOfField) {
|
||||
case 'array': // DBAL < 4
|
||||
case 'json_array': // DBAL < 3
|
||||
// return null if $enumType is set, because we can't determine if collectionKeyType is string or int
|
||||
if ($enumType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true)];
|
||||
|
||||
case Types::SIMPLE_ARRAY:
|
||||
return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, new Type(Type::BUILTIN_TYPE_INT), $enumType ?? new Type(Type::BUILTIN_TYPE_STRING))];
|
||||
}
|
||||
break;
|
||||
case Type::BUILTIN_TYPE_INT:
|
||||
case Type::BUILTIN_TYPE_STRING:
|
||||
if ($enumType) {
|
||||
return [$enumType];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return [new Type($builtinType, $nullable)];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isReadable(string $class, string $property, array $context = []): ?bool
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isWritable(string $class, string $property, array $context = []): ?bool
|
||||
{
|
||||
if (
|
||||
null === ($metadata = $this->getMetadata($class))
|
||||
|| ClassMetadata::GENERATOR_TYPE_NONE === $metadata->generatorType
|
||||
|| !\in_array($property, $metadata->getIdentifierFieldNames(), true)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getMetadata(string $class): ?ClassMetadata
|
||||
{
|
||||
try {
|
||||
return $this->entityManager->getClassMetadata($class);
|
||||
} catch (MappingException|OrmMappingException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether an association is nullable.
|
||||
*
|
||||
* @param array<string, mixed>|AssociationMapping $associationMapping
|
||||
*
|
||||
* @see https://github.com/doctrine/doctrine2/blob/v2.5.4/lib/Doctrine/ORM/Tools/EntityGenerator.php#L1221-L1246
|
||||
*/
|
||||
private function isAssociationNullable(array|AssociationMapping $associationMapping): bool
|
||||
{
|
||||
if (isset($associationMapping['id']) && $associationMapping['id']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($associationMapping['joinColumns'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$joinColumns = $associationMapping['joinColumns'];
|
||||
foreach ($joinColumns as $joinColumn) {
|
||||
if (isset($joinColumn['nullable']) && !$joinColumn['nullable']) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the corresponding built-in PHP type.
|
||||
*/
|
||||
private function getPhpType(string $doctrineType): ?string
|
||||
{
|
||||
return match ($doctrineType) {
|
||||
Types::SMALLINT,
|
||||
Types::INTEGER => Type::BUILTIN_TYPE_INT,
|
||||
Types::FLOAT => Type::BUILTIN_TYPE_FLOAT,
|
||||
Types::BIGINT,
|
||||
Types::STRING,
|
||||
Types::TEXT,
|
||||
Types::GUID,
|
||||
Types::DECIMAL => Type::BUILTIN_TYPE_STRING,
|
||||
Types::BOOLEAN => Type::BUILTIN_TYPE_BOOL,
|
||||
Types::BLOB,
|
||||
Types::BINARY => Type::BUILTIN_TYPE_RESOURCE,
|
||||
'object', // DBAL < 4
|
||||
Types::DATE_MUTABLE,
|
||||
Types::DATETIME_MUTABLE,
|
||||
Types::DATETIMETZ_MUTABLE,
|
||||
'vardatetime',
|
||||
Types::TIME_MUTABLE,
|
||||
Types::DATE_IMMUTABLE,
|
||||
Types::DATETIME_IMMUTABLE,
|
||||
Types::DATETIMETZ_IMMUTABLE,
|
||||
Types::TIME_IMMUTABLE,
|
||||
Types::DATEINTERVAL => Type::BUILTIN_TYPE_OBJECT,
|
||||
'array', // DBAL < 4
|
||||
'json_array', // DBAL < 3
|
||||
Types::SIMPLE_ARRAY => Type::BUILTIN_TYPE_ARRAY,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
Doctrine Bridge
|
||||
===============
|
||||
|
||||
The Doctrine bridge provides integration for
|
||||
[Doctrine](http://www.doctrine-project.org/) with various Symfony components.
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\SchemaListener;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Exception\TableNotFoundException;
|
||||
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
|
||||
|
||||
abstract class AbstractSchemaListener
|
||||
{
|
||||
abstract public function postGenerateSchema(GenerateSchemaEventArgs $event): void;
|
||||
|
||||
protected function getIsSameDatabaseChecker(Connection $connection): \Closure
|
||||
{
|
||||
return static function (\Closure $exec) use ($connection): bool {
|
||||
$checkTable = 'schema_subscriber_check_'.bin2hex(random_bytes(7));
|
||||
$connection->executeStatement(sprintf('CREATE TABLE %s (id INTEGER NOT NULL)', $checkTable));
|
||||
|
||||
try {
|
||||
$exec(sprintf('DROP TABLE %s', $checkTable));
|
||||
} catch (\Exception) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
try {
|
||||
$connection->executeStatement(sprintf('DROP TABLE %s', $checkTable));
|
||||
|
||||
return false;
|
||||
} catch (TableNotFoundException) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\SchemaListener;
|
||||
|
||||
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
|
||||
use Symfony\Component\Cache\Adapter\DoctrineDbalAdapter;
|
||||
|
||||
/**
|
||||
* Automatically adds the cache table needed for the DoctrineDbalAdapter of
|
||||
* the Cache component.
|
||||
*/
|
||||
class DoctrineDbalCacheAdapterSchemaListener extends AbstractSchemaListener
|
||||
{
|
||||
/**
|
||||
* @param iterable<mixed, DoctrineDbalAdapter> $dbalAdapters
|
||||
*/
|
||||
public function __construct(private iterable $dbalAdapters)
|
||||
{
|
||||
}
|
||||
|
||||
public function postGenerateSchema(GenerateSchemaEventArgs $event): void
|
||||
{
|
||||
$connection = $event->getEntityManager()->getConnection();
|
||||
|
||||
foreach ($this->dbalAdapters as $dbalAdapter) {
|
||||
$dbalAdapter->configureSchema($event->getSchema(), $connection, $this->getIsSameDatabaseChecker($connection));
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\SchemaListener;
|
||||
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Doctrine\ORM\Tools\ToolEvents;
|
||||
|
||||
trigger_deprecation('symfony/doctrine-bridge', '6.3', 'The "%s" class is deprecated. Use "%s" instead.', DoctrineDbalCacheAdapterSchemaSubscriber::class, DoctrineDbalCacheAdapterSchemaListener::class);
|
||||
|
||||
/**
|
||||
* Automatically adds the cache table needed for the DoctrineDbalAdapter of
|
||||
* the Cache component.
|
||||
*
|
||||
* @author Ryan Weaver <ryan@symfonycasts.com>
|
||||
*
|
||||
* @deprecated since Symfony 6.3, use {@link DoctrineDbalCacheAdapterSchemaListener} instead
|
||||
*/
|
||||
final class DoctrineDbalCacheAdapterSchemaSubscriber extends DoctrineDbalCacheAdapterSchemaListener implements EventSubscriber
|
||||
{
|
||||
public function getSubscribedEvents(): array
|
||||
{
|
||||
if (!class_exists(ToolEvents::class)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
ToolEvents::postGenerateSchema,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\SchemaListener;
|
||||
|
||||
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\PersistingStoreInterface;
|
||||
use Symfony\Component\Lock\Store\DoctrineDbalStore;
|
||||
|
||||
final class LockStoreSchemaListener extends AbstractSchemaListener
|
||||
{
|
||||
/**
|
||||
* @param iterable<mixed, PersistingStoreInterface> $stores
|
||||
*/
|
||||
public function __construct(private iterable $stores)
|
||||
{
|
||||
}
|
||||
|
||||
public function postGenerateSchema(GenerateSchemaEventArgs $event): void
|
||||
{
|
||||
$connection = $event->getEntityManager()->getConnection();
|
||||
|
||||
$storesIterator = new \ArrayIterator($this->stores);
|
||||
while ($storesIterator->valid()) {
|
||||
try {
|
||||
$store = $storesIterator->current();
|
||||
if (!$store instanceof DoctrineDbalStore) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$store->configureSchema($event->getSchema(), $this->getIsSameDatabaseChecker($connection));
|
||||
} catch (InvalidArgumentException) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
$storesIterator->next();
|
||||
}
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\SchemaListener;
|
||||
|
||||
use Doctrine\DBAL\Event\SchemaCreateTableEventArgs;
|
||||
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
|
||||
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport;
|
||||
use Symfony\Component\Messenger\Transport\TransportInterface;
|
||||
|
||||
/**
|
||||
* Automatically adds any required database tables to the Doctrine Schema.
|
||||
*/
|
||||
class MessengerTransportDoctrineSchemaListener extends AbstractSchemaListener
|
||||
{
|
||||
private const PROCESSING_TABLE_FLAG = self::class.':processing';
|
||||
|
||||
/**
|
||||
* @param iterable<mixed, TransportInterface> $transports
|
||||
*/
|
||||
public function __construct(private iterable $transports)
|
||||
{
|
||||
}
|
||||
|
||||
public function postGenerateSchema(GenerateSchemaEventArgs $event): void
|
||||
{
|
||||
$connection = $event->getEntityManager()->getConnection();
|
||||
|
||||
foreach ($this->transports as $transport) {
|
||||
if (!$transport instanceof DoctrineTransport) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$transport->configureSchema($event->getSchema(), $connection, $this->getIsSameDatabaseChecker($connection));
|
||||
}
|
||||
}
|
||||
|
||||
public function onSchemaCreateTable(SchemaCreateTableEventArgs $event): void
|
||||
{
|
||||
$table = $event->getTable();
|
||||
|
||||
// if this method triggers a nested create table below, allow Doctrine to work like normal
|
||||
if ($table->hasOption(self::PROCESSING_TABLE_FLAG)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->transports as $transport) {
|
||||
if (!$transport instanceof DoctrineTransport) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$extraSql = $transport->getExtraSetupSqlForTable($table)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// avoid this same listener from creating a loop on this table
|
||||
$table->addOption(self::PROCESSING_TABLE_FLAG, true);
|
||||
$createTableSql = $event->getPlatform()->getCreateTableSQL($table);
|
||||
|
||||
/*
|
||||
* Add all the SQL needed to create the table and tell Doctrine
|
||||
* to "preventDefault" so that only our SQL is used. This is
|
||||
* the only way to inject some extra SQL.
|
||||
*/
|
||||
$event->addSql($createTableSql);
|
||||
foreach ($extraSql as $sql) {
|
||||
$event->addSql($sql);
|
||||
}
|
||||
$event->preventDefault();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\SchemaListener;
|
||||
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Doctrine\DBAL\Events;
|
||||
use Doctrine\ORM\Tools\ToolEvents;
|
||||
|
||||
trigger_deprecation('symfony/doctrine-bridge', '6.3', 'The "%s" class is deprecated. Use "%s" instead.', MessengerTransportDoctrineSchemaSubscriber::class, MessengerTransportDoctrineSchemaListener::class);
|
||||
|
||||
/**
|
||||
* Automatically adds any required database tables to the Doctrine Schema.
|
||||
*
|
||||
* @author Ryan Weaver <ryan@symfonycasts.com>
|
||||
*
|
||||
* @deprecated since Symfony 6.3, use {@link MessengerTransportDoctrineSchemaListener} instead
|
||||
*/
|
||||
final class MessengerTransportDoctrineSchemaSubscriber extends MessengerTransportDoctrineSchemaListener implements EventSubscriber
|
||||
{
|
||||
public function getSubscribedEvents(): array
|
||||
{
|
||||
$subscribedEvents = [];
|
||||
|
||||
if (class_exists(ToolEvents::class)) {
|
||||
$subscribedEvents[] = ToolEvents::postGenerateSchema;
|
||||
}
|
||||
|
||||
if (class_exists(Events::class)) {
|
||||
$subscribedEvents[] = Events::onSchemaCreateTable;
|
||||
}
|
||||
|
||||
return $subscribedEvents;
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\SchemaListener;
|
||||
|
||||
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
|
||||
use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler;
|
||||
|
||||
final class PdoSessionHandlerSchemaListener extends AbstractSchemaListener
|
||||
{
|
||||
private PdoSessionHandler $sessionHandler;
|
||||
|
||||
public function __construct(\SessionHandlerInterface $sessionHandler)
|
||||
{
|
||||
if ($sessionHandler instanceof PdoSessionHandler) {
|
||||
$this->sessionHandler = $sessionHandler;
|
||||
}
|
||||
}
|
||||
|
||||
public function postGenerateSchema(GenerateSchemaEventArgs $event): void
|
||||
{
|
||||
if (!isset($this->sessionHandler)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$connection = $event->getEntityManager()->getConnection();
|
||||
|
||||
$this->sessionHandler->configureSchema($event->getSchema(), $this->getIsSameDatabaseChecker($connection));
|
||||
}
|
||||
}
|
||||
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\SchemaListener;
|
||||
|
||||
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
|
||||
use Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider;
|
||||
use Symfony\Component\Security\Http\RememberMe\PersistentRememberMeHandler;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface;
|
||||
|
||||
/**
|
||||
* Automatically adds the rememberme table needed for the {@see DoctrineTokenProvider}.
|
||||
*/
|
||||
class RememberMeTokenProviderDoctrineSchemaListener extends AbstractSchemaListener
|
||||
{
|
||||
/**
|
||||
* @param iterable<mixed, RememberMeHandlerInterface> $rememberMeHandlers
|
||||
*/
|
||||
public function __construct(private iterable $rememberMeHandlers)
|
||||
{
|
||||
}
|
||||
|
||||
public function postGenerateSchema(GenerateSchemaEventArgs $event): void
|
||||
{
|
||||
$connection = $event->getEntityManager()->getConnection();
|
||||
|
||||
foreach ($this->rememberMeHandlers as $rememberMeHandler) {
|
||||
if (
|
||||
$rememberMeHandler instanceof PersistentRememberMeHandler
|
||||
&& ($tokenProvider = $rememberMeHandler->getTokenProvider()) instanceof DoctrineTokenProvider
|
||||
) {
|
||||
$tokenProvider->configureSchema($event->getSchema(), $connection, $this->getIsSameDatabaseChecker($connection));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\SchemaListener;
|
||||
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Doctrine\ORM\Tools\ToolEvents;
|
||||
use Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider;
|
||||
|
||||
trigger_deprecation('symfony/doctrine-bridge', '6.3', 'The "%s" class is deprecated. Use "%s" instead.', RememberMeTokenProviderDoctrineSchemaSubscriber::class, RememberMeTokenProviderDoctrineSchemaListener::class);
|
||||
|
||||
/**
|
||||
* Automatically adds the rememberme table needed for the {@see DoctrineTokenProvider}.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @deprecated since Symfony 6.3, use {@link RememberMeTokenProviderDoctrineSchemaListener} instead
|
||||
*/
|
||||
final class RememberMeTokenProviderDoctrineSchemaSubscriber extends RememberMeTokenProviderDoctrineSchemaListener implements EventSubscriber
|
||||
{
|
||||
public function getSubscribedEvents(): array
|
||||
{
|
||||
if (!class_exists(ToolEvents::class)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
ToolEvents::postGenerateSchema,
|
||||
];
|
||||
}
|
||||
}
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Security\RememberMe;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Driver\Result as DriverResult;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken;
|
||||
use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentTokenInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\RememberMe\TokenVerifierInterface;
|
||||
use Symfony\Component\Security\Core\Exception\TokenNotFoundException;
|
||||
|
||||
/**
|
||||
* This class provides storage for the tokens that is set in "remember-me"
|
||||
* cookies. This way no password secrets will be stored in the cookies on
|
||||
* the client machine, and thus the security is improved.
|
||||
*
|
||||
* This depends only on doctrine in order to get a database connection
|
||||
* and to do the conversion of the datetime column.
|
||||
*
|
||||
* In order to use this class, you need the following table in your database:
|
||||
*
|
||||
* CREATE TABLE `rememberme_token` (
|
||||
* `series` char(88) UNIQUE PRIMARY KEY NOT NULL,
|
||||
* `value` char(88) NOT NULL,
|
||||
* `lastUsed` datetime NOT NULL,
|
||||
* `class` varchar(100) NOT NULL,
|
||||
* `username` varchar(200) NOT NULL
|
||||
* );
|
||||
*/
|
||||
class DoctrineTokenProvider implements TokenProviderInterface, TokenVerifierInterface
|
||||
{
|
||||
private Connection $conn;
|
||||
|
||||
public function __construct(Connection $conn)
|
||||
{
|
||||
$this->conn = $conn;
|
||||
}
|
||||
|
||||
public function loadTokenBySeries(string $series): PersistentTokenInterface
|
||||
{
|
||||
// the alias for lastUsed works around case insensitivity in PostgreSQL
|
||||
$sql = 'SELECT class, username, value, lastUsed AS last_used FROM rememberme_token WHERE series=:series';
|
||||
$paramValues = ['series' => $series];
|
||||
$paramTypes = ['series' => ParameterType::STRING];
|
||||
$stmt = $this->conn->executeQuery($sql, $paramValues, $paramTypes);
|
||||
$row = $stmt instanceof Result || $stmt instanceof DriverResult ? $stmt->fetchAssociative() : $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
if ($row) {
|
||||
return new PersistentToken($row['class'], $row['username'], $series, $row['value'], new \DateTime($row['last_used']));
|
||||
}
|
||||
|
||||
throw new TokenNotFoundException('No token found.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function deleteTokenBySeries(string $series)
|
||||
{
|
||||
$sql = 'DELETE FROM rememberme_token WHERE series=:series';
|
||||
$paramValues = ['series' => $series];
|
||||
$paramTypes = ['series' => ParameterType::STRING];
|
||||
if (method_exists($this->conn, 'executeStatement')) {
|
||||
$this->conn->executeStatement($sql, $paramValues, $paramTypes);
|
||||
} else {
|
||||
$this->conn->executeUpdate($sql, $paramValues, $paramTypes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function updateToken(string $series, #[\SensitiveParameter] string $tokenValue, \DateTime $lastUsed)
|
||||
{
|
||||
$sql = 'UPDATE rememberme_token SET value=:value, lastUsed=:lastUsed WHERE series=:series';
|
||||
$paramValues = [
|
||||
'value' => $tokenValue,
|
||||
'lastUsed' => $lastUsed,
|
||||
'series' => $series,
|
||||
];
|
||||
$paramTypes = [
|
||||
'value' => ParameterType::STRING,
|
||||
'lastUsed' => Types::DATETIME_MUTABLE,
|
||||
'series' => ParameterType::STRING,
|
||||
];
|
||||
if (method_exists($this->conn, 'executeStatement')) {
|
||||
$updated = $this->conn->executeStatement($sql, $paramValues, $paramTypes);
|
||||
} else {
|
||||
$updated = $this->conn->executeUpdate($sql, $paramValues, $paramTypes);
|
||||
}
|
||||
if ($updated < 1) {
|
||||
throw new TokenNotFoundException('No token found.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function createNewToken(PersistentTokenInterface $token)
|
||||
{
|
||||
$sql = 'INSERT INTO rememberme_token (class, username, series, value, lastUsed) VALUES (:class, :username, :series, :value, :lastUsed)';
|
||||
$paramValues = [
|
||||
'class' => $token->getClass(),
|
||||
'username' => $token->getUserIdentifier(),
|
||||
'series' => $token->getSeries(),
|
||||
'value' => $token->getTokenValue(),
|
||||
'lastUsed' => $token->getLastUsed(),
|
||||
];
|
||||
$paramTypes = [
|
||||
'class' => ParameterType::STRING,
|
||||
'username' => ParameterType::STRING,
|
||||
'series' => ParameterType::STRING,
|
||||
'value' => ParameterType::STRING,
|
||||
'lastUsed' => Types::DATETIME_MUTABLE,
|
||||
];
|
||||
if (method_exists($this->conn, 'executeStatement')) {
|
||||
$this->conn->executeStatement($sql, $paramValues, $paramTypes);
|
||||
} else {
|
||||
$this->conn->executeUpdate($sql, $paramValues, $paramTypes);
|
||||
}
|
||||
}
|
||||
|
||||
public function verifyToken(PersistentTokenInterface $token, #[\SensitiveParameter] string $tokenValue): bool
|
||||
{
|
||||
// Check if the token value matches the current persisted token
|
||||
if (hash_equals($token->getTokenValue(), $tokenValue)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Generate an alternative series id here by changing the suffix == to _
|
||||
// this is needed to be able to store an older token value in the database
|
||||
// which has a PRIMARY(series), and it works as long as series ids are
|
||||
// generated using base64_encode(random_bytes(64)) which always outputs
|
||||
// a == suffix, but if it should not work for some reason we abort
|
||||
// for safety
|
||||
$tmpSeries = preg_replace('{=+$}', '_', $token->getSeries());
|
||||
if ($tmpSeries === $token->getSeries()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the previous token is present. If the given $tokenValue
|
||||
// matches the previous token (and it is outdated by at most 60seconds)
|
||||
// we also accept it as a valid value.
|
||||
try {
|
||||
$tmpToken = $this->loadTokenBySeries($tmpSeries);
|
||||
} catch (TokenNotFoundException) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($tmpToken->getLastUsed()->getTimestamp() + 60 < time()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hash_equals($tmpToken->getTokenValue(), $tokenValue);
|
||||
}
|
||||
|
||||
public function updateExistingToken(PersistentTokenInterface $token, #[\SensitiveParameter] string $tokenValue, \DateTimeInterface $lastUsed): void
|
||||
{
|
||||
if (!$token instanceof PersistentToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Persist a copy of the previous token for authentication
|
||||
// in verifyToken should the old token still be sent by the browser
|
||||
// in a request concurrent to the one that did this token update
|
||||
$tmpSeries = preg_replace('{=+$}', '_', $token->getSeries());
|
||||
// if we cannot generate a unique series it is not worth trying further
|
||||
if ($tmpSeries === $token->getSeries()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->conn->beginTransaction();
|
||||
try {
|
||||
$this->deleteTokenBySeries($tmpSeries);
|
||||
$this->createNewToken(new PersistentToken($token->getClass(), $token->getUserIdentifier(), $tmpSeries, $token->getTokenValue(), $lastUsed));
|
||||
|
||||
$this->conn->commit();
|
||||
} catch (\Exception $e) {
|
||||
$this->conn->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the Table to the Schema if "remember me" uses this Connection.
|
||||
*
|
||||
* @param \Closure $isSameDatabase
|
||||
*/
|
||||
public function configureSchema(Schema $schema, Connection $forConnection/* , \Closure $isSameDatabase */): void
|
||||
{
|
||||
if ($schema->hasTable('rememberme_token')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$isSameDatabase = 2 < \func_num_args() ? func_get_arg(2) : static fn () => false;
|
||||
|
||||
if ($forConnection !== $this->conn && !$isSameDatabase($this->conn->executeStatement(...))) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->addTableToSchema($schema);
|
||||
}
|
||||
|
||||
private function addTableToSchema(Schema $schema): void
|
||||
{
|
||||
$table = $schema->createTable('rememberme_token');
|
||||
$table->addColumn('series', Types::STRING, ['length' => 88]);
|
||||
$table->addColumn('value', Types::STRING, ['length' => 88]);
|
||||
$table->addColumn('lastUsed', Types::DATETIME_MUTABLE);
|
||||
$table->addColumn('class', Types::STRING, ['length' => 100]);
|
||||
$table->addColumn('username', Types::STRING, ['length' => 200]);
|
||||
$table->setPrimaryKey(['series']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Security\User;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Doctrine\Persistence\Mapping\ClassMetadata;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Doctrine\Persistence\Proxy;
|
||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* Wrapper around a Doctrine ObjectManager.
|
||||
*
|
||||
* Provides provisioning for Doctrine entity users.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||
*/
|
||||
class EntityUserProvider implements UserProviderInterface, PasswordUpgraderInterface
|
||||
{
|
||||
private ManagerRegistry $registry;
|
||||
private ?string $managerName;
|
||||
private string $classOrAlias;
|
||||
private string $class;
|
||||
private ?string $property;
|
||||
|
||||
public function __construct(ManagerRegistry $registry, string $classOrAlias, ?string $property = null, ?string $managerName = null)
|
||||
{
|
||||
$this->registry = $registry;
|
||||
$this->managerName = $managerName;
|
||||
$this->classOrAlias = $classOrAlias;
|
||||
$this->property = $property;
|
||||
}
|
||||
|
||||
public function loadUserByIdentifier(string $identifier): UserInterface
|
||||
{
|
||||
$repository = $this->getRepository();
|
||||
if (null !== $this->property) {
|
||||
$user = $repository->findOneBy([$this->property => $identifier]);
|
||||
} else {
|
||||
if (!$repository instanceof UserLoaderInterface) {
|
||||
throw new \InvalidArgumentException(sprintf('You must either make the "%s" entity Doctrine Repository ("%s") implement "Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface" or set the "property" option in the corresponding entity provider configuration.', $this->classOrAlias, get_debug_type($repository)));
|
||||
}
|
||||
|
||||
$user = $repository->loadUserByIdentifier($identifier);
|
||||
}
|
||||
|
||||
if (null === $user) {
|
||||
$e = new UserNotFoundException(sprintf('User "%s" not found.', $identifier));
|
||||
$e->setUserIdentifier($identifier);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function refreshUser(UserInterface $user): UserInterface
|
||||
{
|
||||
$class = $this->getClass();
|
||||
if (!$user instanceof $class) {
|
||||
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_debug_type($user)));
|
||||
}
|
||||
|
||||
$repository = $this->getRepository();
|
||||
if ($repository instanceof UserProviderInterface) {
|
||||
$refreshedUser = $repository->refreshUser($user);
|
||||
} else {
|
||||
// The user must be reloaded via the primary key as all other data
|
||||
// might have changed without proper persistence in the database.
|
||||
// That's the case when the user has been changed by a form with
|
||||
// validation errors.
|
||||
if (!$id = $this->getClassMetadata()->getIdentifierValues($user)) {
|
||||
throw new \InvalidArgumentException('You cannot refresh a user from the EntityUserProvider that does not contain an identifier. The user object has to be serialized with its own identifier mapped by Doctrine.');
|
||||
}
|
||||
|
||||
$refreshedUser = $repository->find($id);
|
||||
if (null === $refreshedUser) {
|
||||
$e = new UserNotFoundException('User with id '.json_encode($id).' not found.');
|
||||
$e->setUserIdentifier(json_encode($id));
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
if ($refreshedUser instanceof Proxy && !$refreshedUser->__isInitialized()) {
|
||||
$refreshedUser->__load();
|
||||
}
|
||||
|
||||
return $refreshedUser;
|
||||
}
|
||||
|
||||
public function supportsClass(string $class): bool
|
||||
{
|
||||
return $class === $this->getClass() || is_subclass_of($class, $this->getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* @final
|
||||
*/
|
||||
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
|
||||
{
|
||||
$class = $this->getClass();
|
||||
if (!$user instanceof $class) {
|
||||
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_debug_type($user)));
|
||||
}
|
||||
|
||||
$repository = $this->getRepository();
|
||||
if ($repository instanceof PasswordUpgraderInterface) {
|
||||
$repository->upgradePassword($user, $newHashedPassword);
|
||||
}
|
||||
}
|
||||
|
||||
private function getObjectManager(): ObjectManager
|
||||
{
|
||||
return $this->registry->getManager($this->managerName);
|
||||
}
|
||||
|
||||
private function getRepository(): ObjectRepository
|
||||
{
|
||||
return $this->getObjectManager()->getRepository($this->classOrAlias);
|
||||
}
|
||||
|
||||
private function getClass(): string
|
||||
{
|
||||
if (!isset($this->class)) {
|
||||
$class = $this->classOrAlias;
|
||||
|
||||
if (str_contains($class, ':')) {
|
||||
$class = $this->getClassMetadata()->getName();
|
||||
}
|
||||
|
||||
$this->class = $class;
|
||||
}
|
||||
|
||||
return $this->class;
|
||||
}
|
||||
|
||||
private function getClassMetadata(): ClassMetadata
|
||||
{
|
||||
return $this->getObjectManager()->getClassMetadata($this->classOrAlias);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Security\User;
|
||||
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* Represents a class that loads UserInterface objects from Doctrine source for the authentication system.
|
||||
*
|
||||
* This interface is meant to facilitate the loading of a User from Doctrine source using a custom method.
|
||||
* If you want to implement your own logic of retrieving the user from Doctrine your repository should implement this
|
||||
* interface.
|
||||
*
|
||||
* @see UserInterface
|
||||
*
|
||||
* @author Michal Trojanowski <michal@kmt-studio.pl>
|
||||
*/
|
||||
interface UserLoaderInterface
|
||||
{
|
||||
/**
|
||||
* Loads the user for the given user identifier (e.g. username or email).
|
||||
*
|
||||
* This method must return null if the user is not found.
|
||||
*/
|
||||
public function loadUserByIdentifier(string $identifier): ?UserInterface;
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Types;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Types\ConversionException;
|
||||
use Doctrine\DBAL\Types\Exception\InvalidType;
|
||||
use Doctrine\DBAL\Types\Exception\ValueNotConvertible;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Symfony\Component\Uid\AbstractUid;
|
||||
|
||||
abstract class AbstractUidType extends Type
|
||||
{
|
||||
/**
|
||||
* @return class-string<AbstractUid>
|
||||
*/
|
||||
abstract protected function getUidClass(): string;
|
||||
|
||||
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
|
||||
{
|
||||
if ($this->hasNativeGuidType($platform)) {
|
||||
return $platform->getGuidTypeDeclarationSQL($column);
|
||||
}
|
||||
|
||||
return $platform->getBinaryTypeDeclarationSQL([
|
||||
'length' => 16,
|
||||
'fixed' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ConversionException
|
||||
*/
|
||||
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?AbstractUid
|
||||
{
|
||||
if ($value instanceof AbstractUid || null === $value) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (!\is_string($value)) {
|
||||
$this->throwInvalidType($value);
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->getUidClass()::fromString($value);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$this->throwValueNotConvertible($value, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ConversionException
|
||||
*/
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
|
||||
{
|
||||
$toString = $this->hasNativeGuidType($platform) ? 'toRfc4122' : 'toBinary';
|
||||
|
||||
if ($value instanceof AbstractUid) {
|
||||
return $value->$toString();
|
||||
}
|
||||
|
||||
if (null === $value || '' === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!\is_string($value)) {
|
||||
$this->throwInvalidType($value);
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->getUidClass()::fromString($value)->$toString();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$this->throwValueNotConvertible($value, $e);
|
||||
}
|
||||
}
|
||||
|
||||
public function requiresSQLCommentHint(AbstractPlatform $platform): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
private function hasNativeGuidType(AbstractPlatform $platform): bool
|
||||
{
|
||||
// Compatibility with DBAL < 3.4
|
||||
$method = method_exists($platform, 'getStringTypeDeclarationSQL')
|
||||
? 'getStringTypeDeclarationSQL'
|
||||
: 'getVarcharTypeDeclarationSQL';
|
||||
|
||||
return $platform->getGuidTypeDeclarationSQL([]) !== $platform->$method(['fixed' => true, 'length' => 36]);
|
||||
}
|
||||
|
||||
private function throwInvalidType(mixed $value): never
|
||||
{
|
||||
if (!class_exists(InvalidType::class)) {
|
||||
throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', AbstractUid::class]);
|
||||
}
|
||||
|
||||
throw InvalidType::new($value, $this->getName(), ['null', 'string', AbstractUid::class]);
|
||||
}
|
||||
|
||||
private function throwValueNotConvertible(mixed $value, \Throwable $previous): never
|
||||
{
|
||||
if (!class_exists(ValueNotConvertible::class)) {
|
||||
throw ConversionException::conversionFailed($value, $this->getName(), $previous);
|
||||
}
|
||||
|
||||
throw ValueNotConvertible::new($value, $this->getName(), null, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Types;
|
||||
|
||||
use Symfony\Component\Uid\Ulid;
|
||||
|
||||
final class UlidType extends AbstractUidType
|
||||
{
|
||||
public const NAME = 'ulid';
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
protected function getUidClass(): string
|
||||
{
|
||||
return Ulid::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Types;
|
||||
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
final class UuidType extends AbstractUidType
|
||||
{
|
||||
public const NAME = 'uuid';
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
protected function getUidClass(): string
|
||||
{
|
||||
return Uuid::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Validator\Constraints;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* Constraint for the Unique Entity validator.
|
||||
*
|
||||
* @Annotation
|
||||
* @Target({"CLASS", "ANNOTATION"})
|
||||
*
|
||||
* @author Benjamin Eberlei <kontakt@beberlei.de>
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
|
||||
class UniqueEntity extends Constraint
|
||||
{
|
||||
public const NOT_UNIQUE_ERROR = '23bd9dbf-6b9b-41cd-a99e-4844bcf3077f';
|
||||
|
||||
protected const ERROR_NAMES = [
|
||||
self::NOT_UNIQUE_ERROR => 'NOT_UNIQUE_ERROR',
|
||||
];
|
||||
|
||||
public $message = 'This value is already used.';
|
||||
public $service = 'doctrine.orm.validator.unique';
|
||||
public $em;
|
||||
public $entityClass;
|
||||
public $repositoryMethod = 'findBy';
|
||||
public $fields = [];
|
||||
public $errorPath;
|
||||
public $ignoreNull = true;
|
||||
|
||||
/**
|
||||
* @deprecated since Symfony 6.1, use const ERROR_NAMES instead
|
||||
*/
|
||||
protected static $errorNames = self::ERROR_NAMES;
|
||||
|
||||
/**
|
||||
* @param array|string $fields The combination of fields that must contain unique values or a set of options
|
||||
* @param bool|array|string $ignoreNull The combination of fields that ignore null values
|
||||
*/
|
||||
public function __construct(
|
||||
$fields,
|
||||
?string $message = null,
|
||||
?string $service = null,
|
||||
?string $em = null,
|
||||
?string $entityClass = null,
|
||||
?string $repositoryMethod = null,
|
||||
?string $errorPath = null,
|
||||
bool|string|array|null $ignoreNull = null,
|
||||
?array $groups = null,
|
||||
$payload = null,
|
||||
array $options = []
|
||||
) {
|
||||
if (\is_array($fields) && \is_string(key($fields))) {
|
||||
$options = array_merge($fields, $options);
|
||||
} elseif (null !== $fields) {
|
||||
$options['fields'] = $fields;
|
||||
}
|
||||
|
||||
parent::__construct($options, $groups, $payload);
|
||||
|
||||
$this->message = $message ?? $this->message;
|
||||
$this->service = $service ?? $this->service;
|
||||
$this->em = $em ?? $this->em;
|
||||
$this->entityClass = $entityClass ?? $this->entityClass;
|
||||
$this->repositoryMethod = $repositoryMethod ?? $this->repositoryMethod;
|
||||
$this->errorPath = $errorPath ?? $this->errorPath;
|
||||
$this->ignoreNull = $ignoreNull ?? $this->ignoreNull;
|
||||
}
|
||||
|
||||
public function getRequiredOptions(): array
|
||||
{
|
||||
return ['fields'];
|
||||
}
|
||||
|
||||
/**
|
||||
* The validator must be defined as a service with this name.
|
||||
*/
|
||||
public function validatedBy(): string
|
||||
{
|
||||
return $this->service;
|
||||
}
|
||||
|
||||
public function getTargets(): string|array
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
|
||||
public function getDefaultOption(): ?string
|
||||
{
|
||||
return 'fields';
|
||||
}
|
||||
}
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Validator\Constraints;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Doctrine\Persistence\Mapping\ClassMetadata;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedValueException;
|
||||
|
||||
/**
|
||||
* Unique Entity Validator checks if one or a set of fields contain unique values.
|
||||
*
|
||||
* @author Benjamin Eberlei <kontakt@beberlei.de>
|
||||
*/
|
||||
class UniqueEntityValidator extends ConstraintValidator
|
||||
{
|
||||
private ManagerRegistry $registry;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $entity
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws UnexpectedTypeException
|
||||
* @throws ConstraintDefinitionException
|
||||
*/
|
||||
public function validate(mixed $entity, Constraint $constraint)
|
||||
{
|
||||
if (!$constraint instanceof UniqueEntity) {
|
||||
throw new UnexpectedTypeException($constraint, UniqueEntity::class);
|
||||
}
|
||||
|
||||
if (!\is_array($constraint->fields) && !\is_string($constraint->fields)) {
|
||||
throw new UnexpectedTypeException($constraint->fields, 'array');
|
||||
}
|
||||
|
||||
if (null !== $constraint->errorPath && !\is_string($constraint->errorPath)) {
|
||||
throw new UnexpectedTypeException($constraint->errorPath, 'string or null');
|
||||
}
|
||||
|
||||
$fields = (array) $constraint->fields;
|
||||
|
||||
if (0 === \count($fields)) {
|
||||
throw new ConstraintDefinitionException('At least one field has to be specified.');
|
||||
}
|
||||
|
||||
if (null === $entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!\is_object($entity)) {
|
||||
throw new UnexpectedValueException($entity, 'object');
|
||||
}
|
||||
|
||||
if ($constraint->em) {
|
||||
$em = $this->registry->getManager($constraint->em);
|
||||
|
||||
if (!$em) {
|
||||
throw new ConstraintDefinitionException(sprintf('Object manager "%s" does not exist.', $constraint->em));
|
||||
}
|
||||
} else {
|
||||
$em = $this->registry->getManagerForClass($entity::class);
|
||||
|
||||
if (!$em) {
|
||||
throw new ConstraintDefinitionException(sprintf('Unable to find the object manager associated with an entity of class "%s".', get_debug_type($entity)));
|
||||
}
|
||||
}
|
||||
|
||||
$class = $em->getClassMetadata($entity::class);
|
||||
|
||||
$criteria = [];
|
||||
$hasIgnorableNullValue = false;
|
||||
|
||||
foreach ($fields as $fieldName) {
|
||||
if (!$class->hasField($fieldName) && !$class->hasAssociation($fieldName)) {
|
||||
throw new ConstraintDefinitionException(sprintf('The field "%s" is not mapped by Doctrine, so it cannot be validated for uniqueness.', $fieldName));
|
||||
}
|
||||
|
||||
$fieldValue = $class->reflFields[$fieldName]->getValue($entity);
|
||||
|
||||
if (null === $fieldValue && $this->ignoreNullForField($constraint, $fieldName)) {
|
||||
$hasIgnorableNullValue = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$criteria[$fieldName] = $fieldValue;
|
||||
|
||||
if (null !== $criteria[$fieldName] && $class->hasAssociation($fieldName)) {
|
||||
/* Ensure the Proxy is initialized before using reflection to
|
||||
* read its identifiers. This is necessary because the wrapped
|
||||
* getter methods in the Proxy are being bypassed.
|
||||
*/
|
||||
$em->initializeObject($criteria[$fieldName]);
|
||||
}
|
||||
}
|
||||
|
||||
// validation doesn't fail if one of the fields is null and if null values should be ignored
|
||||
if ($hasIgnorableNullValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
// skip validation if there are no criteria (this can happen when the
|
||||
// "ignoreNull" option is enabled and fields to be checked are null
|
||||
if (empty($criteria)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $constraint->entityClass) {
|
||||
/* Retrieve repository from given entity name.
|
||||
* We ensure the retrieved repository can handle the entity
|
||||
* by checking the entity is the same, or subclass of the supported entity.
|
||||
*/
|
||||
$repository = $em->getRepository($constraint->entityClass);
|
||||
$supportedClass = $repository->getClassName();
|
||||
|
||||
if (!$entity instanceof $supportedClass) {
|
||||
throw new ConstraintDefinitionException(sprintf('The "%s" entity repository does not support the "%s" entity. The entity should be an instance of or extend "%s".', $constraint->entityClass, $class->getName(), $supportedClass));
|
||||
}
|
||||
} else {
|
||||
$repository = $em->getRepository($entity::class);
|
||||
}
|
||||
|
||||
$arguments = [$criteria];
|
||||
|
||||
/* If the default repository method is used, it is always enough to retrieve at most two entities because:
|
||||
* - No entity returned, the current entity is definitely unique.
|
||||
* - More than one entity returned, the current entity cannot be unique.
|
||||
* - One entity returned the uniqueness depends on the current entity.
|
||||
*/
|
||||
if ('findBy' === $constraint->repositoryMethod) {
|
||||
$arguments = [$criteria, null, 2];
|
||||
}
|
||||
|
||||
$result = $repository->{$constraint->repositoryMethod}(...$arguments);
|
||||
|
||||
if ($result instanceof \IteratorAggregate) {
|
||||
$result = $result->getIterator();
|
||||
}
|
||||
|
||||
/* If the result is a MongoCursor, it must be advanced to the first
|
||||
* element. Rewinding should have no ill effect if $result is another
|
||||
* iterator implementation.
|
||||
*/
|
||||
if ($result instanceof \Iterator) {
|
||||
$result->rewind();
|
||||
if ($result instanceof \Countable && 1 < \count($result)) {
|
||||
$result = [$result->current(), $result->current()];
|
||||
} else {
|
||||
$result = $result->valid() && null !== $result->current() ? [$result->current()] : [];
|
||||
}
|
||||
} elseif (\is_array($result)) {
|
||||
reset($result);
|
||||
} else {
|
||||
$result = null === $result ? [] : [$result];
|
||||
}
|
||||
|
||||
/* If no entity matched the query criteria or a single entity matched,
|
||||
* which is the same as the entity being validated, the criteria is
|
||||
* unique.
|
||||
*/
|
||||
if (!$result || (1 === \count($result) && current($result) === $entity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$errorPath = $constraint->errorPath ?? $fields[0];
|
||||
$invalidValue = $criteria[$errorPath] ?? $criteria[$fields[0]];
|
||||
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->atPath($errorPath)
|
||||
->setParameter('{{ value }}', $this->formatWithIdentifiers($em, $class, $invalidValue))
|
||||
->setInvalidValue($invalidValue)
|
||||
->setCode(UniqueEntity::NOT_UNIQUE_ERROR)
|
||||
->setCause($result)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
private function ignoreNullForField(UniqueEntity $constraint, string $fieldName): bool
|
||||
{
|
||||
if (\is_bool($constraint->ignoreNull)) {
|
||||
return $constraint->ignoreNull;
|
||||
}
|
||||
|
||||
return \in_array($fieldName, (array) $constraint->ignoreNull, true);
|
||||
}
|
||||
|
||||
private function formatWithIdentifiers(ObjectManager $em, ClassMetadata $class, mixed $value): string
|
||||
{
|
||||
if (!\is_object($value) || $value instanceof \DateTimeInterface) {
|
||||
return $this->formatValue($value, self::PRETTY_DATE);
|
||||
}
|
||||
|
||||
if ($value instanceof \Stringable) {
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
if ($class->getName() !== $idClass = $value::class) {
|
||||
// non unique value might be a composite PK that consists of other entity objects
|
||||
if ($em->getMetadataFactory()->hasMetadataFor($idClass)) {
|
||||
$identifiers = $em->getClassMetadata($idClass)->getIdentifierValues($value);
|
||||
} else {
|
||||
// this case might happen if the non unique column has a custom doctrine type and its value is an object
|
||||
// in which case we cannot get any identifiers for it
|
||||
$identifiers = [];
|
||||
}
|
||||
} else {
|
||||
$identifiers = $class->getIdentifierValues($value);
|
||||
}
|
||||
|
||||
if (!$identifiers) {
|
||||
return sprintf('object("%s")', $idClass);
|
||||
}
|
||||
|
||||
array_walk($identifiers, function (&$id, $field) {
|
||||
if (!\is_object($id) || $id instanceof \DateTimeInterface) {
|
||||
$idAsString = $this->formatValue($id, self::PRETTY_DATE);
|
||||
} else {
|
||||
$idAsString = sprintf('object("%s")', $id::class);
|
||||
}
|
||||
|
||||
$id = sprintf('%s => %s', $field, $idAsString);
|
||||
});
|
||||
|
||||
return sprintf('object("%s") identified by (%s)', $idClass, implode(', ', $identifiers));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Validator;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Symfony\Component\Validator\ObjectInitializerInterface;
|
||||
|
||||
/**
|
||||
* Automatically loads proxy object before validation.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class DoctrineInitializer implements ObjectInitializerInterface
|
||||
{
|
||||
protected $registry;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function initialize(object $object)
|
||||
{
|
||||
$this->registry->getManagerForClass($object::class)?->initializeObject($object);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Doctrine\Validator;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata as OrmClassMetadata;
|
||||
use Doctrine\ORM\Mapping\MappingException as OrmMappingException;
|
||||
use Doctrine\Persistence\Mapping\MappingException;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
use Symfony\Component\Validator\Constraints\Valid;
|
||||
use Symfony\Component\Validator\Mapping\AutoMappingStrategy;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
use Symfony\Component\Validator\Mapping\Loader\AutoMappingTrait;
|
||||
use Symfony\Component\Validator\Mapping\Loader\LoaderInterface;
|
||||
|
||||
/**
|
||||
* Guesses and loads the appropriate constraints using Doctrine's metadata.
|
||||
*
|
||||
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||
*/
|
||||
final class DoctrineLoader implements LoaderInterface
|
||||
{
|
||||
use AutoMappingTrait;
|
||||
|
||||
private EntityManagerInterface $entityManager;
|
||||
private ?string $classValidatorRegexp;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, ?string $classValidatorRegexp = null)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->classValidatorRegexp = $classValidatorRegexp;
|
||||
}
|
||||
|
||||
public function loadClassMetadata(ClassMetadata $metadata): bool
|
||||
{
|
||||
$className = $metadata->getClassName();
|
||||
try {
|
||||
$doctrineMetadata = $this->entityManager->getClassMetadata($className);
|
||||
} catch (MappingException|OrmMappingException) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$doctrineMetadata instanceof OrmClassMetadata) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$loaded = false;
|
||||
$enabledForClass = $this->isAutoMappingEnabledForClass($metadata, $this->classValidatorRegexp);
|
||||
|
||||
/* Available keys:
|
||||
- type
|
||||
- scale
|
||||
- length
|
||||
- unique
|
||||
- nullable
|
||||
- precision
|
||||
*/
|
||||
$existingUniqueFields = $this->getExistingUniqueFields($metadata);
|
||||
|
||||
// Type and nullable aren't handled here, use the PropertyInfo Loader instead.
|
||||
foreach ($doctrineMetadata->fieldMappings as $mapping) {
|
||||
$enabledForProperty = $enabledForClass;
|
||||
$lengthConstraint = null;
|
||||
foreach ($metadata->getPropertyMetadata($mapping['fieldName']) as $propertyMetadata) {
|
||||
// Enabling or disabling auto-mapping explicitly always takes precedence
|
||||
if (AutoMappingStrategy::DISABLED === $propertyMetadata->getAutoMappingStrategy()) {
|
||||
continue 2;
|
||||
}
|
||||
if (AutoMappingStrategy::ENABLED === $propertyMetadata->getAutoMappingStrategy()) {
|
||||
$enabledForProperty = true;
|
||||
}
|
||||
|
||||
foreach ($propertyMetadata->getConstraints() as $constraint) {
|
||||
if ($constraint instanceof Length) {
|
||||
$lengthConstraint = $constraint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$enabledForProperty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (true === ($mapping['unique'] ?? false) && !isset($existingUniqueFields[$mapping['fieldName']])) {
|
||||
$metadata->addConstraint(new UniqueEntity(['fields' => $mapping['fieldName']]));
|
||||
$loaded = true;
|
||||
}
|
||||
|
||||
if (null === ($mapping['length'] ?? null) || null !== ($mapping['enumType'] ?? null) || !\in_array($mapping['type'], ['string', 'text'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null === $lengthConstraint) {
|
||||
if (isset($mapping['originalClass']) && !str_contains($mapping['declaredField'], '.')) {
|
||||
$metadata->addPropertyConstraint($mapping['declaredField'], new Valid());
|
||||
$loaded = true;
|
||||
} elseif (property_exists($className, $mapping['fieldName']) && (!$doctrineMetadata->isMappedSuperclass || $metadata->getReflectionClass()->getProperty($mapping['fieldName'])->isPrivate())) {
|
||||
$metadata->addPropertyConstraint($mapping['fieldName'], new Length(['max' => $mapping['length']]));
|
||||
$loaded = true;
|
||||
}
|
||||
} elseif (null === $lengthConstraint->max) {
|
||||
// If a Length constraint exists and no max length has been explicitly defined, set it
|
||||
$lengthConstraint->max = $mapping['length'];
|
||||
}
|
||||
}
|
||||
|
||||
return $loaded;
|
||||
}
|
||||
|
||||
private function getExistingUniqueFields(ClassMetadata $metadata): array
|
||||
{
|
||||
$fields = [];
|
||||
foreach ($metadata->getConstraints() as $constraint) {
|
||||
if (!$constraint instanceof UniqueEntity) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (\is_string($constraint->fields)) {
|
||||
$fields[$constraint->fields] = true;
|
||||
} elseif (\is_array($constraint->fields) && 1 === \count($constraint->fields)) {
|
||||
$fields[$constraint->fields[0]] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"name": "symfony/doctrine-bridge",
|
||||
"type": "symfony-bridge",
|
||||
"description": "Provides integration for Doctrine with various Symfony components",
|
||||
"keywords": [],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"doctrine/event-manager": "^1.2|^2",
|
||||
"doctrine/persistence": "^2|^3",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/polyfill-ctype": "~1.8",
|
||||
"symfony/polyfill-mbstring": "~1.0",
|
||||
"symfony/service-contracts": "^2.5|^3"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/cache": "^5.4|^6.0",
|
||||
"symfony/config": "^5.4|^6.0",
|
||||
"symfony/dependency-injection": "^6.2",
|
||||
"symfony/doctrine-messenger": "^5.4|^6.0",
|
||||
"symfony/expression-language": "^5.4|^6.0",
|
||||
"symfony/form": "^5.4.21|^6.2.7",
|
||||
"symfony/http-kernel": "^6.3",
|
||||
"symfony/lock": "^6.3",
|
||||
"symfony/messenger": "^5.4|^6.0",
|
||||
"symfony/property-access": "^5.4|^6.0",
|
||||
"symfony/property-info": "^5.4|^6.0",
|
||||
"symfony/proxy-manager-bridge": "^5.4|^6.0",
|
||||
"symfony/security-core": "^6.0",
|
||||
"symfony/stopwatch": "^5.4|^6.0",
|
||||
"symfony/translation": "^5.4|^6.0",
|
||||
"symfony/uid": "^5.4|^6.0",
|
||||
"symfony/validator": "^5.4.25|~6.2.12|^6.3.1",
|
||||
"symfony/var-dumper": "^5.4|^6.0",
|
||||
"doctrine/annotations": "^1.13.1|^2",
|
||||
"doctrine/collections": "^1.0|^2.0",
|
||||
"doctrine/data-fixtures": "^1.1",
|
||||
"doctrine/dbal": "^2.13.1|^3|^4",
|
||||
"doctrine/orm": "^2.12|^3",
|
||||
"psr/log": "^1|^2|^3"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/annotations": "<1.13.1",
|
||||
"doctrine/dbal": "<2.13.1",
|
||||
"doctrine/lexer": "<1.1",
|
||||
"doctrine/orm": "<2.12",
|
||||
"symfony/cache": "<5.4",
|
||||
"symfony/dependency-injection": "<6.2",
|
||||
"symfony/form": "<5.4.21|>=6,<6.2.7",
|
||||
"symfony/http-foundation": "<6.3",
|
||||
"symfony/http-kernel": "<6.2",
|
||||
"symfony/lock": "<6.3",
|
||||
"symfony/messenger": "<5.4",
|
||||
"symfony/property-info": "<5.4",
|
||||
"symfony/security-bundle": "<5.4",
|
||||
"symfony/security-core": "<6.0",
|
||||
"symfony/validator": "<5.4.25|>=6,<6.2.12|>=6.3,<6.3.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Bridge\\Doctrine\\": "" },
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
Reference in New Issue
Block a user