From 0f85b9fc2db46d29eb26d5fd665f6ac92c4a3f83 Mon Sep 17 00:00:00 2001
From: Mateusz Charytoniuk <mateusz.charytoniuk@protonmail.com>
Date: Tue, 13 Feb 2024 03:35:09 +0100
Subject: [PATCH] chore: initial grpc command

---
 composer.json                                 |  3 +-
 composer.lock                                 | 46 ++++++++++++++++++-
 docs/pages/docs/features/grpc/index.md        | 29 +++++++++++-
 src/Attribute/GrantsFeature.php               |  2 +-
 ...HttpResponse.php => TestsHttpResponse.php} |  4 +-
 src/Command/GrpcGenerate.php                  | 33 +++++++++++++
 src/Command/MailSend.php                      |  3 ++
 src/Command/TestHttpResponders.php            | 14 +++---
 src/DependencyInjectionContainer.php          | 21 +++++----
 .../DisabledFeatureProvider.php               | 30 ++++++++++--
 src/DependencyProvider.php                    |  6 ++-
 src/DependencyProviderIterator.php            | 23 +++++++++-
 src/Feature.php                               |  9 ++--
 src/FeatureInterface.php                      |  5 +-
 src/NameableEnumTrait.php                     |  3 ++
 src/NameableInterface.php                     |  3 ++
 src/ServerTaskHandler/SendsEmailMessage.php   |  1 +
 .../MailerConfigurationProvider.php           |  3 ++
 .../EventListenerAggregateProvider.php        |  3 --
 .../MailerRepositoryProvider.php              | 22 ++++-----
 ...> TestsHttpResponseCollectionProvider.php} | 18 ++++----
 ...on.php => TestsHttpResponseCollection.php} | 22 ++++-----
 22 files changed, 229 insertions(+), 74 deletions(-)
 rename src/Attribute/{TestableHttpResponse.php => TestsHttpResponse.php} (53%)
 create mode 100644 src/Command/GrpcGenerate.php
 rename src/SingletonProvider/{TestableHttpResponseCollectionProvider.php => TestsHttpResponseCollectionProvider.php} (80%)
 rename src/{TestableHttpResponseCollection.php => TestsHttpResponseCollection.php} (50%)

diff --git a/composer.json b/composer.json
index dd114b68..f9d1ae4c 100644
--- a/composer.json
+++ b/composer.json
@@ -50,7 +50,8 @@
         "symfony/mailer": "^7.0",
         "symfony/messenger": "^7.0",
         "symfony/http-client": "^7.0",
