Subida del módulo y tema de PrestaShop

This commit is contained in:
Kaloyan
2026-04-09 18:31:51 +02:00
parent 12c253296f
commit 16b3ff9424
39262 changed files with 7418797 additions and 0 deletions

View File

@@ -0,0 +1,216 @@
<?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 (\is_array($id) && \in_array(null, $id, true)) {
return null;
}
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,
'request' => $request,
]);
try {
return $this->expressionLanguage->evaluate($options->expr, $variables);
} catch (NoResultException|ConversionException) {
return null;
}
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,71 @@
<?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
{
public function __construct(
private readonly ManagerRegistry $registry,
) {
}
/**
* This cache warmer is not optional, without proxies fatal error occurs!
*/
public function isOptional(): bool
{
return false;
}
public function warmUp(string $cacheDir, ?string $buildDir = null): 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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,286 @@
<?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();
if (null === $debugDataHolder) {
trigger_deprecation('symfony/doctrine-bridge', '6.4', 'Not passing an instance of "%s" as "$debugDataHolder" to "%s()" is deprecated.', DebugDataHolder::class, __METHOD__);
}
}
/**
* Adds the stack logger for a connection.
*
* @return void
*
* @deprecated since Symfony 6.4, use a DebugDataHolder instead.
*/
public function addLogger(string $name, DebugStack $logger)
{
trigger_deprecation('symfony/doctrine-bridge', '6.4', '"%s()" is deprecated. Pass an instance of "%s" to the constructor instead.', __METHOD__, DebugDataHolder::class);
$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;
}
}
/**
* @return array
*/
public function getManagers()
{
return $this->data['managers'];
}
/**
* @return array
*/
public function getConnections()
{
return $this->data['connections'];
}
/**
* @return int
*/
public function getQueryCount()
{
return array_sum(array_map('count', $this->data['queries']));
}
/**
* @return array
*/
public function getQueries()
{
return $this->data['queries'];
}
/**
* @return float
*/
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];
}
}

View File

@@ -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\DataCollector;
final class ObjectParameter
{
private bool $stringable;
private string $class;
public function __construct(
private readonly object $object,
private readonly ?\Throwable $error,
) {
$this->stringable = $this->object instanceof \Stringable;
$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;
}
}

View File

@@ -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\DataFixtures;
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\DataFixtures\ReferenceRepository;
if (method_exists(ReferenceRepository::class, 'getReferences')) {
/** @internal */
trait AddFixtureImplementation
{
public function addFixture(FixtureInterface $fixture)
{
$this->doAddFixture($fixture);
}
}
} else {
/** @internal */
trait AddFixtureImplementation
{
public function addFixture(FixtureInterface $fixture): void
{
$this->doAddFixture($fixture);
}
}
}

View File

@@ -0,0 +1,47 @@
<?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;
trigger_deprecation('symfony/dependency-injection', '6.4', '"%s" is deprecated, use dependency injection in your fixtures instead.', ContainerAwareLoader::class);
/**
* 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.
*
* @deprecated since Symfony 6.4, use dependency injection in your fixtures instead
*/
class ContainerAwareLoader extends Loader
{
use AddFixtureImplementation;
public function __construct(
private readonly ContainerInterface $container,
) {
}
private function doAddFixture(FixtureInterface $fixture): void
{
if ($fixture instanceof ContainerAwareInterface) {
$fixture->setContainer($this->container);
}
parent::addFixture($fixture);
}
}

View File

@@ -0,0 +1,486 @@
<?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'));
}
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),
]);
}
if (str_contains($mappingDriverDef->getClass(), 'yml') || str_contains($mappingDriverDef->getClass(), 'xml')
|| str_contains($mappingDriverDef->getClass(), 'Yaml') || 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']));
}
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;
}
}

View File

@@ -0,0 +1,59 @@
<?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
{
public function __construct(
private readonly string $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);
}
}

View File

@@ -0,0 +1,168 @@
<?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 array $connections;
/**
* @var array<string, Definition>
*/
private array $eventManagers = [];
/**
* @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(
private readonly string $connectionsParameter,
private readonly string $managerTemplate,
private readonly string $tagPrefix,
) {
}
/**
* @return void
*/
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);
}
}

View File

