Skip to content

Instantly share code, notes, and snippets.

@dewey92
Last active February 20, 2024 11:37
Show Gist options
  • Save dewey92/76a08748cee5dd54c9e7fb95164f7f7e to your computer and use it in GitHub Desktop.
Save dewey92/76a08748cee5dd54c9e7fb95164f7f7e to your computer and use it in GitHub Desktop.
Slim 3 controller
<?php
$c = new \App\Base\Container();
/**
* Register the CallableResolver we just wrote
* to overwrite the default resolving behaviour of the Slim app
*/
$c['callableResolver'] = function ($c) {
return new \App\Base\CallableResolver($c); // See number 04-CallableResolver.php
};
// Rest is up to you
$c['view'] => function($c) {
// Twig setup
// ...
};
// And we don't have to manually register our action/controller like:
// $c[\App\Actions\Auth\LoginAction::class] = function($c) {
// return new \App\Actions\Auth\LoginAction($c->view, $c->router);
// }
<?php
use App\Actions\Auth;
// This will automatically inject the dependencies to constructor
$this->get('/login', Auth\LoginAction::class)->setName('login');
// Optionally, you can also set the method you wanna execute
// and still provide the constructor a bunch of dependencies you already set
$this->get('/login', Auth\LoginAction::class . ':someMethod')->setName('login');
<?php
namespace App\Actions\Auth;
use Slim\Views\Twig;
use Slim\Router;
class LoginAction
{
/**
* @var \Slim\Views\Twig
*/
private $view;
/**
* @var \Slim\Router
*/
private $router;
/**
* @param \Slim\Views\Twig $view
* @param \Slim\Router $router
*/
public function __construct(Twig $view, Router $router)
{
$this->view = $view;
$this->router = $router;
}
public function __invoke() // or public function someMethod(), whatever
{
$httpGet = $this->request->getQueryParams();
$redirectUrl = count($httpGet) ? $httpGet['redirectUrl'] : $this->router->pathFor('home');
$data = [
'login' => true,
'redirectUrl' => $redirectUrl,
];
return $this->view->render($this->response, 'auth/login.twig', $data);
}
}
<?php
/**
* And here the magic happens
*/
namespace App\Base;
use App\Actions\ActionCommandInterface;
use RuntimeException;
use Interop\Container\ContainerInterface;
use Slim\Interfaces\CallableResolverInterface;
/**
* Class CallableResolver
*
* The alternative of the \Slim\CallableResolver
* to auto resolve the dependencies required by
* any class within the app
*
* @package App\Base
* @see https://www.ltconsulting.co.uk/automatic-dependency-injection-with-phps-reflection-api
*/
class CallableResolver implements CallableResolverInterface
{
/**
* @var ContainerInterface
*/
private $container;
/**
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* Resolve toResolve into a closure that that the router can dispatch.
*
* If toResolve is of the format 'class:method', then try to extract 'class'
* from the container otherwise instantiate it and then dispatch 'method'.
*
* @param callable|string $toResolve
*
* @return callable
*
* @throws \RuntimeException if the callable does not exist
* @throws \RuntimeException if the callable is not resolvable
*/
public function resolve($toResolve)
{
$resolved = $toResolve;
if ( ! is_callable($toResolve) && is_string($toResolve)) {
// check for slim callable as "class:method"
$callablePattern = '!^([^\:]+)\:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!';
if (preg_match($callablePattern, $toResolve, $matches)) {
$class = $matches[1];
$method = $matches[2];
if ($this->container->has($class)) {
$resolved = [$this->container->get($class), $method];
} else {
if (!class_exists($class)) {
throw new \RuntimeException(sprintf('Callable %s does not exist', $class));
}
$resolved = [$this->createClassWithHttp($class), $method];
}
} else {
// check if string is something in the DIC that's callable or is a class name which
// has an __invoke() method
$class = $toResolve;
if ($this->container->has($class)) {
$resolved = $this->container->get($class);
} else {
if (!class_exists($class)) {
throw new \RuntimeException(sprintf('Callable %s does not exist', $class));
}
$resolved = $this->createClassWithHttp($class);
if ($resolved instanceof ActionCommandInterface) {
$resolved = [$resolved, 'execute'];
}
}
}
}
if ( ! is_callable($resolved)) {
throw new \RuntimeException(sprintf('%s is not resolvable', $toResolve));
}
return $resolved;
}
/**
* Get a resolvable class with \Slim\Http\Request and \Slim\Http\Response already set.
* This is very useful so you don't have to pass the same awful repetitive
* arguments to each class method
*
* @param string $class
*
* @return mixed
* @throws \Exception
*/
private function createClassWithHttp($class) {
$resolvedClass = $this->resolveClass($class);
// Inject the request and response to the class
$resolvedClass->request = $this->container->get('request');
$resolvedClass->response = $this->container->get('response');
return $resolvedClass;
}
/**
* Build an instance of the given class
*
* @param string $class
* @return mixed
*
* @throws \Exception
*/
public function resolveClass($class)
{
$reflector = new \ReflectionClass($class);
if ( ! $reflector->isInstantiable()) {
throw new \Exception("[$class] is not instantiable");
}
$constructor = $reflector->getConstructor();
if(is_null($constructor)) {
return new $class;
}
$parameters = $constructor->getParameters();
$dependencies = $this->getDependencies($parameters);
return $reflector->newInstanceArgs($dependencies);
}
/**
* Build up a list of dependencies for a given methods parameters
*
* @param array $parameters
* @return array
*/
public function getDependencies(array $parameters)
{
$dependencies = array();
foreach($parameters as $parameter) {
// If the constructor dependency has the same name as in the container key,
// go with it regardless the type hint
// e.g $mailer in constructor will resolve to $app->getContainer()['mailer']
if ($this->container->has($parameter->name)) {
$dependencies[] = $this->container->get($parameter->name);
continue;
}
$dependency = $parameter->getClass();
if (is_null($dependency)) {
$dependencies[] = $this->resolveNonClass($parameter);
}
else {
$dependencies[] = $this->resolveClass($dependency->name);
}
}
return $dependencies;
}
/**
* Determine what to do with a non-class value
*
* @param \ReflectionParameter $parameter
* @return mixed
*
* @throws \Exception
*/
public function resolveNonClass(\ReflectionParameter $parameter)
{
if ($parameter->isDefaultValueAvailable())
{
return $parameter->getDefaultValue();
}
throw new \Exception("Please check the constructor class you want to resolve. " .
"Either provide a type hint or set default value on it " .
"or name your variable with the one in Service Container"
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment