Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support null values in container variables for factory functions #183

Merged
merged 1 commit into from
Jul 30, 2022
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
6 changes: 3 additions & 3 deletions docs/best-practices/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,9 @@ $container = new FrameworkX\Container([

Factory functions used in the container configuration map may also reference
variables defined in the container configuration. You may use any object or
scalar value for container variables or factory functions that return any such
value. This can be particularly useful when combining autowiring with some
manual configuration like this:
scalar or `null` value for container variables or factory functions that return
any such value. This can be particularly useful when combining autowiring with
some manual configuration like this:

=== "Scalar values"

Expand Down
31 changes: 18 additions & 13 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
*/
class Container
{
/** @var array<string,object|callable():(object|scalar)|scalar>|ContainerInterface */
/** @var array<string,object|callable():(object|scalar|null)|scalar|null>|ContainerInterface */
private $container;

/** @var array<string,callable():(object|scalar) | object | scalar>|ContainerInterface $loader */
/** @var array<string,callable():(object|scalar|null) | object | scalar | null>|ContainerInterface $loader */
public function __construct($loader = [])
{
if (!\is_array($loader) && !$loader instanceof ContainerInterface) {
Expand All @@ -24,7 +24,7 @@ public function __construct($loader = [])

foreach (($loader instanceof ContainerInterface ? [] : $loader) as $name => $value) {
if (
(!\is_object($value) && !\is_scalar($value)) ||
(!\is_object($value) && !\is_scalar($value) && $value !== null) ||
(!$value instanceof $name && !$value instanceof \Closure && !\is_string($value) && \strpos($name, '\\') !== false)
) {
throw new \BadMethodCallException('Map for ' . $name . ' contains unexpected ' . (is_object($value) ? get_class($value) : gettype($value)));
Expand Down Expand Up @@ -125,7 +125,7 @@ public function getErrorHandler(): ErrorHandler
*/
private function loadObject(string $name, int $depth = 64) /*: object (PHP 7.2+) */
{
if (isset($this->container[$name])) {
if (\array_key_exists($name, $this->container)) {
if (\is_string($this->container[$name])) {
if ($depth < 1) {
throw new \BadMethodCallException('Factory for ' . $name . ' is recursive');
Expand Down Expand Up @@ -222,8 +222,8 @@ private function loadParameter(\ReflectionParameter $parameter, int $depth, bool

// load container variables if parameter name is known
assert($type === null || $type instanceof \ReflectionNamedType);
if ($allowVariables && isset($this->container[$parameter->getName()])) {
return $this->loadVariable($parameter->getName(), $type === null ? 'mixed' : $type->getName(), $depth);
if ($allowVariables && \array_key_exists($parameter->getName(), $this->container)) {
return $this->loadVariable($parameter->getName(), $type === null ? 'mixed' : $type->getName(), $parameter->allowsNull(), $depth);
}

// abort if parameter is untyped and not explicitly defined by container variable
Expand All @@ -237,7 +237,7 @@ private function loadParameter(\ReflectionParameter $parameter, int $depth, bool

// use default/nullable argument if not loadable as container variable or by type
assert($type instanceof \ReflectionNamedType);
if ($hasDefault && ($type->isBuiltin() || !isset($this->container[$type->getName()]))) {
if ($hasDefault && ($type->isBuiltin() || !\array_key_exists($type->getName(), $this->container))) {
return $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
}

Expand All @@ -259,12 +259,12 @@ private function loadParameter(\ReflectionParameter $parameter, int $depth, bool
}

/**
* @return object|string|int|float|bool
* @return object|string|int|float|bool|null
* @throws \BadMethodCallException if $name is not a valid container variable
*/
private function loadVariable(string $name, string $type, int $depth) /*: object|string|int|float|bool (PHP 8.0+) */
private function loadVariable(string $name, string $type, bool $nullable, int $depth) /*: object|string|int|float|bool|null (PHP 8.0+) */
{
assert(isset($this->container[$name]));
assert(\array_key_exists($name, $this->container));
if ($this->container[$name] instanceof \Closure) {
if ($depth < 1) {
throw new \BadMethodCallException('Container variable $' . $name . ' is recursive');
Expand All @@ -277,15 +277,20 @@ private function loadVariable(string $name, string $type, int $depth) /*: object
// invoke factory with list of parameters
$value = $params === [] ? ($this->container[$name])() : ($this->container[$name])(...$params);

if (!\is_object($value) && !\is_scalar($value)) {
throw new \BadMethodCallException('Container variable $' . $name . ' expected type object|scalar from factory, but got ' . \gettype($value));
if (!\is_object($value) && !\is_scalar($value) && $value !== null) {
throw new \BadMethodCallException('Container variable $' . $name . ' expected type object|scalar|null from factory, but got ' . \gettype($value));
}

$this->container[$name] = $value;
}

$value = $this->container[$name];
assert(\is_object($value) || \is_scalar($value));
assert(\is_object($value) || \is_scalar($value) || $value === null);

// allow null values if parameter is marked nullable or untyped or mixed
if ($nullable && $value === null) {
return null;
}

// skip type checks and allow all values if expected type is undefined or mixed (PHP 8+)
if ($type === 'mixed') {
Expand Down
Loading