How to dynamically call methods with slim router? - php

I'm setting up slim router v4, and I'd like to be be able to dynamically call the controller methods, using the placeholder from the route.
I.e when a request is made to 'example.com/users/{action}', the router would call the method from Users.php controller automatically without me having to specify the routes manually.
Basically I'm trying to avoid manually adding over 100 group->get(...) when they're all under /user route.
namespace core\router;
use Slim\Interfaces\RouteCollectorProxyInterface;
use app\controllers\users;
$app->group('/user', function(RouteCollectorProxyInterface $group){
$group->get('/get-name', '\Users:name')
$group->get('/get-personality', '\Users:personality');
});
Further explanation is provided here but I'm not sure how to go about this.

The way I would suggest doing this is having a single, catch all route with a placeholder. You can then set action to an invocable controller, and execute a method based on the route parameter.
Route:
$app->get('/user/{method}', Users::class);
Controller
class Users
{
public function __invoke(Request $request, Response $response, $args)
{
if (empty($args['method'])) {
throw new InvalidArgumentException();
}
$methodName = toCamelCase($args['method']);
if (!method_exists($this, $methodName)) {
throw new InvalidArgumentException();
}
return $this->{$methodName};
}
public function getName(Request $request, Response $response)
{
// ...
}
public function getPersonality(Request $request, Response $response)
{
// ...
}
}

Related

Laravel constructor and method injection

