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.
Related
Background. I am using Slim where an ID is either in the endpoint or parameters. Based on the ID, a factory creates the appropriate object to perform the needed action.
I have a service which needs some data obtained in the request injected into it. I could therefore do the following:
//index.php
require '../vendor/autoload.php';
use Pimple\Container;
$container = new Container();
class SomeService
{
private $dataFromTheRequest;
public function __construct($dataFromTheRequest){
$this->dataFromTheRequest=$dataFromTheRequest;
echo($dataFromTheRequest);
}
}
$dataFromTheRequest='stuff1';
$container['someService1'] = function ($c) use($dataFromTheRequest) {
return new SomeService($dataFromTheRequest);
};
$someService=$container['someService1'];
But the service is not used in index.php where the service is defined but in another class, so i can do the following:
class SomeOtherClass1
{
public function someMethod($dataFromTheRequest){
$someService=new SomeService($dataFromTheRequest);
}
}
$someOtherClass1=new SomeOtherClass1();
$someOtherClass1->someMethod('stuff2');
But I want to use the instance assigned in index.php, so I can do the following:
$container['someService2'] = function ($c) {
return new SomeService($c['dataFromTheRequest']);
};
class SomeOtherClass2
{
public function __construct($container){
$this->container=$container;
}
public function someMethod($dataFromTheRequest){
$this->container['dataFromTheRequest']='stuff3';
$someService=$this->container['someService2'];
}
}
$someOtherClass2=new SomeOtherClass2($container);
$someOtherClass2->someMethod();
But using the container to pass data just seems wrong.
How should data be injected in a pimple service if that data is not known when the service is defined?
I'm trying to understand dependency injection, and I in theory, I get it, but, I wanted to make an example just to help me. However, I'm getting the following error
PHP Fatal error: Uncaught ArgumentCountError: Too few arguments to function Main\Services\UserService::__construct(), 0 passed
in ...
Here's my "main" file, I've called it index.php
<?php
#index.php
require_once 'vendor/autoload.php';
use Main\Controllers\UserController;
use Main\Services\UserService;
use Main\Models\UserModel;
use Pimple\Container;
$container = new Container;
$container['UserModel'] = function($c) {
return new UserModel();
};
$container['UserService'] = function ($c) {
return new UserService($c['UserModel']);
};
$container['UserController'] = function ($c) {
echo "creating a new UserController\n";
$aUserService = $c['UserService'];
return new UserController($aUserService);
};
$myUserService = new $container['UserService'];
$myResult = $myUserService->parseGet();
echo $myResult, PHP_EOL;
Here's the model that's being passed into the service
<?php
# Models/UserModel.php
namespace Main\Models;
class UserModel
{
private $record;
public function getRecord()
{
return [
'first_name' => 'Bob',
'last_name' => 'Jones',
'email' => 'bj#example.com',
'date_joined' => '11-12-2014',
];
}
}
And, here's the service, which takes the model as it's constructor argument
<?php
namespace Main\Services;
use Main\Models\UserModel;
class UserService
{
private $userModel;
public function __construct(UserModel $userModel)
{
echo "verifying that the userModel passed in was a valid UserModel\n";
$this->userModel = $userModel;
print_r($this->userModel->getRecord());
}
public function parseGet()
{
$retVal = $this->userModel->getRecord();
return json_encode($retVal);
}
}
So, in theory, Pimple should be able to instantiate a UserService object. I even verified that the UserModel passed into the UserService class is a valid UserModel object (which it is as is evident that it prints out an array)
What am I missing? Is there something that I have failed to account for?
Oh, here's the composer.json file
{
"require": {
"pimple/pimple": "~3.0"
},
"autoload": {
"psr-4": {
"Main\\" : "./"
}
}
}
I made a gitHub link so the project can be checked out and ran without having to copy past everything (https://github.com/gitKearney/pimple-example)
SOLUTION
The problem was I have an extra new in the line
$myUserService = new $container['UserService'];
It was so obvious, I couldn't see it
$container['UserService'] already is a UserService object. check yer service definition:
$container['UserService'] = function ($c) {
return new UserService($c['UserModel']);
};
That sets $container['UserService'] to be return new UserService($c['UserModel']) when it's invoked, right?
Your code is basically going:
$o1 = new UserService($c['UserModel']);
$o2 = new $o2;
You use dependency injection containers to free yourself form the pain of manipulating objects' dependencies. Creating a new UserService is not necessary (if it really is a service). In that case, you define it in your $container once, and use it whenever you need to.
So instead of creating a new UserService object and calling its method parseGet() (what you did in your code) you can do something like this:
$myResult = $container['UserService']->parseGet();
When you define something like:
$container['UserService'] = function ($c) {
return new UserService($c['UserModel']);
};
you are telling Pimple how to handle creation of a UserService once you try to access $container['UserService']
That's why you define dependencies as functions.
This might be related to your question Why use a closure for assignment instead of directly assigning a value to a key?
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'm currently working on my first Silex (2.0) project. I have some Pheasant models defined, which I can access from within my controller:
// it is used both static
$p = \Model\Post::oneById(3);
// .. and with an instance
$p = new \Model\Post;
$p->title = 'foobar';
$p->save();
Now, in some cases I'd like to access the Application class within my model. For example, to check if we're running in debug mode (or not). Right now:
public function beforeSave() {
global $app;
if($app['debug']) {
// ...
}
}
But that doesn't feel like Silex. So I figured I need some kind of thing that'll automatically inject the $app class to my models:
class PheasantModelReflector {
protected $app;
public function __construct(\Silex\Application $app) {
$this->app = $app;
}
public function __get($className) {
$r = (new ReflectionClass(sprintf('Model\%s', $className)))->newInstance();
$r->__invoke($this->app);
return $r;
}
}
$app['model'] = function ($app) {
return new PheasantModelReflector($app);
};
This works to a certain level, but always returns a new instance of the Post model when called like $app['model']->Post.
Is there any way to fix this?
Without having worked with Pheasant, I may try to create a service factory that injects the $app (or the $debug var) into your entity class like so (one problem here is that your entity must extend \Pheasant\DomainObject and you can't override the constructor as it's marked as final):
<?php
// in the example below I inject the whole $app, if you only need the
// debug var, inject only that, injecting the whole $app may
// tempt you to use it as a service locator
// creating a new instance
$app['model_post_factory'] = $app->factory(function ($app) {
$post = new \Model\Post();
$post->setApp($app);
return $post;
});
$post = $app['model_post_factory'];
// getting an instance by ID, here it gets a little tricky
$app['model_post_static_factory'] = function($app, $id) {
$post = \Model\Post::oneById($id);
$post->setApp($app);
return $post;
}
$postById = $app->raw('model_post_static_factory');
$post = $postById($app, $id); // $id comes from somewhere else
// depending on the PHP version you may try directly:
// $post = $app->raw('model_post_static_factory')($app, $id);
The problem with the static method is passing the id parameter. You can import into the factory scope the $id from the outside scope using the use keyword, but IMHO this is too magic (though I don't quite like the alternative either). There may be some more elegant way, but I can't think of right now.
To avoid to generate a new instance of model everytime you need to create a shared service (http://silex.sensiolabs.org/doc/services.html).
For example:
$app['model'] = $app->share(function () {
return new PheasantModelReflector();
});
Anyway inject $app is not necessarily a good idea, maybe you can just inject the dependency every object has? Something like:
new \Model\Post($app['debug']);
Originally, my Slim Framework app had the classic structure
(index.php)
<?php
$app = new \Slim\Slim();
$app->get('/hello/:name', function ($name) {
echo "Hello, $name";
});
$app->run();
But as I added more routes and groups of routes, I moved to a controller based approach:
index.php
<?php
$app = new \Slim\Slim();
$app->get('/hello/:name', 'HelloController::hello');
$app->run();
HelloController.php
<?php
class HelloController {
public static function hello($name) {
echo "Hello, $name";
}
}
This works, and it had been helpful to organize my app structure, while at the same time lets me build unit tests for each controler method.
However, I'm not sure this is the right way. I feel like I'm mocking Silex's mount method on a sui generis basis, and that can't be good. Using the $app context inside each Controller method requires me to use \Slim\Slim::getInstance(), which seems less efficient than just using $app like a closure can.
So... is there a solution allowing for both efficiency and order, or does efficiency come at the cost of route/closure nightmare?
I guess I can share what I did with you guys. I noticed that every route method in Slim\Slim at some point called the method mapRoute
(I changed the indentation of the official source code for clarity)
Slim.php
protected function mapRoute($args)
{
$pattern = array_shift($args);
$callable = array_pop($args);
$route = new \Slim\Route(
$pattern,
$callable,
$this->settings['routes.case_sensitive']
);
$this->router->map($route);
if (count($args) > 0) {
$route->setMiddleware($args);
}
return $route;
}
In turn, the Slim\Route constructor called setCallable
Route.php
public function setCallable($callable)
{
$matches = [];
$app = $this->app;
if (
is_string($callable) &&
preg_match(
'!^([^\:]+)\:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!',
$callable,
$matches
)
) {
$class = $matches[1];
$method = $matches[2];
$callable = function () use ($class, $method) {
static $obj = null;
if ($obj === null) {
$obj = new $class;
}
return call_user_func_array([$obj, $method], func_get_args());
};
}
if (!is_callable($callable)) {
throw new \InvalidArgumentException('Route callable must be callable');
}
$this->callable = $callable;
}
Which is basically
If $callable is a string and (mind the single colon) has the format ClassName:method then it's non static, so Slim will instantiate the class and then call the method on it.
If it's not callable, then throw an exception (reasonable enough)
Otherwise, whatever it is (ClassName::staticMethod, closure, function name) it will be used as-is.
ClassName should be the FQCN, so it's more like \MyProject\Controllers\ClassName.
The point where the controller (or whatever) is instantiated was a good opportunity to inject the App instance. So, for starters, I overrode mapRoute to inject the app instance to it:
\Util\MySlim
protected function mapRoute($args)
{
$pattern = array_shift($args);
$callable = array_pop($args);
$route = new \Util\MyRoute(
$this, // <-- now my routes have a reference to the App
$pattern,
$callable,
$this->settings['routes.case_sensitive']
);
$this->router->map($route);
if (count($args) > 0) {
$route->setMiddleware($args);
}
return $route;
}
So basically \Util\MyRoute is \Slim\Route with an extra parameter in its constructor that I store as $this->app
At this point, getCallable can inject the app into every controller that needs to be instantiated
\Util\MyRoute.php
public function setCallable($callable)
{
$matches = [];
$app = $this->app;
if (
is_string($callable) &&
preg_match(
'!^([^\:]+)\:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!',
$callable,
$matches
)
) {
$class = $matches[1];
$method = $matches[2];
$callable = function () use ($app, $class, $method) {
static $obj = null;
if ($obj === null) {
$obj = new $class($app); // <--- now they have the App too!!
}
return call_user_func_array([$obj, $method], func_get_args());
};
}
if (!is_callable($callable)) {
throw new \InvalidArgumentException('Route callable must be callable');
}
$this->callable = $callable;
}
So there it is. Using this two classes I can have $app injected into whatever Controller I declare on the route, as long as I use a single colon to separate controller from method. Using paamayim nekudotayim will call the method as static and therefore will throw an error if I try to access $this->app inside it.
I ran tests using blackfire.io and... the performance gain is negligible.
Pros:
this saves me the pain of calling $app = \Slim\Slim::getInstance() on every static method call accounting for about 100 lines of text overall.
it opens the way for further optimization by making every controller inherit from an abstract controller class, which in turn wraps the app methods into convenience methods.
it made me understand Slim's request and response lifecycle a little better.
Cons:
performance gains are negligible
you have to convert all your routes to use a single colon instead of paamayin, and all your controller methods from static to dynamic.
inheritance from Slim base classes might break when they roll out v 3.0.0
Epilogue: (4 years later)
In Slim v3 they removed the static accessor. In turn, the controllers are instantiated with the app's container, if you use the same convention FQCN\ClassName:method. Also, the method receives the request, response and $args from the route. Such DI, much IoC. I like it a lot.
Looking back on my approach for Slim 2, it broke the most basic principle of drop in replacement (Liskov Substitution).
class Route extends \Slim\Route
{
protected $app;
public function __construct($app, $pattern, $callable, $caseSensitive = true) {
...
}
}
It should have been
class Route extends \Slim\Route
{
protected $app;
public function __construct($pattern, $callable, $caseSensitive = true, $app = null) {
...
}
}
So it wouldn't break the contract and could be used transparently.