@@ -0,0 +1,218 @@
<?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;
/**
* 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,
* for example 'doctrine.orm.%s_configuration'.
* @param string $registerAliasMethodName Method name to call on the configuration service. This
* depends on the Doctrine implementation.
* For example addEntityNamespace.
* @param string[] $aliasMap Map of alias to namespace
*/
public function __construct(
Definition|Reference $driver,
array $namespaces,
array $managerParameters,
string $driverPattern,
string|false $enabledParameter = false,
private readonly string $configurationPattern = '',
private readonly string $registerAliasMethodName = '',
private readonly array $aliasMap = [],
) {
$this->driver = $driver;
$this->namespaces = $namespaces;
$this->managerParameters = $managerParameters;
$this->driverPattern = $driverPattern;
$this->enabledParameter = $enabledParameter;
if ($aliasMap && (!$configurationPattern || !$registerAliasMethodName)) {
throw new \InvalidArgumentException('configurationPattern and registerAliasMethodName are required to register namespace alias.');
}
}
/**
* 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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\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.
*
* @final since Symfony 6.4
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Christophe Coevoet <stof@notk.org>
*/
class EntityFactory implements UserProviderFactoryInterface
{
public function __construct(
private readonly string $key,
private readonly string $providerId,
) {
}
public function create(ContainerBuilder $container, string $id, array $config): void
{
$container
->setDefinition($id, new ChildDefinition($this->providerId))
->addArgument($config['class'])
->addArgument($config['property'])
->addArgument($config['manager_name'])
;
}
public function getKey(): string
{
return $this->key;
}
public function addConfiguration(NodeDefinition $node): void
{
$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()
;
}
}

View File

@@ -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\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
{
/** @var class-string */
private readonly string $class;
/**
* 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(
private readonly ObjectManager $manager,
string $class,
private readonly ?IdReader $idReader = null,
private readonly ?EntityLoaderInterface $objectLoader = null,
) {
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->class = $manager->getClassMetadata($class)->getName();
}
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);
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,109 @@
<?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 readonly bool $singleId;
private readonly bool $intId;
private readonly string $idField;
private readonly ?self $associationIdReader;
public function __construct(
private readonly ObjectManager $om,
private readonly ClassMetadata $classMetadata,
) {
$ids = $classMetadata->getIdentifierFieldNames();
$idType = $classMetadata->getTypeOfField(current($ids));
$singleId = 1 === \count($ids);
$this->idField = current($ids);
// single field association are resolved, since the schema column could be an int
if ($singleId && $classMetadata->hasAssociation($this->idField)) {
$this->associationIdReader = new self($om, $om->getClassMetadata(
$classMetadata->getAssociationTargetClass($this->idField)
));
$singleId = $this->associationIdReader->isSingleId();
$this->intId = $this->associationIdReader->isIntId();
} else {
$this->intId = $singleId && \in_array($idType, ['integer', 'smallint', 'bigint']);
$this->associationIdReader = null;
}
$this->singleId = $singleId;
}
/**
* 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;
}
}

View File

@@ -0,0 +1,102 @@
<?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
{
public function __construct(
private readonly 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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,206 @@
<?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\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Mapping\FieldMapping;
use Doctrine\ORM\Mapping\JoinColumnMapping;
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 (null === self::getMappingValue($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(!self::getMappingValue($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);
$length = $mapping instanceof FieldMapping ? $mapping->length : ($mapping['length'] ?? null);
if (null !== $length) {
return new ValueGuess($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;
}
/**
* @template T of object
*
* @param class-string<T> $class
*
* @return array{0:ClassMetadata<T>, 1:string}|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);
}
private static function getMappingValue(array|JoinColumnMapping $mapping, string $key): mixed
{
if ($mapping instanceof JoinColumnMapping) {
return $mapping->$key ?? null;
}
return $mapping[$key] ?? null;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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()];
}
}

View File

@@ -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\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
{
public function __construct(
private readonly ?UlidFactory $factory = null,
) {
}
/**
* 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();
}
}

View File

@@ -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 readonly 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
vendor/symfony/doctrine-bridge/LICENSE vendored Normal file
View File

@@ -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.

View File

@@ -0,0 +1,91 @@
<?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;
trigger_deprecation('symfony/doctrine-bridge', '6.4', '"%s" is deprecated, use a middleware instead.', DbalLogger::class);
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated since Symfony 6.4, use a middleware instead.
*/
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;
}
}

View File

