diff --git a/src/Attribute/RequiresPhpExtension.php b/src/Attribute/RequiresPhpExtension.php new file mode 100644 index 0000000000000000000000000000000000000000..8cff848e201867140fa0bd084ce1170fe9a51f1a --- /dev/null +++ b/src/Attribute/RequiresPhpExtension.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance\Attribute; + +use Attribute; +use Distantmagic\Resonance\Attribute as BaseAttribute; + +#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)] +final readonly class RequiresPhpExtension extends BaseAttribute +{ + /** + * @param non-empty-string $name + */ + public function __construct(public string $name) {} +} diff --git a/src/Command/GrpcGenerate.php b/src/Command/GrpcGenerate.php index 6078bd36263114f9eb419d4c3cd7f1c55e44b58f..3d200a94624d110703b85faf48363c28adc2c32a 100644 --- a/src/Command/GrpcGenerate.php +++ b/src/Command/GrpcGenerate.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Distantmagic\Resonance\Command; use Distantmagic\Resonance\Attribute\ConsoleCommand; +use Distantmagic\Resonance\Attribute\RequiresPhpExtension; use Distantmagic\Resonance\Attribute\WantsFeature; use Distantmagic\Resonance\Command; use Distantmagic\Resonance\Feature; @@ -16,6 +17,7 @@ use Symfony\Component\Console\Output\OutputInterface; name: 'grpc:generate', description: 'Generate GRPC stubs' )] +#[RequiresPhpExtension('grpc')] #[WantsFeature(Feature::Grpc)] final class GrpcGenerate extends Command { diff --git a/src/Command/PostfixBounce.php b/src/Command/PostfixBounce.php index 6db07429a0c4650fa7fd306d1390e1b1c9113998..19c79966bf04bf99f824758816353b83e8fd35da 100644 --- a/src/Command/PostfixBounce.php +++ b/src/Command/PostfixBounce.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Distantmagic\Resonance\Command; use Distantmagic\Resonance\Attribute\ConsoleCommand; +use Distantmagic\Resonance\Attribute\RequiresPhpExtension; use Distantmagic\Resonance\Attribute\WantsFeature; use Distantmagic\Resonance\Command; use Distantmagic\Resonance\CoroutineCommand; @@ -22,6 +23,8 @@ use Symfony\Component\Console\Output\OutputInterface; name: 'postfix:bounce', description: 'Handles email bounces (requires mailparse)' )] +#[RequiresPhpExtension('http')] +#[RequiresPhpExtension('mailparse')] #[WantsFeature(Feature::Postfix)] final class PostfixBounce extends CoroutineCommand { @@ -36,10 +39,6 @@ final class PostfixBounce extends CoroutineCommand protected function executeInCoroutine(InputInterface $input, OutputInterface $output): int { - if (!extension_loaded('mailparse') || !extension_loaded('http')) { - throw new RuntimeException('You need to install "http" and "mailparse" extensions'); - } - $content = stream_get_contents(STDIN); if (false === $content || empty($content)) { diff --git a/src/Command/Watch.php b/src/Command/Watch.php index 19bf802c82df8dec5dfd6613dce7a255e07d4772..ab17e8c91a55ad2163da387111cb426e2cdc497d 100644 --- a/src/Command/Watch.php +++ b/src/Command/Watch.php @@ -6,10 +6,10 @@ namespace Distantmagic\Resonance\Command; use Distantmagic\Resonance\ApplicationConfiguration; use Distantmagic\Resonance\Attribute\ConsoleCommand; +use Distantmagic\Resonance\Attribute\RequiresPhpExtension; use Distantmagic\Resonance\Command; use Distantmagic\Resonance\InotifyIterator; use Psr\Log\LoggerInterface; -use RuntimeException; use Swoole\Process; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -17,8 +17,9 @@ use Symfony\Component\Console\Output\OutputInterface; #[ConsoleCommand( name: 'watch:command', - description: 'Watch project files for changes (requires inotify)' + description: 'Watch project files for changes' )] +#[RequiresPhpExtension('inotify')] final class Watch extends Command { private ?Process $process = null; @@ -37,10 +38,6 @@ final class Watch extends Command protected function execute(InputInterface $input, OutputInterface $output): int { - if (!extension_loaded('inotify')) { - throw new RuntimeException('You need to install "inotify" extension'); - } - /** * @var string $childCommandName */ diff --git a/src/DependencyInjectionContainer.php b/src/DependencyInjectionContainer.php index 61b470710576045088a7aeca1686e87163682515..eaaa224fe616bd70ca7d631fe91377f776a42e03 100644 --- a/src/DependencyInjectionContainer.php +++ b/src/DependencyInjectionContainer.php @@ -5,8 +5,10 @@ declare(strict_types=1); namespace Distantmagic\Resonance; use Closure; +use Distantmagic\Resonance\Attribute\RequiresPhpExtension; use Distantmagic\Resonance\DependencyInjectionContainerException\AmbiguousProvider; use Distantmagic\Resonance\DependencyInjectionContainerException\DisabledFeatureProvider; +use Distantmagic\Resonance\DependencyInjectionContainerException\MissingPhpExtension; use Distantmagic\Resonance\DependencyInjectionContainerException\MissingProvider; use Ds\Map; use Ds\Set; @@ -224,6 +226,17 @@ readonly class DependencyInjectionContainer */ private function makeClassFromReflection(ReflectionClass $reflectionClass, DependencyStack $stack): object { + $reflectionClassAttributeManager = new ReflectionClassAttributeManager($reflectionClass); + $requiredPhpExtensions = $reflectionClassAttributeManager->findAttributes(RequiresPhpExtension::class); + + if (!$requiredPhpExtensions->isEmpty()) { + foreach ($requiredPhpExtensions as $requiredPhpExtension) { + if (!extension_loaded($requiredPhpExtension->name)) { + throw new MissingPhpExtension($reflectionClass->name, $requiredPhpExtension->name); + } + } + } + $constructorReflection = $reflectionClass->getConstructor(); if ($constructorReflection) { diff --git a/src/DependencyInjectionContainerException/MissingPhpExtension.php b/src/DependencyInjectionContainerException/MissingPhpExtension.php new file mode 100644 index 0000000000000000000000000000000000000000..35a34078a759656a03efee09bdd751e973d7ada8 --- /dev/null +++ b/src/DependencyInjectionContainerException/MissingPhpExtension.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance\DependencyInjectionContainerException; + +use Distantmagic\Resonance\DependencyInjectionContainerException; +use Throwable; + +class MissingPhpExtension extends DependencyInjectionContainerException +{ + /** + * @param class-string $className + * @param non-empty-string $extensionName + */ + public function __construct( + string $className, + string $extensionName, + ?Throwable $previous = null, + ) { + parent::__construct( + sprintf( + 'To use "%s" you need to install "%s" PHP extension.', + $className, + $extensionName, + ), + $previous + ); + } +} diff --git a/src/DoctrineMySQLDriver.php b/src/DoctrineMySQLDriver.php index a74d99db98fe6fb18af6242b70dbbfd4f1d5b651..ac2636f306de96f3e685403fd9e8a4cf67ddc1ab 100644 --- a/src/DoctrineMySQLDriver.php +++ b/src/DoctrineMySQLDriver.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Distantmagic\Resonance; +use Distantmagic\Resonance\Attribute\RequiresPhpExtension; use Distantmagic\Resonance\Attribute\Singleton; use Doctrine\DBAL\Driver\AbstractMySQLDriver; @@ -13,6 +14,7 @@ use Doctrine\DBAL\Driver\AbstractMySQLDriver; * * @psalm-suppress DeprecatedInterface */ +#[RequiresPhpExtension('pdo')] #[Singleton] class DoctrineMySQLDriver extends AbstractMySQLDriver { diff --git a/src/DoctrinePostgreSQLDriver.php b/src/DoctrinePostgreSQLDriver.php index b0147adfe0eb13d3baa8fef8c382c73f8bcee633..8c4063902603cf20760911627defd6f29c0d099c 100644 --- a/src/DoctrinePostgreSQLDriver.php +++ b/src/DoctrinePostgreSQLDriver.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Distantmagic\Resonance; +use Distantmagic\Resonance\Attribute\RequiresPhpExtension; use Distantmagic\Resonance\Attribute\Singleton; use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver; @@ -13,6 +14,7 @@ use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver; * * @psalm-suppress DeprecatedInterface */ +#[RequiresPhpExtension('pdo')] #[Singleton] class DoctrinePostgreSQLDriver extends AbstractPostgreSQLDriver { diff --git a/src/DoctrineSQLiteDriver.php b/src/DoctrineSQLiteDriver.php index fa6f0f105f968d34a1c14292ba7e277ba89769e3..014e6ec2210aeb2393ff726b548ab14d9f256c64 100644 --- a/src/DoctrineSQLiteDriver.php +++ b/src/DoctrineSQLiteDriver.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Distantmagic\Resonance; +use Distantmagic\Resonance\Attribute\RequiresPhpExtension; use Distantmagic\Resonance\Attribute\Singleton; use Doctrine\DBAL\Driver\AbstractSQLiteDriver; @@ -13,6 +14,7 @@ use Doctrine\DBAL\Driver\AbstractSQLiteDriver; * * @psalm-suppress DeprecatedInterface */ +#[RequiresPhpExtension('pdo')] #[Singleton] class DoctrineSQLiteDriver extends AbstractSQLiteDriver { diff --git a/src/IntlFormatter.php b/src/IntlFormatter.php index 7fbca43c692a9712f5d567e290869083a2e960e2..baa662914e0b630fbb1ed65d0e34a76d584cf6e8 100644 --- a/src/IntlFormatter.php +++ b/src/IntlFormatter.php @@ -6,11 +6,13 @@ namespace Distantmagic\Resonance; use DateTimeImmutable; use DateTimeInterface; +use Distantmagic\Resonance\Attribute\RequiresPhpExtension; use Distantmagic\Resonance\Attribute\Singleton; use IntlDateFormatter; use Swoole\Http\Request; #[Singleton] +#[RequiresPhpExtension('intl')] final readonly class IntlFormatter { private IntlDateFormatterRepository $formatters; diff --git a/src/LlamaCppClient.php b/src/LlamaCppClient.php index 48b298e95550a7bb04ea4d1f753f9cb0a8d66287..09b7a3ef3db8a7ccac6832d2e7bd71646e8722f0 100644 --- a/src/LlamaCppClient.php +++ b/src/LlamaCppClient.php @@ -5,12 +5,14 @@ declare(strict_types=1); namespace Distantmagic\Resonance; use CurlHandle; +use Distantmagic\Resonance\Attribute\RequiresPhpExtension; use Distantmagic\Resonance\Attribute\Singleton; use Generator; use JsonSerializable; use RuntimeException; use Swoole\Coroutine\Channel; +#[RequiresPhpExtension('curl')] #[Singleton] readonly class LlamaCppClient { diff --git a/src/SQLiteVSSConnectionBuilder.php b/src/SQLiteVSSConnectionBuilder.php index be27327ac27885ecc42f8ee7e062fd7b056756ce..0523cb5da0bc09e41233f547133dd24966f9f8c8 100644 --- a/src/SQLiteVSSConnectionBuilder.php +++ b/src/SQLiteVSSConnectionBuilder.php @@ -4,9 +4,11 @@ declare(strict_types=1); namespace Distantmagic\Resonance; +use Distantmagic\Resonance\Attribute\RequiresPhpExtension; use Distantmagic\Resonance\Attribute\Singleton; use SQLite3; +#[RequiresPhpExtension('sqlite3')] #[Singleton] readonly class SQLiteVSSConnectionBuilder { diff --git a/src/SingletonProvider/DatabaseConnectionPoolRepositoryProvider.php b/src/SingletonProvider/DatabaseConnectionPoolRepositoryProvider.php index a79340aad2566e49ecf0c381e44eb04c408635fb..dcfa796603336c0ad6915648155f59df6a8f9515 100644 --- a/src/SingletonProvider/DatabaseConnectionPoolRepositoryProvider.php +++ b/src/SingletonProvider/DatabaseConnectionPoolRepositoryProvider.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Distantmagic\Resonance\SingletonProvider; +use Distantmagic\Resonance\Attribute\RequiresPhpExtension; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\DatabaseConfiguration; use Distantmagic\Resonance\DatabaseConnectionPoolRepository; @@ -17,6 +18,7 @@ use Swoole\Database\PDOConfig; /** * @template-extends SingletonProvider<DatabaseConnectionPoolRepository> */ +#[RequiresPhpExtension('pdo')] #[Singleton(provides: DatabaseConnectionPoolRepository::class)] final readonly class DatabaseConnectionPoolRepositoryProvider extends SingletonProvider { diff --git a/src/SingletonProvider/DoctrineEntityManagerRepositoryProvider.php b/src/SingletonProvider/DoctrineEntityManagerRepositoryProvider.php index 587ab4f6aae342e7a29b1fcb8293a62ac419e5b4..41a8fb9303efc16a27c314f24707eabc7d63f040 100644 --- a/src/SingletonProvider/DoctrineEntityManagerRepositoryProvider.php +++ b/src/SingletonProvider/DoctrineEntityManagerRepositoryProvider.php @@ -6,6 +6,7 @@ namespace Distantmagic\Resonance\SingletonProvider; use Distantmagic\Resonance\ApplicationConfiguration; use Distantmagic\Resonance\Attribute\GrantsFeature; +use Distantmagic\Resonance\Attribute\RequiresPhpExtension; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\DoctrineConnectionRepository; use Distantmagic\Resonance\DoctrineEntityManagerRepository; @@ -21,6 +22,7 @@ use Symfony\Component\Filesystem\Filesystem; /** * @template-extends SingletonProvider<DoctrineEntityManagerRepository> */ +#[RequiresPhpExtension('pdo')] #[GrantsFeature(Feature::Doctrine)] #[Singleton(provides: DoctrineEntityManagerRepository::class)] final readonly class DoctrineEntityManagerRepositoryProvider extends SingletonProvider diff --git a/src/SingletonProvider/PDOPoolConnectionBuilderCollectionProvider.php b/src/SingletonProvider/PDOPoolConnectionBuilderCollectionProvider.php index 740218972742e8eeb45291c64d0f40f7158b4d40..1820af1fbd630b559b37053911667ed92872274d 100644 --- a/src/SingletonProvider/PDOPoolConnectionBuilderCollectionProvider.php +++ b/src/SingletonProvider/PDOPoolConnectionBuilderCollectionProvider.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Distantmagic\Resonance\SingletonProvider; use Distantmagic\Resonance\Attribute\BuildsPDOPoolConnection; +use Distantmagic\Resonance\Attribute\RequiresPhpExtension; use Distantmagic\Resonance\Attribute\RequiresSingletonCollection; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\HttpResponderCollection; @@ -19,6 +20,7 @@ use Distantmagic\Resonance\SingletonProvider; /** * @template-extends SingletonProvider<HttpResponderCollection> */ +#[RequiresPhpExtension('pdo')] #[RequiresSingletonCollection(SingletonCollection::PDOPoolConnectionBuilder)] #[Singleton(provides: PDOPoolConnectionBuilderCollection::class)] final readonly class PDOPoolConnectionBuilderCollectionProvider extends SingletonProvider