-        "rubix/ml": "^2.4"
+        "rubix/ml": "^2.4",
+        "grpc/grpc": "^1.57"
     },
     "require-dev": {
         "mockery/mockery": "^1.6",
diff --git a/composer.lock b/composer.lock
index fbf58695..1dcec6f9 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": "2008e1dfe7cb51ec6de8a61bdd663c0e",
+    "content-hash": "8d3f949c2f347224492b51e19ad57c76",
     "packages": [
         {
             "name": "amphp/amp",
@@ -1973,6 +1973,50 @@
             },
             "time": "2023-11-17T15:01:25+00:00"
         },
+        {
+            "name": "grpc/grpc",
+            "version": "1.57.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/grpc/grpc-php.git",
+                "reference": "b610c42022ed3a22f831439cb93802f2a4502fdf"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/grpc/grpc-php/zipball/b610c42022ed3a22f831439cb93802f2a4502fdf",
+                "reference": "b610c42022ed3a22f831439cb93802f2a4502fdf",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.0.0"
+            },
+            "require-dev": {
+                "google/auth": "^v1.3.0"
+            },
+            "suggest": {
+                "ext-protobuf": "For better performance, install the protobuf C extension.",
+                "google/protobuf": "To get started using grpc quickly, install the native protobuf library."
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Grpc\\": "src/lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "description": "gRPC library for PHP",
+            "homepage": "https://grpc.io",
+            "keywords": [
+                "rpc"
+            ],
+            "support": {
+                "source": "https://github.com/grpc/grpc-php/tree/v1.57.0"
+            },
+            "time": "2023-08-14T23:57:54+00:00"
+        },
         {
             "name": "guzzlehttp/guzzle",
             "version": "7.8.1",
diff --git a/docs/pages/docs/features/grpc/index.md b/docs/pages/docs/features/grpc/index.md
index c131b055..d5f780cf 100644
--- a/docs/pages/docs/features/grpc/index.md
+++ b/docs/pages/docs/features/grpc/index.md
@@ -1,7 +1,6 @@
 ---
 collections:
     - documents
-draft: true
 layout: dm:document
 parent: docs/features/index
 title: gRPC
@@ -16,6 +15,8 @@ description: >
 You can find instruction on the official 
 [Google documentation page](https://cloud.google.com/php/grpc).
 
+## PHP Extensions
+
 On Debian-based systems:
 
 ```shell
@@ -24,4 +25,30 @@ $ sudo pecl install grpc
 $ sudo pecl install protobuf
 ```
 
+Enable both `grpc.so` and `protobuf.so` plugins in your php.ini config.
+
+## Protocol Buffers Compiler
+
+```shell
+$ sudo apt install protobuf-compiler
+```
+
+## GRPC PHP Plugin
+
+You need to clone https://github.com/grpc/grpc repo, then follow their build
+instructions from https://github.com/grpc/grpc/blob/v1.61.0/src/php/README.md 
+
+You can use [bazel](https://bazel.build/) to build the plugin.
+
+```shell
+$ sudo apt install bazel-bootstrap clang
+```
+
+Then, in the folder with `grpc/grpc`:
+
+```shell
+$ bazel build @com_google_protobuf//:protoc
+$ bazel build src/compiler:grpc_php_plugin
+```
+
 #  Usage
diff --git a/src/Attribute/GrantsFeature.php b/src/Attribute/GrantsFeature.php
index 3fbe0286..3f0146b7 100644
--- a/src/Attribute/GrantsFeature.php
+++ b/src/Attribute/GrantsFeature.php
@@ -8,7 +8,7 @@ use Attribute;
 use Distantmagic\Resonance\Attribute as BaseAttribute;
 use Distantmagic\Resonance\FeatureInterface;
 
-#[Attribute(Attribute::TARGET_CLASS)]
+#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)]
 final readonly class GrantsFeature extends BaseAttribute
 {
     public function __construct(
diff --git a/src/Attribute/TestableHttpResponse.php b/src/Attribute/TestsHttpResponse.php
similarity index 53%
rename from src/Attribute/TestableHttpResponse.php
rename to src/Attribute/TestsHttpResponse.php
index 243a1ccd..a06750e5 100644
--- a/src/Attribute/TestableHttpResponse.php
+++ b/src/Attribute/TestsHttpResponse.php
@@ -7,5 +7,5 @@ namespace Distantmagic\Resonance\Attribute;
 use Attribute;
 use Distantmagic\Resonance\Attribute as BaseAttribute;
 
-#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)]
-final readonly class TestableHttpResponse extends BaseAttribute {}
+#[Attribute(Attribute::TARGET_CLASS)]
+final readonly class TestsHttpResponse extends BaseAttribute {}
diff --git a/src/Command/GrpcGenerate.php b/src/Command/GrpcGenerate.php
new file mode 100644
index 00000000..6078bd36
--- /dev/null
+++ b/src/Command/GrpcGenerate.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\Command;
+
+use Distantmagic\Resonance\Attribute\ConsoleCommand;
+use Distantmagic\Resonance\Attribute\WantsFeature;
+use Distantmagic\Resonance\Command;
+use Distantmagic\Resonance\Feature;
+use Nette\PhpGenerator\Printer;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[ConsoleCommand(
+    name: 'grpc:generate',
+    description: 'Generate GRPC stubs'
+)]
+#[WantsFeature(Feature::Grpc)]
+final class GrpcGenerate extends Command
+{
+    public function __construct(private readonly Printer $printer)
+    {
+        parent::__construct();
+    }
+
+    protected function configure(): void {}
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        return Command::SUCCESS;
+    }
+}
diff --git a/src/Command/MailSend.php b/src/Command/MailSend.php
index dd5644bf..8aab03a7 100644
--- a/src/Command/MailSend.php
+++ b/src/Command/MailSend.php
@@ -5,7 +5,9 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance\Command;
 
 use Distantmagic\Resonance\Attribute\ConsoleCommand;
+use Distantmagic\Resonance\Attribute\WantsFeature;
 use Distantmagic\Resonance\Command;
+use Distantmagic\Resonance\Feature;
 use Distantmagic\Resonance\MailerRepository;
 use RuntimeException;
 use Swoole\Event;
@@ -19,6 +21,7 @@ use Symfony\Component\Mime\Email;
     name: 'mail:send',
     description: 'Send email using the selected transport'
 )]
+#[WantsFeature(Feature::Mailer)]
 final class MailSend extends Command
 {
     public function __construct(
diff --git a/src/Command/TestHttpResponders.php b/src/Command/TestHttpResponders.php
index ef0e27a0..ab192b32 100644
--- a/src/Command/TestHttpResponders.php
+++ b/src/Command/TestHttpResponders.php
@@ -12,7 +12,7 @@ use Distantmagic\Resonance\HttpResponderAggregate;
 use Distantmagic\Resonance\HttpResponderInterface;
 use Distantmagic\Resonance\InspectableSwooleResponse;
 use Distantmagic\Resonance\SwooleCoroutineHelper;
-use Distantmagic\Resonance\TestableHttpResponseCollection;
+use Distantmagic\Resonance\TestsHttpResponseCollection;
 use Ds\Map;
 use RuntimeException;
 use Swoole\Http\Request;
@@ -28,7 +28,7 @@ final class TestHttpResponders extends Command
     public function __construct(
         private readonly HttpRecursiveResponder $recursiveResponder,
         private readonly HttpResponderAggregate $httpResponderAggregate,
-        private readonly TestableHttpResponseCollection $testableHttpResponseCollection,
+        private readonly TestsHttpResponseCollection $testsHttpResponseCollection,
     ) {
         parent::__construct();
     }
@@ -40,12 +40,12 @@ final class TestHttpResponders extends Command
          */
         $isValid = true;
 
-        foreach ($this->testableHttpResponseCollection->httpResponder as $httpResponder => $testableHttpResponses) {
-            foreach ($testableHttpResponses as $testableHttpResponse) {
+        foreach ($this->testsHttpResponseCollection->httpResponder as $httpResponder => $testsHttpResponses) {
+            foreach ($testsHttpResponses as $testsHttpResponse) {
                 $potentialResponses = $this
-                    ->testableHttpResponseCollection
-                    ->testableHttpResponse
-                    ->get($testableHttpResponse)
+                    ->testsHttpResponseCollection
+                    ->testsHttpResponse
+                    ->get($testsHttpResponse)
                 ;
 
                 $result = SwooleCoroutineHelper::mustRun(function () use (
diff --git a/src/DependencyInjectionContainer.php b/src/DependencyInjectionContainer.php
index 8f736643..61b47071 100644
--- a/src/DependencyInjectionContainer.php
+++ b/src/DependencyInjectionContainer.php
@@ -13,7 +13,6 @@ use Ds\Set;
 use ReflectionClass;
 use ReflectionFunction;
 use ReflectionFunctionAbstract;
-use RuntimeException;
 use Swoole\Event;
 
 readonly class DependencyInjectionContainer
@@ -113,8 +112,8 @@ readonly class DependencyInjectionContainer
                 throw new AmbiguousProvider($providedClassName);
             }
 
-            if ($dependencyProvider->wantsFeature) {
-                $this->enableFeature($dependencyProvider->wantsFeature);
+            foreach ($dependencyProvider->wantsFeatures as $wantedFeature) {
+                $this->enableFeature($wantedFeature);
             }
 
             $this->dependencyProviders->put($providedClassName, $dependencyProvider);
@@ -191,11 +190,17 @@ readonly class DependencyInjectionContainer
 
     private function isDependencyProviderWanted(DependencyProvider $dependencyProvider): bool
     {
-        if (is_null($dependencyProvider->grantsFeature)) {
+        if ($dependencyProvider->grantsFeatures->isEmpty()) {
             return true;
         }
 
-        return $this->wantedFeatures->contains($dependencyProvider->grantsFeature);
+        foreach ($dependencyProvider->grantsFeatures as $grantedFeature) {
+            if ($this->wantedFeatures->contains($grantedFeature)) {
+                return true;
+            }
+        }
+
+        return false;
     }
 
     /**
@@ -272,13 +277,9 @@ readonly class DependencyInjectionContainer
         }
 
         if (!$this->isDependencyProviderWanted($dependencyProvider)) {
-            if (!$dependencyProvider->grantsFeature) {
-                throw new RuntimeException('Classname with no provider, not wanted: '.$className);
-            }
-
             throw new DisabledFeatureProvider(
                 $className,
-                $dependencyProvider->grantsFeature,
+                $dependencyProvider->grantsFeatures->diff($this->wantedFeatures),
                 $stack,
             );
         }
diff --git a/src/DependencyInjectionContainerException/DisabledFeatureProvider.php b/src/DependencyInjectionContainerException/DisabledFeatureProvider.php
index c710648d..e28eb929 100644
--- a/src/DependencyInjectionContainerException/DisabledFeatureProvider.php
+++ b/src/DependencyInjectionContainerException/DisabledFeatureProvider.php
@@ -7,27 +7,49 @@ namespace Distantmagic\Resonance\DependencyInjectionContainerException;
 use Distantmagic\Resonance\DependencyInjectionContainerException;
 use Distantmagic\Resonance\DependencyStack;
 use Distantmagic\Resonance\FeatureInterface;
+use Ds\Set;
 use Throwable;
 
 class DisabledFeatureProvider extends DependencyInjectionContainerException
 {
     /**
-     * @param class-string $className
+     * @param class-string          $className
+     * @param Set<FeatureInterface> $features
      */
     public function __construct(
         string $className,
-        FeatureInterface $feature,
+        Set $features,
         DependencyStack $stack,
         ?Throwable $previous = null,
     ) {
         parent::__construct(
             sprintf(
-                "Enable '%s' feature to use this provider:\n-> %s\nDependency stack:\n-> %s\n",
-                $feature->getName(),
+                "Enable '%s' %s to use this provider:\n-> %s\nDependency stack:\n-> %s\n",
+                $this->serializeFeatures($features)->join("', '"),
+                1 === $features->count() ? 'feature' : 'features',
                 $className,
                 $stack->join("\n-> "),
             ),
             $previous
         );
     }
+
+    /**
+     * @param Set<FeatureInterface> $features
+     *
+     * @return Set<non-empty-string>
+     */
+    private function serializeFeatures(Set $features): Set
+    {
+        /**
+         * @var Set<non-empty-string>
+         */
+        $ret = new Set();
+
+        foreach ($features as $feature) {
+            $ret->add($feature->getName());
+        }
+
+        return $ret;
+    }
 }
diff --git a/src/DependencyProvider.php b/src/DependencyProvider.php
index 4efa02fa..1ceaa9fc 100644
--- a/src/DependencyProvider.php
+++ b/src/DependencyProvider.php
@@ -10,15 +10,17 @@ use ReflectionClass;
 readonly class DependencyProvider
 {
     /**
+     * @param Set<FeatureInterface>             $grantsFeatures
      * @param Set<SingletonCollectionInterface> $requiredCollections
      * @param class-string                      $providedClassName
+     * @param Set<FeatureInterface>             $wantsFeatures
      */
     public function __construct(
-        public ?FeatureInterface $grantsFeature,
+        public Set $grantsFeatures,
         public ReflectionClass $providerReflectionClass,
         public Set $requiredCollections,
         public ?SingletonCollectionInterface $collection,
         public string $providedClassName,
-        public ?FeatureInterface $wantsFeature,
+        public Set $wantsFeatures,
     ) {}
 }
diff --git a/src/DependencyProviderIterator.php b/src/DependencyProviderIterator.php
index 1d7a41bd..b271a120 100644
--- a/src/DependencyProviderIterator.php
+++ b/src/DependencyProviderIterator.php
@@ -30,11 +30,11 @@ readonly class DependencyProviderIterator implements IteratorAggregate
 
             yield $providedClassName => new DependencyProvider(
                 collection: $reflectionAttribute->attribute->collection,
-                grantsFeature: $reflectionClassAttributeManager->findAttribute(GrantsFeature::class)?->feature,
+                grantsFeatures: $this->pluckFeature($reflectionClassAttributeManager->findAttributes(GrantsFeature::class)),
                 providedClassName: $providedClassName,
                 providerReflectionClass: $reflectionAttribute->reflectionClass,
                 requiredCollections: $this->findRequiredCollections($reflectionClassAttributeManager),
-                wantsFeature: $reflectionClassAttributeManager->findAttribute(WantsFeature::class)?->feature,
+                wantsFeatures: $this->pluckFeature($reflectionClassAttributeManager->findAttributes(WantsFeature::class)),
             );
         }
     }
@@ -57,4 +57,23 @@ readonly class DependencyProviderIterator implements IteratorAggregate
 
         return $requiredCollections;
     }
+
+    /**
+     * @param Set<GrantsFeature>|Set<WantsFeature> $attributes
+     *
+     * @return Set<FeatureInterface>
+     */
+    private function pluckFeature(Set $attributes): Set
+    {
+        /**
+         * @var Set<FeatureInterface>
+         */
+        $ret = new Set();
+
+        foreach ($attributes as $attribute) {
+            $ret->add($attribute->feature);
+        }
+
+        return $ret;
+    }
 }
diff --git a/src/Feature.php b/src/Feature.php
index fc72fa54..a18b1a66 100644
--- a/src/Feature.php
+++ b/src/Feature.php
@@ -6,16 +6,15 @@ namespace Distantmagic\Resonance;
 
 enum Feature implements FeatureInterface
 {
+    use NameableEnumTrait;
+
     case Doctrine;
+    case Grpc;
     case HttpSession;
+    case Mailer;
     case OAuth2;
     case Postfix;
     case StaticPages;
     case SwooleTaskServer;
     case WebSocket;
-
-    public function getName(): string
-    {
-        return $this->name;
-    }
 }
diff --git a/src/FeatureInterface.php b/src/FeatureInterface.php
index e58b1883..0b56c536 100644
--- a/src/FeatureInterface.php
+++ b/src/FeatureInterface.php
@@ -4,7 +4,4 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
-interface FeatureInterface
-{
-    public function getName(): string;
-}
+interface FeatureInterface extends NameableInterface {}
diff --git a/src/NameableEnumTrait.php b/src/NameableEnumTrait.php
index 45368bac..4207f14e 100644
--- a/src/NameableEnumTrait.php
+++ b/src/NameableEnumTrait.php
@@ -6,6 +6,9 @@ namespace Distantmagic\Resonance;
 
 trait NameableEnumTrait
 {
+    /**
+     * @return non-empty-string
+     */
     public function getName(): string
     {
         return $this->name;
diff --git a/src/NameableInterface.php b/src/NameableInterface.php
index e871e077..0f41001d 100644
--- a/src/NameableInterface.php
+++ b/src/NameableInterface.php
@@ -6,5 +6,8 @@ namespace Distantmagic\Resonance;
 
 interface NameableInterface
 {
+    /**
+     * @return non-empty-string
+     */
     public function getName(): string;
 }
diff --git a/src/ServerTaskHandler/SendsEmailMessage.php b/src/ServerTaskHandler/SendsEmailMessage.php
index 776f0f4b..1b1bfa65 100644
--- a/src/ServerTaskHandler/SendsEmailMessage.php
+++ b/src/ServerTaskHandler/SendsEmailMessage.php
@@ -17,6 +17,7 @@ use Distantmagic\Resonance\SingletonCollection;
  * @template-extends ServerTaskHandler<SendEmailMessage>
  */
 #[GrantsFeature(Feature::SwooleTaskServer)]
+#[GrantsFeature(Feature::Mailer)]
 #[HandlesServerTask(SendEmailMessage::class)]
 #[Singleton(collection: SingletonCollection::ServerTaskHandler)]
 readonly class SendsEmailMessage extends ServerTaskHandler
diff --git a/src/SingletonProvider/ConfigurationProvider/MailerConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/MailerConfigurationProvider.php
index 5ef41c5d..1aa00bf8 100644
--- a/src/SingletonProvider/ConfigurationProvider/MailerConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/MailerConfigurationProvider.php
@@ -4,11 +4,13 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 
+use Distantmagic\Resonance\Attribute\GrantsFeature;
 use Distantmagic\Resonance\Attribute\Singleton;
 use Distantmagic\Resonance\Constraint;
 use Distantmagic\Resonance\Constraint\MapConstraint;
 use Distantmagic\Resonance\Constraint\ObjectConstraint;
 use Distantmagic\Resonance\Constraint\StringConstraint;
+use Distantmagic\Resonance\Feature;
 use Distantmagic\Resonance\MailerConfiguration;
 use Distantmagic\Resonance\MailerTransportConfiguration;
 use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
@@ -26,6 +28,7 @@ use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
  *     }>
  * >
  */
+#[GrantsFeature(Feature::Mailer)]
 #[Singleton(provides: MailerConfiguration::class)]
 final readonly class MailerConfigurationProvider extends ConfigurationProvider
 {
diff --git a/src/SingletonProvider/EventListenerAggregateProvider.php b/src/SingletonProvider/EventListenerAggregateProvider.php
index acbe6ed7..f8cb8b46 100644
--- a/src/SingletonProvider/EventListenerAggregateProvider.php
+++ b/src/SingletonProvider/EventListenerAggregateProvider.php
@@ -22,9 +22,6 @@ use Distantmagic\Resonance\SingletonProvider;
 #[Singleton(provides: EventListenerAggregate::class)]
 final readonly class EventListenerAggregateProvider extends SingletonProvider
 {
-    public function __construct(
-    ) {}
-
     public function provide(SingletonContainer $singletons, PHPProjectFiles $phpProjectFiles): EventListenerAggregate
     {
         $eventListenerAggregate = new EventListenerAggregate();
diff --git a/src/SingletonProvider/MailerRepositoryProvider.php b/src/SingletonProvider/MailerRepositoryProvider.php
index 8850a8b8..1ed0c7bb 100644
--- a/src/SingletonProvider/MailerRepositoryProvider.php
+++ b/src/SingletonProvider/MailerRepositoryProvider.php
@@ -4,7 +4,10 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance\SingletonProvider;
 
+use Distantmagic\Resonance\Attribute\GrantsFeature;
 use Distantmagic\Resonance\Attribute\Singleton;
+use Distantmagic\Resonance\DependencyInjectionContainer;
+use Distantmagic\Resonance\Feature;
 use Distantmagic\Resonance\Mailer;
 use Distantmagic\Resonance\MailerConfiguration;
 use Distantmagic\Resonance\MailerRepository;
@@ -16,17 +19,18 @@ use Distantmagic\Resonance\SwooleTaskServerMessageBus;
 use Psr\Log\LoggerInterface;
 use RuntimeException;
 use Symfony\Component\Mailer\Transport;
-use Symfony\Component\Mailer\Transport\TransportInterface;
 use Symfony\Component\Mime\Crypto\DkimSigner;
 use Symfony\Contracts\HttpClient\HttpClientInterface;
 
 /**
  * @template-extends SingletonProvider<MailerRepository>
  */
+#[GrantsFeature(Feature::Mailer)]
 #[Singleton(provides: MailerRepository::class)]
 final readonly class MailerRepositoryProvider extends SingletonProvider
 {
     public function __construct(
+        private DependencyInjectionContainer $dependencyInjectionContainer,
         private HttpClientInterface $httpClient,
         private LoggerInterface $logger,
         private MailerConfiguration $mailerConfiguration,
@@ -44,7 +48,12 @@ final readonly class MailerRepositoryProvider extends SingletonProvider
                     dkimSigner: $this->buildDkimSigner($name, $transportConfiguration),
                     name: $name,
                     messageBus: $this->swooleTaskServerMessageBus,
-                    transport: $this->buildTransport($transportConfiguration),
+                    transport: Transport::fromDsn(
+                        client: $this->httpClient,
+                        // dispatcher: $eventDispatcher,
+                        dsn: $transportConfiguration->transportDsn,
+                        logger: $this->logger,
+                    ),
                 )
             );
         }
@@ -89,13 +98,4 @@ final readonly class MailerRepositoryProvider extends SingletonProvider
             $name,
         ));
     }
-
-    private function buildTransport(MailerTransportConfiguration $transportConfiguration): TransportInterface
-    {
-        return Transport::fromDsn(
-            client: $this->httpClient,
-            dsn: $transportConfiguration->transportDsn,
-            logger: $this->logger,
-        );
-    }
 }
diff --git a/src/SingletonProvider/TestableHttpResponseCollectionProvider.php b/src/SingletonProvider/TestsHttpResponseCollectionProvider.php
similarity index 80%
rename from src/SingletonProvider/TestableHttpResponseCollectionProvider.php
rename to src/SingletonProvider/TestsHttpResponseCollectionProvider.php
index 021bb1c0..46d4cba0 100644
--- a/src/SingletonProvider/TestableHttpResponseCollectionProvider.php
+++ b/src/SingletonProvider/TestsHttpResponseCollectionProvider.php
@@ -7,7 +7,7 @@ namespace Distantmagic\Resonance\SingletonProvider;
 use Distantmagic\Resonance\Attribute\RequiresSingletonCollection;
 use Distantmagic\Resonance\Attribute\RespondsWith;
 use Distantmagic\Resonance\Attribute\Singleton;
-use Distantmagic\Resonance\Attribute\TestableHttpResponse;
+use Distantmagic\Resonance\Attribute\TestsHttpResponse;
 use Distantmagic\Resonance\HttpResponderInterface;
 use Distantmagic\Resonance\PHPProjectFiles;
 use Distantmagic\Resonance\ReflectionClassAttributeManager;
@@ -15,20 +15,20 @@ use Distantmagic\Resonance\SingletonAttribute;
 use Distantmagic\Resonance\SingletonCollection;
 use Distantmagic\Resonance\SingletonContainer;
 use Distantmagic\Resonance\SingletonProvider;
-use Distantmagic\Resonance\TestableHttpResponseCollection;
+use Distantmagic\Resonance\TestsHttpResponseCollection;
 use LogicException;
 use ReflectionClass;
 
 /**
- * @template-extends SingletonProvider<TestableHttpResponseCollection>
+ * @template-extends SingletonProvider<TestsHttpResponseCollection>
  */
 #[RequiresSingletonCollection(SingletonCollection::HttpResponder)]
-#[Singleton(provides: TestableHttpResponseCollection::class)]
-final readonly class TestableHttpResponseCollectionProvider extends SingletonProvider
+#[Singleton(provides: TestsHttpResponseCollection::class)]
+final readonly class TestsHttpResponseCollectionProvider extends SingletonProvider
 {
-    public function provide(SingletonContainer $singletons, PHPProjectFiles $phpProjectFiles): TestableHttpResponseCollection
+    public function provide(SingletonContainer $singletons, PHPProjectFiles $phpProjectFiles): TestsHttpResponseCollection
     {
-        $testableHttpResponseCollection = new TestableHttpResponseCollection();
+        $testableHttpResponseCollection = new TestsHttpResponseCollection();
 
         foreach ($this->collectResponders($singletons) as $testableHttpResponseAttribute) {
             $reflectionClass = new ReflectionClass($testableHttpResponseAttribute->singleton);
@@ -57,14 +57,14 @@ final readonly class TestableHttpResponseCollectionProvider extends SingletonPro
     }
 
     /**
-     * @return iterable<SingletonAttribute<HttpResponderInterface,TestableHttpResponse>>
+     * @return iterable<SingletonAttribute<HttpResponderInterface,TestsHttpResponse>>
      */
     private function collectResponders(SingletonContainer $singletons): iterable
     {
         return $this->collectAttributes(
             $singletons,
             HttpResponderInterface::class,
-            TestableHttpResponse::class,
+            TestsHttpResponse::class,
         );
     }
 }
diff --git a/src/TestableHttpResponseCollection.php b/src/TestsHttpResponseCollection.php
similarity index 50%
rename from src/TestableHttpResponseCollection.php
rename to src/TestsHttpResponseCollection.php
index 21538558..dd69732a 100644
--- a/src/TestableHttpResponseCollection.php
+++ b/src/TestsHttpResponseCollection.php
@@ -5,44 +5,44 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance;
 
 use Distantmagic\Resonance\Attribute\RespondsWith;
-use Distantmagic\Resonance\Attribute\TestableHttpResponse;
+use Distantmagic\Resonance\Attribute\TestsHttpResponse;
 use Ds\Map;
 use Ds\Set;
 
-readonly class TestableHttpResponseCollection
+readonly class TestsHttpResponseCollection
 {
     /**
-     * @var Map<HttpResponderInterface,Set<TestableHttpResponse>>
+     * @var Map<HttpResponderInterface,Set<TestsHttpResponse>>
      */
     public Map $httpResponder;
 
     /**
-     * @var Map<TestableHttpResponse,Map<int,RespondsWith>>
+     * @var Map<TestsHttpResponse,Map<int,RespondsWith>>
      */
-    public Map $testableHttpResponse;
+    public Map $testsHttpResponse;
 
     public function __construct()
     {
         $this->httpResponder = new Map();
-        $this->testableHttpResponse = new Map();
+        $this->testsHttpResponse = new Map();
     }
 
     public function registerTestableHttpResponse(
         HttpResponderInterface $httpResponder,
-        TestableHttpResponse $testableHttpResponse,
+        TestsHttpResponse $testsHttpResponse,
         RespondsWith $respondsWith,
     ): void {
         if (!$this->httpResponder->hasKey($httpResponder)) {
             $this->httpResponder->put($httpResponder, new Set());
         }
 
-        $this->httpResponder->get($httpResponder)->add($testableHttpResponse);
+        $this->httpResponder->get($httpResponder)->add($testsHttpResponse);
 
-        if (!$this->testableHttpResponse->hasKey($testableHttpResponse)) {
-            $this->testableHttpResponse->put($testableHttpResponse, new Map());
+        if (!$this->testsHttpResponse->hasKey($testsHttpResponse)) {
+            $this->testsHttpResponse->put($testsHttpResponse, new Map());
         }
 
-        $this->testableHttpResponse->get($testableHttpResponse)->put(
+        $this->testsHttpResponse->get($testsHttpResponse)->put(
             $respondsWith->status,
             $respondsWith,
         );
-- 
GitLab