@@ -0,0 +1,78 @@
<?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);
} elseif ((new \ReflectionMethod($this, $this->methodMap[$name]))->isStatic()) {
$wrappedInstance = $this->{$this->methodMap[$name]}($this, false);
} else {
$wrappedInstance = $this->{$this->methodMap[$name]}(false);
}
$manager->setProxyInitializer(null);
return true;
},
$this->container,
Container::class
));
}
}

View File

@@ -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 $managerRegistry;
protected ?string $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;
}

View File

@@ -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\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
{
public function __construct(
private readonly 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();
}
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -0,0 +1,57 @@
<?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 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 bool $isHandling = false;
public function __construct(
ManagerRegistry $managerRegistry,
?string $entityManagerName = null,
private readonly ?LoggerInterface $logger = null,
) {
parent::__construct($managerRegistry, $entityManagerName);
}
protected function handleForManager(EntityManagerInterface $entityManager, Envelope $envelope, StackInterface $stack): Envelope
{
if ($this->isHandling) {
return $stack->next()->handle($envelope, $stack);
}
$this->isHandling = true;
$initialTransactionLevel = $entityManager->getConnection()->getTransactionNestingLevel();
try {
return $stack->next()->handle($envelope, $stack);
} finally {
if ($entityManager->getConnection()->getTransactionNestingLevel() > $initialTransactionLevel) {
$this->logger?->error('A handler opened a transaction but did not close it.', [
'message' => $envelope->getMessage(),
]);
}
$this->isHandling = false;
}
}
}

View File

@@ -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());
}
}

View File

@@ -0,0 +1,56 @@
<?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();
$success = false;
try {
$envelope = $stack->next()->handle($envelope, $stack);
$entityManager->flush();
$entityManager->getConnection()->commit();
$success = true;
return $envelope;
} catch (\Throwable $exception) {
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->getWrappedExceptions());
}
throw $exception;
} finally {
$connection = $entityManager->getConnection();
if (!$success && $connection->isTransactionActive()) {
$connection->rollBack();
}
}
}
}

View File

@@ -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 readonly DebugDataHolder $debugDataHolder,
private readonly ?Stopwatch $stopwatch,
private readonly 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');
}
}
}

View File

@@ -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 readonly DebugDataHolder $debugDataHolder,
private readonly ?Stopwatch $stopwatch,
private readonly 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');
}
}
}

View File

@@ -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 readonly Query $query;
public function __construct(
StatementInterface $statement,
private readonly DebugDataHolder $debugDataHolder,
private readonly string $connectionName,
string $sql,
private readonly ?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');
}
}
}

View File

@@ -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 = [];
}
}

View File

@@ -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(ConnectionInterface::class, 'commit'))->getReturnType()) {
return new DBAL3\Connection(
$connection,
$this->debugDataHolder,
$this->stopwatch,
$this->connectionName
);
}
return new Connection(
$connection,
$this->debugDataHolder,
$this->stopwatch,
$this->connectionName
);
}
}

View File

@@ -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 readonly DebugDataHolder $debugDataHolder,
private readonly ?Stopwatch $stopwatch,
private readonly string $connectionName = 'default',
) {
}
public function wrap(DriverInterface $driver): DriverInterface
{
return new Driver($driver, $this->debugDataHolder, $this->stopwatch, $this->connectionName);
}
}

View File

@@ -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;
}
}

View File

@@ -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 readonly DebugDataHolder $debugDataHolder,
private readonly string $connectionName,
string $sql,
private readonly ?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');
}
}
}

View File

