diff --git a/src/.vitepress/config.js b/src/.vitepress/config.js index daf8b6a0..2111823b 100644 --- a/src/.vitepress/config.js +++ b/src/.vitepress/config.js @@ -41,6 +41,7 @@ export default { nav: [ {text: 'Guide', link: '/guide/'}, {text: 'Cookbook', link: '/cookbook/'}, + {text: 'Third-Party Integrations', link: '/third-party-integrations/'}, {text: 'Internals', link: '/internals/'}, {text: 'Site', link: 'https://www.yiiframework.com'} ], @@ -207,7 +208,8 @@ export default { {text: 'Disabling CSRF Protection', link: '/cookbook/disabling-csrf-protection'}, {text: 'Sentry Integration', link: '/cookbook/sentry-integration'}, {text: 'Using a Custom Migration Template', link: '/cookbook/custom-migration-template'}, - {text: 'Using Yii in Third-Party Applications', link: '/cookbook/using-yii-in-third-party-apps'} + {text: 'Using Yii in Third-Party Applications', link: '/cookbook/using-yii-in-third-party-apps'}, + {text: 'Third-Party Integrations', link: '/third-party-integrations/'} ] }, { @@ -247,6 +249,14 @@ export default { ] } ], + '/third-party-integrations/': [ + { + text: 'Third-Party Integrations', + items: [ + {text: 'Symfony Messenger integration guide', link: '/third-party-integrations/symfony-messenger-integration-guide'}, + ] + } + ], '/internals/': [ { text: 'Internals', diff --git a/src/index.md b/src/index.md index 7e249969..2237c734 100644 --- a/src/index.md +++ b/src/index.md @@ -32,5 +32,6 @@ features: - [The Definitive Guide](guide/) — the comprehensive guide covering all aspects of the framework. - [Community Cookbook](cookbook/) — a collection of community-contributed tips, tricks, and solutions for common Yii development tasks. +- [Third-Party Integrations](third-party-integrations/) — a collection of community-contributed guides for Third-Party integrated packages. - [Internals](internals/) — documentation for developers contributing to the Yii framework itself, including guidelines, workflows, and best practices. diff --git a/src/third-party-integrations/index.md b/src/third-party-integrations/index.md new file mode 100644 index 00000000..008c6bba --- /dev/null +++ b/src/third-party-integrations/index.md @@ -0,0 +1,12 @@ +# Third-Party Integrations + +Third-Party Integrations is a collection of community-contributed guides for Third-Party integrated packages. + +- The Yii community creates the guides. +- Yii core team members curate and edit it. + +Feel free to pull-request your own writings. Team members will review it, give feedback and merge the best possible way. + +--- + +- [Symfony Messenger integration guide](symfony-messenger-integration-guide.md) \ No newline at end of file diff --git a/src/third-party-integrations/symfony-messenger-integration-guide.md b/src/third-party-integrations/symfony-messenger-integration-guide.md new file mode 100644 index 00000000..1d3e67b5 --- /dev/null +++ b/src/third-party-integrations/symfony-messenger-integration-guide.md @@ -0,0 +1,345 @@ +# Symfony Messenger integration guide + +The [Symfony Messenger](https://symfony.com/doc/current/components/messenger.html) component helps applications send and receive messages to/from other applications or via message queues. + +## Installation + +```shell +composer require symfony/messenger +``` + +## How to use with Yii + +### 1. Create a Message and Handler. + +```php +namespace App\Messages; + +final readonly class MyMessage +{ + public function __construct(public string $content) + { + } +} +``` + +```php +namespace App\Messages; + +use Psr\Log\LoggerInterface; + +final readonly class MyMessageHandler +{ + public function __construct(private LoggerInterface $logger) + { + } + public function __invoke(MyMessage $message): void + { + // ... + $this->logger->info("The message with the content '$message->content' has been received."); + } +} +``` + +### 2. Implement the ServiceProviderInterface. + +This instance of the ServiceProviderInterface is used to configure certain Symfony Messenger commands. + +```php +namespace App\Services; + +use Psr\Container\ContainerInterface; +use Symfony\Contracts\Service\ServiceProviderInterface; + +final readonly class ServiceProvider implements ServiceProviderInterface +{ + /** + * @param ContainerInterface $container + * @param array $serviceMap + */ + public function __construct( + private ContainerInterface $container, + private array $serviceMap) + { + } + + /** + * @inheritDoc + */ + public function get(string $id): mixed + { + return $this->container->get($id); + } + + public function has(string $id): bool + { + return $this->container->has($id); + } + + /** + * @inheritDoc + */ + public function getProvidedServices(): array + { + return $this->serviceMap; + } +} +``` + +### 3. Configure the Symfony Messenger. + +Example uses the [symfony/doctrine-messenger](https://packagist.org/packages/symfony/doctrine-messenger) package for transport: + +Add the Doctrine packages to the `composer.json`: +```json +{ + // ... + "require": { + // ... + "doctrine/orm": "^3", + "doctrine/dbal": "^4", + "symfony/cache": "^7" + }, +} +``` + +Install Doctrine: +```shell +composer install +``` + +Configure the Doctrine connection in the `config/common/params.php`: +```php +return [ + // ... + 'doctrine' => [ + 'paths' => [ + ], + 'isDevMode' => false, + 'connection' => [ + 'driver' => 'pdo_mysql', + 'user' => 'site1', + 'password' => 'secret', + 'dbname' => 'site3' + ] + ], +]; +``` + +Install the Symfony Doctrine Messenger package: +```shell +composer require symfony/doctrine-messenger +``` + +Configure the Doctrine and Symfony Messenger dependencies in the `config/common/di/application.php`: +```php +use App\Messages\MyMessage; +use App\Messages\MyMessageHandler; +use App\Services\ServiceProvider; +use Doctrine\DBAL\DriverManager; +use Doctrine\ORM\Configuration; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\ORMSetup; +use Doctrine\ORM\Proxy\ProxyFactory; +use Doctrine\ORM\Tools\Console\EntityManagerProvider; +use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider; +use Psr\Cache\CacheItemPoolInterface; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Messenger\Bridge\Doctrine\Transport\Connection; +use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport; +use Symfony\Component\Messenger\Command\ConsumeMessagesCommand; +use Symfony\Component\Messenger\Command\DebugCommand; +use Symfony\Component\Messenger\Command\FailedMessagesRemoveCommand; +use Symfony\Component\Messenger\Command\FailedMessagesRetryCommand; +use Symfony\Component\Messenger\Command\FailedMessagesShowCommand; +use Symfony\Component\Messenger\Command\SetupTransportsCommand; +use Symfony\Component\Messenger\Command\StatsCommand; +use Symfony\Component\Messenger\EventListener\StopWorkerOnRestartSignalListener; +use Symfony\Component\Messenger\Handler\HandlersLocator; +use Symfony\Component\Messenger\MessageBus; +use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware; +use Symfony\Component\Messenger\Middleware\SendMessageMiddleware; +use Symfony\Component\Messenger\RoutableMessageBus; +use Symfony\Component\Messenger\Transport\Sender\SendersLocator; +use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer; +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; +use Symfony\Contracts\Service\ServiceProviderInterface; +use Yiisoft\Aliases\Aliases; +use Yiisoft\Definitions\Reference; + +class_alias(MessageBus::class, 'MyMessageBus'); + +return [ + // ... + CacheItemPoolInterface::class => static function (ContainerInterface $container) { + return new FilesystemAdapter(directory: $container->get(Aliases::class)->get('@runtime')); + }, //One of the following adapters can be used instead: Psr16Adapter, RedisAdapter, MemcachedAdapter, DoctrineDbalAdapter, and so forth. + + Configuration::class => static function (ContainerInterface $container) use ($params) { + $config = ORMSetup::createAttributeMetadataConfiguration( // on PHP >= 8.4, use ORMSetup::createAttributeMetadataConfig() + paths: $params['doctrine']['paths'], + isDevMode: $params['doctrine']['isDevMode'], + cache: $container->get(CacheItemPoolInterface::class)); + $config->setAutoGenerateProxyClasses(ProxyFactory::AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED); + return $config; + }, + + EntityManagerInterface::class => static function (ContainerInterface $container) use ($params) { + $configuration = $container->get(Configuration::class); + return new EntityManager( + DriverManager::getConnection( + $params['doctrine']['connection'], + $configuration + ), + $configuration); + }, + EntityManagerProvider::class => SingleManagerProvider::class, + + SerializerInterface::class => PhpSerializer::class, + 'DoctrineTransport' => static function (ContainerInterface $container) use ($params) { + $configuration = $container->get(Configuration::class); + $connection = new Connection([], DriverManager::getConnection( + $params['doctrine']['connection'], + $configuration + )); + return new DoctrineTransport($connection, $container->get(SerializerInterface::class)); + }, + + MyMessageBus::class => static function (ContainerInterface $container) { + return new MyMessageBus([ + new SendMessageMiddleware(sendersLocator: new SendersLocator([ + MyMessage::class => ['DoctrineTransport'] + ], $container)), + new HandleMessageMiddleware(new HandlersLocator([ + MyMessage::class => [$container->get(MyMessageHandler::class)] + ])), + ]); + }, + + EventDispatcherInterface::class => static function (ContainerInterface $container) { + $eventDispatcher = new EventDispatcher(); + $eventDispatcher->addSubscriber($container->get(StopWorkerOnRestartSignalListener::class)); + return $eventDispatcher; + }, + ServiceProviderInterface::class => [ + 'class' => ServiceProvider::class, + '__construct()' => [ + 'container' => Reference::to(ContainerInterface::class), + 'serviceMap' => [ + 'DoctrineTransport' => DoctrineTransport::class + ] + ], + ], + + ConsumeMessagesCommand::class => static function (ContainerInterface $container) { + return new ConsumeMessagesCommand( + $container->get(RoutableMessageBus::class), + $container, + $container->get(EventDispatcherInterface::class), + $container->get(LoggerInterface::class), + array_keys($container->get(ServiceProviderInterface::class)->getProvidedServices()) + ); + }, + DebugCommand::class => [ + '__construct()' => [ + 'mapping' => [ + MyMessageBus::class => [Reference::to(MyMessageHandler::class)] + ] + ], + ], + FailedMessagesRemoveCommand::class => [ + '__construct()' => [ + 'globalFailureReceiverName' => null, + 'failureTransports' => Reference::to(ServiceProviderInterface::class) + ], + ], + FailedMessagesRetryCommand::class => [ + '__construct()' => [ + 'globalReceiverName' => null, + 'failureTransports' => Reference::to(ServiceProviderInterface::class), + 'messageBus' => Reference::to(MyMessageBus::class), + 'eventDispatcher' => Reference::to(EventDispatcherInterface::class), + 'logger' => Reference::to(LoggerInterface::class), + ], + ], + FailedMessagesShowCommand::class => [ + '__construct()' => [ + 'globalFailureReceiverName' => null, + 'failureTransports' => Reference::to(ServiceProviderInterface::class) + ], + ], + SetupTransportsCommand::class => static function (ContainerInterface $container) { + return new SetupTransportsCommand( + $container, + array_keys($container->get(ServiceProviderInterface::class)->getProvidedServices()) + ); + }, + StatsCommand::class => static function (ContainerInterface $container) { + return new StatsCommand( + $container, + array_keys($container->get(ServiceProviderInterface::class)->getProvidedServices()) + ); + }, +]; +``` + +### 4. Integrate the Symfony Messenger commands into the Yii console. + +Add the commands to `config/console/commands.php`: +```php +use Symfony\Component\Messenger\Command\ConsumeMessagesCommand; +use Symfony\Component\Messenger\Command\DebugCommand; +use Symfony\Component\Messenger\Command\FailedMessagesRemoveCommand; +use Symfony\Component\Messenger\Command\FailedMessagesRetryCommand; +use Symfony\Component\Messenger\Command\FailedMessagesShowCommand; +use Symfony\Component\Messenger\Command\SetupTransportsCommand; +use Symfony\Component\Messenger\Command\StatsCommand; +use Symfony\Component\Messenger\Command\StopWorkersCommand; + +return [ + // ... + 'symfony:messenger:consume' => ConsumeMessagesCommand::class, + 'symfony:messenger:debug' => DebugCommand::class, + 'symfony:messenger:failed:remove' => FailedMessagesRemoveCommand::class, + 'symfony:messenger:failed:retry' => FailedMessagesRetryCommand::class, + 'symfony:messenger:failed:show' => FailedMessagesShowCommand::class, + 'symfony:messenger:setup-transports' => SetupTransportsCommand::class, + 'symfony:messenger:stats' => StatsCommand::class, + 'symfony:messenger:stop-workers' => StopWorkersCommand::class +]; +``` + +### 5. Dispatch a message. + +Example: + +```php +use App\Messages\MyMessage; +use MyMessageBus; +use Psr\Http\Message\ResponseInterface; + +final readonly class MyController +{ + public function __construct(private MyMessageBus $bus) + { + } + + public function sendMessage(): ResponseInterface + { + $this->bus->dispatch(new MyMessage('Hello Symfony Messenger!')); + // ... + } +} +``` + +### 6. Consume messages. + +Example: +```bash +./yii symfony:messenger:consume DoctrineTransport --bus=MyMessageBus +```