diff --git a/composer.json b/composer.json index 0fdf1de..dd114b6 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "distantmagic/swoole-futures": "^0.1.2", "doctrine/dbal": "^3.7", "doctrine/migrations": "^3.6", - "doctrine/orm": "^2.16", + "doctrine/orm": "^3.0", "doctrine/sql-formatter": "^1.1", "ezyang/htmlpurifier": "^4.16", "guzzlehttp/guzzle": "^7.8", diff --git a/composer.lock b/composer.lock index d929842..fbf5869 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a48f98db536d0c397573db6845651935", + "content-hash": "2008e1dfe7cb51ec6de8a61bdd663c0e", "packages": [ { "name": "amphp/amp", @@ -955,97 +955,6 @@ ], "time": "2023-10-03T09:22:33+00:00" }, - { - "name": "doctrine/common", - "version": "3.4.3", - "source": { - "type": "git", - "url": "https://github.com/doctrine/common.git", - "reference": "8b5e5650391f851ed58910b3e3d48a71062eeced" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/8b5e5650391f851ed58910b3e3d48a71062eeced", - "reference": "8b5e5650391f851ed58910b3e3d48a71062eeced", - "shasum": "" - }, - "require": { - "doctrine/persistence": "^2.0 || ^3.0", - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9.0 || ^10.0", - "doctrine/collections": "^1", - "phpstan/phpstan": "^1.4.1", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5.20 || ^8.5 || ^9.0", - "squizlabs/php_codesniffer": "^3.0", - "symfony/phpunit-bridge": "^6.1", - "vimeo/psalm": "^4.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - }, - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com" - } - ], - "description": "PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflection support, proxies and much more.", - "homepage": "https://www.doctrine-project.org/projects/common.html", - "keywords": [ - "common", - "doctrine", - "php" - ], - "support": { - "issues": "https://github.com/doctrine/common/issues", - "source": "https://github.com/doctrine/common/tree/3.4.3" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcommon", - "type": "tidelift" - } - ], - "time": "2022-10-09T11:47:59+00:00" - }, { "name": "doctrine/dbal", "version": "3.8.1", @@ -1639,61 +1548,48 @@ }, { "name": "doctrine/orm", - "version": "2.18.0", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "f2176a9ce56cafdfd1624d54bfdb076819083d5b" + "reference": "5b8b5f28f535e1f03b54dcfb0427407ed92f5b72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/f2176a9ce56cafdfd1624d54bfdb076819083d5b", - "reference": "f2176a9ce56cafdfd1624d54bfdb076819083d5b", + "url": "https://api.github.com/repos/doctrine/orm/zipball/5b8b5f28f535e1f03b54dcfb0427407ed92f5b72", + "reference": "5b8b5f28f535e1f03b54dcfb0427407ed92f5b72", "shasum": "" }, "require": { "composer-runtime-api": "^2", - "doctrine/cache": "^1.12.1 || ^2.1.1", - "doctrine/collections": "^1.5 || ^2.1", - "doctrine/common": "^3.0.3", - "doctrine/dbal": "^2.13.1 || ^3.2", + "doctrine/collections": "^2.1", + "doctrine/dbal": "^3.6 || ^4", "doctrine/deprecations": "^0.5.3 || ^1", "doctrine/event-manager": "^1.2 || ^2", "doctrine/inflector": "^1.4 || ^2.0", "doctrine/instantiator": "^1.3 || ^2", - "doctrine/lexer": "^2 || ^3", - "doctrine/persistence": "^2.4 || ^3", + "doctrine/lexer": "^3", + "doctrine/persistence": "^3.1.1", "ext-ctype": "*", - "php": "^7.1 || ^8.0", + "php": "^8.1", "psr/cache": "^1 || ^2 || ^3", - "symfony/console": "^4.2 || ^5.0 || ^6.0 || ^7.0", - "symfony/polyfill-php72": "^1.23", - "symfony/polyfill-php80": "^1.16" - }, - "conflict": { - "doctrine/annotations": "<1.13 || >= 3.0" + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/var-exporter": "~6.2.13 || ^6.3.2 || ^7.0" }, "require-dev": { - "doctrine/annotations": "^1.13 || ^2", - "doctrine/coding-standard": "^9.0.2 || ^12.0", - "phpbench/phpbench": "^0.16.10 || ^1.0", - "phpstan/phpstan": "~1.4.10 || 1.10.35", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", + "doctrine/coding-standard": "^12.0", + "phpbench/phpbench": "^1.0", + "phpstan/phpstan": "1.10.35", + "phpunit/phpunit": "^10.4.0", "psr/log": "^1 || ^2 || ^3", "squizlabs/php_codesniffer": "3.7.2", - "symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7.0", - "symfony/var-exporter": "^4.4 || ^5.4 || ^6.2 || ^7.0", - "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "vimeo/psalm": "4.30.0 || 5.16.0" + "symfony/cache": "^5.4 || ^6.2 || ^7.0", + "vimeo/psalm": "5.16.0" }, "suggest": { "ext-dom": "Provides support for XSD validation for XML mapping files", - "symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0", - "symfony/yaml": "If you want to use YAML Metadata Mapping Driver" + "symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0" }, - "bin": [ - "bin/doctrine" - ], "type": "library", "autoload": { "psr-4": { @@ -1734,9 +1630,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/2.18.0" + "source": "https://github.com/doctrine/orm/tree/3.0.0" }, - "time": "2024-01-31T15:53:12+00:00" + "time": "2024-02-03T16:50:09+00:00" }, { "name": "doctrine/persistence", diff --git a/src/Attribute/ListensToDoctrineEntityEvents.php b/src/Attribute/ListensToDoctrineEntityEvents.php new file mode 100644 index 0000000..7378937 --- /dev/null +++ b/src/Attribute/ListensToDoctrineEntityEvents.php @@ -0,0 +1,17 @@ +configuration, + driver: $this->getDriver($name), + eventManager: $this->eventManager, + params: [ 'driverOptions' => [ 'connectionPoolName' => $name, ], ], - $this->getDriver($name), - $this->configuration, ); } diff --git a/src/DoctrineEntityListener.php b/src/DoctrineEntityListener.php new file mode 100644 index 0000000..47ba961 --- /dev/null +++ b/src/DoctrineEntityListener.php @@ -0,0 +1,7 @@ +> + */ + public Map $listeners; + + public function __construct() + { + $this->listeners = new Map(); + } + + /** + * @param class-string $className + */ + public function addEntityListener(string $className, object $listener): void + { + if (!$this->listeners->hasKey($className)) { + $this->listeners->put($className, new Set()); + } + + $this->listeners->get($className)->add($listener); + } +} diff --git a/src/DoctrineEntityListenerResolver.php b/src/DoctrineEntityListenerResolver.php new file mode 100644 index 0000000..142898d --- /dev/null +++ b/src/DoctrineEntityListenerResolver.php @@ -0,0 +1,75 @@ + + */ + private Map $instances; + + public function __construct() + { + $this->instances = new Map(); + } + + public function clear($className = null): never + { + throw new LogicException('Listeners cannot be cleared on runtime'); + } + + /** + * @param mixed $object explicitly mixed for typechecks + */ + public function register($object): void + { + if (!is_object($object)) { + throw new InvalidArgumentException(sprintf( + 'Expected object, got "%s"', + gettype($object) + )); + } + + if ($this->instances->hasKey($object::class)) { + throw new RuntimeException(sprintf( + 'Listener is already registered: "%s"', + $object::class, + )); + } + + $this->instances->put($object::class, $object); + } + + /** + * @param string $className + */ + public function resolve($className): object + { + if (!class_exists($className)) { + throw new InvalidArgumentException(sprintf( + 'Expected a class-string, got "%s"', + $className + )); + } + + $listener = $this->instances->get($className, null); + + if (is_null($listener)) { + throw new RuntimeException(sprintf( + 'Could not find listener: "%s"', + $className, + )); + } + + return $listener; + } +} diff --git a/src/DoctrineEntityManagerRepository.php b/src/DoctrineEntityManagerRepository.php index 92b658f..b0c2465 100644 --- a/src/DoctrineEntityManagerRepository.php +++ b/src/DoctrineEntityManagerRepository.php @@ -4,6 +4,8 @@ namespace Distantmagic\Resonance; +use Doctrine\Common\EventManager; +use Doctrine\DBAL\Connection; use Doctrine\ORM\Configuration; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManagerInterface; @@ -23,8 +25,9 @@ private WeakMap $entityManagers; public function __construct( - private DoctrineConnectionRepository $doctrineConnectionRepository, private Configuration $configuration, + private DoctrineConnectionRepository $doctrineConnectionRepository, + private EventManager $eventManager, ) { /** * @var WeakMap> @@ -37,9 +40,8 @@ public function __construct( */ public function buildEntityManager(string $name = 'default'): EntityManagerInterface { - return new EntityManager( - $this->doctrineConnectionRepository->buildConnection($name), - $this->configuration, + return $this->buildEntityManagerFromConnection( + connection: $this->doctrineConnectionRepository->buildConnection($name), ); } @@ -76,8 +78,9 @@ public function getEntityManager(Request $request, string $name = 'default'): En return $context[$contextKey]->getEntityManager(); } - $conn = $this->doctrineConnectionRepository->getConnection($request, $name); - $entityManager = new EntityManager($conn, $this->configuration); + $entityManager = $this->buildEntityManagerFromConnection( + connection: $this->doctrineConnectionRepository->getConnection($request, $name), + ); if ($context) { $context[$contextKey] = new EntityManagerWeakReference($entityManager); @@ -152,6 +155,15 @@ public function withRepository(string $className, callable $callback, string $na }, $name, $flush); } + private function buildEntityManagerFromConnection(Connection $connection): EntityManagerInterface + { + return new EntityManager( + conn: $connection, + config: $this->configuration, + eventManager: $this->eventManager, + ); + } + /** * @param non-empty-string $name */ diff --git a/src/DoctrineEventSubscriber.php b/src/DoctrineEventSubscriber.php new file mode 100644 index 0000000..05db9b1 --- /dev/null +++ b/src/DoctrineEventSubscriber.php @@ -0,0 +1,9 @@ +getClassMetadata(); + + $entityListeners = $this + ->doctrineEntityListenerCollection + ->listeners + ->get($classMetadata->name, null) + ; + + if (is_null($entityListeners)) { + return; + } + + foreach ($entityListeners as $entityListener) { + EntityListenerBuilder::bindEntityListener($classMetadata, $entityListener::class); + } + } +} diff --git a/src/SingletonCollection.php b/src/SingletonCollection.php index 33d2aa2..f120428 100644 --- a/src/SingletonCollection.php +++ b/src/SingletonCollection.php @@ -9,6 +9,8 @@ enum SingletonCollection implements SingletonCollectionInterface case AuthenticatedUserStore; case CronJob; case CrudActionGate; + case DoctrineEntityListener; + case DoctrineEventListener; case EventListener; case GraphQLRootField; case HttpControllerParameterResolver; diff --git a/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php index 14fe195..dddd3e4 100644 --- a/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php @@ -25,7 +25,7 @@ * driver: string, * host: non-empty-string, * log_queries: bool, - * password: string, + * password: null|non-empty-string, * pool_prefill: bool, * pool_size: int, * port: int, diff --git a/src/SingletonProvider/DatabaseConnectionPoolRepositoryProvider.php b/src/SingletonProvider/DatabaseConnectionPoolRepositoryProvider.php index e3e216d..a79340a 100644 --- a/src/SingletonProvider/DatabaseConnectionPoolRepositoryProvider.php +++ b/src/SingletonProvider/DatabaseConnectionPoolRepositoryProvider.php @@ -43,8 +43,12 @@ public function provide(SingletonContainer $singletons, PHPProjectFiles $phpProj $pdoConfig->withDbName($connectionPoolConfiguration->database); $pdoConfig->withDriver($connectionPoolConfiguration->driver->value); + $pdoConfig->withUsername($connectionPoolConfiguration->username); - $pdoConfig->withPassword($connectionPoolConfiguration->password); + + if (is_string($connectionPoolConfiguration->password)) { + $pdoConfig->withPassword($connectionPoolConfiguration->password); + } $pdoPool = new PDOPool( $this->pdoPoolConnectionBuilderCollection->getBuildersForConnection($name), diff --git a/src/SingletonProvider/DoctrineEntityListenerCollectionProvider.php b/src/SingletonProvider/DoctrineEntityListenerCollectionProvider.php new file mode 100644 index 0000000..6150c6a --- /dev/null +++ b/src/SingletonProvider/DoctrineEntityListenerCollectionProvider.php @@ -0,0 +1,40 @@ + + */ +#[GrantsFeature(Feature::Doctrine)] +#[RequiresSingletonCollection(SingletonCollection::DoctrineEntityListener)] +#[Singleton(provides: DoctrineEntityListenerCollection::class)] +final readonly class DoctrineEntityListenerCollectionProvider extends SingletonProvider +{ + public function provide(SingletonContainer $singletons, PHPProjectFiles $phpProjectFiles): DoctrineEntityListenerCollection + { + $entityListenerCollection = new DoctrineEntityListenerCollection(); + + foreach (new SingletonContainerAttributeIterator($singletons, ListensToDoctrineEntityEvents::class) as $listenerAttribute) { + $entityListenerCollection->addEntityListener( + $listenerAttribute->attribute->className, + $listenerAttribute->singleton, + ); + } + + return $entityListenerCollection; + } +} diff --git a/src/SingletonProvider/DoctrineEntityListenerResolverProvider.php b/src/SingletonProvider/DoctrineEntityListenerResolverProvider.php new file mode 100644 index 0000000..546f227 --- /dev/null +++ b/src/SingletonProvider/DoctrineEntityListenerResolverProvider.php @@ -0,0 +1,42 @@ + + */ +#[GrantsFeature(Feature::Doctrine)] +#[RequiresSingletonCollection(SingletonCollection::DoctrineEntityListener)] +#[Singleton(provides: DoctrineEntityListenerResolver::class)] +final readonly class DoctrineEntityListenerResolverProvider extends SingletonProvider +{ + public function __construct( + private DoctrineEntityListenerCollection $doctrineEntityListenerCollection, + ) {} + + public function provide(SingletonContainer $singletons, PHPProjectFiles $phpProjectFiles): DoctrineEntityListenerResolver + { + $entityListenerResolver = new DoctrineEntityListenerResolver(); + + foreach ($this->doctrineEntityListenerCollection->listeners as $listeners) { + foreach ($listeners as $listener) { + $entityListenerResolver->register($listener); + } + } + + return $entityListenerResolver; + } +} diff --git a/src/SingletonProvider/DoctrineEntityManagerRepositoryProvider.php b/src/SingletonProvider/DoctrineEntityManagerRepositoryProvider.php index cc77f59..6167b48 100644 --- a/src/SingletonProvider/DoctrineEntityManagerRepositoryProvider.php +++ b/src/SingletonProvider/DoctrineEntityManagerRepositoryProvider.php @@ -14,6 +14,7 @@ use Distantmagic\Resonance\PHPProjectFiles; use Distantmagic\Resonance\SingletonContainer; use Distantmagic\Resonance\SingletonProvider; +use Doctrine\Common\EventManager; use Doctrine\ORM\Configuration; use Symfony\Component\Filesystem\Filesystem; @@ -26,15 +27,17 @@ { public function __construct( private ApplicationConfiguration $applicationConfiguration, - private DoctrineConnectionRepository $doctrineConnectionRepository, private Configuration $configuration, + private DoctrineConnectionRepository $doctrineConnectionRepository, + private EventManager $eventManager, ) {} public function provide(SingletonContainer $singletons, PHPProjectFiles $phpProjectFiles): DoctrineEntityManagerRepository { $doctrineEntityManagerRepository = new DoctrineEntityManagerRepository( - $this->doctrineConnectionRepository, $this->configuration, + $this->doctrineConnectionRepository, + $this->eventManager, ); if (Environment::Development !== $this->applicationConfiguration->environment) { diff --git a/src/SingletonProvider/DoctrineEventManagerProvider.php b/src/SingletonProvider/DoctrineEventManagerProvider.php new file mode 100644 index 0000000..6de1db1 --- /dev/null +++ b/src/SingletonProvider/DoctrineEventManagerProvider.php @@ -0,0 +1,47 @@ + + */ +#[RequiresSingletonCollection(SingletonCollection::DoctrineEventListener)] +#[Singleton(provides: EventManager::class)] +final readonly class DoctrineEventManagerProvider extends SingletonProvider +{ + public function provide(SingletonContainer $singletons, PHPProjectFiles $phpProjectFiles): EventManager + { + $eventManger = new EventManager(); + + foreach ($this->collectEventSubscribers($singletons) as $subscriberAttribute) { + $eventManger->addEventSubscriber($subscriberAttribute->singleton); + } + + return $eventManger; + } + + /** + * @return iterable> + */ + private function collectEventSubscribers(SingletonContainer $singletons): iterable + { + return $this->collectAttributes( + $singletons, + EventSubscriber::class, + ListensToDoctrineEvents::class, + ); + } +} diff --git a/src/SingletonProvider/DoctrineORMConfigurationProvider.php b/src/SingletonProvider/DoctrineORMConfigurationProvider.php index 7db8792..5167ace 100644 --- a/src/SingletonProvider/DoctrineORMConfigurationProvider.php +++ b/src/SingletonProvider/DoctrineORMConfigurationProvider.php @@ -8,6 +8,7 @@ use Distantmagic\Resonance\Attribute\GrantsFeature; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\DoctrineAttributeDriver; +use Distantmagic\Resonance\DoctrineEntityListenerResolver; use Distantmagic\Resonance\Environment; use Distantmagic\Resonance\Feature; use Distantmagic\Resonance\PHPProjectFiles; @@ -28,18 +29,22 @@ public function __construct( private ApplicationConfiguration $applicationConfiguration, private DoctrineAttributeDriver $doctrineAttributeDriver, + private DoctrineEntityListenerResolver $doctrineEntityListenerResolver, ) {} public function provide(SingletonContainer $singletons, PHPProjectFiles $phpProjectFiles): Configuration { $isDevMode = Environment::Development === $this->applicationConfiguration->environment; + $cache = new ArrayAdapter(storeSerialized: false); + $configuration = ORMSetup::createConfiguration( - cache: new ArrayAdapter(storeSerialized: false), + cache: $cache, proxyDir: DM_ROOT.'/cache/doctrine', isDevMode: $isDevMode, ); + $configuration->setEntityListenerResolver($this->doctrineEntityListenerResolver); $configuration->setMetadataDriverImpl($this->doctrineAttributeDriver); $configuration->setAutoGenerateProxyClasses( $isDevMode diff --git a/src/SingletonProvider/MailerRepositoryProvider.php b/src/SingletonProvider/MailerRepositoryProvider.php index 5f7e095..8850a8b 100644 --- a/src/SingletonProvider/MailerRepositoryProvider.php +++ b/src/SingletonProvider/MailerRepositoryProvider.php @@ -5,7 +5,6 @@ namespace Distantmagic\Resonance\SingletonProvider; use Distantmagic\Resonance\Attribute\Singleton; -use Distantmagic\Resonance\EventDispatcherInterface; use Distantmagic\Resonance\Mailer; use Distantmagic\Resonance\MailerConfiguration; use Distantmagic\Resonance\MailerRepository; @@ -28,7 +27,6 @@ final readonly class MailerRepositoryProvider extends SingletonProvider { public function __construct( - private EventDispatcherInterface $eventDispatcher, private HttpClientInterface $httpClient, private LoggerInterface $logger, private MailerConfiguration $mailerConfiguration, @@ -96,7 +94,6 @@ private function buildTransport(MailerTransportConfiguration $transportConfigura { return Transport::fromDsn( client: $this->httpClient, - dispatcher: $this->eventDispatcher, dsn: $transportConfiguration->transportDsn, logger: $this->logger, );