@@ -0,0 +1,297 @@
<?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\BigIntType;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\EmbeddedClassMapping;
use Doctrine\ORM\Mapping\FieldMapping;
use Doctrine\ORM\Mapping\JoinColumnMapping;
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
{
public function __construct(
private readonly EntityManagerInterface $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 (self::getMappingValue($associationMapping, 'indexBy')) {
$subMetadata = $this->entityManager->getClassMetadata(self::getMappingValue($associationMapping, 'targetEntity'));
// Check if indexBy value is a property
$fieldName = self::getMappingValue($associationMapping, 'indexBy');
if (null === ($typeOfField = $subMetadata->getTypeOfField($fieldName))) {
$fieldName = $subMetadata->getFieldForColumn(self::getMappingValue($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(self::getMappingValue($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, self::getMappingValue($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);
// DBAL 4 has a special fallback strategy for BINGINT (int -> string)
if (Types::BIGINT === $typeOfField && !method_exists(BigIntType::class, 'getName')) {
return [
new Type(Type::BUILTIN_TYPE_INT, $nullable),
new Type(Type::BUILTIN_TYPE_STRING, $nullable),
];
}
$enumType = null;
if (null !== $enumClass = self::getMappingValue($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 (self::getMappingValue($associationMapping, 'id')) {
return false;
}
if (!self::getMappingValue($associationMapping, 'joinColumns')) {
return true;
}
$joinColumns = self::getMappingValue($associationMapping, 'joinColumns');
foreach ($joinColumns as $joinColumn) {
if (false === self::getMappingValue($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,
};
}
private static function getMappingValue(array|AssociationMapping|EmbeddedClassMapping|FieldMapping|JoinColumnMapping $mapping, string $key): mixed
{
if ($mapping instanceof AssociationMapping || $mapping instanceof EmbeddedClassMapping || $mapping instanceof FieldMapping || $mapping instanceof JoinColumnMapping) {
return $mapping->$key ?? null;
}
return $mapping[$key] ?? null;
}
}

View File

@@ -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\SchemaListener;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Types\Types;
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 {
$schemaManager = method_exists($connection, 'createSchemaManager') ? $connection->createSchemaManager() : $connection->getSchemaManager();
$checkTable = 'schema_subscriber_check_'.bin2hex(random_bytes(7));
$table = new Table($checkTable);
$table->addColumn('id', Types::INTEGER)
->setAutoincrement(true)
->setNotnull(true);
$table->setPrimaryKey(['id']);
$schemaManager->createTable($table);
try {
$exec(\sprintf('DROP TABLE %s', $checkTable));
} catch (\Exception) {
// ignore
}
try {
$schemaManager->dropTable($checkTable);
return false;
} catch (TableNotFoundException) {
return true;
}
};
}
}

View File

@@ -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\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 readonly 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));
}
}
}

View File

@@ -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,
];
}
}

View File

@@ -0,0 +1,40 @@
<?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\PersistingStoreInterface;
use Symfony\Component\Lock\Store\DoctrineDbalStore;
final class LockStoreSchemaListener extends AbstractSchemaListener
{
/**
* @param iterable<mixed, PersistingStoreInterface> $stores
*/
public function __construct(
private readonly iterable $stores,
) {
}
public function postGenerateSchema(GenerateSchemaEventArgs $event): void
{
$connection = $event->getEntityManager()->getConnection();
foreach ($this->stores as $store) {
if (!$store instanceof DoctrineDbalStore) {
continue;
}
$store->configureSchema($event->getSchema(), $this->getIsSameDatabaseChecker($connection));
}
}
}

View File

@@ -0,0 +1,83 @@
<?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 readonly 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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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\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 readonly 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));
}
}
}
}

View File

@@ -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,
];
}
}

View File

