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
30 changes: 23 additions & 7 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ private function loadParameter(\ReflectionParameter $parameter, int $depth, bool

// skip type checks and allow all values if expected type is undefined or mixed (PHP 8+)
// allow null values if parameter is marked nullable or untyped or mixed
if ($type === null || ($value === null && $parameter->allowsNull()) || ($type instanceof \ReflectionNamedType && $type->getName() === 'mixed') || $this->validateType($value, $type)) {
if ($type === null || ($value === null && $parameter->allowsNull()) || ($type instanceof \ReflectionNamedType && $type->getName() === 'mixed') || $this->validateType($value, $type, $parameter->getDeclaringClass())) {
return $value;
}

Expand All @@ -355,23 +355,31 @@ private function loadParameter(\ReflectionParameter $parameter, int $depth, bool
);
}

// get class type (if any), resolve `self` and `parent` types against the parameter's declaring class
$class = $type instanceof \ReflectionNamedType && !$type->isBuiltin() ? $type->getName() : null;
if ($class === 'self' && ($declaring = $parameter->getDeclaringClass()) !== null) {
$class = $declaring->getName();
} elseif ($class === 'parent' && ($declaring = $parameter->getDeclaringClass()) !== null && ($parent = $declaring->getParentClass()) !== false) {
$class = $parent->getName();
}

// use default argument if not loadable as container variable or by type
if (
$parameter->isDefaultValueAvailable() &&
(!$type instanceof \ReflectionNamedType || $type->isBuiltin() || !\array_key_exists($type->getName(), $this->container))
($class === null || !\array_key_exists($class, $this->container))
) {
return $parameter->getDefaultValue();
}

// abort if required container variable is not defined or for any other primitive types (array etc.)
if (!$type instanceof \ReflectionNamedType || $type->isBuiltin()) {
if ($class === null) {
throw new \Error(
self::parameterError($parameter, $for) . ' requires container config' . ($type !== null ? ' with type ' . self::typeName($type) : '') . ', none given'
);
}

// @phpstan-ignore-next-line because `$type->getName()` is a `class-string` by definition
return $this->loadObject($type->getName(), $depth - 1);
// @phpstan-ignore-next-line because `$class` is a `class-string` by definition
return $this->loadObject($class, $depth - 1);
}

private function hasVariable(string $name): bool
Expand Down Expand Up @@ -430,17 +438,18 @@ private function loadVariable(string $name, int $depth = 64) /*: mixed (PHP 8.0+
/**
* @param mixed $value
* @param \ReflectionType $type
* @param ?\ReflectionClass<object> $scope
* @throws void
*/
private function validateType($value, \ReflectionType $type): bool
private function validateType($value, \ReflectionType $type, ?\ReflectionClass $scope): bool
{
// check union types (PHP 8.0+) and intersection types (PHP 8.1+) and DNF types (PHP 8.2+)
if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) { // @codeCoverageIgnoreStart
$early = $type instanceof \ReflectionUnionType;
foreach ($type->getTypes() as $type) {
// return early success if any union type matches
// return early failure if any intersection type doesn't match
if ($this->validateType($value, $type) === $early) {
if ($this->validateType($value, $type, $scope) === $early) {
return $early;
}
}
Expand All @@ -454,6 +463,13 @@ private function validateType($value, \ReflectionType $type): bool
// nullable types and mixed already handled before entering this check
\assert($type !== 'null' && $type !== 'mixed');

// resolve `self` and `parent` against the given class scope (e.g. as part of a union type)
if ($type === 'self' && $scope !== null) {
$type = $scope->getName();
} elseif ($type === 'parent' && $scope !== null && ($parent = $scope->getParentClass()) !== false) {
$type = $parent->getName();
}

return (
(\is_object($value) && ($value instanceof $type || $type === 'object')) || // instanceof or object for PHP 7.2+
(\is_string($value) && $type === 'string') ||
Expand Down
115 changes: 100 additions & 15 deletions tests/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1734,21 +1734,6 @@ public function testCallableReturnsCallableThatThrowsWhenFactoryReturnsClassName
$callable($request);
}

public function testCallableReturnsCallableThatThrowsWhenFactoryRequiresInvalidClassName(): void
{
$request = new ServerRequest('GET', 'http://example.com/');

$container = new Container([
\stdClass::class => function (self $instance) { return $instance; }
]);

$callable = $container->callable(\stdClass::class);

$this->expectException(\Error::class);
$this->expectExceptionMessage('Class self not found');
$callable($request);
}

public function testCallableReturnsCallableThatThrowsWhenFactoryRequiresUntypedArgument(): void
{
$request = new ServerRequest('GET', 'http://example.com/');
Expand Down Expand Up @@ -2831,6 +2816,106 @@ public function testGetObjectReturnsDefaultStdclassInstanceWhenFactoryFunctionUs
$this->assertInstanceOf(\stdClass::class, $ret);
}

public function testGetObjectReturnsDefaultInstanceWhenFactoryFunctionHasSelfArgument(): void
{
$controller = new class {
public static function factory(): \Closure
{
return function (self $instance): self { return $instance; };
}
};

$container = new Container([
get_class($controller) => $controller::factory()
]);

$ret = $container->getObject(get_class($controller));

$this->assertInstanceOf(get_class($controller), $ret);
}

public function testGetObjectReturnsDefaultStdclassInstanceWhenFactoryFunctionHasParentArgument(): void
{
$controller = new class extends \stdClass {
public static function factory(): \Closure
{
return function (parent $instance) { return $instance; };
}
};

$container = new Container([
\stdClass::class => $controller::factory()
]);

$ret = $container->getObject(\stdClass::class);

$this->assertEquals(new \stdClass(), $ret);
}

public function testGetObjectReturnsInstanceFromConfigWhenFactoryFunctionHasSelfArgument(): void
{
$controller = new class {
public static function factory(): \Closure
{
return function (self $instance): self { return $instance; };
}
};

$container = new Container([
get_class($controller) => $controller::factory(),
'instance' => $controller
]);

$ret = $container->getObject(get_class($controller));

$this->assertSame($controller, $ret);
}

public function testGetObjectReturnsStdclassInstanceFromConfigWhenFactoryFunctionHasParentArgument(): void
{
$controller = new class extends \stdClass {
public static function factory(): \Closure
{
return function (parent $instance) { return $instance; };
}
};

$instance = new \stdClass();
$container = new Container([
\stdClass::class => $controller::factory(),
'instance' => $instance
]);

$ret = $container->getObject(\stdClass::class);

$this->assertSame($instance, $ret);
}

/**
* @requires PHP 8
*/
public function testGetObjectReturnsInstanceFromConfigWhenFactoryFunctionHasSelfUnionArgument(): void
{
$controller = new class {
public static function factory(): \Closure
{
$fn = function () { }; // PHP < 8.0 workaround ignoring assignment on next line
$fn = #[PHP8] function (self|\stdClass $instance) { return $instance; };
$fn = $fn;
return $fn;
}
};

$container = new Container([
get_class($controller) => $controller::factory(),
'instance' => $controller
]);

$ret = $container->getObject(get_class($controller));

$this->assertSame($controller, $ret);
}

public function testGetObjectReturnsSelfContainerByDefault(): void
{
$container = new Container([]);
Expand Down