So I’m learning how to write a Slim 3 PHP authentication app and I'm using an example code structure to get me started. The example code has a file called dependencies.php which has a series of functions that create object instances of other classes. These are then assigned to a $container variable with a name for each function. An example of these functions in the dependencies.php file can be seen here:
$container['view'] = function ($container) {
$view = new \Slim\Views\Twig(
$container['settings']['view']['template_path'],
$container['settings']['view']['twig'],
[
'debug' => true // This line should enable debug mode
]
);
$basePath = rtrim(str_ireplace('index.php', '', $container['request']->getUri()->getBasePath()), '/');
$view->addExtension(new Slim\Views\TwigExtension($container['router'], $basePath));
$view->addExtension(new \Twig_Extension_Debug());
return $view;
};
$container['validate_sanitize'] = function ($container)
{
$class_path = $container->get('settings')['class_path'];
require $class_path . 'ValidateSanitize.php';
$validator = new ValidateSanitize();
return $validator;
};
$container['hash_password'] = function($container)
{
$class_path = $container->get('settings')['class_path'];
require $class_path . 'HashPassword.php';
$hash = new HashPassword();
return $hash;
};
These functions are then somehow called in my slim routes. For example in my register.php slim route the validate_sanitize class object is called with a simple
$this->get(‘validate_sanitize’);
and assigned to a variable which I can then use to call methods from the validate_sanitize class.
However what I don’t understand is how this get method works on calling a class object from the dependencies.php file.
Here is the aforementioned register route that is a post request for incoming form data:
$app->post('/register', function(Request $request, Response $response)
{
$arr_tainted_params = $request->getParsedBody();
$sanitizer_validator = $this->get('validate_sanitize'); //here for example
$password_hasher = $this->get('hash_password');
$tainted_email = $arr_tainted_params['email'];
$tainted_username = $arr_tainted_params['username'];
$tainted_password = $arr_tainted_params['password'];
$model = $this->get('model');
$sql_wrapper = $this->get('sql_wrapper');
$sql_queries = $this->get('sql_queries');
$db_handle = $this->get('dbase');
$cleaned_email = $sanitizer_validator->sanitize_input($tainted_email, FILTER_SANITIZE_EMAIL);
$cleaned_username = $sanitizer_validator->sanitize_input($tainted_username, FILTER_SANITIZE_STRING);
$cleaned_password = $sanitizer_validator->sanitize_input($tainted_password, FILTER_SANITIZE_STRING);
});
All my routes are contained within a routes.php file that looks like this:
<?php
require 'routes/change_password.php';
require 'routes/forgot_password.php';
require 'routes/homepage.php';
require 'routes/login.php';
require 'routes/logout.php';
require 'routes/register.php';
There is also a bootstrap file that creates a new Slim container, Slim App instance and also includes necessary files. I’m also not entirely sure what a Slim\Container is or what it does. This bootstrap file looks like this:
<?php
session_start();
require __DIR__ . '/../vendor/autoload.php';
$settings = require __DIR__ . '/app/settings.php'; //an array of options containing database configurations and the path to twig templates
$container = new \Slim\Container($settings); //not sure what this does
require __DIR__ . '/app/dependencies.php';
$app = new \Slim\App($container);
require __DIR__ . '/app/routes.php';
$app→run();
I’ve tried reading a number of articles as well as watching various YouTube videos but they do things differently with controllers which just adds more complexity and confusion for me. I’d much rather an explanation on this specific example as I find the code structure fairly simple.
Thanks.
Inside route callable, $this will point to $container instance.
In Slim 3.0, if you take a look at map() method of Slim\App class, you will see following code:
if ($callable instanceof Closure) {
$callable = $callable->bindTo($this->container);
}
bindTo() is what makes you have access to container inside route callable using $this variable.
If you want to use class as route handler and want access to container instance inside class, you need to pass it manually. For example
<?php
namespace App\Controller;
class MyPostRegisterController
{
private $container;
public function __constructor($container)
{
$this->container = $container;
}
public function __invoke(Request $request, Response $response)
{
$sanitizer_validator = $this->container->get('validate_sanitize');
//do something
}
}
Then you can define route as following
$app->post('/register', App\Controller\MyPostRegisterController::class);
If Slim can not find MyPostController class in dependency container, it tries to create them and pass container instance.
Update
To call other method, append method name after class name separated by colon. For example, following route registration will call method home() in MyPostRegisterController class.
$app->post('/register', App\Controller\MyPostRegisterController::class . ':home');
Related
I'm building a web application right now and I'm facing a problem with my controller.
I want to send to my controller my League\Plate\Engine (registred in my Container) but I keep having the same error : Argument 3 passed to App\Controller\Main::index() must be an instance of League\Plates\Engine, array given
Here is my files :
dependencies.php
use League\Container\Container;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Yajra\Pdo\Oci8;
use League\Container\ReflectionContainer;
$container = new Container();
// Active auto-wiring
$container->delegate(
new ReflectionContainer
);
// Others dependencies
// ...
// Views
$container->add('view', function () {
$templates = new League\Plates\Engine();
$templates->addFolder('web', __DIR__ . '/templates/views/');
$templates->addFolder('emails', __DIR__ . '/templates/emails/');
// Extension
//$templates->loadExtension(new League\Plates\Extension\Asset('/path/to/public'));
//$templates->loadExtension(new League\Plates\Extension\URI($_SERVER['PATH_INFO']));
return $templates;
});
return $container;
routes.php
use League\Route\RouteCollection;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
$route = new RouteCollection($container);
// Page index
$route->get('/', 'App\Controller\Main::index');
// Others routes...
return $route;
Main.php
namespace App\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use League\Plates\Engine;
class Main
{
public function index(ServerRequestInterface $request, ResponseInterface $response, Engine $templates) {
//return $response->getBody()->write($this->templates->render('web::home'));
return $response;
}
}
Thank you in advance
EDIT
I've made a progress.
I extended the Main class to extends the abstract class BaseController which looks like this :
namespace App\Controller;
use League\Plates\Engine;
class BaseController
{
protected $templates;
public function __construct(Engine $templates) {
$this->templates = $templates;
}
}
The first error goes away, but another one show up. In the Main class, I would like to use the view object that I instanciate in the container, but the object passed to the constructor is an empty one :
Main.php
class Main extends BaseController
{
public function index(ServerRequestInterface $request, ResponseInterface $response) {
echo '<pre>'.print_r($this->templates,1).'</pre>'; // Return an empty Plate Engine object
return $response->getBody()->write($this->templates->render('web::home'));
//return $response;
}
}
And this doesn't explain why the first error shows up
EDIT 2
After some digging, I finally make it works, but I sense that something is wrong.
I replaced in the container the term view by the namespace of the Engine class :
$container->add('League\Plates\Engine', function () {
// The same as before
});
In Main.php I've updated the index function like this :
public function index(ServerRequestInterface $request, ResponseInterface $response) {
$body = $response->getBody();
$body->write($this->templates->render('web::home'));
return $response->withBody($body);
}
And the page doesn't throw a 500 error and the html file is displayed correctly.
But, what if I want to change the template engine by Twig for example ? This would mean I'll need to change all the call to $container->get('League\Plate\Engine'); by $container->get('What\Ever'); ? That's not very practical!
I probably missed something!
And the problem will rise again when I'll want to use my PDO object... or every other object.
Ok so I solved my problem by registering my Controllers classes in the container itself.
For example, for displaying the index page, the Main class call the index function. In my container, I call
$container->add('App\Controller\Main')
->withArgument($container->get('view'));
To summary :
bootstap.php (called by index.php)
require __DIR__ . '/../../vendor/autoload.php';
$dotenv = new \Dotenv\Dotenv(__DIR__ . '/../');
$dotenv->load();
$config = new Config(__DIR__ . '/../config/');
$container = require __DIR__ . '/../dependencies.php';
$route = require __DIR__ . '/../routes.php';
$response = $route->dispatch($container->get('request'), $container->get('response'));
$container->get('emitter')->emit($response);
dependencies.php
$container = new Container();
// activate auto-wiring
$container->delegate(
new ReflectionContainer
);
// Others dependencies...
// Views
$container->add('view', function () {
$templates = new League\Plates\Engine();
$templates->addFolder('web', __DIR__ . '/templates/views/');
$templates->addFolder('emails', __DIR__ . '/templates/emails/');
// Extension
//$templates->loadExtension(new League\Plates\Extension\Asset('/path/to/public'));
//$templates->loadExtension(new League\Plates\Extension\URI($_SERVER['PATH_INFO']));
return $templates;
});
// THIS IS THE TRICK
$container->add('App\Controller\Main')
->withArgument($container->get('view'));
// others controllers...
return $container;
routes.php
$route = new RouteCollection($container);
// Page index
$route->get('/', 'App\Controller\Main::index');
// Others routes...
return $route;
I have no idea why or how this work !
This works, because you are registering with the container your controller class, and telling the container to dependency inject your View class in the constructor..
The line you added, here, is doing that:
$container->add('App\Controller\Main')
->withArgument($container->get('view'));
The Hello World documentation example explains it perfectly.
http://container.thephpleague.com/3.x/constructor-injection/
I read the documentation here about creating middleware. But which folder or file i must be create it? Documentation is not contain this information.
Under the src folder i have middleware.php.
For example i want to get post information like this:
$app->post('/search/{keywords}', function ($request, $response, $args) {
$data = $request->getParsedBody();
//Here is some codes connecting db etc...
return json_encode($query_response);
});
i made this under the routes.php but i want to create class or middleware for this. How can i do? Which folder or file i must be use.
Slim3 does not tie you to a particular folder structure, but it does (rather) assume you use composer and use one of the PSR folder structures.
Personally, that's what I use (well, a simplified version):
in my index file /www/index.php:
include_once '../vendor/autoload.php';
$app = new \My\Slim\Application(include '../DI/services.php', '../config/slim-routes.php');
$app->run();
In /src/My/Slim/Application.php:
class Application extends \Slim\App
{
function __construct($container, $routePath)
{
parent::__construct($container);
include $routePath;
$this->add(new ExampleMiddleWareToBeUsedGlobally());
}
}
I define all the dependency injections in DI/services.php and all the route definitions in config/slim-routes.php. Note that since I include the routes inside the Application constructor, they will have $this refer to the application inside the include file.
Then in DI/services.php you can have something like
$container = new \Slim\Container();
$container['HomeController'] = function ($container) {
return new \My\Slim\Controller\HomeController();
};
return $container;
in config/slim-routes.php something like
$this->get('/', 'HomeController:showHome'); //note the use of $this here, it refers to the Application class as stated above
and finally your controller /src/My/Slim/Controller/HomeController.php
class HomeController extends \My\Slim\Controller\AbstractController
{
function showHome(ServerRequestInterface $request, ResponseInterface $response)
{
return $response->getBody()->write('hello world');
}
}
Also, the best way to return json is with return $response->withJson($toReturn)
I have a problem with DI, it's new for me. I would like to give the Container as DI in my Project class, but i got an error :
Argument 1 passed to Project::__construct() must be an instance of Slim\Container, none given
I created a class Project :
use Slim\Container;
class Project {
protected $ci;
public function __construct(Container $ci) {
$this->ci = $ci;
}
}
Here my DIC configuration :
//dependencies.php
$container = $app->getContainer();
$container[Project::class] = function ($c) {
return new Project($c);
};
And here my code index.php to run the code
// Instantiate the app
$settings = require __DIR__ . '/../app/configs/settings.php';
$app = new \Slim\App($settings);
// Set up dependencies
require __DIR__ . '/../app/configs/dependencies.php';
// Register middleware
require __DIR__ . '/../app/configs/middleware.php';
// Register routes
require __DIR__ . '/../app/configs/routes.php';
require __DIR__ . '/../app/model/Project.php';
$app->get('/project/{id}', function (Request $request, Response $response) {
$project = new Project();
return $response;
});
$app->run();
I dont know where i failed, i even tried to use slim-brige, but i got the same result. I also tried to give a string instead of the the Container, and i still get null
Argument 1 passed to Project::__construct() must be an instance of Slim\Container, none given
Well, no argument given to the constructor indeed:
$app->get('/project/{id}', function (Request $request, Response $response) {
$project = new Project(); // You should pass container instance here, but you don't
return $response;
});
Since you have registered your Project class in the container, you can get instance from the container:
$app->get('/project/{id}', function (Request $request, Response $response) {
$project = $this->container->get('Project'); // Get Project instance from the container, you have already registered it
return $response;
});
Im having trouble understanding how to access the instance of Slim when a route is in a seperate class than index.php
When using Slim Framework 2 I always used the following, but its not working in Slim 3:
$this->app = \Slim\Slim::getInstance();
Im trying to access a database connection I have setup in the container, but from a separate class. This is what I currently got in my index.php to initiate a Slim app:
require_once("rdb/rdb.php");
$conn = r\connect('localhost');
$container = new \Slim\Container;
$container['rdb'] = function ($c){return $conn;}
$app = new \Slim\App($container);
And here is my route:
$app->get('/test','\mycontroller:test');
And this is what I got in my mycontroller.php class which my route points to, which obviously is not working as $this->app doesn't exist:
class mycontroller{
public function test($request,$response){
$this->app->getContainer()->get('rdb');
}
The error message is the following, due to getinstance not being part of Slim 3 compared to Slim 2:
Call to undefined method Slim\App::getInstance()
Grateful for any help,
Regards
Dan
Have a look at the Slim 3 Skeleton created by Rob Allen.
Slim 3 heavily uses dependency injection, so you might want to use it too.
In your dependencies.php add something like:
$container = $app->getContainer();
$container['rdb'] = function ($c) {
return $conn;
};
$container['Your\Custom\Class'] = function ($c) {
return new \Your\Custom\Class($c['rdb']);
};
And in your Your\Custom\Class.php:
class Class {
private $rdb;
function __construct($rdb) {
$this->rdb = $rdb;
}
public function test($request, $response, $args) {
$this->rdb->doSomething();
}
}
I hope this helps, if you have any more questions feel free to ask.
Update:
When you define your route like this
$app->get('/test', '\mycontroller:test');
Slim looks up \mycontroller:test in your container:
$container['\mycontroller'] = function($c) {
return new \mycontroller($c['rdb']);
}
So when you open www.example.com/test in your browser, Slim automatically creates a new instance of \mycontroller and executes the method test with the arguments $request, $response and $args.
And because you accept the database connection as an argument for the constructor of your mycontroller class, you can use it in the method as well :)
With Slim 3 RC2 and onwards given a route of:
$app->get('/test','MyController:test');
The CallableResolver will look for a key in the DIC called 'MyController' and expect that to return the controller, so you can register with the DIC like this:
// Register controller with DIC
$container = $app->getContainer();
$container['MyController'] = function ($c) {
return new MyController($c->get('rdb'));
}
// Define controller as:
class MyController
{
public function __construct($rdb) {
$this->rdb = $rdb;
}
public function test($request,$response){
// do something with $this->rdb
}
}
Alternatively, if you don't register with the DIC, then the CallableResolver will pass the container to your constructor, so you can just create a controller like this:
class MyController
{
public function __construct($container) {
$this->rdb = $container->get('rdb');
}
public function test($request,$response){
// do something with $this->rdb
}
}
I created the following base controller and extended from that. Only just started playing with Slim but it works if you need access to to the DI in your controllers.
namespace App\Controllers;
use Interop\Container\ContainerInterface;
abstract class Controller
{
protected $ci;
/**
* Controller constructor.
*
* #param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
$this->ci = $container;
}
/**
* #param $name
* #return mixed
*/
public function __get($name)
{
if ($this->ci->has($name)) {
return $this->ci->get($name);
}
}
}
Then in your other controllers you can use it like this.
namespace App\Controllers;
/**
* Class HomeController
*
* #package App\Controllers
*/
class HomeController extends Controller
{
/**
* #param $request
* #param $response
* #param $args
* #return \Slim\Views\Twig
*/
public function index($request, $response, $args)
{
// Render index view
return $this->view->render($response, 'index.twig');
}
}
Important
I upvoted #mgansler and you should read that first if dealing with slim 3, and read this only if interested in differences to slim 2.
Update
So it seems those usages were just old code no one cleaned.
However im leaving this post here as it should be helpful to anyone using Slim 2 (as slim 3 is very much still beta) and as a referance point to help see differences.
Old Update (see above)
Following update of OP, i looked at github source code and found that getInstance is still very much there, but with some slight differences perhaps...
https://github.com/slimphp/Slim/search?utf8=%E2%9C%93&q=getInstance
Test files (which maybe outdated, but unlikely) show something like this:
public function testGetCallableAsStaticMethod()
{
$route = new \Slim\Route('/bar', '\Slim\Slim::getInstance');
$callable = $route->getCallable();
$this->assertEquals('\Slim\Slim::getInstance', $callable);
}
But at the same time we see calls like this in some files, which are obviously contextual and either return diff object ($env) or are in same static file (Slim.php)
$env = \Slim\Environment::getInstance(true);
static::getInstance();
But this does show the static function still exists, so use my examples below and try to figure out why not working for you in current form.
Also, this 'maybe' of interest, as only obvious example of slim3 in usage: https://github.com/akrabat/slim3-skeleton
Though other projects prob exist, search with github filters if still having issues.
Original Answer content
Please include more detail on the route and the other class, but here are 3 ways, with execution examples detailed further down.
This info does relate to Slim Framework 2, not the Slim 3 beta, but slim 3 beta shows similar example code and makes no mention of overhauling changes, and in fact links to the Slim 2 documentation: http://docs.slimframework.com/configuration/names-and-scopes/
$this->app->getContainer()->get('rdb');
// Recommended approach, can be used in any file loaded via route() or include()
$app = \Slim\Slim::getInstance();
Slim::getInstance();
App::config('filename');
Slim3 Beta has only one code example, which looks like this:
$app = new \Slim\App();
// which would by extension mean that this 'might' work too
$app = \Slim\App::getInstance();
// but be sure to try with slim2 naming just in case
$app = \Slim\Slim::getInstance()
Though obviously this doesnt fit outside of index.php, but is consistent with Slim2 doco showing GetInstance works.
Which one fits you?
I have multiple files that use these different approaches, though i cant say what fits best as too little context on how this external class fits in and what its composition is.
For example, my controllers (which are endpoints of most my routes) use the same approach, through a base class or just direct:
class ApiBaseController /// extends \BaseController
{
protected $app;
protected $data;
public function __construct()
{
$this->app = Slim\Slim::getInstance();
$this->data = array();
}
//...
}
class VideoApiController extends \ApiBaseController
{
// ...
public function embed($uid)
{
// trace($this->app->response->headers());
$vid = \R::findOne('videos'," uid = ? ",array($uid));
if(!empty($vid))
{
// embed logic
}else{
// see my baseclass
$this->app->render('api/404.html', array(), 404);
}
}
// ...
// Returns the video file, keeping actual location obscured
function video($uid)
{
require_once(APP_PATH.'helpers/player_helper.php');
$data = \R::findOne('videos'," uid = ? ",array($uid));
/// trace($_SERVER); die();
if($data)
{
stream_file($data['filename']);
}else{
$app = \Slim\Slim::getInstance();
$app->render('404.html');
}
/// NOTE - only same domain for direct /v/:uid call
header('Access-Control-Allow-Origin : '.$_SERVER['HTTP_HOST']);
// header('X-Frame-Options: SAMEORIGIN');
// Exit to be certain nothing else returned
exit();
}
//...
}
My helper files show code like this:
function get_permissions_options_list($context = null)
{
if(empty($context)) $context = 'user';
return App::config('permissions')[$context];
}
My middleware:
function checkAdminRoutePermissions($route)
{
$passed = runAdminRoutePermissionsCheck($route);
if($passed)
return true;
// App::notFound();
// App::halt(403, $route->getPattern());
if(!Sentry::check())
App::unauthorizedNoLogin();
else
App::unauthorized();
return false;
}
Thats example of how i access in the various files, though the code you shared already shows that you have used the recommended approach already
$app = \Slim\Slim::getInstance();
Though again, need more info to say for sure how your external file fits in, but if its at the end of a route or in an 'include()', then it should work.
You said your old approach didnt work though, but gave no info on what the actual result vs expected result was (error msg, ect), so if this doesnt work please update the OP.
This was a tough one. #mgansler answer was really helpful, but in his answer he passed a database connection, and not exactly $app inside the controller
Following the same idea it is possible to send $app though.
First inside your dependencies.php you need to grab the $app and throw it in a container to inject it to the Controller later.
$container['slim'] = function ($c) {
global $app;
return $app;
};
Then you got to inject it:
// Generic Controller
$container['App\Controllers\_Controller'] = function ($c) {
return new _Controller($c->get('slim'));
};
Now on your controller.php:
private $slim;
/**
* #param \Psr\Log\LoggerInterface $logger
* #param \App\DataAccess $dataaccess
* #param \App\$app $slim
*/
public function __construct(LoggerInterface $logger, _DataAccess $dataaccess, $slim)
{
$this->logger = $logger;
$this->dataaccess = $dataaccess;
$this->slim = $slim;
}
Now you just got call it like this:
$this->slim->doSomething();
<?php
require 'vendor/autoload.php';
// Include all controllers
foreach(glob("controllers/*.php") as $controller)
{
include $controller;
}
// Instantiate a new Slip application
$app = new \Slim\Slim(array(
'debug' => true
));
// HOME CONTROLLER
$home = new Home;
$vr = $home->index();
// Register application routes
$app->get('/', function () {
echo $vr;
});
// Run application
$app->run();
This is my controller I want to use controllers and not keep everything in this single file. Anyhow I have a controllers map where I keep all my controllers. I automatically include them all at start however I can't seem to pass $home variable to get() method so I could call $vr indede it or $home->index()
You can pass it to your function like this:
..., function () use($home){
...
I think Slim also passes $app as the first argument to your function.
Edit: actually it doesn't according to its docs, so you'll have to pass that too inside the use statement (function arguments are URL parameters):
$app->get('/', function () use($home, $app) {
$vr = $home->index();
echo $vr;
// $app is accesible too...
});