Skip to content

Commit

Permalink
Merge pull request #14945 from cakephp/feature-dic
Browse files Browse the repository at this point in the history
Add a dependency injection container
  • Loading branch information
othercorey committed Oct 1, 2020
2 parents 6b06d83 + 957901c commit 6b8ea04
Show file tree
Hide file tree
Showing 23 changed files with 784 additions and 24 deletions.
1 change: 1 addition & 0 deletions composer.json
Expand Up @@ -36,6 +36,7 @@
"composer/ca-bundle": "^1.2",
"laminas/laminas-diactoros": "^2.2.2",
"laminas/laminas-httphandlerrunner": "^1.1",
"league/container": "^3.0",
"psr/http-client": "^1.0",
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0",
Expand Down
4 changes: 4 additions & 0 deletions src/Console/BaseCommand.php
Expand Up @@ -242,6 +242,10 @@ public function abort(int $code = self::CODE_ERROR): void
/**
* Execute another command with the provided set of arguments.
*
* If you are using a string command name, that command's dependencies
* will not be resolved with the application container. Instead you will
* need to pass the command as an object with all of its dependencies.
*
* @param string|\Cake\Console\CommandInterface $command The command class name or command instance.
* @param array $args The arguments to invoke the command with.
* @param \Cake\Console\ConsoleIo $io The ConsoleIo instance to use for the executed command.
Expand Down
23 changes: 22 additions & 1 deletion src/Console/CommandFactory.php
Expand Up @@ -14,6 +14,7 @@
*/
namespace Cake\Console;

use Cake\Core\ContainerInterface;
use InvalidArgumentException;

