I have a Slim Framework application with a custom errorHandler, and a small middleware stack. My middleware adds attributes to the Request object which I would like to have access to from my error handler in the event of an exception. For example:
$app->get('/endpoint', function($request $response, $args) {
$myAttribute = $request->getAttribute('myAttribute'); //returns 'myValue'
throw new \Exception(); //any code that throws an error
})->add(function($request, $response, $next) {
$request = $request->withAttribute('myAttribute', 'myValue');
$response = $next($request, $response);
return $response;
});
$app->getContainer['errorHandler'] = function($c) {
return function($request, $response, $exception) {
$myAttribute = $request->getAttribute('myAttribute'); //returns null
return $response;
}
};
The attribute does not exist within the Request object inside the error handler because the cloned Request from inside the route has not been returned after traversing through the middleware stack. Is it possible to access the Request and Response objects as they exist, at the time (in the location) the exception is thrown? I can't explicitly pass them (for example, SlimException) because I'm trying to handle unexpected errors as well.
I've created two, somewhat hacky, solutions to capturing Request and Response state when exceptions are thrown. Both attempt to insert a try/catch as close to the center of the middleware stack as possible (without repeating code), and involve wrapping the original exception in a new exception class together with the modified endpoint parameters.
Middleware
This works as long as attention is paid to the order that middleware is added. Unfortunately it does require the innermost middleware is added to each route, or at the very least, "before" the middleware modifying the Request and/or Response objects. This won't catch any changes to Request/Response made inside, nor after the route.
class WrappedException extends \Exception {
public $exception;
public $request;
public $response;
public function __construct($exception, $request, $response) {
$this->exception = $exception;
$this->request = $request;
$this->response = $response;
}
}
$app->get('/endpoint', function($request $response, $args) {
throw new \Exception(); //any code that throws an error
})->add(function($request, $response, $next) {
try {
$response = $next($request, $response);
} catch (\Exception $exception) {
throw new WrappedException($exception, $request, $response);
}
return $response;
});
$app->add(function($request, $response, $next) {
$request = $request->withAttribute('myAttribute', 'myValue');
$response = $next($request, $response);
return $response;
});
$app->getContainer['errorHandler'] = function($c) {
return function($request, $response, $exception) {
if ($exception instanceof WrappedException) {
//returns 'myValue'
$myAttribute = $exception->request->getAttribute('myAttribute');
}
return $response;
}
};
Route Class
This wraps all routes in a try block, and makes for a bit cleaner route code, but you must be sure to extend all routes from the RouteBase class.
class WrappedException extends \Exception {
public $exception;
public $request;
public $response;
public function __construct($exception, $request = null, $response = null) {
$this->exception = $exception;
$this->request = $request;
$this->response = $response;
}
}
class RouteBase {
public function __call($method, $arguments) {
if (method_exists($this, $method)) {
try {
$this::$method(...$arguments);
} catch (\Exception $e) {
throw new WrappedException($e, ...$arguments);
}
} else {
throw new \Exception('Route method not found.');
}
}
}
class RouteClass extends RouteBase {
//PROTECTED -- must be callable by parent class, but not outside class
protected function get(Request $request, Response $response, $args) {
throw new \Exception('A new exception!');
$response->write('hey');
return $response;
}
}
$app->get('/endpoint', 'RouteClass:get');
$app->add(function($request, $response, $next) {
$request = $request->withAttribute('myAttribute', 'myValue');
$response = $next($request, $response);
return $response;
});
$app->getContainer['errorHandler'] = function($c) {
return function($request, $response, $exception) {
if ($exception instanceof WrappedException) {
//returns 'myValue'
$myAttribute = $exception->request->getAttribute('myAttribute');
}
return $response;
}
};
Upgrade to Slim 4 (if you can)
Worth noting now that Slim 4 fixes this issue. Personally, it was kind of a pain to upgrade as a lot changed, especially with the Dependency Injection containers (I went from Slim v3.12 => 4.8).
In Slim 4 they moved both the routing and the error handling into (separate) middlewares.
Related
I'm using postman to test my API, but right now I have a problem with the put routes.
So this is a put route I wrote:
$app->put('/setting/{id}/settingvalue', function (ServerRequestInterface $request, Response
Interface $response, $args) {
try {
$user = new \Riecken\PBS\controller\SettingsValueController();
$result = $user->updateOneSettingsValueinDetail( $args['id'], $request->getParsedBody());
$response = $response->withJson($result);
$response = $response->withStatus(200);
return $response;
}catch(Exception $e) {
$response->getBody()->write($e->getMessage());
return $response->withStatus($e->getCode());
}
});
And this is the function which you see above (updateOneSettingsValueinDetail):
public function updateOneSettingsValueinDetail ($settingsvalueIdToUpdate, $body) {
try {
return $this->SettingsValueDao->update($settingsvalueIdToUpdate, $body);
}catch(DAOException $e) {
throw new \Exception($e->returnErrorMessage(), $e->returnHttpCode());
}catch(\Exception $e) {
throw new \Exception("System Error", 500);
}
}
The problem is that Postman tells me that the Method is not allowed, only POST and GET is allowed:
enter image description here
Does anybody know what type of problem that is and what the solution could be?
This response is come from Slim's NotAllowedHandler. And it is not only POST and GET as default. This response is not related to your mentioned above code.
Are you sure you don't customize "NotAllowedHandler" and you don't bind to app as middleware?
I wrote this code that containing that would create the same situation:
<?php
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\App;
require __DIR__ . '/../vendor/autoload.php';
$app = new App([]);
$container = $app->getContainer();
$app->add(function ($request, $response, $next) {
$allowed = ['GET', 'POST'];
if (!in_array($request->getMethod(), $allowed)) {
$notAllowed = new \Slim\Handlers\NotAllowed();
return $notAllowed($request, $response, $allowed);
}
$next($request, $response);
});
$app->put('/setting/{id}/settingvalue', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
die("Expected Context via PUT");
});
$app->get('/setting/{id}/settingvalue', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
die("Expected Other Context via GET");
});
$app->run();
I hope to it help you.
I am creating an API and need to implement the following functionality. Some roots need to be accessible to users that are:
List item
not loged in
logged in
logged in and are premium users
logged in and are premium users with x amount of points.
For this reason I chose to use slim groups. Mind you, I'm not sure that this is the proper usage for group middle ware, but this is what I did.
<?php
$ifa = function($request, $response, $next){
$a=true;
if ($a) {
$request = $request->withAttribute('foo', 'bar');
$response = $next($request, $response);
}
return $response;
};
$ifb = function($request, $response, $next){
$b=true;
if ($b) {
$response = $next($request, $response);
}
return $response;
};
$ifc = function($request, $response, $next){
$c=true;
if ($c) {
$response = $next($request, $response);
}
return $response;
};
$app->group('',function() use ($app, $ifb,$ifc){
$app->get('/home', 'AuthController:home')->setName('home');
$app->group('',function() use ($app,$ifc){
$app->get('/school', 'AuthController:school');
$app->group('',function() use ($app){
$app->get('/farm', 'AuthController:farm');
})->add($ifc);
})->add($ifb);
})->add($ifa);
I would like to separate my middleware code (named functions or classes) from my routes. Even though this set up currently works, I would prefer to work with classes, what would be a cleaner way of writing my code? I would appreciate any suggestions.
First $ifa can be simplified as
$ifa = function($request, $response, $next){
$request = $request->withAttribute('foo', 'bar');
return $next($request, $response);
};
$ifb and $ifc are useless and can be omitted and removed from route as they are essentially doing nothing (CMIIW).
To make it $ifa as class instead of anonymous function, you can declare it like this
<?php
class Ifa
{
public function __invoke($request, $response, $next)
{
$request = $request->withAttribute('foo', 'bar');
return $next($request, $response);
};
}
and in routes declaration
$app->group('/', function() {
//do something here
})->add(Ifa::class);
I'm trying to pass an array with data to middleware and format it based on Accept HTTP header.
The controller gets the data from db and should pass it to the response object. Response object write() method accepts only strings:
public function getData(Request $request, Response $response): Response {
return $response->write($this->getUsers());
# This line of code should be fixed
}
The middleware should get the response and format it correctly:
public function __invoke(Request $request, Response $response, callable $next) {
$response = $next($request, $response);
$body = $response->getBody();
switch ($request->getHeader('Accept')) {
case 'application/json':
return $response->withJson($body);
break;
case 'application/xml':
# Building an XML with the data
$newResponse = new \Slim\Http\Response();
return $newResponse->write($xml)->withHeader('Content-type', 'application/xml');
break;
case 'text/html':
# Building a HTML list with the data
$newResponse = new \Slim\Http\Response();
return $newResponse->write($list)->withHeader('Content-type', 'text/html;charset=utf-8');
break;
}
}
I have a few routes behaves similarly:
$app->get('/api/users', 'UsersController:getUsers')->add($formatDataMiddleware);
$app->get('/api/products', 'UsersController:getProducts')->add($formatDataMiddleware);
By using middlewares I can add such functionality in a declarative way, keeping my controller thin.
How can I pass the original data array to response and implementing this pattern?
The Response-Object doesn't provide this functionallity nor has some extensions to do that. So you need to adjust the Response-Class
class MyResponse extends \Slim\Http\Response {
private $data;
public function getData() {
return $this->data;
}
public function withData($data) {
$clone = clone $this;
$clone->data = $data;
return $clone;
}
}
Then you need to add the new Response to the Container
$container = $app->getContainer();
$container['response'] = function($container) { // this stuff is the default from slim
$headers = new Headers(['Content-Type' => 'text/html; charset=UTF-8']);
$response = new MyResponse(200, $headers); // <-- adjust that to the new class
return $response->withProtocolVersion($container->get('settings')['httpVersion']);
}
Now change the Response type to MyResponse and use the withData method
public function getData(Request $request, \MyResponse $response): Response {
return $response->withData($this->getUsers());
}
At the end you can use the getData method and use its value and process it inside the middleware.
public function __invoke(Request $request, \MyResponse $response, callable $next) {
$response = $next($request, $response);
$data = $response->getData();
// [..]
}
That would be the answer to your question. A better solution in my opinion, would be a helper class which does what your middleware does, then you could something like this:
public function getData(Request $request, Response $response): Response {
$data = $this->getUsers();
return $this->helper->formatOutput($request, $response, $data);
}
For that there is already a lib for: rka-content-type-renderer
According to http://www.slimframework.com/docs/concepts/middleware.html, route middleware is added as follows.
<?php
$app = new \Slim\App();
$mw = function ($request, $response, $next) {
// How is $arg accessed?
$response->getBody()->write('BEFORE');
$response = $next($request, $response);
$response->getBody()->write('AFTER');
return $response;
};
$app->get('/ticket/{id}', function ($request, $response, $args) {
$response->getBody()->write(' Hello ');
// $arg will be ['id'=>123]
return $response;
})->add($mw);
$app->run();
$arg will be the parameters array. How can this be accessed in the middleware?
http://help.slimframework.com/discussions/questions/31-how-pass-route-pram-to-middleware shows an approach to do so, however, appears to be an earlier release of Slim, and errors Fatal error: Call to undefined method Slim\\Route::getParams().
Route Params can be accessed through the routeInfo Attribute on Request, as such
$app->get('/hello/{name}', \HelloWorld::class . ':execute')
->add(function ($request, $response, $next) {
var_dump($request->getAttribute('routeInfo')[2]['name']);exit();
return $next($request, $response);
});
This is noted in this Github issue
I am building an RESTful API in Laravel 5.2.
In my resource controllers I want to use implicit model binding to show resources. e.g.
public function show(User $users)
{
return $this->respond($this->userTransformer->transform($users));
}
When a request is made for a resource that doesn't exist Laravel automatically returns the NotFoundHttpException
NotFoundHttpException
I want to return my own custom response but how can I do that for a query that is done using route model binding?
Would something like this Dingo API response answer be able to be implemented?
Or will I stick with my old code which was something like this:
public function show($id)
{
$user = User::find($id);
if ( ! $user ) {
return $this->respondNotFound('User does not exist');
}
return $this->respond($this->userTransformer->transform($users));
}
So I could see if a resource (user) was not found and return an appropriate response.
See if you can catch ModelNotFound instead.
public function render($request, Exception $e)
{
if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
dd('model not found');
}
return parent::render($request, $e);
}
I think a good place would be in the Handler.php file under /app/Exceptions
public function render($request, Exception $e)
{
if ($e instanceof NotFoundHttpException) {
// return your custom response
}
return parent::render($request, $e);
}
In Laravel 7 and 8 you can do something like this.
In app/Exception/Handler.php class, add the render() method like below(if it doesn't exist).
Note that instead of type hinting Exception class you should use Throwable .
use Throwable;
public function render($request, Throwable $e)
{
if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
//For API (json)
if (request()->wantsJson()) {
return response()->json([
'message' => 'Record Not Found !!!'
], 404);
}
//Normal
return view('PATH TO YOUR ERROR PAGE');
}
return parent::render($request, $e);
}