@@ -0,0 +1,217 @@
<?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
* );
*
* @final since Symfony 6.4
*/
class DoctrineTokenProvider implements TokenProviderInterface, TokenVerifierInterface
{
public function __construct(
private readonly Connection $conn,
) {
}
public function loadTokenBySeries(string $series): PersistentTokenInterface
{
$sql = 'SELECT class, username, value, lastUsed FROM rememberme_token WHERE series=:series';
$paramValues = ['series' => $series];
$paramTypes = ['series' => ParameterType::STRING];
$stmt = $this->conn->executeQuery($sql, $paramValues, $paramTypes);
// fetching numeric because column name casing depends on platform, eg. Oracle converts all not quoted names to uppercase
$row = $stmt instanceof Result || $stmt instanceof DriverResult ? $stmt->fetchNumeric() : $stmt->fetch(\PDO::FETCH_NUM);
if ($row) {
[$class, $username, $value, $last_used] = $row;
return new PersistentToken($class, $username, $series, $value, new \DateTimeImmutable($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];
$this->conn->executeStatement($sql, $paramValues, $paramTypes);
}
public function updateToken(string $series, #[\SensitiveParameter] string $tokenValue, \DateTimeInterface $lastUsed): void
{
$sql = 'UPDATE rememberme_token SET value=:value, lastUsed=:lastUsed WHERE series=:series';
$paramValues = [
'value' => $tokenValue,
'lastUsed' => \DateTimeImmutable::createFromInterface($lastUsed),
'series' => $series,
];
$paramTypes = [
'value' => ParameterType::STRING,
'lastUsed' => Types::DATETIME_IMMUTABLE,
'series' => ParameterType::STRING,
];
$updated = $this->conn->executeStatement($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' => \DateTimeImmutable::createFromInterface($token->getLastUsed()),
];
$paramTypes = [
'class' => ParameterType::STRING,
'username' => ParameterType::STRING,
'series' => ParameterType::STRING,
'value' => ParameterType::STRING,
'lastUsed' => Types::DATETIME_IMMUTABLE,
];
$this->conn->executeStatement($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);
$lastUsed = \DateTime::createFromInterface($lastUsed);
$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_IMMUTABLE);
$table->addColumn('class', Types::STRING, ['length' => 100]);
$table->addColumn('username', Types::STRING, ['length' => 200]);
$table->setPrimaryKey(['series']);
}
}

View File

@@ -0,0 +1,160 @@
<?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>
*
* @template TUser of UserInterface
*
* @template-implements UserProviderInterface<TUser>
*/
class EntityUserProvider implements UserProviderInterface, PasswordUpgraderInterface
{
private string $class;
public function __construct(
private readonly ManagerRegistry $registry,
private readonly string $classOrAlias,
private readonly ?string $property = null,
private readonly ?string $managerName = null,
) {
}
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();
} elseif (\PHP_VERSION_ID >= 80400 && ($r = new \ReflectionClass($refreshedUser))->isUninitializedLazyObject($refreshedUser)) {
$r->initializeLazyObject($refreshedUser);
}
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 ($user instanceof PasswordAuthenticatedUserInterface && $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);
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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';
}
}

View File

@@ -0,0 +1,246 @@
<?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
{
public function __construct(
private readonly ManagerRegistry $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) {
try {
$em = $this->registry->getManager($constraint->em);
} catch (\InvalidArgumentException $e) {
throw new ConstraintDefinitionException(\sprintf('Object manager "%s" does not exist.', $constraint->em), 0, $e);
}
} 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));
}
if (property_exists($class, 'propertyAccessors')) {
$fieldValue = $class->propertyAccessors[$fieldName]->getValue($entity);
} else {
$fieldValue = $class->reflFields[$fieldName]->getValue($entity);
}
if (null === $fieldValue && $this->ignoreNullForField($constraint, $fieldName)) {
$hasIgnorableNullValue = true;
continue;
}
$criteria[$fieldName] = $fieldValue;
if (\is_object($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));
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,145 @@
<?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\FieldMapping;
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;
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly ?string $classValidatorRegexp = null,
) {
}
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(self::getFieldMappingValue($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 === (self::getFieldMappingValue($mapping, 'unique') ?? false) && !isset($existingUniqueFields[self::getFieldMappingValue($mapping, 'fieldName')])) {
$metadata->addConstraint(new UniqueEntity(['fields' => self::getFieldMappingValue($mapping, 'fieldName')]));
$loaded = true;
}
if (null === (self::getFieldMappingValue($mapping, 'length') ?? null) || null !== (self::getFieldMappingValue($mapping, 'enumType') ?? null) || !\in_array(self::getFieldMappingValue($mapping, 'type'), ['string', 'text'], true)) {
continue;
}
if (null === $lengthConstraint) {
if (self::getFieldMappingValue($mapping, 'originalClass') && !str_contains(self::getFieldMappingValue($mapping, 'declaredField'), '.')) {
$metadata->addPropertyConstraint(self::getFieldMappingValue($mapping, 'declaredField'), new Valid());
$loaded = true;
} elseif (property_exists($className, self::getFieldMappingValue($mapping, 'fieldName')) && (!$doctrineMetadata->isMappedSuperclass || $metadata->getReflectionClass()->getProperty(self::getFieldMappingValue($mapping, 'fieldName'))->isPrivate())) {
$metadata->addPropertyConstraint(self::getFieldMappingValue($mapping, 'fieldName'), new Length(['max' => self::getFieldMappingValue($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 = self::getFieldMappingValue($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;
}
private static function getFieldMappingValue(array|FieldMapping $mapping, string $key): mixed
{
if ($mapping instanceof FieldMapping) {
return $mapping->$key ?? null;
}
return $mapping[$key] ?? null;
}
}