I have a Slim4 Application composed of several modules separated in different routing groups, like so:
$app->group('/app', function(RouteCollectorProxy $app) {
/*blah blah*/
})->add(MyMiddleWare::class);
$app->group('/api', function(RouteCollectorProxy $app) {
/*blah blah*/
})->add(MyMiddleware::class);
$app->group('/admin', function(RouteCollectorProxy $app) {
/*blah blah*/
})->add(MyMiddleware::class);
MyMiddleware receives an Interface
class MyMiddleware
{
public function __construct(IMyInterface $myServiceImplementingInterface) { /*blah blah*/ }
}
When we setup the container, we tell it which class to inject so PHP-DI know which class to construct the middleware with:
/* bootstraping */
$containerBuilder = new ContainerBuilder();
$containerBuilder->addDefinitions(__DIR__ . '/container.php');
$container = $containerBuilder->build();
and
/*container.php*/
return [
IMyInterface::class => function (ContainerInterface $container) {
return new MyServiceImplementingInterface();
},
];
My main question is:
Would it be possible to somehow override the implementation of the container setup for IMyInterface::class based on the Routing Group ? so I could have something like:
Main container setup:
/*container.php*/
return [
IMyInterface::class => function (ContainerInterface $container) {
return new MyServiceImplementingInterface();
},
];
Specific route group container setup:
/*container.admin.php*/
return [
IMyInterface::class => function (ContainerInterface $container) {
return new AnotherServiceImplementingInterface();
},
];
I suggest using two different objects of MyMiddleware class for different groups, each constructed using appropriate implementation of IMyInterface. You can tell PHP-DI to call the constructor with the parameters you want.
Here I created two instances of MyMiddleware, one with the name AdminMiddleware and the other named ApiMiddleware in the container. using DI\create()->constructor() method, I configure the DI to inject different implementations of IMyInterface while building these two objects:
<?php
use DI\ContainerBuilder;
use Slim\Factory\AppFactory;
// this is the path of autoload.php relative to my index.php file
// change it according to your directory structure
require __DIR__ . '/../vendor/autoload.php';
interface IMyInterface {
public function sampleMethod();
}
class MyServiceImplementingInterface implements IMyInterface {
public function sampleMethod() {
return 'This implementation is supposed to be used for API endpoint middleware';
}
}
class AnotherServiceImplementingInterface implements IMyInterface {
public function sampleMethod() {
return 'This implementation is supposed to be used for Admin middleware';
}
}
class MyMiddleware
{
private $service;
public function __construct(IMyInterface $myServiceImplementingInterface) {
$this->service = $myServiceImplementingInterface;
}
public function __invoke($request, $handler)
{
$response = $handler->handle($request);
$response->getBody()->write($this->service->sampleMethod());
return $response;
}
}
$containerBuilder = new ContainerBuilder();
$containerBuilder->addDefinitions([
'AdminMiddleware' => DI\create(MyMiddleware::class)->constructor(DI\get(AnotherServiceImplementingInterface::class)),
'ApiMiddleware' => DI\create(MyMiddleware::class)->constructor(DI\get(MyServiceImplementingInterface::class))
]);
$container = $containerBuilder->build();
AppFactory::setContainer($container);
$app = AppFactory::create();
$app->group('/admin', function($app) {
$app->get('/dashboard', function($request, $response, $args){
return $response;
});
})->add($container->get('AdminMiddleware'));
$app->group('/api', function($app) {
$app->get('/endpoint', function($request, $response, $args){
return $response;
});
})->add($container->get('ApiMiddleware'));
$app->run();
Related
I am trying to use CacheManager class as a dependency in my service:
<?php
declare(strict_types=1);
namespace App;
use GuzzleHttp\Client;
use Illuminate\Cache\CacheManager;
class MatrixWebService implements WebServiceInterface
{
public function __construct(Client $client, CacheManager $cache)
{
$this->client = $client;
$this->cache = $cache;
}
}
Because there can be multiple implementations of WebserviceInterface I need to define it in the AppServiceProvider accordingly:
$this->app->bind(WebServiceInterface::class, function () {
return new MatrixWebService(
new Client(),
$this->app->get(CacheManager::class)
);
});
The problem is when I am trying to use my service, Laravel is not able to resolve CacheManager class (there is some substitution for cache instead):
[2018-07-02 22:45:26] local.ERROR: Class cache does not exist {"exception":"[object] (ReflectionException(code: -1): Class cache does not exist at /var/www/html/vendor/laravel/framework/src/Illuminate/Container/Container.php:769)
[stacktrace]
#0 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/Container.php(769): ReflectionClass->__construct('cache')
#1 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/Container.php(648): Illuminate\\Container\\Container->build('cache')
#2 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/Container.php(610): Illuminate\\Container\\Container->resolve('cache')
#3 /var/www/html/app/Providers/AppServiceProvider.php(28): Illuminate\\Container\\Container->get('Illuminate\\\\Cach...')
...
Any ideas on how to approach this correctly?
When your application has multiple implementations of an interface, contextual binding is useful:
namespace App;
use GuzzleHttp\Client;
use Cache\Repository;
class MatrixWebService implements WebServiceInterface
{
public function __construct(Client $client, Repository $cache)
{
$this->client = $client;
$this->cache = $cache;
}
}
// AppServiceProvider.php
public function register()
{
$this->app->bind(WebServiceInterface::class, MatrixWebServiceInterface::class);
// bind MatrixWebService
$this->app->when(MatrixWebService::class)
->needs(Repository::class)
->give(function () {
return app()->makeWith(CacheManager::class, [
'app' => $this->app
]);
});
// bind some other implementation of WebServiceInterface
$this->app->when(FooService::class)
->needs(Repository::class)
->give(function () {
return new SomeOtherCacheImplementation();
});
}
I am trying to access the $container from my middleware, but i am not getting much luck.
In my index.php file I have
require '../../vendor/autoload.php';
include '../bootstrap.php';
use somename\Middleware\Authentication as Authentication;
$app = new \Slim\App();
$container = $app->getContainer();
$app->add(new Authentication());
And then I have a class Authentication.php like this
namespace somename\Middleware;
class Authentication {
public function __invoke($request, $response, $next) {
$this->logger->addInfo('Hi from Authentication middleware');
but i get an error
Undefined property: somename\Middleware\Authentication::$logger in ***
I have also tried adding the following constructor to the class but I also get no joy.
private $container;
public function __construct($container) {
$this->container = $container;
}
Could anyone help please?
Best Practice to Middleware Implementation is Something like this :
Place this code inside your dependency section :
$app = new \Slim\App();
$container = $app->getContainer();
/** Container will be passed to your function automatically **/
$container['MyAuthenticator'] = function($c) {
return new somename\Middleware\Authentication($c);
};
then inside your Authentication class create constructor function like you mentioned :
namespace somename\Middleware;
class Authentication {
protected $container;
public function __invoke($request, $response, $next)
{
$this->container->logger->addInfo('Hi from Authentication middleware');
}
public function __construct($container) {
$this->container = $container;
}
/** Optional : Add __get magic method to easily use container
dependencies
without using the container name in code
so this code :
$this->container->logger->addInfo('Hi from Authentication middleware');
will be this :
$this->logger->addInfo('Hi from Authentication middleware');
**/
public function __get($property)
{
if ($this->container->{$property}) {
return $this->container->{$property};
}
}
}
After inside your index.php add Middleware using name resolution like this:
$app->add('MyAuthenticator');
I disagree with Ali Kaviani's Answer. When adding this PHP __magic function (__get), the code will be a lot more difficult to test.
All the required dependencies should be specified on the constructor.
The benefit is, that you can easily see what dependencies a class has and therefore only need to mock these classes in unit-tests, otherwise you would've to create a container in every test. Also Keep It Simple Stupid
I'll show that on the logger example:
class Authentication {
private $logger;
public function __construct($logger) {
$this->logger = $logger;
}
public function __invoke($request, $response, $next) {
$this->logger->addInfo('Hi from Authentication middleware');
}
}
Then add the middleware with the logger parameter to the container:
$app = new \Slim\App();
$container = $app->getContainer();
$container['MyAuthenticator'] = function($c) {
return new somename\Middleware\Authentication($c['logger']);
};
Note: the above registration to the container could be done automatically with using PHP-DI Slim (but that should be also slower).
I'm relatively new to Slim Framework 3. One thing I'm trying to understand is how to use the router, $this->router, in a "global" template.
What I mean by this is a template such as a navigation menu - something that appears on every page.
For templates I'm using the "php-view" library as per the example tutorial which I installed with:
composer require slim/php-view
In my templates directory I have a file called nav.php where I want to output my links.
I understand how to call the router like so
Sign Up
But... the example tutorial only shows how you would pass that link from 1 individual place, e.g. $app->get('/sign-up' ... })->setName("sign-up");
How can you use the router globally in any template, without passing it into every individual URL route as a parameter?
I'm more familiar with frameworks like CakePHP where there is an "AppController" which allows you to set things globally, i.e. available in every request. I don't know if this is how it's done in Slim but this is the effect I'm after.
Well, you can pass it as template variable.
When you instantiate or register PhpRenderer in a container, you have multiple options to define a "global" variable, i.e. a variable that is accessible in all of your templates:
// via the constructor
$templateVariables = [
"router" => "Title"
];
$phpView = new PhpRenderer("./path/to/templates", $templateVariables);
// or setter
$phpView->setAttributes($templateVariables);
// or individually
$phpView->addAttribute($key, $value);
Assuming you're registering PhpRenderer via Pimple:
<?php
// Create application instance
$app = new \Slim\App();
// Get container
$container = $app->getContainer();
// Register PhpRenderer in the container
$container['view'] = function ($container) {
// Declaring "global" variables
$templateVariables = [
'router' => $container->get('router')
];
// And passing the array as second argument to the contructor
return new \Slim\Views\PhpRenderer('path/to/templates/with/trailing/slash/', $templateVariables);
};
<?php namespace App\Helpers;
/********************/
//LinksHelper.php
/********************/
use Interop\Container\ContainerInterface;
class LinksHelper
{
protected $ci;
public function __construct(ContainerInterface $container){
$this->ci = $container;
}
public function __get($property){
if ($this->ci->has($property)) {
return $this->ci->get($property);
}
}
public function pathFor($name, $data = [], $queryParams = [], $appName = 'default')
{
return $this->router->pathFor($name, $data, $queryParams);
}
public function baseUrl()
{
if (is_string($this->uri)) {
return $this->uri;
}
if (method_exists($this->uri, 'getBaseUrl')) {
return $this->uri->getBaseUrl();
}
}
public function isCurrentPath($name, $data = [])
{
return $this->router->pathFor($name, $data) === $this->uri->getPath();
}
public function setBaseUrl($baseUrl)
{
$this->uri = $baseUrl;
}
}
?>
<?php
/********************/
//dependencies.php
/********************/
$container['link'] = function ($c) {
return new \App\Helpers\LinksHelper($c);
};
// view renderer
$container['view'] = function ($c) {
$settings = $c->get('settings');
$view = new App\Views\MyPhpRenderer($settings['renderer']['template_path']);
$view->setLayout('default.php');
//$view->addAttribute('title_for_layout', $settings['title_app'] .' :: ');
$view->setAttributes([
'title_for_layout'=>$settings['title_app'] .' :: ',
'link' => $c->get('link')
]);
return $view;
};
?>
<?php
/********************/
//routes.php
/********************/
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
$app->get('/', function (Request $request, Response $response, array $args) {
return $this->view->render($response, 'your_view.php');
})->setName('home');
?>
<?php
/********************/
//your_view.php
/********************/
?>
Home
You should create a new class, e.g. MainMenu, and there you should create an array with all paths for menu. Object of MainMenu should return an array with labels and paths and then you can pass that array to your view:
$menu = (new MainMenu())->buildMenu();
$response = $this->view->render($response, "index.phtml", [
'menu' => $menu
]);
Then in your *.phtml file you have access to the $menu variable. But what if you do not want repeat that code in each route?
Use middlewares. You can pass a variable from middleware using
$request = $request->withAttribute('foo', 'bar');
and retrieve
$foo = $request->getAttribute('foo');
I'm using Slim Framework 3 to create an API. The app structure is: MVCP (Model, View, Controller, Providers).
Is it possible to have Slim Dependency Inject all my classes?
I'm using composer to autoload all my dependencies.
My directory structure looks like this:
/app
- controllers/
- Models/
- services/
index.php
/vendor
composer.json
Here's my composer.json file.
{
"require": {
"slim/slim": "^3.3",
"monolog/monolog": "^1.19"
},
"autoload" : {
"psr-4" : {
"Controllers\\" : "app/controllers/",
"Services\\" : "app/services/",
"Models\\" : "app/models/"
}
}
}
Here's my index.php file. Again, the dependencies are being auto injected by composer
<?php
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;
require '../vendor/autoload.php';
$container = new \Slim\Container;
$app = new \Slim\App($container);
$app->get('/test/{name}', '\Controllers\PeopleController:getEveryone');
$app->run();
My controller looks like this
<?php #controllers/PeopleController.php
namespace Controllers;
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;
class PeopleController
{
protected $peopleService;
protected $ci;
protected $request;
protected $response;
public function __construct(Container $ci, PeopleService $peopleService)
{
$this->peopleService = $peopleService;
$this->ci = $ci;
}
public function getEveryone($request, $response)
{
die($request->getAttribute('name'));
return $this->peopleService->getAllPeoples();
}
}
My PeopleService file looks like this:
<?php
namespace Services;
use Model\PeopleModel;
use Model\AddressModel;
use Model\AutoModel;
class PeopleService
{
protected $peopleModel;
protected $autoModel;
protected $addressModel;
public function __construct(PeopleModel $peopleModel, AddressModel $addressModel, AutoModel $autoModel)
{
$this->addressModel = $addressModel;
$this->autoModel = $autoModel;
$this->peopleModel = $peopleModel;
}
public function getAllPeopleInfo()
{
$address = $this->addressModel->getAddress();
$auto = $this->autoModel->getAutoMake();
$person = $this->peopleModel->getPeople();
return [
$person[1], $address[1], $auto[1]
];
}
}
Models/AddressModels.php
<?php
namespace Model;
class AddressModel
{
public function __construct()
{
// do stuff
}
public function getAddress()
{
return [
1 => '123 Maple Street',
];
}
}
Models/AutoModel.php
namespace Model;
class AutoModel
{
public function __construct()
{
// do stuff
}
public function getAutoMake()
{
return [
1 => 'Honda'
];
}
}
Models/PeopleModel.php
<?php
namespace Model;
class PeopleModel
{
public function __construct()
{
// do stuff
}
public function getPeople()
{
return [
1 => 'Bob'
];
}
}
ERROR
I'm getting the following error now:
PHP Catchable fatal error: Argument 2 passed to Controllers\PeopleController::__construct() must be an instance of Services\PeopleService, none given, called in /var/www/vendor/slim/slim/Slim/CallableResolver.php on line 64 and defined in /var/www/app/controllers/PeopleController.php on line 21
THE QUESTION
How do I dependency inject all my classes? Is there a way to automagically tell Slim's DI Container to do it?
When you reference a class in the route callable Slim will ask the DIC for it. If the DIC doesn't have a registration for that class name, then it will instantiate the class itself, passing the container as the only argument to the class.
Hence, to inject the correct dependencies for your controller, you just have to create your own DIC factory:
$container = $app->getContainer();
$container['\Controllers\PeopleController'] = function ($c) {
$peopleService = $c->get('\Services\PeopleService');
return new Controllers\PeopleController($c, $peopleService);
};
Of course, you now need a DIC factory for the PeopleService:
$container['\Services\PeopleService'] = function ($c) {
$peopleModel = new Models\PeopleModel;
$addressModel = new Models\AddressModel;
$autoModel = new Models\AutoModel;
return new Services\PeopleService($peopleModel, $addressModel, $autoModel);
};
(If PeopleModel, AddressModel, or AutoModel had dependencies, then you would create DIC factories for those too.)
I am just starting a new Silex project. I am using the Cartalyst Sentry Authentication package and I wish to inject into my controller Service Controllers. Here is my attempt at using Silex's built in dependency container which extends Pimple. I would just like some feedback on whether I am going about things the right way and what I can improve.
$app['sentry'] = $app->share(function() use ($app) {
$hasher = new Cartalyst\Sentry\Hashing\NativeHasher;
$userProvider = new Cartalyst\Sentry\Users\Eloquent\Provider($hasher);
$groupProvider = new Cartalyst\Sentry\Groups\Eloquent\Provider;
$throttleProvider = new Cartalyst\Sentry\Throttling\Eloquent\Provider($userProvider);
$session = new Cartalyst\Sentry\Sessions\NativeSession;
$cookie = new Cartalyst\Sentry\Cookies\NativeCookie(array());
$sentry = new Cartalyst\Sentry\Sentry(
$userProvider,
$groupProvider,
$throttleProvider,
$session,
$cookie
);
Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(new PDO(
$app['db.dsn'],
$app['db.options']['user'],
$app['db.options']['password']
));
return $sentry;
});
Defining my controller:
// General Service Provder for Controllers
$app->register(new Silex\Provider\ServiceControllerServiceProvider());
$app['user.controller'] = $app->share(function() use ($app) {
return new MyNS\UserController($app);
});
$app->get('/user', "user.controller:indexAction");
Here is my controller, note that app['sentry'] is available to my controller by injecting it into the constructor.
class UserController
{
private $app;
public function __construct(Application $app)
{
$this->app = $app;
}
public function indexAction()
{
// just testing various things here....
$user = $this->app['sentry']->getUserProvider()->findById(1);
$sql = "SELECT * FROM genes";
$gene = $this->app['db']->fetchAssoc($sql);
$this->app['monolog']->addDebug(print_r($gene,true));
return new JsonResponse($user);
}
}
This is the process I go through after finding a vendor package I'd like to use. I'm using a simpler library in order to focus on setting up the service provider.
Install the new package via composer.
~$ composer require ramsey/uuid
Create the service provider.
<?php
namespace My\Namespaced\Provider;
use Silex\Application;
use Silex\ServiceProviderInterface;
use Rhumsaa\Uuid\Uuid;
class UuidServiceProvider implements ServiceProviderInterface
{
public function register(Application $app)
{
$app['uuid1'] = $app->share(function () use ($app) {
$uuid1 = Uuid::uuid1();
return $uuid1;
});
$app['uuid4'] = $app->share(function () use ($app) {
$uuid4 = Uuid::uuid4();
return $uuid4;
});
}
public function boot(Application $app)
{
}
}
Register the service provider.
$app->register(new My\Namespaced\Provider\UuidServiceProvider());
Use the new service in a controller.
<?php
namespace My\Namespaced\Controller;
use Silex\Application;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class ExampleController
{
public function indexAction(Application $app, Request $request)
{
$uuid = $app['uuid4']->toString();
return new Response('<h2>'.$uuid.'</h2>');
}
}