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