Pass arguments in Slim DI service - php

I have a service that I want to access from a route but pass arguments to.
$container = new \Slim\Container();
$container['myService'] = function($arg1, $arg2) {
//my code here
};
$app = new \Slim\App($container);
and inside my route, I try to call the service like so:
$this->myService('my arg1', 'my arg2');
This is not working. When I try to call it without specifying the arguments, it works.
How to call with arguments? Or is that an alternative way to specify a function or method to be called from inside a route?

so you are pretty close.
$container = new \Slim\Container();
$container['myService'] = function ($c) {
return function($arg1, $arg2) {
//my code here
}
};
$app = new \Slim\App($container);
$app->get('/', function ($req, $res, $args) {
$this->myService($a, $b);
});
This should work.
Optionally, with your original code... you have to save it to a variable first before invoking it.
$service = $this->myService;
$service('my arg1', 'my arg2');
Both of these should work.

Related

How to inject DI container into a controller?

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.

Best design method to access container functions

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;
});

Slim PHP Framework: can middleware be lazy-loaded?

While building a small Slim app, I noticed that all my middleware instances are getting created for every request. I have four routes in my app, only one of which requires a PDO instance, but all of which create an instance. Another route requires a SoapClient instance, but again, requests to any route result in instances being created. Can I do something differently to avoid that?
Here is a stripped down example showing what I mean:
<?php
require __DIR__ . '/../vendor/autoload.php';
class Middleware {
private $dep;
public function __construct($dep = null) {
$this->dep = $dep;
}
public function __invoke($req, $res) {
$res->getBody()->write($this->dep ? 'Got dependency' : 'No dependency');
return $res;
}
}
$app = new \Slim\App();
$c = $app->getContainer();
$c['pdo'] = function ($c) {
echo "PDO connection here<br/>\n";
return (object) ['pdo' => true];
};
$c['middleware.nodep'] = function ($c) {
return new Middleware();
};
$c['middleware.withdep'] = function ($c) {
return new Middleware($c['pdo']);
};
$app->get('/nopdo', function () {})->add($c['middleware.nodep']);
$app->get('/withpdo', function () {})->add($c['middleware.withdep']);
$app->run();
Requests to /withpdo get the following response, as you would expect:
PDO connection hereGot dependency
Requests to /nopdo get this, which I don't want:
PDO connection here No dependency
So, is there a way to only instantiate the "PDO" instance for requests to /withpdo?
You are calling the factory in add(), rather than just telling Slim about the DIC key.
i.e. change:
->add($c['middleware.withdep']);
to
->add('middleware.withdep');
This way, Slim will ask the DIC for 'middleware.withdep' when it needs it.

PHP Type Hinting is not working

I am trying to use Type Hinting feature in my app but something is not working correctly. I tried the following
define('PULSE_START', microtime(true));
require('../Pulse/Bootstrap/Bootstrap.php');
$app = new Application();
$app->run();
$app->get('/404', function(Application $app)
{
$app->error(404);
});
And instead of 404 output i got this
Catchable fatal error: Argument 1 passed to {closure}() must be an instance of Pulse\Core\Application, none given in E:\Server\xampp\htdocs\web\pulse\WWW\Index.php on line 23
I dont understand it, the Application class is a namespaced class (Pulse\Core\Application) but I have created an Alias, so I dont think thats the issue.
From the fact that none is being given as the passed in type value I'm thinking get isn't passing a parameter when using the closure. To get $app into the closure you can use the application instead.
$app->get('/404', function() use ($app)
{
$app->error(404);
});
And verify that your get method is passing $this as the first argument of the anonymous function.
Typehinting does not work that way - it requires parameter to be of given type, but you have to create code that will adjust parameters passed to the closure for yourself. Very simple implementation of such smart arguments:
class Application{
private $args = array(); //possible arguments for closure
public function __construct(){
$this->args[] = $this; //Application
$this->args[] = new Request;
$this->args[] = new Session;
$this->args[] = new DataBase;
}
public function get($function){
$rf = new ReflectionFunction($function);
$invokeArgs = array();
foreach($rf->getParameters() as $param){
$class = $param->getClass()->getName();
foreach($this->args as $arg) {
if(get_class($arg) == $class) {
$invokeArgs[] = $arg;
break;
}
}
}
return $rf->invokeArgs($invokeArgs);
}
}
$app = new Application();
$app->get(function (Application $app){
var_dump($app);
});

how to pass a parameter to method with php's is_callable

I have to create a variable that is callable with php's is_callable
I have done this:
$callable = array(new MyClass, 'methodName');
But I want to pass a parameter to the method.
How can I do that?
Cause using symfony's event dispatcher component will be like:
$sfEventDispatcher->connect('log.write', array(new IC_Log('logfile.txt'), 'write'));
The first parameter is just a event name, the second is the callable variable.
But I can only call the write method, I want to pass a parameter to it.
Since PHP 5.3 you can use anonymous functions.
You should connect the listener like this:
$dispatcher->connect('my.event', function($event) {
$my_class = new MyClass;
$my_class->myMethod($event, array('my_param' => 'my_value'));
});
Then you will be able to get the parameters array in the listener:
class MyClass {
public function myMethod(sfEvent $event, $parameters) {
$my_value = $parameters['my_param'];
$event_param_value = $event['event_param'];
}
}
Now you can notify the event normally:
$dispatcher->notify(new sfEvent($this, 'my.event', array('event_param' => 'event_param_value')));
Take care that this listener can't be disconnected.
If you need to disconnect it, put the anonymous function in a variable:
$dispatcher->connect('my.event', $my_listener = function($event) {
$my_class = new MyClass;
$my_class->myMethod($event, array('my_param' => 'my_value'));
});
You should be able to disconnect with:
$dispatcher->disconnect('my.event', $my_listener);
You don't pass parameters to your listener callbacks (without extending the core). Symfony will be the one calling it and will pass an event object. If you need additional info, you can create a different method that calls another method where you can control the parameters.
$callable1 = array(new MyWriter, 'write1');
$callable2 = array(new MyWriter, 'write2'); // or $callable2 = array($callable1[0], 'write2');
$sfEventDispatcher->connect('log.write', $callable1);
$sfEventDispatcher->connect('log.write', $callable2);
And your callback class methods can be something like:
class MyWriter
{
public function write($event, $num)
{
// do something
}
public function write1($event)
{
$this->write($event, 1);
}
public function write2($event)
{
$this->write($event, 2);
}
}
Alternatively, you can create properties that act as state that your write function can check:
class MyWriter
{
public $state = 1;
public function write($event)
{
if ($this->state == 1) {
// do this
} else {
// do this instead
}
}
}
This is trickier as you'd have to set state before a pertinent event is triggered which may not prove feasable, depending on the specifics of your situation:
$callable[0]->state = 2;

Categories