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
72 changes: 57 additions & 15 deletions docs/best-practices/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,10 @@ covers most common use cases:
* Class names need to be loadable through the autoloader. See
[composer autoloading](#composer-autoloading) above.
* Each class may or may not have a constructor.
* If the constructor has an optional argument, it will be omitted.
* If the constructor has a nullable argument, it will be given a `null` value.
* If the constructor has an optional argument, it will be omitted unless an
explicit [container configuration](#container-configuration) is used.
* If the constructor has a nullable argument, it will be given a `null` value
unless an explicit [container configuration](#container-configuration) is used.
* If the constructor references another class, it will load this class next.

This covers most common use cases where the request handler class uses a
Expand Down Expand Up @@ -290,22 +292,62 @@ 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:

```php title="public/index.php"
<?php
=== "Scalar values"

require __DIR__ . '/../vendor/autoload.php';
```php title="public/index.php"
<?php

$container = new FrameworkX\Container([
Acme\Todo\UserController::class => function (bool $debug, string $hostname) {
// example UserController class uses two container variables
return new Acme\Todo\UserController($debug, $hostname);
},
'debug' => false,
'hostname' => fn(): string => gethostname()
]);
require __DIR__ . '/../vendor/autoload.php';

// …
```
$container = new FrameworkX\Container([
Acme\Todo\UserController::class => function (bool $debug, string $hostname) {
// example UserController class uses two container variables
return new Acme\Todo\UserController($debug, $hostname);
},
'debug' => false,
'hostname' => fn(): string => gethostname()
]);

// …
```

=== "Default values"

```php title="public/index.php"
<?php

require __DIR__ . '/../vendor/autoload.php';

$container = new FrameworkX\Container([
Acme\Todo\UserController::class => function (bool $debug = false) {
// example UserController class uses $debug, apply default if not set
return new Acme\Todo\UserController($debug);
},
'debug' => true
]);


// …
```

=== "Nullable values"

```php title="public/index.php"
<?php

require __DIR__ . '/../vendor/autoload.php';

$container = new FrameworkX\Container([
Acme\Todo\UserController::class => function (?string $name) {
// example UserController class uses $name, defaults to null if not set
return new Acme\Todo\UserController($name ?? 'ACME');
},
'name' => 'Demo'
]);


// …
```

> ℹ️ **Avoiding name collisions**
>
Expand Down
89 changes: 47 additions & 42 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,57 +197,65 @@ private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $
{
$params = [];
foreach ($function->getParameters() as $parameter) {
assert($parameter instanceof \ReflectionParameter);
$params[] = $this->loadParameter($parameter, $depth, $allowVariables);
}

// stop building parameters when encountering first optional parameter
if ($parameter->isOptional()) {
break;
}
return $params;
}

// ensure parameter is typed
$type = $parameter->getType();
if ($type === null) {
throw new \BadMethodCallException(self::parameterError($parameter) . ' has no type');
/**
* @return mixed
* @throws \BadMethodCallException if $parameter can not be loaded
*/
private function loadParameter(\ReflectionParameter $parameter, int $depth, bool $allowVariables) /*: mixed (PHP 8.0+) */
{
// ensure parameter is typed
$type = $parameter->getType();
if ($type === null) {
if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}
throw new \BadMethodCallException(self::parameterError($parameter) . ' has no type');
}

// if allowed, use null value without injecting any instances
assert($type instanceof \ReflectionType);
if ($type->allowsNull()) {
$params[] = null;
continue;
}
$hasDefault = $parameter->isDefaultValueAvailable() || $parameter->allowsNull();

// abort for union types (PHP 8.0+) and intersection types (PHP 8.1+)
if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) {
throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type); // @codeCoverageIgnore
// abort for union types (PHP 8.0+) and intersection types (PHP 8.1+)
if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) { // @codeCoverageIgnoreStart
if ($hasDefault) {
return $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
}
throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type);
} // @codeCoverageIgnoreEnd

assert($type instanceof \ReflectionNamedType);
assert($type instanceof \ReflectionNamedType);

// load variables from container for primitive/scalar types
if ($allowVariables && \in_array($type->getName(), ['string', 'int', 'float', 'bool'])) {
$params[] = $this->loadVariable($parameter->getName(), $type->getName(), $depth);
continue;
}
// load container variables if parameter name is known
if ($allowVariables && isset($this->container[$parameter->getName()])) {
return $this->loadVariable($parameter->getName(), $type->getName(), $depth);
}

// abort for other primitive types (array etc.)
if ($type->isBuiltin()) {
throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type->getName());
}
// use null for nullable arguments if not already loaded above
if ($hasDefault && !isset($this->container[$type->getName()])) {
return $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
}

// abort for unreasonably deep nesting or recursive types
if ($depth < 1) {
throw new \BadMethodCallException(self::parameterError($parameter) . ' is recursive');
}
// abort if required container variable is not defined
if ($allowVariables && \in_array($type->getName(), ['string', 'int', 'float', 'bool'])) {
throw new \BadMethodCallException(self::parameterError($parameter) . ' is not defined');
}

if ($allowVariables && isset($this->container[$parameter->getName()])) {
$params[] = $this->loadVariable($parameter->getName(), $type->getName(), $depth);
} else {
$params[] = $this->loadObject($type->getName(), $depth - 1);
}
// abort for other primitive types (array etc.)
if ($type->isBuiltin()) {
throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type->getName());
}

return $params;
// abort for unreasonably deep nesting or recursive types
if ($depth < 1) {
throw new \BadMethodCallException(self::parameterError($parameter) . ' is recursive');
}

return $this->loadObject($type->getName(), $depth - 1);
}

/**
Expand All @@ -256,10 +264,7 @@ private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $
*/
private function loadVariable(string $name, string $type, int $depth) /*: object|string|int|float|bool (PHP 8.0+) */
{
if (!isset($this->container[$name])) {
throw new \BadMethodCallException('Container variable $' . $name . ' is not defined');
}

assert(isset($this->container[$name]));
if ($this->container[$name] instanceof \Closure) {
if ($depth < 1) {
throw new \BadMethodCallException('Container variable $' . $name . ' is recursive');
Expand Down
Loading