I have successfuly installed Router using PHP-DI.
There is some simple app example based on the article.
There are 2 files: index.php and controller.php. I want to use $container from controller. However, I have no idea how to inject it?
// index.php
use...
require_once...
$containerBuilder = new ContainerBuilder();
....
$container = $containerBuilder->build(); // I succesfuilly build a container here with all needed definitions, including Database, Classes and so on.
$routes = simpleDispatcher(function (RouteCollector $r) {
$r->get('/hello', Controller::class);
});
$middlewareQueue[] = new FastRoute($routes);
$middlewareQueue[] = new RequestHandler($container);
$requestHandler = new Relay($middlewareQueue);
$response = $requestHandler->handle(ServerRequestFactory::fromGlobals());
$emitter = new SapiEmitter();
return $emitter->emit($response);
So the code just receive Response from the dispatcher and pass it off to the emitter.
namespace ExampleApp;
use Psr\Http\Message\ResponseInterface;
class Controller
{
private $foo;
private $response;
public function __construct(
string $foo,
ResponseInterface $response
) {
$this->foo = $foo;
$this->response = $response;
}
public function __invoke(): ResponseInterface
{
$response = $this->response->withHeader('Content-Type', 'text/html');
$response->getBody()
->write("<html><head></head><body>Hello, {$this->foo} world!</body></html>");
return $response;
}
}
Now I want to add logic into Controller based on my $container: database, logger and so on. I want somehow to use $container instance which was created in index.php. I have tried a lot of ways, but nothing works proper.
Related
Our company is using their framework using the Symfony DependencyInjection Component and Symfony Routing Component (and without Symfony HttpKernel Component).
The request and the response would be created in the container.
There are some compiler pass classes as well, so we have to compile the container. After the container compilation, unused private services would be removed and we don't have access to them anymore(default behavior of Symfony DependencyInjection Component).
We are resolving the arguments in the controllers' functions (action functions) using a compiler pass named ResponsePass.
We decided to cache the container to increase the speed. Container cached the request and response, so the routing system which is based on the request didn't work anymore and the page always was routed to the previous response.
To solve this problem, I tried to create the request and response out of the container.
and resolve the arguments in the controllers' functions(action functions) using different resolvers like DefaultValueResolver or ServiceValueResolver(which should use the container I think to find the matching service; the same way that ResponsePass was doing before).
The problem is by separating them, ServiceValueResolver doesn't have access to the services of the container which are removed after the compilation.
So my question is how to resolve the services injected in a controller::action() as an argument using a container?
Below is the main simplified structure of the system:
new index.php:
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
Kernel.php:
class Kernel
{
public function handle(Request $request): Response
{
$container = Container::getContainer();
$controller = $this->getControllerInstance($request); // returns controller object
$action = $this->getAction($request); // returns action name
$arguments = $this->argumentResolver->getArguments($request, $controller, $action);
// call controller
$response = $controller->$action(...$arguments);
// some code to check the response ...
return $response;
}
// some other functions ...
}
ArgumentResolver.php:
class ArgumentResolver
{
public function getArguments($request, $controller, $action): array
{
$arguments = [];
foreach ($this->getActionArguments($controller, $action) as $argument) {
foreach($this->getDefaultArgumentValueResolvers() as $resolver){
if (!$resolver->supports($request, $controller, $action, $argument)) {
continue;
}
$resolver->resolve($request, $controller, $action, $argument);
if($resolve->foundValue()){
$arguments[] = $resolver->getValue();
continue 2; // resolve the next action's argument
}
}
}
return $arguments;
}
private static function getDefaultArgumentValueResolvers(): iterable
{
return [
new RequestValueResolver(),
new EntityValueResolver(),
new DefaultValueResolver(),
new ServiceValueResolver(),
];
}
// some other functions ...
}
Container.php
class Container extends ContainerBuilder
{
public static function getContainer(): ContainerInterface
{
$file = $this->rootPath . '/core/var/cache/container.php';
$containerConfigCache = new ConfigCache($file, $isDebug);
if ( $containerConfigCache->isFresh())
{
$container = new CachedContainer();
}
else
{
$container = self::buildContainer();
// some code to cache the container
}
return $container;
}
private static function buildContainer(): Container
{
$container = new self();
// some code to add compiler passes ...
// $container->addCompilerPass(new ResponsePass()); // Based on the previous version, this pass used to create the response
// some code to set other container configs ...
$container->compile();
return $container;
}
// prevents the container to be called directly
public function get($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE): ?object
{
if (strtolower($id) == 'service_container') {
if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $invalidBehavior) {
return null;
}
throw new ServiceNotFoundException($id);
}
return parent::get($id, $invalidBehavior);
}
}
old index.php:
$container = Container::getContainer();
$response = $container->get(Response::class);
ResponsePass.php:
class ResponsePass implements CompilerPassInterface
{
public function process(ContainerBuilder $container):void
{
$this->container = $container;
$this->setPathInfo(); // set the controller and action based on request
$this->resolveActionMethodArguments(); // resolves the action's arguments
$this->setResponse($container);
}
private function setPathInfo():void
{
// some code ...
}
private function resolveActionMethodArguments():void
{
// some code ...
}
private function setResponse(ContainerBuilder $container): void
{
$content = call_user_func_array([$this->container->get($this->controller), $this->action], $this->arguments);
// some code to check $content ...
$definition = new Definition(Response::class);
$definition->addArgument($content)
->setPublic(true)
;
$container->setDefinition(Response::class, $definition);
}
// some other functions ...
}
I try to work with a simple Router class (learning basics before a framework, but I think I got something wrong with the example router I used. Below is a very small router class I got from a colleague and I tried to integrate it into my code to substitute previous uses where I just used echo before (commented out part of the code).
both loginController showLoggedInUser() and registerController index() are just used to render an html template.
Both $router->add() would work if I use it just to add a single route, however my router does not save multiple routes in the array because it seems every route will be saved under the key '/' and in case I provide mutiple routes it seems my previous routes are simply overwritten. So I guess I would need to adjust the Router class. How can I fix this?
PHP 7.4 used
Router.php
<?php
declare(strict_types=1);
class Router
{
private array $route;
public function add(string $url, callable $method): void
{
$this->route[$url] = $method;
}
public function run()
{
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if(!array_key_exists($path, $this->route))
{
exit();
}
return call_user_func($this->route[$path]);
}
}
index.php
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
session_start();
$router = new Router();
$mysqliConnection = new MysqliConnection();
$session = new SessionService();
$loginController = new Login($mysqliConnection);
$router->add('/', [$loginController, 'showLoggedInUser']);
//echo $loginController->showLoggedInUser();
$registerController = new Register($mysqliConnection);
$router->add('/', [$registerController, 'index']);
//echo $registerController->index();
echo $router->run();
Not sure of the overall principle of having two routes with the same name, but you could achieve this using a list of callables for each route.
I've made some changes (including the callable passed for each route) to show the principle, but you should get the idea...
class Router
{
private array $route;
public function add(string $url, callable $method): void
{
$this->route[$url][] = $method;
}
public function run()
{
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if(!array_key_exists($path, $this->route))
{
exit();
}
foreach ( $this->route[$path] as $paths ) {
$paths();
}
// Not sure what to return in this case.
// return call_user_func($this->route[$path]);
}
}
$router = new Router();
// $mysqliConnection = new MysqliConnection();
// $session = new SessionService();
// $loginController = new Login($mysqliConnection);
$router->add('/', function () { echo "login"; } );
// $registerController = new Register($mysqliConnection);
$router->add('/', function () { echo "Register"; });
echo $router->run();
I would instead recommend having separate url's, /login and /register so that they can be called separately.
My Setup is like this:
I request my settings from this file and store them in the settings variable.
$settings = require __DIR__ . '/settings.php';
Next create a new Slim instance like so:
$app = new \Slim\App($settings);
$container = $app->getContainer();
$container['logger'] = function($c) {
$settings = $c->get('settings')['logger'];
$logger = new \Monolog\Logger($settings['name']);
$file_handler = new \Monolog\Handler\StreamHandler($settings['path']);
$logger->pushHandler($file_handler);
return $logger;
};
Then i am calling my route:
$this->get('/testlogger, __testReq::class . ':test);
The above route calls the "test" method inside of my class. Which gets loaded over autoload. Below my class (controller) in which i am trying to access the container like explained on Slim Website.
class __testReq {
function test($request, $response){
//According to Documentation i am supposed to be able to call logger like so:
$this->logger->addInfo("YEY! I am logging...");
}
}
Why is it not working?
From Slim documentation (Documentation uses HomeController class as example):
Slim first looks for an entry of HomeController in the container, if it’s found it will use that instance otherwise it will call it’s constructor with the container as the first argument.
So in your class __testReq constructor, you need to set up the object:
class __testReq {
// logger instance
protected $logger;
// Use container to set up our newly created instance of __testReq
function __construct($container) {
$this->logger= $container->get('logger');
}
function test($request, $response){
// Now we can use $this->logger that we set up in constructor
$this->logger->addInfo("YEY! I am logging...");
}
}
I'm new to SLIM3 and followed the tutorial to get some functions in the container that I want to access from anywhere in the code. So here's my index.php file where I initalise everything:
<?php
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;
// Require for loading the vendor libraries installed by composer
require 'vendor/autoload.php';
$config['displayErrorDetails'] = true;
$config['addContentLengthHeader'] = false;
$app = new \Slim\App(["settings" => $config]);
$container = $app->getContainer();
// Monolog initalisation. To use it write: $this->logger->addInfo("what you want to write here");
$container['logger'] = function($c) {
$logger = new \Monolog\Logger('eq_logger');
$file_handler = new \Monolog\Handler\StreamHandler("logs/app.log");
$logger->pushHandler($file_handler);
return $logger;
};
// Database connections
$container['dbteacher'] = function ($c) {
$pdo = new PDO($_SERVER['PGSQL_CONNECTION_STR']);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
return $pdo;
};
$container['dbagent'] = function ($c) {
$pdo = new PDO($_SERVER['PGSQL_CONNECTION_STR_AGENT']);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
return $pdo;
};
$app->post('/{controller}/{function}', function (Request $request, Response $response) {
$headers = $request->getHeaders();
$params = $request->getParsedBody();
$classname = $request->getAttribute('controller');
$controller = new $classname($this->logger);
$function = $request->getAttribute('function');
$result = $controller->$function($params);
$response->getBody()->write($result);
return $response;
});
$app->run();
Here I can access logger by typing $this->logger, same goes for the dbteacher and dbagent, but I can only do that inside where these containers are created, when I am calling another function from a different class I want to be able to access them as well but I don't want to pass them in the parameter because that will be hard to maintain, I also though about having a config.php class that initalises these containers and the $app variable and extending it in every class I use but that doesn't sound right.
What's the best way to approach this?
You should use the functions of the dependency injection container (Pimple) which Slim3 uses.
That being said, I want to say that dynamically creating "controller"s isn't that nice, that abstraction shouldn't be there and you should just do $response->getBody()->write($result); or the simpler method $response->write($result); in every controller. Also, I don't see why a whole routing framework is needed for this construct.
But anyway if you want to stay with that solution, you can use Pimple, I'll explain that on an example.
You have several classes with different constructor parameter:
class A {
public function __construct($logger) {}
}
class B {
public function __construct($logger, $myHelper) {}
}
Firstly you add them all to the Pimple container:
$container['A'] = function($c) { // or $container[A::class] for type safety
return new A($c['logger']);
};
$container['B'] = function($c) {
return new A($c['logger'], $c['myHelper']);
};
And then you can get them in your route by calling get on the container on the app instance.
$app->post('/{controller}/{function}', function (Request $request, Response $response) {
$headers = $request->getHeaders();
$params = $request->getParsedBody();
$classname = $request->getAttribute('controller');
$controller = $this->getContainer()->get($classname);
$function = $request->getAttribute('function');
$result = $controller->$function($params);
$response->getBody()->write($result);
return $response;
});
I want to make simple template rendering in Slim3 but I get an error:
Here is my code :
namespace controller;
class Hello
{
function __construct() {
// Instantiate the app
$settings = require __DIR__ . '/../../src/settings.php';
$this->app = new \Slim\App($settings);
}
public function index(){
return $this->app->render('web/pages/hello.phtml'); //LINE20
}
}
This is the error I get :
Message: Method render is not a valid method
The App object doesn't handle any rendering on its own, you'll need a template add-on for that, probably this one based on your template's .phtml extension. Install with composer:
composer require slim/php-view
Then your controller method will do something like this:
$view = new \Slim\Views\PhpRenderer('./web/pages');
return $view->render($response, '/hello.phtml');
You'll eventually want to put the renderer in the dependency injection container instead of creating a new instance in your controller method, but this should get you started.
I handle this by sticking my renderer in the container. Stick this in your main index.php file.
$container = new \Slim\Container($configuration);
$app = new \Slim\App($container);
$container['renderer'] = new \Slim\Views\PhpRenderer("./web/pages");
Then in your Hello class's file.
class Hello
{
protected $container;
public function __construct(\Slim\Container $container) {
$this->container = $container;
}
public function __invoke($request, $response, $args) {
return $this->container->renderer->render($response, '/hello.php', $args);
}
}
To clean up this code, make a base handler that has this render logic encapsulated for you.