Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions src/Container/ArgonContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class ArgonContainer implements ContainerInterface
private readonly ServiceBinderInterface $binder;
private readonly ParameterStoreInterface $parameterStore;
private readonly InterceptorRegistryInterface $interceptors;
private bool $strictMode = false;

/**
* @throws ContainerException
Expand All @@ -61,8 +62,10 @@ public function __construct(
?ServiceProviderRegistryInterface $providers = null,
?ServiceResolverInterface $serviceResolver = null,
?ArgumentResolverInterface $argumentResolver = null,
?ServiceBinderInterface $binder = null
?ServiceBinderInterface $binder = null,
bool $strictMode = false
) {
$this->strictMode = $strictMode;
$argumentMap = $argumentMap ?? new ArgumentMap();
$this->parameterStore = $parameters ?? new ParameterStore();
$this->interceptors = $interceptors ?? new InterceptorRegistry();
Expand All @@ -84,9 +87,16 @@ public function __construct(
$this->binder,
$reflectionCache,
$this->interceptors,
$argumentResolver
$argumentResolver,
$strictMode
);

if ($serviceResolver instanceof ServiceResolverInterface && $serviceResolver !== $this->serviceResolver) {
if (method_exists($serviceResolver, 'setStrictMode')) {
$serviceResolver->setStrictMode($strictMode);
}
}

$argumentResolver->setServiceResolver($this->serviceResolver);

if (method_exists($this->interceptors, 'setResolver')) {
Expand All @@ -107,6 +117,11 @@ public function getContextualBindings(): ContextualBindingsInterface
return $this->contextualBindings;
}

public function isStrictMode(): bool
{
return $this->strictMode;
}

/**
* @template TGet of object
* @param class-string<TGet>|string $id
Expand Down
3 changes: 2 additions & 1 deletion src/Container/Compiler/CompilationContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public function __construct(
public readonly ArgonContainer $container,
public readonly PhpFile $file,
public readonly PhpNamespace $namespace,
public readonly ClassType $class
public readonly ClassType $class,
public readonly bool $strictMode
) {
}
}
7 changes: 5 additions & 2 deletions src/Container/Compiler/CompilationContextFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,28 @@

use Maduser\Argon\Container\ArgonContainer;
use Maduser\Argon\Container\Exceptions\ContainerException;
use Maduser\Argon\Container\Exceptions\NotFoundException;
use Nette\PhpGenerator\PhpFile;

final class CompilationContextFactory
{
public function create(
ArgonContainer $container,
string $namespace,
string $className
string $className,
bool $strictMode = false
): CompilationContext {
$file = new PhpFile();
$file->setStrictTypes();

$namespaceGen = $file->addNamespace($namespace);
$namespaceGen->addUse(ArgonContainer::class);
$namespaceGen->addUse(ContainerException::class);
$namespaceGen->addUse(NotFoundException::class);

$class = $namespaceGen->addClass($className);
$class->setExtends(ArgonContainer::class);

return new CompilationContext($container, $file, $namespaceGen, $class);
return new CompilationContext($container, $file, $namespaceGen, $class, $strictMode);
}
}
14 changes: 11 additions & 3 deletions src/Container/Compiler/ContainerCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,17 @@ public function __construct(
* @throws ContainerException
* @throws ReflectionException
*/
public function compile(string $filePath, string $className, string $namespace = 'App\\Compiled'): void
{
$context = $this->contextFactory->create($this->container, $namespace, $className);
public function compile(
string $filePath,
string $className,
string $namespace = 'App\\Compiled',
?bool $strictMode = null
): void {
$strictMode ??= method_exists($this->container, 'isStrictMode')
? $this->container->isStrictMode()
: false;

$context = $this->contextFactory->create($this->container, $namespace, $className, $strictMode);

$this->coreGenerator->generate($context);
$this->serviceDefinitionGenerator->generate($context);
Expand Down
110 changes: 99 additions & 11 deletions src/Container/Compiler/CoreContainerGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@
namespace Maduser\Argon\Container\Compiler;

use Maduser\Argon\Container\ArgonContainer;
use Maduser\Argon\Container\Exceptions\NotFoundException;
use Nette\PhpGenerator\ClassType;

final class CoreContainerGenerator
{
private bool $strictMode = false;

public function __construct(private readonly ArgonContainer $container)
{
}

public function generate(CompilationContext $context): void
{
$this->strictMode = $context->strictMode;
$class = $context->class;

$this->generateConstructor($class);
Expand All @@ -33,7 +37,11 @@ public function generate(CompilationContext $context): void
private function generateConstructor(ClassType $class): void
{
$constructor = $class->addMethod('__construct')->setPublic();
$constructor->addBody('parent::__construct();');
if ($this->strictMode) {
$constructor->addBody('parent::__construct(strictMode: true);');
} else {
$constructor->addBody('parent::__construct();');
}

$parameterStore = $this->container->getParameters()->all();
if (!empty($parameterStore)) {
Expand Down Expand Up @@ -105,19 +113,37 @@ private function generateInterceptorMethods(ClassType $class): void
private function generateGetMethod(ClassType $class): void
{
$method = $class->addMethod('get')
->setReturnType('object')
->setBody(<<<'PHP'
->setReturnType('object');

$strictBody = <<<'PHP'
$instance = $this->applyPreInterceptors($id, $args);
if ($instance !== null) {
return $instance;
}


if (!isset($this->serviceMap[$id])) {
throw new NotFoundException($id, 'compiled');
}

$instance = $this->{$this->serviceMap[$id]}($args);

return $this->applyPostInterceptors($instance);
Comment on lines 113 to +130

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve parent fallback when strict compiled container resolves services

The strict get() body now throws NotFoundException whenever $id is not in $this->serviceMap, instead of delegating to parent::get(). This means any service bound after compilation (via set() or a provider at runtime) can never be resolved in a strict compiled container even though the base container still allows explicit bindings while merely disallowing unregistered autowiring. As a result a strict compiled container becomes unusable for dynamic registrations, which is not necessary for enforcing strictness because ArgonContainer’s resolver already respects the mode. Consider retaining the parent fallback so registered-but-uncompiled services continue to work while strict mode still prevents autowiring.

Useful? React with 👍 / 👎.

PHP;

$lenientBody = <<<'PHP'
$instance = $this->applyPreInterceptors($id, $args);
if ($instance !== null) {
return $instance;
}

$instance = isset($this->serviceMap[$id])
? $this->{$this->serviceMap[$id]}($args)
: parent::get($id, $args);

return $this->applyPostInterceptors($instance);
PHP);
PHP;

$method->setBody($this->strictMode ? $strictBody : $lenientBody);

$method->addParameter('id')->setType('string');
$method->addParameter('args')->setType('array')->setDefaultValue([]);
Expand Down Expand Up @@ -162,9 +188,13 @@ private function generateGetTaggedMetaMethod(ClassType $class): void

private function generateHasMethod(ClassType $class): void
{
$body = $this->strictMode
? 'return isset($this->serviceMap[$id]);'
: 'return isset($this->serviceMap[$id]) || parent::has($id);';

$class->addMethod('has')
->setReturnType('bool')
->setBody('return isset($this->serviceMap[$id]) || parent::has($id);')
->setBody($body)
->addParameter('id')->setType('string');
}

Expand All @@ -177,7 +207,7 @@ private function generateInvokeMethod(ClassType $class): void
$invoke->addParameter('target')->setType('callable|object|array|string');
$invoke->addParameter('arguments')->setType('array')->setDefaultValue([]);

$invoke->setBody(<<<'PHP'
$lenientBody = <<<'PHP'
if (is_callable($target) && !is_array($target)) {
$reflection = new \ReflectionFunction($target);
$instance = null;
Expand Down Expand Up @@ -230,23 +260,81 @@ private function generateInvokeMethod(ClassType $class): void
}

return $reflection->invokeArgs($instance, $params);
PHP);
PHP;

$strictBody = <<<'PHP'
if (is_callable($target) && !is_array($target)) {
$reflection = new \ReflectionFunction($target);
$instance = null;
} elseif (is_array($target) && count($target) === 2) {
[$controller, $method] = $target;
$instance = is_object($controller) ? $controller : $this->get($controller);
$reflection = new \ReflectionMethod($instance, $method);
} else {
$instance = is_object($target) ? $target : $this->get($target);
$reflection = new \ReflectionMethod($instance, '__invoke');
}

$params = [];

foreach ($reflection->getParameters() as $param) {
$name = $param->getName();
$type = $param->getType()?->getName();

if (array_key_exists($name, $arguments)) {
$params[] = $arguments[$name];
continue;
}

if ($type && $this->has($type)) {
$params[] = $this->get($type);
continue;
}

if ($param->isDefaultValueAvailable()) {
$params[] = $param->getDefaultValue();
continue;
}

if ($param->allowsNull()) {
$params[] = null;
continue;
}

throw new NotFoundException($name, 'compiled invoke');
}

return $reflection->invokeArgs($instance, $params);
PHP;

$invoke->setBody($this->strictMode ? $strictBody : $lenientBody);
}

private function generateInvokeServiceMethod(ClassType $class): void
{
$method = $class->addMethod('invokeServiceMethod')
->setPrivate()
->setReturnType('mixed')
->setBody(<<<'PHP'
->setBody($this->strictMode
? <<<'PHP'
$compiledMethod = $this->buildCompiledInvokerMethodName($serviceId, $method);

if (method_exists($this, $compiledMethod)) {
return $this->{$compiledMethod}($args);
}

throw new NotFoundException($serviceId, 'compiled invoke');
PHP
: <<<'PHP'
$compiledMethod = $this->buildCompiledInvokerMethodName($serviceId, $method);

if (method_exists($this, $compiledMethod)) {
return $this->{$compiledMethod}($args);
}

return $this->invoke([$serviceId, $method], $args);
PHP);
PHP
);

$method->addParameter('serviceId')->setType('string');
$method->addParameter('method')->setType('string');
Expand Down
10 changes: 8 additions & 2 deletions src/Container/ServiceResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,16 @@ public function __construct(
private readonly ServiceBinderInterface $binder,
private readonly ReflectionCacheInterface $reflectionCache,
private readonly InterceptorRegistryInterface $interceptors,
private readonly ArgumentResolverInterface $argumentResolver
private readonly ArgumentResolverInterface $argumentResolver,
private bool $strictMode = false
) {
}

public function setStrictMode(bool $strict): void
{
$this->strictMode = $strict;
}

/**
* Resolves a service by ID or class name.
*
Expand Down Expand Up @@ -83,7 +89,7 @@ public function resolve(string $id, array $args = []): object

if ($descriptor !== null) {
$instance = $this->resolveFromDescriptor($id, $descriptor, $args);
} elseif (class_exists($id)) {
} elseif (!$this->strictMode && class_exists($id)) {
$instance = $this->resolveUnregistered($id, $args);
} else {
$requestedBy = self::$resolutionStack[count(self::$resolutionStack) - 2] ?? 'unknown';
Expand Down
54 changes: 51 additions & 3 deletions tests/integration/Compiler/ContainerCompilerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,11 @@ class ContainerCompilerTest extends TestCase
* @throws ContainerException
* @throws ReflectionException
*/
private function compileAndLoadContainer(ArgonContainer $container, string $className): ArgonContainer
{
private function compileAndLoadContainer(
ArgonContainer $container,
string $className,
?bool $strictMode = null
): ArgonContainer {
$namespace = 'Tests\\Integration\\Compiler';
$file = __DIR__ . "/../../resources/cache/{$className}.php";

Expand All @@ -56,7 +59,7 @@ private function compileAndLoadContainer(ArgonContainer $container, string $clas
}

$compiler = new ContainerCompiler($container);
$compiler->compile($file, $className, $namespace);
$compiler->compile($file, $className, $namespace, $strictMode);

require_once $file;

Expand Down Expand Up @@ -488,6 +491,51 @@ public function testCompiledInterceptorsResolveDependenciesViaContainer(): void
$this->assertTrue($logger->intercepted);
}

/**
* @throws ContainerException
* @throws NotFoundException
* @throws ReflectionException
*/
public function testStrictCompiledContainerDisallowsUnregisteredServices(): void
{
$container = new ArgonContainer(strictMode: true);

$compiled = $this->compileAndLoadContainer($container, 'StrictDisallowsAutowiring', strictMode: true);

$this->expectException(NotFoundException::class);
$compiled->get(Logger::class);
}

/**
* @throws ContainerException
* @throws NotFoundException
* @throws ReflectionException
*/
public function testStrictCompiledContainerResolvesRegisteredServices(): void
{
$container = new ArgonContainer(strictMode: true);
$container->set(Logger::class);

$compiled = $this->compileAndLoadContainer($container, 'StrictResolvesRegistered', strictMode: true);

$this->assertInstanceOf(Logger::class, $compiled->get(Logger::class));
}

/**
* @throws ContainerException
* @throws NotFoundException
* @throws ReflectionException
*/
public function testStrictCompiledInvokeThrowsForMissingDependency(): void
{
$container = new ArgonContainer(strictMode: true);

$compiled = $this->compileAndLoadContainer($container, 'StrictInvokeFails', strictMode: true);

$this->expectException(NotFoundException::class);
$compiled->invoke([ServiceWithDependency::class, 'doSomething']);
}

/**
* @throws ContainerException
* @throws NotFoundException
Expand Down
Loading