/**
Expand All @@ -24,12 +25,32 @@
*/
class CommandFactory implements CommandFactoryInterface
{
/**
* @var \Cake\Core\ContainerInterface|null
*/
protected $container;

/**
* Constructor
*
* @param \Cake\Core\ContainerInterface|null $container The container to use if available.
*/
public function __construct(?ContainerInterface $container = null)
{
$this->container = $container;
}

/**
* @inheritDoc
*/
public function create(string $className)
{
$command = new $className();
if ($this->container && $this->container->has($className)) {
$command = $this->container->get($className);
} else {
$command = new $className();
}

if (!($command instanceof CommandInterface) && !($command instanceof Shell)) {
/** @psalm-suppress DeprecatedClass */
$valid = implode('` or `', [Shell::class, CommandInterface::class]);
Expand Down
13 changes: 11 additions & 2 deletions src/Console/CommandRunner.php
Expand Up @@ -21,6 +21,7 @@
use Cake\Console\Exception\MissingOptionException;
use Cake\Console\Exception\StopException;
use Cake\Core\ConsoleApplicationInterface;
use Cake\Core\ContainerApplicationInterface;
use Cake\Core\PluginApplicationInterface;
use Cake\Event\EventDispatcherInterface;
use Cake\Event\EventDispatcherTrait;
Expand Down Expand Up @@ -49,7 +50,7 @@ class CommandRunner implements EventDispatcherInterface
/**
* The application console commands are being run for.
*
* @var \Cake\Console\CommandFactoryInterface
* @var \Cake\Console\CommandFactoryInterface|null
*/
protected $factory;

Expand Down Expand Up @@ -81,7 +82,7 @@ public function __construct(
) {
$this->app = $app;
$this->root = $root;
$this->factory = $factory ?: new CommandFactory();
$this->factory = $factory;
$this->aliases = [
'--version' => 'version',
'--help' => 'help',
Expand Down Expand Up @@ -368,6 +369,14 @@ protected function runShell(Shell $shell, array $argv)
*/
protected function createCommand(string $className, ConsoleIo $io)
{
if (!$this->factory) {
$container = null;
if ($this->app instanceof ContainerApplicationInterface) {
$container = $this->app->getContainer();
}
$this->factory = new CommandFactory($container);
}

$shell = $this->factory->create($className);
if ($shell instanceof Shell) {
$shell->setIo($io);
Expand Down
85 changes: 74 additions & 11 deletions src/Controller/ControllerFactory.php
Expand Up @@ -17,13 +17,17 @@
namespace Cake\Controller;

use Cake\Core\App;
use Cake\Core\ContainerInterface;
use Cake\Http\ControllerFactoryInterface;
use Cake\Http\Exception\MissingControllerException;
use Cake\Http\ServerRequest;
use Cake\Utility\Inflector;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use ReflectionClass;
use ReflectionFunction;
use ReflectionNamedType;

/**
* Factory method for building controllers for request.
Expand All @@ -32,6 +36,21 @@
*/
class ControllerFactory implements ControllerFactoryInterface
{
/**
* @var \Cake\Core\ContainerInterface
*/
protected $container;

/**
* Constructor
*
* @param \Cake\Core\ContainerInterface $container The container to build controllers with.
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}

/**
* Create a controller for a given request.
*
Expand All @@ -43,17 +62,24 @@ public function create(ServerRequestInterface $request): Controller
{
$className = $this->getControllerClass($request);
if ($className === null) {
$this->missingController($request);
throw $this->missingController($request);
}

/** @psalm-suppress PossiblyNullArgument */
$reflection = new ReflectionClass($className);
if ($reflection->isAbstract()) {
$this->missingController($request);
throw $this->missingController($request);
}

/** @var \Cake\Controller\Controller $controller */
$controller = $reflection->newInstance($request);
// If the controller has a container definition
// add the request as a service.
if ($this->container->has($className)) {
$this->container->add(ServerRequest::class, $request);
$controller = $this->container->get($className);
} else {
/** @var \Cake\Controller\Controller $controller */
$controller = $reflection->newInstance($request);
}

return $controller;
}
Expand All @@ -73,9 +99,47 @@ public function invoke($controller): ResponseInterface
if ($result instanceof ResponseInterface) {
return $result;
}

$action = $controller->getAction();
$args = array_values($controller->getRequest()->getParam('pass'));

$args = [];
$reflection = new ReflectionFunction($action);
$passed = array_values((array)$controller->getRequest()->getParam('pass'));
foreach ($reflection->getParameters() as $i => $parameter) {
$position = $parameter->getPosition();

// If there is no type we can't look in the container
// assume the parameter is a passed param
$type = $parameter->getType();
if (!$type) {
$args[$position] = array_shift($passed);
continue;
}
$typeName = $type instanceof ReflectionNamedType ? ltrim($type->getName(), '?') : null;

// Primitive types are passed args as they can't be looked up in the container.
// We only handle strings currently.
if ($typeName === 'string') {
if (count($passed) || !$type->allowsNull()) {
$args[$position] = array_shift($passed);
} else {
$args[$position] = null;
}
continue;
}

// Check the container and parameter default value.
if ($typeName && $this->container->has($typeName)) {
$args[$position] = $this->container->get($typeName);
} elseif ($parameter->isDefaultValueAvailable()) {
$args[$position] = $parameter->getDefaultValue();
}
if (!array_key_exists($position, $args)) {
throw new InvalidArgumentException(
"Could not resolve action argument `{$parameter->getName()}`. " .
'It has no definition in the container, no passed parameter, and no default value.'
);
}
}
$controller->invokeAction($action, $args);

$result = $controller->shutdownProcess();
Expand Down Expand Up @@ -138,7 +202,7 @@ function ($val) {
strpos($controller, '.') !== false ||
$firstChar === strtolower($firstChar)
) {
$this->missingController($request);
throw $this->missingController($request);
}

// phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.InvalidFormat
Expand All @@ -150,12 +214,11 @@ function ($val) {
* Throws an exception when a controller is missing.
*
* @param \Cake\Http\ServerRequest $request The request.
* @throws \Cake\Http\Exception\MissingControllerException
* @return void
* @return \Cake\Http\Exception\MissingControllerException
*/
protected function missingController(ServerRequest $request): void
protected function missingController(ServerRequest $request)
{
throw new MissingControllerException([
return new MissingControllerException([
'class' => $request->getParam('controller'),
'plugin' => $request->getParam('plugin'),
'prefix' => $request->getParam('prefix'),
Expand Down
26 changes: 22 additions & 4 deletions src/Core/BasePlugin.php
Expand Up @@ -37,11 +37,11 @@ class BasePlugin implements PluginInterface
protected $bootstrapEnabled = true;

/**
* Load routes or not
* Console middleware
*
* @var bool
*/
protected $routesEnabled = true;
protected $consoleEnabled = true;

/**
* Enable middleware
Expand All @@ -51,11 +51,18 @@ class BasePlugin implements PluginInterface
protected $middlewareEnabled = true;

/**
* Console middleware
* Register container services
*
* @var bool
*/
protected $consoleEnabled = true;
protected $registerEnabled = true;

/**
* Load routes or not
*
* @var bool
*/
protected $routesEnabled = true;

/**
* The path to this plugin.
Expand Down Expand Up @@ -281,4 +288,15 @@ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
return $middlewareQueue;
}

/**
* Register container services for this plugin.
*
* @param \Cake\Core\ContainerInterface $container The container to add services to.
* @return \Cake\Core\ContainerInterface The updated container
*/
public function register(ContainerInterface $container): ContainerInterface
{
return $container;
}
}
31 changes: 31 additions & 0 deletions src/Core/Container.php
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);

/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;

use League\Container\Container as LeagueContainer;

/**
* Dependency Injection container
*
* Based on the container out of League\Container
*
* @experimental This class' interface is not stable and may change
* in future minor releases.
*/
class Container extends LeagueContainer implements ContainerInterface
{
}
48 changes: 48 additions & 0 deletions src/Core/ContainerApplicationInterface.php
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);

/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;

/**
* Interface for applications that configure and use a dependency injection container.
*
* @experimental This interface is not final and can have additional
* methods and parameters added in future minor releases.
*/
interface ContainerApplicationInterface
{
/**
* Register services to the container
*
* Registered services can have instances fetched out of the container
* using `get()`. Dependencies and parameters will be resolved based
* on service definitions.
*
* @param \Cake\Core\ContainerInterface $container The container to add services to
* @return \Cake\Core\ContainerInterface The updated container.
*/
public function register(ContainerInterface $container): ContainerInterface;

/**
* Create a new container and register services.
*
* This will `register()` services provided by both the application
* and any plugins if the application has plugin support.
*
* @return \Cake\Core\ContainerInterface A populated container
*/
public function getContainer(): ContainerInterface;
}

0 comments on commit 6b8ea04

Please sign in to comment.