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'm currently re-writing an API with multiple endpoints. However, for legacy purposes it's required that there is a single endpoint which can be used to access all other endpoints. Which endpoint we should redirect to is based upon a custom action header send along with the request.
Example:
Input: Header -> Action A
Output: Redirect to route '/some/url' 'ControllerA#someAction'
Input: Header -> Action B
Output: Redirect to route '/some/other/url' 'ControllerB#someOtherAction'
Normally, I could use the redirect() method but then I lose the body of the POST method. All the endpoints I declared are POST methods.
Basically, the question is how can I properly redirect a POST to another route?
Also note I can't use:
App::call('App\Http\Controllers\PlanningController#addOrUpdate', ['request' => $request]);
Since my method uses a custom Request class to handle the validation. I get an exception telling the argument should be the type of my custom class and Illuminate\Http\Request is given.
I've actually found the answer to my problem. I've created a middleware which will re-create the request based upon the value found in the header.
Here's the handle function of the middleware (only tested on Laravel 5.2):
use Request;
use Route;
use Illuminate\Http\Response;
...
public function handle($request, Closure $next, $guard = null)
{
// Get the header value
$action = $request->header('action');
// Find the route by the action name
$route = Actions::getRouteByName(action); // This returns some route, i.e.: 'api/v1/some/url'
// Perform the action
$request = Request::create(route, 'POST', ['body' => $request->getContent()]);
$response = Route::dispatch($request);
return new Response($response->getContent(), $response->status(), ['Content-Type' => 'text/xml']); // the last param can be any headers you like
}
Please note that this might conflict on your project with other middleware. I've disabled other middleware and created a special routegroup for this. Since we're redirecting the call manually to another route the middleware on that route is called anyway. However, you can also implement this code inside a controller function then there are no conflicting middleware problems!
Let's say that I have a resource defined in my Routes as:
Route::resource('account', 'AccountController', ['only'=> ['index','update']]);
And then I have the Middleware attached to the Controller from within as:
public function __construct() {
$this->middleware('BeforeAccount', ['only' => ['update']]);
}
Let's say I want to access the uri parameter that happens after account (i.e. example.com/account/2) within my Middleware - how do I go about grabbing that variable?
You can use the following code to achieve that:
public function handle($request, Closure $next)
{
$account_id = $request->route()->parameter('accounts');
//...
}
Since the handle method receives the Request object as the first argument. The middleware gets executed only after the route has been matched so the Request object contains the current route and no need to match the route again using Route::getRoutes()->match($request).
Doing this way you don't have to supply the \Request object:
Route::current()->parameter('parameter');
I have an app where the user submits a form which performs a SOAP exchange to get some data from a Web API. If there are too many requests in a certain time, the Throttle server denies access. I have made a custom error view for this called throttle.blade.php which is saved under resources\views\pages. In routes.php I have named the route as:
Route::get('throttle', 'PagesController#throttleError');
In PagesController.php I have added the relevant function as:
public function throttleError() {
return view('pages.throttle');
}
Here is the SoapWrapper class I have created to perform the SOAP exchanges:
<?php namespace App\Models;
use SoapClient;
use Illuminate\Http\RedirectResponse;
use Redirect;
class SoapWrapper {
public function soapExchange() {
try {
// set WSDL for authentication
$auth_url = "http://search.webofknowledge.com/esti/wokmws/ws/WOKMWSAuthenticate?wsdl";
// set WSDL for search
$search_url = "http://search.webofknowledge.com/esti/wokmws/ws/WokSearch?wsdl";
// create SOAP Client for authentication
$auth_client = #new SoapClient($auth_url);
// create SOAP Client for search
$search_client = #new SoapClient($search_url);
// run 'authenticate' method and store as variable
$auth_response = $auth_client->authenticate();
// add SID (SessionID) returned from authenticate() to cookie of search client
$search_client->__setCookie('SID', $auth_response->return);
} catch (\SoapFault $e) {
// if it fails due to throttle error, route to relevant view
return Redirect::route('throttle');
}
}
}
Everything works as it should until I reach the maximum number of requests allowed by the Throttle server, at which point it should display my custom view, but it displays the error:
InvalidArgumentException in UrlGenerator.php line 273:
Route [throttle] not defined.
I cannot figure out why it is saying that the Route is not defined.
You did not define a name for your route, only a path. You can define your route like this:
Route::get('throttle', ['as' => 'throttle', 'uses' => 'PagesController#throttleError']);
The first part of the method is the path of the route in your case you defined it like /throttle. As a second argument you can pass array with options in which you can specify the unique name of the route (as) and the callback (in this case the controller).
You can read more about routes in the documentation.