Last active
February 20, 2024 11:37
-
-
Save dewey92/76a08748cee5dd54c9e7fb95164f7f7e to your computer and use it in GitHub Desktop.
Slim 3 controller
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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); | |
// } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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