Per http://www.slimframework.com/docs/concepts/middleware.html, one can add middleware to an application, route, or group.
How should one add middleware to all routes (i.e. to the application), but exclude it from specific routes?
EDIT. As a possible solution, I am thinking of adding some logic in my application middleware to bypass the middleware functionality. Getting the method is easy enough using $request->getMethod(), however, the other URL methods described by http://www.slimframework.com/docs/objects/request.html are not available in the middleware.
I think you need to add middleware to group of route, sample admin route group and user group.
If you would like to change the behaviour of dynamic you have to modify your middleware logic to check your specific request,
sample your route returns JSON if client set type of request.
EDIT:
$app = new \Slim\App();
//pass $app to middleware
$app->add(function ($request, $response, $next) use ($app) {
//do what you want with $app.
//reads config file with routes for exclude
$response = $next($request, $response);
return $response;
});
$app->get('/', function ($request, $response, $args) {
$response->getBody()->write(' Hello ');
return $response;
});
$app->run();
I believe, as of writing this post (I'm using Slim 3.12.1), the answer to your question is it's not possible to remove a previously added middleware from a route.
The solution suggested by the accepted answer makes middleware implementation tightly coupled with route definition. If route (or in similar suggested solution, route name) changes, then middleware code needs to change as well. This is due to the fact that the middleware is technically not removed from route middleware stack. This gets complicated in some situations. For example, if the middleware is written by someone else. You probably want to avoid changing code of a third party library, so it's possible that you end up extending the third party middleware to override its functionality in some way, or some similar solution, yet to make the implementation coupled with your route definitions.
My suggestion is, instead of changing the logic of your middleware to prevent its execution for some routes, add logic to your code to control which middlewares should be added to which routes. Here is a fully functional example to demonstrate how to add a common middleware to all routes while adding some middlewares only to some specific routes but not all (the ones you meant to exclude). To keep things simple, there is no logic for determining which middlewares should be added to which routes, but this demonstrates the general idea. Here are the assumptions:
paths starting with /user should be protected by Authorization middleware
/user/login and /user/signup are exceptions and do not need to be protected by Authentication middleware. Instead, we want to allow login/signup attempts from specific IP addresses only, so we need to protect this route with IP Access Control middleware
all requests to all paths starting with /user should be logged using Logger middleware (no exception here)
other routes don't need any middleware (so we should avoid using $app->add())
<?php
require __DIR__ . '/../vendor/autoload.php';
$app = new Slim\App;
// A simple middleware to write route path and middleware name
class SampleMiddleware
{
public function __construct($name)
{
$this->name = $name;
}
public function __invoke($request, $response, $next)
{
$response->write($request->getUri()->getPath() . ' invokes ' . $this->name . "\n");
return $next($request, $response);
}
}
// Three middleware instances with different names
$auth = new SampleMiddleware('Authorization');
$ipAC = new SampleMiddleware('IP Access Control');
$logger = new SampleMiddleware('Logger');
$group = $app->group('/user', function($app) {
$app->get('/profile', function($request, $response, $args){
return $response;
});
$app->get('/messages', function($request, $response, $args){
return $response;
});
})->add($auth)
->add($logger);
$group = $app->group('/user', function($app) {
$app->get('/login', function($request, $response, $args){
return $response;
});
$app->get('/signup', function($request, $response, $args){
return $response;
});
})->add($ipAC)
->add($logger);
$app->get('{p:.*}', function($request, $response, $args){
$response->write('No middleware for ' . $request->getUri()->getPath());
return $response;
});
$app->run();
Output:
/user/profile
/user/profile invokes Logger
/user/profile invokes Authorization
/user/login
/user/login invokes Logger
/user/login invokes IP Access Control
/something/else
No middleware for /something/else
You can access the uri inside middleware with the following.
$uri = $request->getUri()->getPath();
With that information you can return from the middleware early if the $uri matches path you want to exclude from the middleware.
If you want to add a global middleware the easiest way to achieve this is the following.
$app->group('', function () {
//Add your routes here, note that it is $this->get() ... etc inside the closure
})->add(MyNewMiddlewareClass::class);
Related
Using Laravel 7.
In the controller constructor, I then hoped to get access to the current user details so I could load main site widgets (buttons links etc) and custom user widgets in to one to be displayed in the view
use Illuminate\Support\Facades\Auth;
...
$widgets = Cache::get("widgets");
$usersdata = Cache::get("userdata");
$this->middleware('auth');
$widgets = array_merge($widgets, $usersdata[Auth::user()->id]["widgets"]);
View::share([
"widgets" => json_encode($widgets)
]);
however at this stage from research the user data is not available (even after authentication ?).
Not sure of best way to access this, or better practice might be to override the middleware auth (where?) so that it could return user id or something eg:
$userid=$this->middleware('auth');
I would like this in the constructor so the same method is in place for all controllers which extend this main controller.
This is intended behavior from laravel, you can read more about it here.
Laravel collects all route specific middlewares first before running
the request through the pipeline, and while collecting the controller
middleware an instance of the controller is created, thus the
constructor is called, however at this point the request isn’t ready
yet.
You can find Taylor's reasoning behind it here:
It’s very bad to use session or auth in your constructor as no request
has happened yet and session and auth are INHERENTLY tied to an HTTP
request. You should receive this request in an actual controller
method which you can call multiple times with multiple different
requests. By forcing your controller to resolve session or auth
information in the constructor you are now forcing your entire
controller to ignore the actual incoming request which can cause
significant problems when testing, etc.
So one solution would be to create a new middleware and then apply it to all routes, something like this, where widgets is your new middleware:
Route::group(['middleware' => ['auth', 'widgets']], function () {
// your routes
});
But if you really want to keep it in the constructor you could implement the following workaround:
class YourController extends Controller
{
public function __construct(Request $request)
{
$this->middleware('auth');
$this->middleware(function ($request, $next) {
$widgets = Cache::get("widgets");
$usersdata = Cache::get("userdata");
$widgets = array_merge($widgets, $usersdata[$request->user()->id]["widgets"]);
View::share([
"widgets" => json_encode($widgets)
]);
return $next($request);
});
}
}
I have a generic route like below -
$app->map(['GET', 'POST'], "/", function (\Slim\Http\Request $request, \Slim\Http\Response $response) use ($app) {
// Some random version 1 processing here ...
}
And I have a v2 version of the same like below -
$app->map(['GET', 'POST'], "/api/v2", function (\Slim\Http\Request $request, \Slim\Http\Response $response) use ($app) {
// Some random version 2 processing here ...
}
I have a problem to solve -
The frontend always hits the v1 of the API (the generic route). I need to internally redirect to v2, based on the parameter appversion's value.
How can I do this in slim framework routing without issuing 302 redirect headers back to the client? Basically, I need to redirect internally to the server itself.
Note: I am aware of Nginx and Apache rewrites. Let us keep them aside and limit the scope to slim framework routing alone.
I'd do it using optional segments in a single route definition.
use Psr\Http\Message\{
ServerRequestInterface as Request,
ResponseInterface as Response
};
$app->map(['GET', 'POST'], '/[{v2:api/v2}]', function (Request $request, Response $response, array $args) {
if (isset($args['v2'])) { // You may also check $request->getAttribute('appversion')
// Version 2 processing here...
// return $response;
}
// Version 1 processing here...
// return $response;
});
What you want to achieve is technically possible, but to me, it seems the reason behind your question is that you want to introduce a new version of your API, yet you want not to (or you can not) update the front end to call the new version, and instead, you want to handle this in the back end.
If you are going to decide which version of your API needs to be called based on appversion but not the endpoint that was hit, then what is the benefit of defining v1 and v2 endpoints?
If someone calls your v1 endpoint, they want your v1 response, and if someone needs your v2 response, they must call your v2 endpoint. If you return the same response for both v1 and v2 endpoints, then you're basically updating your v1 endpoint behavior.
Anyway, you want to dispatch another route in another route callback, and here is a fully working example showing how it's done using a subRequest:
<?php
require 'vendor/autoload.php';
$app = new \Slim\App;
$app->map(['GET', 'POST'], '/v1', function($request, $response) use ($app) {
// Get the appversion query parameter and make the decision based on its value
$appVersion = $request->getParam('appversion');
if($appVersion == '1.0') {
return $app->subRequest($request->getMethod(), '/v2',
http_build_query($request->getQueryParams()),
$request->getHeaders(),
$request->getCookieParams(),
$request->getBody()->getContents()
);
}
return 'API version: v1, appversion: ' . $appVersion;
});
$app->map(['GET', 'POST'], '/v2', function($request, $response) {
return 'API version: v2, request method: ' . $request->getMethod() .', appversion: '. $request->getParam('appversion') . ', body: <pre>' . print_r($request->getParsedBody(), 1);
});
$app->get('/form', function() {
return <<<form
<form method="POST" action="/v1?appversion=1.0">
<input type="text" name="foo" value="bar">
<button type="submit" value="submit">Submit</button>
</form>
form;
});
$app->run();
Now if you try to reach /v1?appversion=1.0 the response from /v2 callback will be returned. Trying to reach /v1 with appversion equal to any other value (for example /v1?appversion=2.0) causes the app to return the v1 response.
The subRequest method is also capable of handling POST requests. Please refer to method documentation in Slim code repository. The example provides a /form URI to demonstrate that.
You'd need to abstract your invocations and create controllers (i.e. your code won't be so slim).
Example:
function doV2() {
// ... do V2 things
}
function doV1() {
// ... do V1 things
}
$app->map(['GET', 'POST'], "/", function (\Slim\Http\Request $request, \Slim\Http\Response $response) use ($app) {
if($app->request()->params('appversion') === 'v2') return doV2();
doV1();
}
$app->map(['GET', 'POST'], "/api/v2", function (\Slim\Http\Request $request, \Slim\Http\Response $response) use ($app) {
doV2();
}
The reason you cannot 'redirect' internally is because your 'controllers' are anonymous closures so you have no way to name/reference/call them. Instead, if you abstract them out to functions (and yes, you'll probably need to pass in $request/$response too) then you have named methods you can invoke for the appropriate routes.
Instead of defining closures or functions, you could also define your controllers and use SlimPHP's Container Resolution within the router - there's a great example in the router docs.
Last up, you could get tricky with middleware to change what happens based on your appversion param depending on the complexity of what you're wanting to achieve.
I have a requirement where there is an API method (guarded by the well-known package tymon/jwt-auth) and I need to also be able to access it using the basic session based web middleware.
I don't want to repeat the route in both api.php and web.php even though that would totally work.
I tried adding both to the route but they simply don't work, like: ['auth:api', 'web']
I also tried creating a new middleware with the intention of checking both api and web like so:
class CombinedAuth
{
public function handle($request, Closure $next)
{
$web = Auth::guard('web')->user();
if ($web) {
return $next($request);
}
$api = Auth::guard('api')->user();
if ($api) {
return $next($request);
}
if(!$web && !$api) {
throw new AuthorizationException;
}
}
}
and that also doesn't work. The api middleware works fine but the web middleware doesn't and it always signs me out and redirects to the login page.
So Is there a neat way of protecting a route with api and web middlewares at the same time in Laravel 5.8?
You can use 'auth:api,web' to check for multiple guards.
Using multiple can middleware calls in Laravel 9 route;
<?php
Route::get('/', function () {
return view('money');
})
->middleware([
'can:manage,App\Models\Payment',
'can:manage,App\Models\Withdraw',
]);
?>
I created a download route from which my client successfully downloads the file by adding the following code to /app/Http/routes.php:
$app->get('/dl', function() use ($app){
return response()->download(getcwd() . "/targetfile");
});
but now I'd like to somehow have a callback be executed once the file transfer has been complete for a given instance of the get request. How would I do that?
To be a little more specific I'm trying to delete the file once it has been downloaded but this doesn't work:
$app->get('/dl', function() use ($app){
unlink(getcwd() . "/targetfile");
return response()->download(getcwd() . "/targetfile");
});
Answer to original Question
So in order to have a 'callback' once a response has been sent you use a so-called 'terminable' middleware.
In order for a middleware to be terminable:
1) it has to be global; I.E registered with the middleware function instead of the routeMiddleware
$app->middleware([
App\Http\Middleware\Myterminable::class
]);
2) It has to have the method 'terminate ($request, $response)' this is the method that will be called once the response has been sent.
public function terminate($request, $response)
{
// your code here
}
An alternative
A global middleware seems to be like a waste of computation resources as it's called for every request/response pair so instead I just looked through Lumen's code to find that the download method is actually returning an instance of
Symfony\Component\HttpFoundation\BinaryFileResponse;
On which I called
deleteFileAfterSend(true);
Which served my original goal
return response()->download(getcwd() . "/targetfile")->deleteFileAfterSend(true);
You need to register an after Middleware and apply it to your route.
Documentation about Middleware
I am using silex to create an api and my routes look something similar to this:
$api = $app['controllers_factory'];
$users = $app['controllers_factory'];
$users->match('/', UsersController::action);
$api->mount('/users', $users);
$app->mount('/api', $api);
So the route would be baseurl/api/users
What I want to do now is to attach a before() to the $api controller group and enforce validation for the api, so any link prefixed with /api/...users, posts
would require validation.
But it seems that is not the way it works, when i put a before to the $api, it only works for the root of /api, not api/users or api/posts or api/categories, they require their own middlewares.
So my question is : how can I enforce everything after baseurl/api/... to require validation in a Silex enviroment.
You can add before() to your $app instance (see http://silex.sensiolabs.org/doc/middlewares.html#before-middleware).
And test the $request object to see if authentication is required.
You could also use the SecurityServiceProvider, but that may be overkill depending on your use case.