I am having an issue setting up an injection on both the constructor and the method in a controller.
What I need to achieve is to be able to set up a global controller variable without injecting the same on the controller method.
From below route;
Route::group(['prefix' => 'test/{five}'], function(){
Route::get('/index/{admin}', 'TestController#index');
});
I want the five to be received by the constructor while the admin to be available to the method.
Below is my controller;
class TestController extends Controller
{
private $five;
public function __construct(PrimaryFive $five, Request $request)
{
$this->five = $five;
}
public function index(Admin $admin, Request $request)
{
dd($request->segments(), $admin);
return 'We are here: ';
}
...
When I run the above, which I'm looking into using, I get an error on the index method:
Symfony\Component\Debug\Exception\FatalThrowableError thrown with message "Argument 1 passed to App\Http\Controllers\TestController::index() must be an instance of App\Models\Admin, string given"
Below works, but I don't need the PrimaryFive injection at the method.
class TestController extends Controller
{
private $five;
public function __construct(PrimaryFive $five, Request $request)
{
$this->five = $five;
}
public function index(PrimaryFive $five, Admin $admin, Request $request)
{
dd($request->segments(), $five, $admin);
return 'We are here: ';
}
...
Is there a way I can set the constructor injection with a model (which works) and set the method injection as well without having to inject the model set in the constructor?
One way you could do this is to use controller middleware:
public function __construct()
{
$this->middleware(function (Request $request, $next) {
$this->five = PrimaryFive::findOrFail($request->route('five'));
$request->route()->forgetParameter('five');
return $next($request);
});
}
The above is assuming that PrimaryFive is an Eloquent model.
This will mean that $this->five is set for the controller, however, since we're using forgetParameter() it will no longer be passed to your controller methods.
If you've specific used Route::model() or Route::bind() to resolve your five segment then you can retrieve the instance straight from $request->route('five') i.e.:
$this->five = $request->route('five');
The error is because of you cannot pass a model through the route. it should be somethiing like /index/abc or /index/123.
you can use your index function as below
public function index($admin,Request $request){}
This will surely help you.
Route::group(['prefix' => 'test/{five}'], function () {
Route::get('/index/{admin}', function ($five, $admin) {
$app = app();
$ctr = $app->make('\App\Http\Controllers\TestController');
return $ctr->callAction("index", [$admin]);
});
});
Another way to call controller from the route. You can control what do you want to pass from route to controller

Slim 3 framework - Passing data from middleware to controller - action args

I'm using a controller/middleware build with slim 3 and i want from the middleware attached to a group, to pass some data to the $args parameter in my controller - action.
Here's some code:
class MyController
{
protected $container;
public function __construct(ContainerInterface $container) {
$this->container = $container;
}
public function index(Request $request, Response $response, $args) {
return $this->container->get('renderer')->render($response, "index.html.twig", $args);
}
}
class MyMiddleware
{
public function __invoke(Request $request, Response $response, $next)
{
// do some stuff to inject further down to $args some data
return $next($request, $response);
}
}
$app->group('/group', function () use ($app){
//routes
})->add(new MyMiddleware());
My use case is to send stuff to all the views rendered by the actions of these controllers, so i'm also fine with other ways to do this :)
Thanks.
so you need just pass data from Middleware to Controller
what about
class MyMiddleware
{
public function __invoke(Request $request, Response $response, $next)
{
$request = $request->withAttribute('myMagicArgument', 42);
return $next($request, $response);
}
}
and then in controller
class MyController
{
//...
public function index(Request $request, Response $response) {
$yourAttributeFromMiddleware = $request->getAttribute('myMagicArgument');
//...
}
}
For completeness, I'm going to extend the excellent answer given #jDolba
Unfortunately, though it got me going in the right direction, it still took a little experimentation to get everything working.
Basically, as explained in the slim router docs
The route callback signature is determined by a route strategy. By
default, Slim expects route callbacks to accept the request, response,
and an array of route placeholder arguments. This is called the
RequestResponse strategy. However, you can change the expected route
callback signature by simply using a different strategy. As an
example, Slim provides an alternative strategy called
RequestResponseArgs that accepts request and response, plus each route
placeholder as a separate argument. Here is an example of using this
alternative strategy; simply replace the foundHandler dependency
provided by the default \Slim\Container:
$c = new \Slim\Container();
$c['foundHandler'] = function() {
return new \Slim\Handlers\Strategies\RequestResponseArgs();
};
$app = new \Slim\App($c);
$app->get('/hello/{name}', function ($request, $response, $name) {
return $response->write($name);
});
You can provide your own route strategy by implementing the
\Slim\Interfaces\InvocationStrategyInterface.
however, for the task of injecting some standardised data into the $args[] array, the default \Slim\Handlers\Strategies\RequestResponse class does everything it needs to minus injecting the data.
As such, I simply extended that class:
<?php
namespace MyProject\Handlers\Strategies;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use \Slim\Handlers\Strategies\RequestResponse;
class SomeNewInvocationStrategy extends RequestResponse
{
/**
* Invoke a route callable.
*
* #param callable $callable The callable to invoke using the strategy.
* #param ServerRequestInterface $request The request object.
* #param ResponseInterface $response The response object.
* #param array $routeArguments The route's placholder arguments
*
* #return ResponseInterface|string The response from the callable.
*/
public function __invoke( callable $callable, ServerRequestInterface $request, ResponseInterface $response, array $routeArguments)
{
$routeArguments['test'] = 'testing testing 123';
return parent::__invoke( $callable, $request, $response, $routeArguments );
}
}
My container declaration looks like this:
<?php
use Slim\App;
return function (App $app) {
$container = $app->getContainer();
$container['foundHandler'] = function() {
return new MyProject\Handlers\Strategies\SomeNewInvocationStrategy();
};
}
Then in all of my controller actions I have access to $args['test']. Further, this can be passed straight through to any Twig views.
This is useful for tasks like access control where by I always want to load the roles of users before processing the request but I'm sure there'll be many other use-cases for it.
I Hope this helps somebody.

Pass in an id for Authorization Laravel

In my user controller I have an posts function, which gives access to a sub-resource of users. This is accessed through the /users/{id}/posts endpoint.
I want the pass the $id from the request URL into a UserPolicy method:
public function resource($user, $id)
{
return $user->id === $id;
}
My UserController method:
public function posts(Request $request, $id)
{
$this->authorize('resource', $id);
return response()->json(['events' => []], 200);
}
Is there anyway to do this? I notice that Policy methods seem to ignore anything that isn't an object.
Edit:
I am currently using a helper method for this authorization but would like to move it to my Policy to keep all rules together:
public function authorizeResource($id)
{
if ((int)$id !== (int)$this->auth->user()->id) {
throw new \Exception;
}
}
Laravel needs to know which policy class to use. For that you need to specify the model, in this case passing an array with an instance of user first and then the $id. Laravel uses the spread operator and will inject the $id as a parameter on your callback function.
//UserController.php
public function posts(Request $request, $id)
{
$this->authorize('resource', [User::class, $id]);
return response()->json(['events' => []], 200);
}

How to pass POST request to constructor in Laravel?

I'd like to access a request variable from the constructor of my controller in Laravel. How can I do this?
this is my route:
Route::post('bookGetById', ['uses' => 'v1\BookController#getBookById']);
and here is my controller:
public function __construct(Request $request = null)
{
parent::__construct();
$this->bookStructure = new bookStructure($request->imageHeight);
}
but the request variable is always null. How can I pass the request into the constructor?
Don't know what version of laravel you're using but I think in 5.2+ the controller is created before the request is bound. There's a workaround:
public function __construct()
{
parent::__construct();
$this->middleware(function (Request $r, $next) {
$this->bookStructure = new bookStructure($request->imageHeight);
return $next($r);
});
}
However I would recommend doing this is via the service container:
File AppServiceProvider
$this->app->bind("bookstructure.withheight", function ($app) {
return new bookStructure($app->make("request")->get("imageHeight",0));
});
You can instantiate your bookStructure wherever you need it via:
app()->make("bookstructure.withheight");
I know you are asking how to pass a request to a class constructor but you may want to rethink it and pass it to a controller that validates the request and instantiates your class with validated request variables.

Ho to Implement Slim like middleware mechanism

I decided to implement my own small framework to implement such stuff like dependency injection etc.
Now I'm stucking at my middleware implementation. I can add middleware to a route but I im wondering how slim loops through the attached middleware.
I'd like to do it the slim way, so in every middleware I can return a request or a response or the next middleware. But how do I have too iterate over my attached middleware.
Here is my stack I want to proceed
class MiddlewareStack
{
private $stack;
public function addMiddleware(Middleware $middleware)
{
$this->stack[] = $middleware;
}
public function processMiddleware(Request $request, Response $response)
{
}
}
and thats the middleware interface
public function __invoke(Request $request, Response $response, $next);
I want to
return $next($request,$response);
in my middleware classes or just a response or a request.
Here's how to create middlware callable in slim.
http://www.slimframework.com/docs/concepts/middleware.html#invokable-class-middleware-example
Slim 3 first adds itself to the stack which is the Slim\App#__invoke() which executes the route.
Then, when you add a middleware it does the following: (before this slim wrapps the callable (annonymous function/invokable class) inside a DeferredCallable which helps to execute both the function and class equally (See Slim\App#add()).
protected function addMiddleware(callable $callable) // $callable is a DeferredCallable
{
$next = $this->stack->top(); // when it the first middleware this would be the route execution
$this->stack[] = function (ServerRequestInterface $req, ResponseInterface $res) use ($callable, $next) {
$result = call_user_func($callable, $req, $res, $next);
return $result;
};
}
(This is only the simple code, for full code see: Slim\MiddlewareAwareTrait#addMiddleware())
So the middleware which is on top of the stack executes the other middleware as well, because it is provided in the next method.
Then when you want to execute the middleware, get the middleware which is on top of the stack and execute it.
$start = $this->stack->top();
$resp = $start($req, $res);
// $resp is now the final response.
(see Slim\MiddlewareAwareTrait#callMiddlewareStack())

Categories