After upgrading to Slim v4 I'm trying to replace my simple $app->subRequest call with $app->handle as specified in the changelog. However there are no details on how to do this in either the changelog or upgrade guide and my best effort to fix it ends up creating an infinite loop:
$app->get("/foo", function (Request $req) use ($app) {
$uri = $req->getUri();
$newUri = $uri->withPath("/bar");
$barReq = $req->withUri($newUri);
// Here we get stuck in endless loop instead of ending up in the /bar route handler below
$app->handle($barReq);
});
$app->get("/bar", function (Request $req) use ($app) {
echo 'bar!';
die;
});
It's like even though $barReq is a new request object with a completely new uri (and path) the router does not resolve which route handler that should handle it, instead it's just handled by the same one again.
My previous simplified (v3) code looked like and worked fine to get the result of the /bar route when calling /foo:
$app->get("/foo", function (Request $req) use ($app) {
$app->subRequest('GET', '/bar');
});
I'm probably missing some central concept on how Slim 4 handles requests and routes internally and would appreciate some help!
Edit: Should perhaps add that what I mean with internal redirect is that client should not be aware that a redirect has been made. I.e. any regular redirect function returning something to client is not applicable here.
As #remy stated, use the ServerRequestFactory implementing ServerRequestFactoryInterface.
For slim/psr7 it is: Slim\Psr7\Factory\ServerRequestFactory
A silent redirect to another route is then as simple as:
use Slim\Psr7\Factory\ServerRequestFactory;
...
...
$app->get('/foo', function ($request, $response, $args)
{
global $app;
return $app
->handle((new ServerRequestFactory())->createServerRequest('GET', '/bar'));
});
Related
I want to create a Slim 4 compatible custom error page /JSON reply that is returned, when a non-existing route is request.
Default route (Slim 3)
I've recently upgraded from Slim 3 to Slim 4. With Slim 3, I had a default route that perfectly did the job:
$app->any('/[{path:.*}]', function (Request $request, Response $response, array $args) {
// catching any other requests...
/* ... creating JSON error object and write it to $slimresponse ... */
return ($slimresponse);
});
However, when I do this in Slim 4, I get an error
Type: FastRoute\BadRouteException
Code: 0
Message: Cannot register two routes matching "/" for method "GET"
This obviouosly means that Slim recognizes this as double entry for GET /, which is disallowed in Slim 4.
This article also provided no help for Slim 4, unfortunately.
notFound
Furthermore, according to https://www.javaer101.com/en/article/13830039.html, I've tried to add
$app->notFound(function () use ($app) {
$app->response->setStatus(403);
echo "Forbidden";
//output 'access denied', redirect to login page or whatever you want to do.
});
to my routes.php, but it doesn't work:
Call to undefined method Slim\App::notFound()
HttpNotFoundException
Finally, I've also tried to create an error handling method (specifically for HttpNotFoundException, although I don't know how to separate HttpNotImplementedException) https://www.slimframework.com/docs/v4/middleware/error-handling.html, without any success.
Any help is highly appreciated.
I'd posted the question after searching for two or more hours.
After submitting the question, I've found the answer here.
https://odan.github.io/2020/05/27/slim4-error-handling.html#catching-404-not-found-errors
Here is my new middleware.php:
use Slim\App;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Exception\HttpNotFoundException;
use Slim\Middleware\ErrorMiddleware;
use Middlewares\TrailingSlash;
use Slim\Psr7\Response;
return function (App $app) {
// Parse json, form data and xml
$app->addBodyParsingMiddleware();
// Add the Slim built-in routing middleware
$app->addRoutingMiddleware();
// always add a trailing slash
$app->add(new TrailingSlash(true));
// Add BasePathMiddleware
$app->add(BasePathMiddleware::class);
// HttpNotFoundException
$app->add(function (
ServerRequestInterface $request,
RequestHandlerInterface $handler
) {
try {
return $handler->handle($request);
} catch (HttpNotFoundException $httpException) {
$response = (new Response())->withStatus(404);
$response->getBody()->write('404 Not found');
return $response;
}
});
// Catch exceptions and errors
$app->add(ErrorMiddleware::class);
};
I have two POST routes /test and /test_new. The objective is to redirect all incoming requests from /test_new to /test along with the body contents. Hence The following code aims to use a named route for redirection
$app->post('/test', function (Request $request, Response $response, $args) {
$response->getBody()->write($request->getBody()->getContents());
return $response;
})->setName('test');
$app->post('/test_new', function (Request $request, Response $response, $args) use ($app) {
$routeParser = RouteContext::fromRequest($request)->getRouteParser();
// $routeParser = $app->getRouteCollector()->getRouteParser();
return $response->withStatus(307)->withBody($request->getBody())
->withHeader('Location', $routeParser->urlFor('test'));
});
This piece of code fails with the message
/test_new - Uncaught RuntimeException: Cannot create RouteContext before routing has been completed in /home/biswa/test/test-slim/vendor/slim/slim/Slim/Routing/RouteContext.php:40
If I simply replace the $routeParser with the commented line in the second route handler and use the $app for the RouterParser it works fine. But In my actual code I have the handling logic in a class function and have no access to $app in there.
Can someone please help me to sort this out
Missed the loading of RoutingMiddleware as stated here
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 cloned slim skeleton (https://github.com/slimphp/Slim-Skeleton) which already have CORS implemented.
But still when API calls OPTIONS before GET, it sends 405 ERROR "Method not allowed. Must be one of: GET"
Here is my route where I face this error. $group->get('/users', ListUsersAction::class);
$app->group('', function (Group $group) {
$group->post('/user/create', CreateUsersAction::class);
$group->get('/users', ListUsersAction::class);
$group->get('/user/{id}', ViewUserAction::class);
})->add(AuthenticationMiddleware::class);
The same route is working from postman. And same route is working if I remove Authorization token from header.
Execution does not even reach to first line of "AuthenticationMiddleware".
However I tested it by adding same option route without "AuthenticationMiddleware".
like this:
$app->options('/users', function(Request $request, Response $response) {return $response;});
$app->group('', function (Group $group) {
$group->post('/user/create', CreateUsersAction::class);
$group->get('/users', ListUsersAction::class);
$group->get('/user/{id}', ViewUserAction::class);
})->add(AuthenticationMiddleware::class);
This is working. So I guess I forgot to add some code or I did any miskate which causing the error, or the skeleton has a bug.
Can anyone help on this?
Thanks in advance.
Okay I found the solution.
You can use a wildcard OPTIONS request to avoid this issue / error.
Below is an example:
$app->options('/{routes:.+}', function ($request, $response, $args) {
return $response;
});
I have tested it and currently working fine for me.
In my test, as mentioned, I tried it by creating OPTIONS route for /users, it was working but creating OPTINOS route for all API route get created is doesn't make sense, here is the solution as wildcard OPTIONS route.
thanks #odan for taking time to comment, but wildcard OPTIONS route is better solution.
I just added options all route, where I wanted use:
Example If I use it in /api/user route, then I add this code:
$app->options('/api/user', function ($request, $response, $args) {
return $response;
});
Is it possible to forward a request in Slim?
The meaning of "forward", like in JavaEE, is to internally redirect to another route without return the response to the client and maintaining the model.
For example:
$app->get('/logout',function () use ($app) {
//logout code
$app->view->set("logout",true);
$app->forward('login'); //no redirect to client please
})->name("logout");
$app->get('/login',function () use ($app) {
$app->render('login.html');
})->name("login");
In my opinion, the best way to do this would be by using Slim's internal router (Slim\Router) capabilities and dispatching (Slim\Route::dispatch()) the matched route (meaning: executing the callable from a matched route without any redirect). There are a couple of options that come to mind (depending on your setup):
1. calling a named route + callable doesn't take any arguments (your example)
$app->get('/logout',function () use ($app) {
$app->view->set("logout",true);
// here comes the magic:
// getting the named route
$route = $app->router()->getNamedRoute('login');
// dispatching the matched route
$route->dispatch();
})->name("logout");
This should definitely do the trick for you, but I still want to show the other scenarios ...
2. calling a named route + callable with arguments
The above example will fail ... because now we need to pass arguments to the callable
// getting the named route
$route = $app->router()->getNamedRoute('another_route');
// calling the function with an argument or array of arguments
call_user_func($route->getCallable(), 'argument');
Dispatching the route (with $route->dispatch()) will invoke all middleware, but here we are just calling the the callable directly ... so to get the full package we should consider the next option ...
3. calling any route
Without named routes we can get a route by finding the one matching the a http method and pattern. For this we use Router::getMatchedRoutes($httpMethod, $pattern, $reload) with reload set to TRUE.
// getting the matched route
$matched = $app->router()->getMatchedRoutes('GET','/classes/name', true);
// dispatching the (first) matched route
$matched[0]->dispatch();
Here you might want to add some checks and for example dispatch notFound in case no route is matched.
I hope you get the idea =)
There is redirect() method. However it sends an 302 Temporary Redirect response which you do not want.
$app->get("/foo", function () use ($app) {
$app->redirect("/bar");
});
Another possibility is pass() which tells application to continue to next matching route. When pass() is called Slim will immediately stop processing the current matching route and invoke the next matching route.
If no subsequent matching route is found, a 404 Not Found is sent to the client.
$app->get('/hello/foo', function () use ($app) {
echo "You won't see this...";
$app->pass();
});
$app->get('/hello/:name', function ($name) use ($app) {
echo "But you will see this!";
});
I think you have to redirect them. There is no forward in Slim. But you can set a status code for example in the redirect function. When you redirect to a route you should get that functionality you want.
// With route
$app->redirect('login');
// With path and status code
$app->redirect('/foo', 303);
here is an example from the documentation:
<?php
$authenticateForRole = function ( $role = 'member' ) {
return function () use ( $role ) {
$user = User::fetchFromDatabaseSomehow();
if ( $user->belongsToRole($role) === false ) {
$app = \Slim\Slim::getInstance();
$app->flash('error', 'Login required');
$app->redirect('/login');
}
};
};