diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c3743b9..5768a4b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,15 +13,11 @@ jobs: fail-fast: false matrix: # normal, highest, non-dev installs - php-version: [ '8.2' ] + php-version: [ '8.4' ] dependency-versions: [ 'highest' ] include: - # testing lowest PHP version with the lowest dependencies - - php-version: '8.2' - dependency-versions: 'lowest' - # testing dev versions with the highest PHP - - php-version: '8.2' + - php-version: '8.4' dependency-versions: 'highest' steps: @@ -31,7 +27,7 @@ jobs: - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: - coverage: "none" + coverage: "xdebug" php-version: "${{ matrix.php-version }}" - name: "Composer install" @@ -40,5 +36,5 @@ jobs: dependency-versions: "${{ matrix.dependency-versions }}" composer-options: "--prefer-dist --no-progress" - - name: "RUn tests" + - name: "Run tests" run: "composer coverage-html" diff --git a/.gitignore b/.gitignore index c40cd9d..720af22 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,10 @@ vendor composer.lock .phpunit.result.cache +.phpunit.cache .php-cs-fixer.cache test-coverage-report phpunit.xml .php-cs-fixer.php -phpstan.neon \ No newline at end of file +phpstan.neon +tests/Unit/var \ No newline at end of file diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index ded2263..ade2941 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -24,7 +24,8 @@ For the full copyright and license information, please view the LICENSE file that was distributed with this source code. EOF - ] + ], + 'phpdoc_to_comment' => false, // отключаем )) ->setRiskyAllowed(true) ->setFinder($finder); \ No newline at end of file diff --git a/composer.json b/composer.json index 3971ce1..73e71d3 100755 --- a/composer.json +++ b/composer.json @@ -11,15 +11,15 @@ ], "require": { "php": "^8.2", - "psr/container": "^2.0" + "psr/container": "^2" }, "require-dev": { - "ergebnis/composer-normalize": "^2.29", - "friendsofphp/php-cs-fixer": "^3.13", - "phpstan/phpstan": "^1.9", - "phpunit/php-code-coverage": "^9.2", - "phpunit/phpunit": "^9.5", - "vimeo/psalm": "^5.2" + "ergebnis/composer-normalize": "^2", + "friendsofphp/php-cs-fixer": "^3", + "phpstan/phpstan": "^2", + "phpunit/php-code-coverage": "^10 || ^11 || ^12", + "phpunit/phpunit": "^10 || ^11 || ^12", + "vimeo/psalm": "^6" }, "suggest": { "micro/autowire": "Autowire helper for dependency injection" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f113387..a2cc4d5 100755 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,20 +1,12 @@ - - - - src/ - - + stopOnFailure="false" + cacheDirectory=".phpunit.cache" + backupStaticProperties="false"> tests/Unit @@ -23,4 +15,9 @@ + + + src/ + + diff --git a/src/Container.php b/src/Container.php index df5844f..298718f 100755 --- a/src/Container.php +++ b/src/Container.php @@ -13,7 +13,6 @@ use Micro\Component\DependencyInjection\Exception\ServiceNotRegisteredException; use Micro\Component\DependencyInjection\Exception\ServiceRegistrationException; -use Psr\Container\ContainerInterface; /** * @author Stanislau Komar @@ -26,12 +25,12 @@ class Container implements ContainerInterface, ContainerRegistryInterface, Conta private array $services = []; /** - * @var array + * @var array */ private array $servicesRaw = []; /** - * @var array>> + * @var array>> */ private array $decorators = []; @@ -45,6 +44,7 @@ class Container implements ContainerInterface, ContainerRegistryInterface, Conta * * @psalm-return T */ + #[\Override] public function get(string $id): object { if (!empty($this->services[$id])) { @@ -61,15 +61,18 @@ public function get(string $id): object * * @psalm-suppress MoreSpecificImplementedParamType */ + #[\Override] public function has(string $id): bool { - return !empty($this->servicesRaw[$id]) || !empty($this->services[$id]); + return \array_key_exists($id, $this->servicesRaw) + || \array_key_exists($id, $this->services); } + #[\Override] public function register(string $id, callable $service, bool $force = false): void { if ($this->has($id) && !$force) { - throw new ServiceRegistrationException(sprintf('Service "%s" already registered', $id)); + throw new ServiceRegistrationException(\sprintf('Service "%s" already registered', $id)); } $this->servicesRaw[$id] = $service; @@ -78,11 +81,23 @@ public function register(string $id, callable $service, bool $force = false): vo /** * @psalm-suppress InvalidPropertyAssignmentValue */ + #[\Override] public function decorate(string $id, callable $service, int $priority = 0): void { if (!\array_key_exists($id, $this->decorators)) { + /** + * @psalm-suppress PropertyTypeCoercion + * + * @phpstan-ignore-next-line + */ $this->decorators[$id] = []; } + + /** + * @psalm-suppress PropertyTypeCoercion + * + * @phpstan-ignore-next-line + */ $this->decorators[$id][$priority][] = $service; } @@ -93,7 +108,7 @@ public function decorate(string $id, callable $service, int $priority = 0): void */ protected function initializeService(string $serviceId): void { - if (empty($this->servicesRaw[$serviceId])) { + if (!isset($this->servicesRaw[$serviceId])) { throw new ServiceNotRegisteredException($serviceId); } diff --git a/src/ContainerCompiled.php b/src/ContainerCompiled.php new file mode 100644 index 0000000..4b9466d --- /dev/null +++ b/src/ContainerCompiled.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Micro\Component\DependencyInjection; + +use Micro\Component\DependencyInjection\Exception\ServiceRegistrationException; +use Micro\Component\DependencyInjection\Proxy\ProxyBuilderInterface; +use Micro\Component\DependencyInjection\Proxy\ProxyClassNameGeneratorInterface; +use Psr\Container\ContainerInterface as PsrContainerInterface; + +/** @psalm-suppress UnusedClass */ +final class ContainerCompiled extends Container implements ContainerRegistryCompiledInterface +{ + private bool $isCompiled; + + private PsrContainerInterface $decorated; + + public function __construct( + private readonly ProxyClassNameGeneratorInterface $classNameGenerator, + private readonly ProxyBuilderInterface $proxyBuilder, + ?PsrContainerInterface $decorated = null, + ) { + $this->isCompiled = false; + $this->decorated = $decorated ?? new Container(); + } + + /** + * @template T of object + * + * @param class-string $id + * + * @psalm-suppress MoreSpecificImplementedParamType + * + * @return T + */ + #[\Override] + public function get(string $id): object + { + $proxyClass = $this->classNameGenerator->createProxyClassName($id); + if (!class_exists($proxyClass)) { + /* @psalm-suppress MixedReturnStatement */ + return $this->decorated->get($id); + } + + /** + * @var T $instance + * + * @psalm-suppress MixedMethodCall + */ + $instance = new $proxyClass($this); + + return $instance; + } + + #[\Override] + public function has(string $id): bool + { + return $this->decorated->has($id); + } + + #[\Override] + public function register(string $id, callable $service, bool $force = false): void + { + if (!$this->decorated instanceof ContainerRegistryInterface) { + throw new ServiceRegistrationException('Container can not decorate service because container does not implement '.ContainerRegistryInterface::class); + } + + if ($this->isCompiled) { + throw new ServiceRegistrationException('Can not register service because container already compiled.'); + } + + if (interface_exists($id) || class_exists($id)) { + $this->proxyBuilder->add($id); + } + + /** @psalm-suppress ArgumentTypeCoercion */ + $this->decorated->register($id, $service, $force); + } + + /** + * @psalm-suppress InvalidPropertyAssignmentValue + */ + #[\Override] + public function decorate(string $id, callable $service, int $priority = 0): void + { + if (!$this->decorated instanceof ContainerDecoratorInterface) { + throw new ServiceRegistrationException('Container can not decorate service because container does not implement '.ContainerDecoratorInterface::class); + } + + if ($this->isCompiled) { + throw new ServiceRegistrationException('Can not register service because container already compiled.'); + } + + if (interface_exists($id) || class_exists($id)) { + $this->proxyBuilder->add($id); + } + + /** @psalm-suppress ArgumentTypeCoercion */ + $this->decorated->decorate($id, $service, $priority); + } + + #[\Override] + public function compile(): void + { + $this->proxyBuilder->build(); + + $this->isCompiled = true; + } +} diff --git a/src/ContainerDecoratorInterface.php b/src/ContainerDecoratorInterface.php index 8a7e4eb..a7d2b5e 100644 --- a/src/ContainerDecoratorInterface.php +++ b/src/ContainerDecoratorInterface.php @@ -16,7 +16,8 @@ interface ContainerDecoratorInterface /** * @template T of object * - * @param class-string $id + * @param class-string|non-empty-string $id + * @param callable(T, ContainerInterface): T $service */ public function decorate(string $id, callable $service, int $priority = 0): void; } diff --git a/src/ContainerInterface.php b/src/ContainerInterface.php new file mode 100644 index 0000000..f4771c6 --- /dev/null +++ b/src/ContainerInterface.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Micro\Component\DependencyInjection; + +interface ContainerInterface extends \Psr\Container\ContainerInterface +{ + /** + * @param class-string $id + * + * @psalm-suppress MoreSpecificImplementedParamType + */ + #[\Override] + public function has(string $id): bool; + + /** + * @template T of object + * + * @param class-string $id + * + * @psalm-return T + * + * @psalm-suppress MoreSpecificImplementedParamType + */ + #[\Override] + public function get(string $id): mixed; +} diff --git a/src/ContainerRegistryCompiledInterface.php b/src/ContainerRegistryCompiledInterface.php new file mode 100644 index 0000000..c9a37a5 --- /dev/null +++ b/src/ContainerRegistryCompiledInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Micro\Component\DependencyInjection; + +interface ContainerRegistryCompiledInterface extends ContainerRegistryInterface +{ + /** @psalm-suppress PossiblyUnusedMethod */ + public function compile(): void; +} diff --git a/src/ContainerRegistryInterface.php b/src/ContainerRegistryInterface.php index 304de0d..27222aa 100755 --- a/src/ContainerRegistryInterface.php +++ b/src/ContainerRegistryInterface.php @@ -18,8 +18,8 @@ interface ContainerRegistryInterface * * @template T of Object * - * @param class-string $id service alias - * @param callable $service service initialization callback + * @param class-string $id service alias + * @param callable(object, Container): object $service service initialization callback */ public function register(string $id, callable $service, bool $force = false): void; } diff --git a/src/Exception/ServiceNotRegisteredException.php b/src/Exception/ServiceNotRegisteredException.php index e754903..6ddd3ab 100755 --- a/src/Exception/ServiceNotRegisteredException.php +++ b/src/Exception/ServiceNotRegisteredException.php @@ -13,17 +13,18 @@ use Psr\Container\NotFoundExceptionInterface; -class ServiceNotRegisteredException extends \RuntimeException implements NotFoundExceptionInterface +final class ServiceNotRegisteredException extends \RuntimeException implements NotFoundExceptionInterface { private string $serviceId; - public function __construct(string $serviceId, int $code = 0, \Throwable $previous = null) + public function __construct(string $serviceId, int $code = 0, ?\Throwable $previous = null) { $this->serviceId = $serviceId; - parent::__construct(sprintf('Service "%s" not registered.', $this->serviceId), $code, $previous); + parent::__construct(\sprintf('Service "%s" not registered.', $this->serviceId), $code, $previous); } + /** @psalm-suppress PossiblyUnusedMethod */ public function getServiceId(): string { return $this->serviceId; diff --git a/src/Exception/ServiceRegistrationException.php b/src/Exception/ServiceRegistrationException.php index 522a11d..71fa85f 100755 --- a/src/Exception/ServiceRegistrationException.php +++ b/src/Exception/ServiceRegistrationException.php @@ -13,6 +13,6 @@ use Psr\Container\ContainerExceptionInterface; -class ServiceRegistrationException extends \RuntimeException implements ContainerExceptionInterface +final class ServiceRegistrationException extends \RuntimeException implements ContainerExceptionInterface { } diff --git a/src/Proxy/ProxyBuilder.php b/src/Proxy/ProxyBuilder.php new file mode 100644 index 0000000..4576e15 --- /dev/null +++ b/src/Proxy/ProxyBuilder.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Micro\Component\DependencyInjection\Proxy; + +/** @psalm-suppress UnusedClass */ +final class ProxyBuilder implements ProxyBuilderInterface +{ + /** + * @var class-string[] + */ + private array $classCollection; + + public function __construct( + private readonly ProxyClassContentFactoryInterface $classContentFactory, + private readonly ProxyFileManagerInterface $proxyFileManager, + private readonly string $proxyClassesNamespace = 'Micro\ClassProxy', + ) { + $this->classCollection = []; + } + + #[\Override] + public function add(string $className): ProxyBuilderInterface + { + if (\in_array($className, $this->classCollection)) { + return $this; + } + + $this->classCollection[] = $className; + + return $this; + } + + #[\Override] + public function build(): void + { + $namespace = $this->proxyClassesNamespace ? 'namespace '.$this->proxyClassesNamespace : ''; + $proxyClassContent = " +classCollection as $className) { + $proxyClassContent .= "\n".$this->classContentFactory->create($className); + } + + $this->proxyFileManager->writeProxiesFileContent($proxyClassContent); + $this->proxyFileManager->requireProxiesFile(); + } +} diff --git a/src/Proxy/ProxyBuilderInterface.php b/src/Proxy/ProxyBuilderInterface.php new file mode 100644 index 0000000..c0ac2d2 --- /dev/null +++ b/src/Proxy/ProxyBuilderInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Micro\Component\DependencyInjection\Proxy; + +interface ProxyBuilderInterface +{ + /** + * @param class-string $className + * + * @psalm-suppress PossiblyUnusedReturnValue + */ + public function add(string $className): self; + + public function build(): void; +} diff --git a/src/Proxy/ProxyBuilderProductionDecorator.php b/src/Proxy/ProxyBuilderProductionDecorator.php new file mode 100644 index 0000000..6690e41 --- /dev/null +++ b/src/Proxy/ProxyBuilderProductionDecorator.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Micro\Component\DependencyInjection\Proxy; + +/** @psalm-suppress UnusedClass */ +final readonly class ProxyBuilderProductionDecorator implements ProxyBuilderInterface +{ + public function __construct( + private ProxyBuilderInterface $proxyBuilder, + private ProxyFileManagerInterface $proxyWriter, + ) { + } + + #[\Override] + public function add(string $className): ProxyBuilderInterface + { + if ($this->proxyWriter->proxyBuildAlreadyExists()) { + return $this; + } + + $this->proxyBuilder->add($className); + + return $this; + } + + #[\Override] + public function build(): void + { + if (!$this->proxyWriter->proxyBuildAlreadyExists()) { + $this->proxyBuilder->build(); + } + + $this->proxyWriter->requireProxiesFile(); + } +} diff --git a/src/Proxy/ProxyClassContentFactoryInterface.php b/src/Proxy/ProxyClassContentFactoryInterface.php new file mode 100644 index 0000000..99eed6a --- /dev/null +++ b/src/Proxy/ProxyClassContentFactoryInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Micro\Component\DependencyInjection\Proxy; + +interface ProxyClassContentFactoryInterface +{ + /** + * @param class-string $className + */ + public function create(string $className): string; +} diff --git a/src/Proxy/ProxyClassNameGenerator.php b/src/Proxy/ProxyClassNameGenerator.php new file mode 100644 index 0000000..064bf62 --- /dev/null +++ b/src/Proxy/ProxyClassNameGenerator.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Micro\Component\DependencyInjection\Proxy; + +/** @psalm-suppress UnusedClass */ +final class ProxyClassNameGenerator implements ProxyClassNameGeneratorInterface +{ + /** @var array */ + private array $classmapCache; + + public function __construct(private readonly string $proxyClassNamespace) + { + $this->classmapCache = []; + } + + #[\Override] + public function createProxyClassName(string $className): string + { + if (\array_key_exists($className, $this->classmapCache)) { + return $this->classmapCache[$className]; + } + + $shortName = $this->createShortProxyClassName($className); + if (!$this->proxyClassNamespace) { + return $shortName; + } + + $realName = '\\'.trim($this->proxyClassNamespace, '\\').'\\'.$shortName; + $this->classmapCache[$className] = $realName; + + return $realName; + } + + #[\Override] + public function createShortProxyClassName(string $className): string + { + return str_replace('\\', '_', $className); + } + + #[\Override] + public function getRealClassName(string $className): string + { + return '\\'.ltrim($className, '\\'); + } +} diff --git a/src/Proxy/ProxyClassNameGeneratorInterface.php b/src/Proxy/ProxyClassNameGeneratorInterface.php new file mode 100644 index 0000000..a5bfb83 --- /dev/null +++ b/src/Proxy/ProxyClassNameGeneratorInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Micro\Component\DependencyInjection\Proxy; + +interface ProxyClassNameGeneratorInterface +{ + public function createProxyClassName(string $className): string; + + public function createShortProxyClassName(string $className): string; + + public function getRealClassName(string $className): string; +} diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php new file mode 100644 index 0000000..64312c3 --- /dev/null +++ b/src/Proxy/ProxyFactory.php @@ -0,0 +1,234 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Micro\Component\DependencyInjection\Proxy; + +/** @psalm-suppress UnusedClass */ +final class ProxyFactory implements ProxyClassContentFactoryInterface +{ + public function __construct( + private readonly ProxyClassNameGeneratorInterface $classNameGenerator, + ) { + } + + private function createNamespacedName(string $name): string + { + $isObjAlias = class_exists($name) || interface_exists($name); + if ( + $isObjAlias && !str_starts_with($name, '\\') + ) { + return '\\'.$name; + } + + return $name; + } + + /** + * @throws \ReflectionException + */ + #[\Override] + public function create(string $className): string + { + $ref = new \ReflectionClass($className); + $isInterface = $ref->isInterface(); + if ( + !$isInterface + && ($ref->isAbstract() || $ref->isFinal() || $ref->isEnum() || $ref->isAnonymous() || $ref->isTrait()) + ) { + throw new \LogicException(\sprintf('`%s` should be non abstract, no enum, no final and not anonymous class or interface or trait.', $className)); + } + $methods = []; + foreach ($ref->getMethods() as $refMethod) { + if (!$refMethod->isPublic() || $refMethod->isConstructor()) { + continue; + } + + $methods[] = $this->createProxyMethod($refMethod); + } + + $methods = implode("\n", $methods); + $proxyClass = $this->createProxyClass($ref, $methods, $isInterface); + + return $proxyClass; + } + + private function createMethodArguments(\ReflectionMethod $refMethod): string + { + $arguments = []; + foreach ($refMethod->getParameters() as $refParam) { + $arg = trim($this->createTypes($refParam->getType()).' $'.$refParam->getName()); + try { + $defaultValue = $refParam->getDefaultValueConstantName(); + if (null === $defaultValue) { + try { + $defaultValue = (string) $refParam->getDefaultValue(); + } catch (\ReflectionException) { + $defaultValue = null; + } + } + + if (null === $defaultValue || '' === $defaultValue) { + $defaultValue = 'null'; + } + + $arg .= ' = '.$defaultValue; + } catch (\ReflectionException $exception) { + } + + $arguments[$refParam->getPosition()] = $arg; + } + + return implode(', ', $arguments); + } + + private function createTypes(?\ReflectionType $refType): string + { + if (!$refType) { + return 'mixed'; + } + + $allowNull = $refType->allowsNull(); + + if ($refType instanceof \ReflectionNamedType) { + $type = $this->createNamespacedName($refType->getName()); + if ($allowNull && !\in_array($type, ['null', 'mixed', 'void'], true)) { + return 'null|'.$type; + } + + return $type; + } + + if ($refType instanceof \ReflectionUnionType || $refType instanceof \ReflectionIntersectionType) { + $returnTypes = []; + foreach ($refType->getTypes() as $returnType) { + $returnTypes[] = $returnType instanceof \ReflectionNamedType + ? $this->createNamespacedName($returnType->getName()) + : 'mixed'; + } + + if ($allowNull && !\in_array('null', $returnTypes, true)) { + $returnTypes[] = 'null'; + } + + $separator = $refType instanceof \ReflectionUnionType ? '|' : '&'; + + return implode($separator, $returnTypes); + } + + return 'mixed'; + } + + private function createProxyMethod(\ReflectionMethod $method): string + { + $template = ' + public function %s(%s): %s + { + %s$this->_proxied()->%s(%s); + } + '; + + $returnTypes = $this->createTypes($method->getReturnType()); + $isVoid = 'void' === $returnTypes; + $methodName = $method->getName(); + $arguments = []; + foreach ($method->getParameters() as $parameter) { + $arguments[] = '$'.$parameter->getName(); + } + + return \sprintf( + $template, + $methodName, + $this->createMethodArguments($method), + $returnTypes, + $isVoid ? '' : 'return ', + $methodName, + implode(', ', $arguments), + ); + } + + /** + * @template T of object + * + * @param \ReflectionClass $classRef + */ + private function createProxyClassConstructor(\ReflectionClass $classRef): string + { + return \sprintf('return $this->container->get(\%s::class, true);', $classRef->getName()); + /*if($classRef->isInterface()) { + return sprintf('return $this->container->get(\%s::class, true);', $classRef->getName()); + } + + $className = $classRef->getName(); + $constructor = $classRef->getConstructor(); + $refParameters = $constructor?->getParameters() ?: []; + $parameters = []; + foreach ($refParameters as $refParam) { + $refType = $refParam->getType(); + if($refType instanceof \ReflectionUnionType) { + throw new ServiceRegistrationException( + sprintf('Autowire union type is not possible. Class `%s`', $className) + ); + } + + $refType = $refParam->getType(); + $parameters[] = sprintf('$this->container->get(\%s::class)', $refType->getName()); + } + + return sprintf( + '$this->_proxiedObject = new \%s(%s);',$className, implode(',', $parameters) + ); + */ + } + + /** + * @template T of object + * + * @param \ReflectionClass $classRef + */ + private function createProxyClass( + \ReflectionClass $classRef, + string $methods, + bool $isInterface, + ): string { + $template = "final readonly class %s %s %s +{ + private ?%s \$_proxiedObject; + + public function __construct( + private \Psr\Container\ContainerInterface \$container + ) { + } + + private function _proxied(): %s + { + %s + } + %s +} +"; + $refName = $classRef->getName(); + $proxyClassName = $this->classNameGenerator->createShortProxyClassName($refName); + $fullClassName = $this->classNameGenerator->getRealClassName($refName); + + return \sprintf( + $template, + $proxyClassName, + $isInterface ? 'implements' : 'extends', + $fullClassName, + $fullClassName, + $fullClassName, + $this->createProxyClassConstructor($classRef), + $methods + ); + } +} diff --git a/src/Proxy/ProxyFileManager.php b/src/Proxy/ProxyFileManager.php new file mode 100644 index 0000000..1aaf99b --- /dev/null +++ b/src/Proxy/ProxyFileManager.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Micro\Component\DependencyInjection\Proxy; + +/** @psalm-suppress UnusedClass */ +final readonly class ProxyFileManager implements ProxyFileManagerInterface +{ + public function __construct( + private string $proxyClassFileDestination, + ) { + } + + #[\Override] + public function writeProxiesFileContent(string $classesContent): void + { + file_put_contents( + $this->proxyClassFileDestination, + trim($classesContent) + ); + } + + #[\Override] + public function proxyBuildAlreadyExists(): bool + { + return file_exists($this->proxyClassFileDestination); + } + + #[\Override] + public function requireProxiesFile(): void + { + /** @psalm-suppress UnresolvableInclude */ + require_once $this->proxyClassFileDestination; + } +} diff --git a/src/Proxy/ProxyFileManagerInterface.php b/src/Proxy/ProxyFileManagerInterface.php new file mode 100644 index 0000000..a61b1a3 --- /dev/null +++ b/src/Proxy/ProxyFileManagerInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Micro\Component\DependencyInjection\Proxy; + +interface ProxyFileManagerInterface +{ + public function proxyBuildAlreadyExists(): bool; + + public function writeProxiesFileContent(string $classesContent): void; + + public function requireProxiesFile(): void; +} diff --git a/tests/Unit/ContainerTest.php b/tests/Unit/ContainerTest.php index 3c372da..779163c 100755 --- a/tests/Unit/ContainerTest.php +++ b/tests/Unit/ContainerTest.php @@ -11,20 +11,50 @@ namespace Micro\Component\DependencyInjection\Tests; -use Micro\Component\DependencyInjection\Container; +use Micro\Component\DependencyInjection\ContainerCompiled; use Micro\Component\DependencyInjection\Exception\ServiceNotRegisteredException; use Micro\Component\DependencyInjection\Exception\ServiceRegistrationException; +use Micro\Component\DependencyInjection\Proxy\ProxyBuilder; +use Micro\Component\DependencyInjection\Proxy\ProxyClassNameGenerator; +use Micro\Component\DependencyInjection\Proxy\ProxyFactory; +use Micro\Component\DependencyInjection\Proxy\ProxyFileManager; use PHPUnit\Framework\TestCase; class ContainerTest extends TestCase { + protected function createContainer(): ContainerCompiled + { + $fileManager = new ProxyFileManager( + __DIR__.'/var/proxy.cache.php', + ); + + $classNameGenerator = new ProxyClassNameGenerator( + 'Micro' + ); + + $classContentFactory = new ProxyFactory( + $classNameGenerator + ); + + $proxyBuilder = new ProxyBuilder( + $classContentFactory, + $fileManager, + ); + + return new ContainerCompiled( + $classNameGenerator, + $proxyBuilder + ); + } + public function testContainerResolveDependencies(): void { - $container = new Container(); + $container = $this->createContainer(); $container->register('test', function () { return new NamedService('success'); }); + $container->compile(); /** @var NamedInterface $service */ $service = $container->get('test'); @@ -37,27 +67,31 @@ public function testContainerResolveDependencies(): void public function testRegisterTwoServicesWithEqualAliasesException(): void { $this->expectException(ServiceRegistrationException::class); - $container = new Container(); + $container = $this->createContainer(); + + $container->register('test', function () { return new class {}; }); + $container->register('test', function () { return new class {}; }); - $container->register('test', function () { return new class() {}; }); - $container->register('test', function () { return new class() {}; }); + $container->compile(); } public function testContainerUnresolvedException(): void { $this->expectException(ServiceNotRegisteredException::class); - $container = new Container(); + $container = $this->createContainer(); $container->register(NamedInterface::class, function (): NamedInterface { return new NamedService('success'); }); + $container->compile(); + $container->get('test2'); } public function testDecorateService(): void { - $container = new Container(); + $container = $this->createContainer(); $container->register(NamedInterface::class, function (): NamedInterface { return new NamedService('A'); @@ -75,6 +109,8 @@ public function testDecorateService(): void return new NamedServiceDecorator($decorated, 'C'); }, 5); + $container->compile(); + $result = $container->get(NamedInterface::class); $this->assertInstanceOf(NamedServiceDecorator::class, $result); @@ -86,7 +122,7 @@ public function testDecorateService(): void public function testUnregisteredException() { - $container = new Container(); + $container = $this->createContainer(); $service = 'UnresolvedService'; $this->expectException(ServiceNotRegisteredException::class); @@ -102,7 +138,7 @@ public function testUnregisteredException() public function testDecoratorsWithSamePriority(): void { - $container = new Container(); + $container = $this->createContainer(); $container->register(NamedInterface::class, function (): NamedInterface { return new NamedService('A'); @@ -124,6 +160,8 @@ public function testDecoratorsWithSamePriority(): void return new NamedServiceDecorator($decorated, 'C'); }, 10); + $container->compile(); + $result = $container->get(NamedInterface::class); $this->assertInstanceOf(NamedServiceDecorator::class, $result); $this->assertInstanceOf(NamedInterface::class, $result); @@ -152,7 +190,7 @@ public function getName(): string { public function __construct( private object $decorated, - private string $name + private string $name, ) { } diff --git a/tests/Unit/var/.gitkeep b/tests/Unit/var/.gitkeep new file mode 100644 index 0000000..e69de29