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);
Related
I have some response values I'm caching using middleware in Laravel, like this:
public function handle($request, Closure $next)
{
$domain = parse_url($request->headers->get('origin'), PHP_URL_HOST);
$key = 'request|'.$domain.'|dashboard';
return cache()->rememberForever($key, function () use ($request, $next) {
return $next($request);
});
}
If values within that response function change, I'd like to reset the cache, and ideally pre-set it before any user experiences the load time.
However I'm not entirely sure how to go about that given that I'm setting the cache value as a closure - $next($request). Inherently, it is caching the actual serialized response.
Is there an easy way for me to mock this response programatically?
Edit: to clarify - the above functionality works fine, what I'd like is something like below:
public function resetCache($key)
{
// Clear out old cache:
cache()->forget($key);
// Preload new cache:
cache()->set($key, function ($request, $next) {
return $next($request);
});
}
Obviously the function above won't work since I have no context on $request or $next.
I'm starting to think I'm going about this the wrong way, inherently.
Try this:
public function handle($request, Closure $next)
{
$domain = parse_url($request->headers->get('origin'), PHP_URL_HOST);
$key = 'request|'.$domain.'|dashboard';
$response = $next($request);
if($response !== Cache:get($key)) {
Cache:forget($key);
}
return cache()->rememberForever($key, function () use ($reponse) {
return $response;
});
}
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.
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.
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
In Slim Framework v2 I use simple authentication function as a hook to check if a route needs login.
This is authentication code:
$authenticate = function ( $app ) {
return function () use ( $app ) {
if ( !isset( $_SESSION['userid'] ) ) {
$_SESSION['urlRedirect'] = $app->request()->getPathInfo();
$app->flash('danger', 'You need to login');
$app->redirect('/login');
}
};
};
this is how I use in Slim v2:
$app->get("/user/change-password", $authenticate($app), function () use ( $app ) {
$userStuff->changePassowrd($_SESSION['userid'], $newPassword, $oldPassword);
});
I can implement this to Slim v3 without a problem but I can't seem to understand how I supposed to do this with middleware(to learn and use the functionally)
I have tried this: this is my middleware class;
<?php
namespace entity;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
class Auth
{
/**
* Example middleware invokable class
*
* #param \Psr\Http\Message\ServerRequestInterface $request PSR7 request
* #param \Psr\Http\Message\ResponseInterface $response PSR7 response
* #param callable $next Next middleware
*
* #return \Psr\Http\Message\ResponseInterface
*/
public function __invoke($request, $response, $next)
{
if ($this->authenticate()) {
$response = $next($request, $response);
} else {
echo 'You need to login';
}
return $response;
}
public function authenticate() {
if ( !isset( $_SESSION['userid'] ) ) {
return true;
}
return false;
}
}
?>
Registered it:
$app->add( new entity\Auth() );
and I don't know how to use this on a route as I did in Slim v2, where do I add this in the route to check if that route needs an authentication?
Thanks.
What you are looking for is actually excluding a route for the Auth middleware.
You do want that check for all other routes but not for changePassword.
In your __invoke you need to get the current route and only do authenticate if the route is not changePassword (if that's the name of your route).
public function __invoke($request, $response, $next)
{
$route = $request->getAttribute('route');
if (in_array($route->getName(), ['changePassword']) || $this->authenticate()) {
$response = $next($request, $response);
} else {
echo 'You need to login';
}
return $response;
}
Well the basic example is next
<?php
require 'vendor/autoload.php';
session_start();
class Auth {
public function __invoke($request, $response, $next) {
if ($this->authenticate()) {
$response = $next($request, $response);
} else {
echo 'You need to login';
}
return $response;
}
public function authenticate() {
if (isset($_SESSION['userid'])) {
return true;
}
return false;
}
}
$app = new \Slim\App();
$app->get('/open', function($request, $response, $args){
echo 'HAPPINESS FOR EVERYBODY, FREE, AND LET NO ONE BE LEFT BEHIND!';
});
$app->get('/closed', function($request, $response, $args){
echo 'You are authenticated!';
})->add(new Auth());
$app->get('/login', function($request, $response, $args){
$_SESSION['userid'] = 1;
});
$app->run();
I think there was a mistake in as authenticate function as !isset means there is no $_SESSION['userid'] and probably user is not logged in and your function tells that he is logged.
But anyway you can add middleware to single route or group of routes with add() method.