Im working on quite a big app using slim 3 as my router.
The menu structure is like this. Some routes need to be grouped and some not (i think). Im not sure how i should do this.
Example menu structure. But it will hold alot more items. Is grouping a good idea here? I mean, as you see not all "clients" has the "settings" sub page/route, but it may be will in the future. How would one write the grouping logic for this?
client 1
info
settings
loremipsum
loremipsum
client 2
info
loremipsum
loremipsum
client 3
info
loremipsum
loremipsum
You could implement a small middleware and check the persmissions per client.
The setting "determineRouteBeforeAppMiddleware" must be set to true.
use Slim\App;
use Slim\Http\Request;
use Slim\Http\Response;
$app = new App([
'settings' => [
// Must be set to true to get access to route within middleware
'determineRouteBeforeAppMiddleware' => true
]
]);
Then add this middleware and customize it:
$container = $app->getContainer();
// Simple Route Access Control Middleware
$app->add(function (Request $request, Response $response, $next) use ($container) {
// Retrieving Current Route
/* #var \Slim\Route $route */
$route = $request->getAttribute('route');
if (!$route) {
return $next($request, $response);
}
$name = $route->getName();
$groups = $route->getGroups();
$methods = $route->getMethods();
$arguments = $route->getArguments();
// Do something with that information
// Check the permissions here...
$routePermission = $container->get(RoutePermission::class);
if (!$routePermission->isRouteAllowed($name, $arguments['client'])) {
// Permission denied
return $response->withStatus(403, 'Forbidden');
} else {
// OK :-)
return $next($request, $response);
}
});
Related
Hi Folks i upgrading my slim framework from slim 2 to slim 4 for older project
for one route i added the one value before route using slim 2 slim.before in index.php
example code:
$app->hook('slim.before', function () use ($app) {
$env = $app->environment();
$path = $env['PATH_INFO'];
// spliting the route and adding the dynamic value to the route
$uriArray = explode('/', $path);
$dynamicvalue = 'value';
if(array_key_exists($uriArray[1], array)) {
$dynamicvalue = $uriArray[1];
//we are trimming the api route
$path_trimmed = substr($path, strlen($dynamicvalue) + 1);
$env['PATH_INFO'] = $path_trimmed;
}
});
i read about the add beforemiddleware but cannot able find correct way to add it and i cannot able to find the replacement for $app->environment();
i want to append the dynamic value directly to route
for example
i have one route like this
https://api.fakedata.com/fakeid
by using the above route splitting code i appending the value route using slim.before in slim 2
for example take the dynamic value as test
the route will be
https://api.fakedata.com/test/fakeid
the response of the both api will be same we want to just add value to the route
can any one help me how to do with slim 4
I assume you need to and PATH_INFO to the environment so you can later refer to it in the route callback. You can add a middleware to add attributes to the $request the route callback receives:
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
class PathInfoMiddleware {
public function __invoke(Request $request, RequestHandler $handler) : Response {
$info = 'some value, path_trimmed for example...'; // this could be whatever you need it to be
$request = $request->withAttribute('PATH_INFO', $info);
return $handler->handle($request);
}
}
// Add middleware to all routes
$app->add(PathInfoMiddleware::class);
// Use the attribute in a route
$app->get('/pathinfo', function(Request $request, Response $response){
$response->getBody()->write($request->getAttribute('PATH_INFO'));
return $response;
});
Now visiting /pathinfo gives the following output:
some value, path_trimmed for example...
I'm working on a Slim 3 based application with a Twig frontend and I'm also making a REST API.
I've implemented slimphp\Slim-Csrf for the entire app but I now want to exclude this CSRF check from every "API" routes.
I'm trying to implement the "Option 2" of this post :
Slim3 exclude route from CSRF Middleware
Here is the code :
File App\Middleware\CsrfMiddleware.php :
namespace App\Middleware;
class CsrfMiddleware extends \Slim\Csrf\Guard {
public function processRequest($request, $response, $next) {
// Check if this route is in the "Whitelist"
$route = $request->getAttribute('route');
if ($route->getName() == 'token') {
var_dump('! problem HERE, this middleware is executed after the CsrfMiddleware !');
// supposed to SKIP \Slim\Csrf\Guard
return $next($request, $response);
} else {
// supposed to execute \Slim\Csrf\Guard
return $this($request, $response, $next);
}
}
}
File app\app.php :
$app = new \Slim\App([
'settings' => [
'determineRouteBeforeAppMiddleware' => true
]
]);
require('container.php');
require('routes.php');
$app->add($container->csrf);
$app->add('csrf:processRequest');
File app\container.php :
$container['csrf'] = function ($container) {
return new App\Middleware\CsrfMiddleware;
};
File app\routes.php :
<?php
$app->get('/', \App\PagesControllers\LieuController::class.':home')->setName('home');
$app->post('/api/token', \App\ApiControllers\AuthController::class.'postToken')->setName('token');
When I do a POST request on http://localhost/slim3/public/api/token I've got :
Failed CSRF check!string(70) "! problem HERE, this middleware is executed after the CsrfMiddleware !"
Like if my CsrfMiddleware was executed after \Slim\Csrf\Guard...
Anyone has an idea ?
Thank you.
In Slim 3 the middleware is LIFO (last in first out).
Add the middleware in the opposite direction:
Before
$app->add($container->csrf);
$app->add('csrf:processRequest');
After
$app->add('csrf:processRequest');
$app->add($container->csrf);
Notice: The public directory should not be part of the url
Not correct: http://localhost/slim3/public/api/token
Correct: http://localhost/slim3/api/token
To skip the processing within the middleware, just return the $response object.
// supposed to SKIP \Slim\Csrf\Guard
return $response;
Here is how I achieved this with Slim 3.
1) Create a class that extends \Slim\Csrf\Guard as follows.
The CsrfGuardOverride class is key to enabling or disabling CSRF checking for a path. If the current path is whitelist'ed, then the __invoke() method just skips the core CSRF checking, and proceeds by executing the next middleware layer.
If the current path is not in the whitelist (i.e., CSRF should be checked), then the __invoke method defers to its parent \Slim\Csrf\Guard::__invoke() to handle CSRF in the normal manner.
<?php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use \Slim\Csrf\Guard;
class CsrfGuardOverride extends Guard {
/**
* Invoke middleware
*
* #param ServerRequestInterface $request PSR7 request object
* #param ResponseInterface $response PSR7 response object
* #param callable $next Next middleware callable
*
* #return ResponseInterface PSR7 response object
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
{
// Set the name of the route we want whitelisted with a name
// prefix of 'whitelist'. check for that here, and add
// any path to the white list
$route = $request->getAttribute('route');
$routeName = $route->getName();
$whitelisted = strpos($routeName, 'whitelist');
// if url is whitelisted from being CSRF checked, then bypass checking by skipping directly to next middleware
if ($whitelisted !== FALSE) {
return $next($request, $response);
}
return parent::__invoke($request, $response, $next);
}
}
2) Register the CsrfGuardOverride class. Be sure to set settings.determineRouteBeforeAppMiddleware => true as this forces Slim to evaluate routes prior to executing any middleware.
// Method on App Class
protected function configureContainer(ContainerBuilder $builder)
{
parent::configureContainer($builder);
$definitions = [
'settings.displayErrorDetails' => true,
'settings.determineRouteBeforeAppMiddleware' => true,
// Cross-Site Request Forgery protection
\App\Middleware\CsrfGuardOverride::class => function (ContainerInterface $container) {
$guard = new \App\Middleware\CsrfGuardOverride;
$guard->setPersistentTokenMode(true); // allow same CSRF token for multiple ajax calls per session
return $guard;
},
'csrf' => DI\get(\App\Middleware\CsrfGuardOverride::class),
// add others here...
];
$builder->addDefinitions($definitions);
}
3) Add the path that you want to by bypass CSRF checking and give it a name with the prefix 'whitelist':
$app->post('/events/purchase', ['\App\Controllers\PurchaseController', 'purchaseCallback'])->setName('whitelist.events.purchase');
I'm trying to implement json-schema validator from justinrainbow as middleware in Slim 3.
I can't figure out how to get the clients input from GET/POST requests in middleware.
tried like this:
$mw = function ($request, $response, $next) {
$data = $request->getParsedBody();
print_r($data); // prints nothing
$id = $request->getAttribute('loan_id');
print_r($id); // prints nothing
// here I need to validate the user input from GET/POST requests with json-schema library and send the result to controller
$response = $next($request, $response);
return $response;
};
$app->get('/loan/{loan_id}', function (Request $request, Response $response) use ($app, $model) {
$loanId = $request->getAttribute('loan_id'); // here it works
$data = $model->getLoan($loanId);
$newResponse = $response->withJson($data, 201);
return $newResponse;
})->add($mw);
There are 2 possible ways of how I need it. what i'm doing wrong ?
validate it in middleware and send some array/json response to the controller, which i will then get as I understood with $data = $request->getParsedBody();
validate it in middleware but final check will be in controller like this:
$app->get('/loan/{loan_id}', function (Request $request, Response $response) use ($app, $model) {
if($validator->isValid()){
//
}
$loanId = $request->getAttribute('loan_id'); // here it works
$data = $model->getLoan($loanId);
$newResponse = $response->withJson($data, 201);
return $newResponse;
})->add($mw);
Best option for me it do something like here
but I don't understand what should i return in container, and how to pass get/post input to container
Your code in the first point seems alright, you only try to access route parameter from within middleware. At that point the route is not yet resolved and therefore parameters are not parsed from the URL.
This is a known use case and is described in Slim's documentation. Add the following setting to your app configuration to get your code working:
$app = new App([
'settings' => [
// Only set this if you need access to route within middleware
'determineRouteBeforeAppMiddleware' => true
]
]);
In order to understand how middleware works and how to manipulate response object, I suggest you read the User Guide - it's not that long and explains it really well.
I am trying to cache entire response using middleware
Steps i followed
Generated two middleware
AfterCacheMiddleware
BeforeCacheMiddleware
With in BeforeCacheMiddleware:
public function handle($request, Closure $next)
{
$key = $request->url();
if(Cache::has($key)) return Cache::get($key);
return $next($request);
}
With in AfterCacheMiddleware
public function handle ($request, Closure $next)
{
$response = $next($request);
$key = $request->url();
if (!Cache::has($key)) Cache::put($key, $response->getContent(), 60);
return $response;
}
Registered middleware in $routeMiddleware array of kernal.php
'cacheafter' => 'App\Http\Middleware\AfterCacheMiddleware',
'cachebefore' => 'App\Http\Middleware\BeforeCacheMiddleware',
With in routes.php i am calling this dummy routes like this
Route::get('middle', ['middleware' => 'cachebefore', 'cacheafter', function()
{
echo "From route";
}]);
Issue:
only cachebefore middleware is getting called. cacheafter is not getting called at all
Can anyone suggest what i am missing here ?
I found this question while I was looking for solution myself. I am aware that there is the Flatten package which does this caching, but I couldn't find nice examples on how to do this by myself. The solution attempt in this question contains ideas that were useful for my own solution, although I chose to go with a single middleware only.
Although the question is old and asker probably doesn't need the answer anymore, I will share my solution here as I feel that SO (and internet) lacks this caching sample for Laravel 5. I will try to explain as much as I can, but for most benefit you should be familiar with Routing, Caching and Middlewaring in Laravel 5. So here goes the solution:
Middleware
Create a middleware, those are usually placed in app/Http/Middleware folder and I will call the file CachePage.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
use Cache;
class CachePage
{
public function handle($request, Closure $next)
{
$key = $request->fullUrl(); //key for caching/retrieving the response value
if (Cache::has($key)) //If it's cached...
return response(Cache::get($key)); //... return the value from cache and we're done.
$response = $next($request); //If it wasn't cached, execute the request and grab the response
$cachingTime = 60; //Let's cache it for 60 minutes
Cache::put($key, $response->getContent(), $cachingTime); //Cache response
return $response;
}
}
Change $key according to your needs... You have all the $request with all the parameters... Change Cache::put($key, $value, $minutes) to Cache::forever($key, $value) if you will clear the cache manually and don't want it to ever expire.
Using the URL as key for storing cache is usable in most cases, but one might think of something more appropriate for a certain project.
Registering middleware
Register it in app/Http/Kernel.php by adding the middleware to $routeMiddleware array like this:
protected $routeMiddleware = [
/* ... */
/* Other middleware that you already have there */
/* ... */
'cachepage' => \App\Http\Middleware\CachePage::class,
];
Of course, you should change \App\Http\Middleware\CachePage if you placed it elsewhere or gave it another name. Also the key name cachepage is up to you - it will be used to invoke the middleware.
Usage
In your app/Http/routes.php use the middleware just like auth or other middlewares, for example, you might make a route group for all the pages that should be cached:
Route::group(['middleware' => 'cachepage'], function ()
{
Route::get('/', 'HomeController#home');
Route::get('/contacts', 'SectionController#contacts');
});
The list of middlewares has to be inside square brackets:
Route::get('middle', ['middleware' => ['cachebefore', 'cacheafter'], function()
{
echo "From route";
}]);
I need to have the current found controller and action in a middleware, so that I can do some authentication. But I found it impossible, because the pipe is like Middleware1 -> Middleware2-> do the dispatching -> controller#action() -> Middleware2 -> Middleware1.
Therefore before the dispatching, I cannot get the route info. It is definitely not right to do it after the $controller->action().
I did some research and found this.
$allRoutes = $this->app->getRoutes();
$method = \Request::getMethod();
$pathInfo = \Request::getPathInfo();
$currentRoute = $allRoutes[$method.$pathInfo]['action']['uses'];
But this does not work when visiting URI like app/role/1, because $allRoutes only have index of app/role/{id} instead of app/role/1.
Is there any workaround about this?
After do some research, I got solution. Here they go:
Create Custom Dispatcher
First, you have to make your own custom dispatcher, mine is:
App\Dispatcher\GroupCountBased
Stored as:
app/Dispatcher/GroupCountBased.php
Here's the content of GroupCountBased:
<?php namespace App\Dispatcher;
use FastRoute\Dispatcher\GroupCountBased as BaseGroupCountBased;
class GroupCountBased extends BaseGroupCountBased
{
public $current;
protected function dispatchVariableRoute($routeData, $uri) {
foreach ($routeData as $data) {
if (!preg_match($data['regex'], $uri, $matches)) continue;
list($handler, $varNames) = $data['routeMap'][count($matches)];
$vars = [];
$i = 0;
foreach ($varNames as $varName) {
$vars[$varName] = $matches[++$i];
}
// HERE WE SET OUR CURRENT ROUTE INFORMATION
$this->current = [
'handler' => $handler,
'args' => $vars,
];
return [self::FOUND, $handler, $vars];
}
return [self::NOT_FOUND];
}
}
Register Your Custom Dispatcher in Laravel Container
Then, register your own custom dispatcher via singleton() method. Do this after you register all your routes! In my case, I add custom dispatcher in bootstrap/app.php after this line:
require __DIR__.'/../app/Http/routes.php';
This is what it looks like:
/*
|--------------------------------------------------------------------------
| Load The Application Routes
|--------------------------------------------------------------------------
|
| Next we will include the routes file so that they can all be added to
| the application. This will provide all of the URLs the application
| can respond to, as well as the controllers that may handle them.
|
*/
require __DIR__.'/../app/Http/routes.php';
// REGISTER YOUR CUSTOM DISPATCHER IN LARAVEL CONTAINER VIA SINGLETON METHOD
$app->singleton('dispatcher', function () use ($app) {
return FastRoute\simpleDispatcher(function ($r) use ($app) {
foreach ($app->getRoutes() as $route) {
$r->addRoute($route['method'], $route['uri'], $route['action']);
}
}, [
'dispatcher' => 'App\\Dispatcher\\GroupCountBased',
]);
});
// SET YOUR CUSTOM DISPATCHER IN APPLICATION CONTEXT
$app->setDispatcher($app['dispatcher']);
Call In Middleware (UPDATE)
NOTE: I understand it's not elegant, since dispatch called after middleware executed, you must dispatch your dispatcher manually.
In your middleware, inside your handle method, do this:
app('dispatcher')->dispatch($request->getMethod(), $request->getPathInfo());
Example:
public function handle($request, Closure $next)
{
app('dispatcher')->dispatch($request->getMethod(), $request->getPathInfo());
dd(app('dispatcher')->current);
return $next($request);
}
Usage
To get your current route:
app('dispatcher')->current;
I found the correct answer to this problem. I missed one method named routeMiddleware() of Application. This method registers the route-specific middleware which is invoked after dispatching. So Just use $app->routeMiddleware() to register you middleware. And get the matched route info by $request->route() in your middleware.
$methodName = $request->getMethod();
$pathInfo = $request->getPathInfo();
$route = app()->router->getRoutes()[$methodName . $pathInfo]['action']['uses'];
$classNameAndAction = class_basename($route);
$className = explode('#', $classNameAndAction)[0];