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>');
}
}
Related
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();
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 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.)
Synopsis
I am building a system with at least two levels of Authentication and both have separate User models and tables in the database. A quick search on google and the only solution thus far is with a MultiAuth package that shoehorns multiple drivers on Auth.
My goal
I am attempting to remove Auth which is fairly straight-forward. But I would like CustomerAuth and AdminAuth using a separate config file as per config/customerauth.php and config\adminauth.php
Solution
I'm assuming you have a package available to work on. My vendor namespace in this example will simply be: Example - all code snippets can be found following the instructions.
I copied config/auth.php to config/customerauth.php and amended the settings accordingly.
I edited the config/app.php and replaced the Illuminate\Auth\AuthServiceProvider with Example\Auth\CustomerAuthServiceProvider.
I edited the config/app.php and replaced the Auth alias with:
'CustomerAuth' => 'Example\Support\Facades\CustomerAuth',
I then implemented the code within the package for example vendor/example/src/. I started with the ServiceProvider: Example/Auth/CustomerAuthServiceProvider.php
<?php namespace Example\Auth;
use Illuminate\Auth\AuthServiceProvider;
use Example\Auth\CustomerAuthManager;
use Example\Auth\SiteGuard;
class CustomerAuthServiceProvider extends AuthServiceProvider
{
public function register()
{
$this->app->alias('customerauth', 'Example\Auth\CustomerAuthManager');
$this->app->alias('customerauth.driver', 'Example\Auth\SiteGuard');
$this->app->alias('customerauth.driver', 'Example\Contracts\Auth\SiteGuard');
parent::register();
}
protected function registerAuthenticator()
{
$this->app->singleton('customerauth', function ($app) {
$app['customerauth.loaded'] = true;
return new CustomerAuthManager($app);
});
$this->app->singleton('customerauth.driver', function ($app) {
return $app['customerauth']->driver();
});
}
protected function registerUserResolver()
{
$this->app->bind('Illuminate\Contracts\Auth\Authenticatable', function ($app) {
return $app['customerauth']->user();
});
}
protected function registerRequestRebindHandler()
{
$this->app->rebinding('request', function ($app, $request) {
$request->setUserResolver(function() use ($app) {
return $app['customerauth']->user();
});
});
}
}
Then I implemented: Example/Auth/CustomerAuthManager.php
<?php namespace Example\Auth;
use Illuminate\Auth\AuthManager;
use Illuminate\Auth\EloquentUserProvider;
use Example\Auth\SiteGuard as Guard;
class CustomerAuthManager extends AuthManager
{
protected function callCustomCreator($driver)
{
$custom = parent::callCustomCreator($driver);
if ($custom instanceof Guard) return $custom;
return new Guard($custom, $this->app['session.store']);
}
public function createDatabaseDriver()
{
$provider = $this->createDatabaseProvider();
return new Guard($provider, $this->app['session.store']);
}
protected function createDatabaseProvider()
{
$connection = $this->app['db']->connection();
$table = $this->app['config']['customerauth.table'];
return new DatabaseUserProvider($connection, $this->app['hash'], $table);
}
public function createEloquentDriver()
{
$provider = $this->createEloquentProvider();
return new Guard($provider, $this->app['session.store']);
}
protected function createEloquentProvider()
{
$model = $this->app['config']['customerauth.model'];
return new EloquentUserProvider($this->app['hash'], $model);
}
public function getDefaultDriver()
{
return $this->app['config']['customerauth.driver'];
}
public function setDefaultDriver($name)
{
$this->app['config']['customerauth.driver'] = $name;
}
}
I then implemented Example/Auth/SiteGuard.php (note the methods implemented have an additional site_ defined, this should be different for other Auth drivers):
<?php namespace Example\Auth;
use Illuminate\Auth\Guard;
class SiteGuard extends Guard
{
public function getName()
{
return 'login_site_'.md5(get_class($this));
}
public function getRecallerName()
{
return 'remember_site_'.md5(get_class($this));
}
}
I then implemented Example/Contracts/Auth/SiteGuard.php
use Illuminate\Contracts\Auth\Guard;
interface SiteGuard extends Guard {}
Finally I implemented the Facade; Example/Support/Facades/Auth/CustomerAuth.php
<?php namespace Example\Support\Facades;
class CustomerAuth extends Facade
{
protected static function getFacadeAccessor()
{
return 'customerauth';
}
}
A quick update, when trying to use these custom auth drivers with phpunit you may get the following error:
Driver [CustomerAuth] not supported.
You also need to implement this, the easiest solution is override the be method and also creating a trait similar to it:
<?php namespace Example\Vendor\Testing;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
trait ApplicationTrait
{
public function be(UserContract $user, $driver = null)
{
$this->app['customerauth']->driver($driver)->setUser($user);
}
}
My directory Structure is shown above. I am trying to build a Framework with Symfony Components. but one problem, when I hit a route that I defined, it doesn't give me back the response.
Here is my index.php
<?php
$loader = require 'vendor/autoload.php';
$loader->register();
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
require 'lib/Framework/Core.php';
$request = Request::createFromGlobals();
// Our Framework is now handling itself the request
$app = new Framework\Core();
$app->map('/', function () {
return new Response('This is the home page');
});
$app->map('/about', function () {
return new Response('This is the about page');
});
$response = $app->handle($request);
and my Core.php looks like this
<?php namespace Framework;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface as HttpKernelInterface;
class Core implements HttpKernelInterface
{
protected $routes = array();
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
{
$path = $request->getPathInfo();
// Does this URL match a route?
if (array_key_exists($path, $this->routes)) {
// execute the callback
$controller = $this->routes[$path];
$response = $controller();
} else {
// no route matched, this is a not found.
$response = new Response('Not found!', Response::HTTP_NOT_FOUND);
}
return $response;
}
// Associates an URL with a callback function
public function map($path, $controller) {
$this->routes[$path] = $controller;
}
}
Does anyone know what the bug is? What did I screw up?
As I mentioned in the comments, you're just missing the last little detail (->send()) where you tell the Response object to send the headers and echo the content. So:
$response = $app->handle($request);
$response->send();
And I think that should do it!
Cheers!