Symfony nested resource routing not passing ids - php

Here's the gist of my problem. I'm creating an API in Symfony2 and I cannot seem to get nested routes to work.
This Works
api_v1_role_show:
pattern: /api/v1/roles/{roleId}
defaults: { _controller: rest_v1_roles_controller:showAction }
methods: [GET]
I can prove that it works by doing something like this
function showAction()
{
var_dump(func_get_args()); // array(1)
}
This Does Not Work
api_v1_permission_post:
pattern: /api/v1/roles/{roleId}/permissions
defaults: { _controller: rest_v1_role_permissions_controller:createAction }
methods: [POST]
I end up getting something like this:
function createAction()
{
//This should be array(1)
var_dump(func_get_args()); // array()
}
What am I missing? I've tried looking online for over an hour now and I can't seem to find anything on the subject. I have to wonder if it's a post action security thing.
REST Class Structuring
We have a lot of rest end-points in our application. We created a rest class that allows us to quickly add new end-points called the RestBaseController and it looks something like this:
class RestBaseController {
protected $urlParams;
public function showAction()
{
$this->urlParams = func_get_args();
//Shows the resource based on ID now stored in $this->urlParams;
}
public function createAction()
{
$this->urlParams = func_get_args();
$this->adjustParameters();
//creates the resource from JSON body, essentially
}
protected function adjustParameters()
{
return null;
}
}
Then comes the class that I'm having a problem with:
class RolePermissionsController extends RestBaseController
{
protected function adjustParameters()
{
$role = $this->em()->getRepository('AppBundle:Role')
->find($this->urlParams[0]); //This will give me an error saying offset 0 does not exist.
$this->roleId = $role->getRoleId();
}
}
My Question:
How would I get a nested URL to work in Symfony?

if I'm not mistaken, symfony routing usin Reflection and "named parameters".
For example: route: /api/v1/roles/{roleId}/permissions/{otherId}
public fucntion action($otherId, $roleId) // position here is not important, important name
Also you can:
public fucntion action(Request $request, $otherId, $roleId)
and first argument will be $request.
So man, change your architecture until it's not too late

In the end, I found two ways to fix this issue. Dmitry pointed out that symfony uses Reflection and "named parameters" to break down the routes into passable arguments that are similar to function(Request $request, [ ...$uriExtractedName1, $uriExtractedName2]) which means that I would have been able to something like this in
PHP 5.6+
RestBaseController.php
public function createAction(Request $request, ...$args)
{
$this->request = $request;
$this->urlParams = $args;
//...
}
But I'm currently working on a project in an earlier version so I had to something a little more simplistic.
PHP 5.4
routing.yml
api_v1_permission_post:
pattern: /api/v1/roles/{parentId}/permissions
defaults: { _controller: rest_v1_role_permissions_controller:createAction }
methods: [POST]
RestBaseController.php
public function createAction($parentId = null)
{
$this->parentId = $parentId;
//...
}
This solution has worked out quite elegantly. I don't like having to always call the parent id parentId but it's a small price to pay for having exceptionally small controller classes. I realized that the other routers always worked because we call the passed id id.

Related

Laravel singleton not working across controller/ViewComposer

In Laravel, I have a class that I would like to make available to the service controller, make some changes to in the controller action, and then render out with a ViewComposer.
I have done this several times before without issue, but for some reason this time my usual approach is not working - clearly I'm doing something different, and I'm beginning to suspect I've fundamentally misunderstood an aspect of what I am doing.
I have a ServiceProvider with this register() method:
public function register()
{
$this->app->singleton(HelperTest::class, function ($app) {
$pb = new HelperTest();
$pb->test = "jokes on you batman";
return $pb;
});
}
Then in my controller I'm doing the following:
private $helper;
public function __construct(HelperTest $pb)
{
$this->helper = $pb;
$this->helper->test = "hahah";
}
And then I have a viewcomposer doing the following:
private $helper;
public function __construct(HelperTest $pb)
{
$this->helper = $pb;
}
public function compose(View $view)
{
$view->with('output', $this->helper->test);
}
When I call {{ $output }} in the blade view, I expect to see hahah, but instead I get jokes on you batman.
My debugging has shown that all three of these methods are definitely being called. It looks to me like the ViewComposer is for some reason instantiating its own, fresh instance of the class. What am I doing wrong?
Thanks!
Execute php artisan optimize on your console, this will generate an optimized class loader for your application, then check if you can find your class HelperTest registered in services.php inside boostrap/cache. Until HelperTest is not registered there, Laravel IoC can't resolve your class.

Laravel lazy eager loading with repository

I started working with repositories in Laravel and came across a situation where I'm not sure if I'm handeling this the right way...
User repository:
interface UserRepositoryInterface
{
public function findByID(int $user_id);
public function load(User $user, array $relations);
}
class UserRepository implements UserRepositoryInterface
{
public function findByID(int $user_id)
{
return User::find($user_id);
}
public function load(User $user, array $relations)
{
$user->load($relations);
}
}
Client basecontroller:
protected $user_repository, $client;
class Controller extends BaseController
{
public function __construct(Request $request, UserRepositoryInterface $user_repository)
{
$this->user_repository = $user_repository;
$this->client = $this->user_repository->findByID($request->route('client_id'));
}
}
Some extension of the client basecontroller:
use App\Http\Controllers\...\Controller as ClientsController;
class SomeController extends ClientsController
{
public function index()
{
$this->user_repository->load($this->client, ['addresses', 'bank_accounts', 'etc']);
return $this->client;
}
}
While the index() functions does show the client with related models, it feels like my approach is wrong, but sinds I already have the client it feels more natural to load the missing related models then to do the call below (where I fetch the user again):
$this->client = $this->user_repository->findByIDWithRelations($user_id, ['...']);
Because the load() function in the repository doesn't return anything and I assign nothing in the index() function it feels bogus somehow... Can anyone confirm or deny this?
Update:
Take for example this piece of code below (does not work):
function addToArray($array, $value)
{
array_push($array, $value);
}
$array = ['a', 'b', 'c'];
addToArray($array, 'd');
foreach($array as $value)
{
echo $value;
}
The way I approached this in Laravel feels very similar, which makes me feel it's wrong :-).
While this topic may have different opinions, there's already some opinionated open-source packages that provides a simple but intuitive implementation of repositories for Laravel that allows you to use the following syntax for example:
$repository->with(['relationship']);
So that your repository is clean, all relations are lazy loaded unless you call the with method and pass an array to eager load some relations. The API of the following package is pretty simple & intuitive and I think it's going to help you in your approach.
Checkout: https://github.com/rinvex/repository/

Laravel Object Oriented

I am somewhat new to OOP, although I know about interfaces and abstract classes a bit. I have a lot of resource controllers that are somewhat similar in the bigger scheme of things, they all look like the example below, the only main difference is the index and what I pass to the index view.
What I simply need to know is, can I OO things up a bit with my resource controllers? For example, create one "main" resource controller in which I simply pass the correct instances using an interface for example? I tried playing around with this but I got an error that the interface wasn't instantiable, so I had to bind it. But that means I could only bind an interface to a specific controller.
Any advice, tips and pointers will help me out :)
class NotesController extends Controller
{
public function index()
{
$notes = Note::all();
return view('notes.index', compact('notes'));
}
public function create()
{
return view('notes.create');
}
public function show(Note $note)
{
return view('notes.show', compact('note'));
}
public function edit(Note $note)
{
return view('notes.edit', compact('note'));
}
public function store(Request $request, User $user)
{
$user->getNotes()->create($request->all());
flash()->success('The note has been stored in the database.', 'Note created.');
return Redirect::route('notes.index');
}
public function update(Note $note, Request $request)
{
$note->update($request->all());
flash()->success('The note has been successfully edited.', 'Note edited.');
return Redirect::route('notes.index');
}
public function delete($slug)
{
Note::where('slug', '=', $slug)->delete();
return Redirect::to('notes');
}
}
Note: Totally my opinion!
I would keep them how you have them. It makes them easier to read and understand later. Also will save you time when you need to update one to do something different from the rest. We tried this in a project I worked on and while granted it wasn't the best implementation, it is still a pain point to this day.
Up to you though. I'm sure people have done that in a way that they love and works great. Just hasn't been the case in my experience. I doubt anyone would look at your code though and criticize you for not doing it.
In Case you need to bind different Model instanses then you may use Contextual Binding, for example, put the following code in AppServiceProvider's register() method:
$this->app->when('App\Http\Controllers\MainController')
->needs('Illuminate\Database\Eloquent\Model')
->give(function () {
$path = $this->app->request->path();
$resource = trim($path, '/');
if($pos = strpos($path, '/')) {
$resource = substr($path, 0, $pos);
}
$modelName = studly_case(str_singular($resource));
return app('App\\'.$modelName); // return the appropriate model
});
In your controller, use a __construct method to inject the model like this:
// Put the following at top of the class: use Illuminate\Database\Eloquent\Model;
public function __construct(Model $model)
{
$this->model = $model;
}
Then you may use something like this:
public function index()
{
// Extract this code in a separate method
$array = explode('\\', get_class($this->model));
$view = strtolower(end($array));
// Load the result
$result = $this->model->all();
return view($view.'.index', compact('result'));
}
Hope you got the idea so implement the rest of the methods.

Symfony2: FOSRest: Allow route for both "/api/items" and "api/items/{id}"

So, I'm trying to create a simple REST api with Symfony2 and the FOSRestBundle.
It's going really well, however I've hit a wall that Google apparantly hasnt been able to answer :)
I understand that this:
public function getUsersAction(){
return 'A list of all the users';
}
Will create a route as such:
api_get_users GET ANY ANY /api/users.{_format}
And I know that add a function parameter:
public function getUsersAction($id){
return 'A specific users (with $id) details';
}
Will create a route as such:
api_get_users GET ANY ANY /api/users/{userid}.{_format}
However what if I want to make it, so both routes are available (like a proper REST api design)?
I tried doing something like
public function getUsersAction($userid = NULL){
But it didnt work (not a huge surprise)..
I'm pretty sure that I'm missing a simple piece of the puzzle, but I have no idea what.
Creating a getUserAction($id) and a getUsersAction() (not plural) should work, according to the documentation:
class UserController implements ClassResourceInterface
{
// ...
public function getUsersAction()
{} // "get_users" [GET] /users
public function getUserAction($slug)
{} // "get_user" [GET] /users/{slug}
// or when omittig "User":
public function cgetAction()
{} // "get_users" [GET] /users
public function getAction($slug)
{} // "get_user" [GET] /users/{slug}
// ...
}

How does Symfony2 passes the parameter of a URI to the controller Action method?

I have started learning Symfony2. I came across a doubt: if I have this route:
# app/config/routing.yml
hello:
path: /hello/{name}
defaults: { _controller: AcmeHelloBundle:Hello:index }
And this controller:
// src/Acme/HelloBundle/Controller/HelloController.php
namespace Acme\HelloBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
class HelloController
{
public function indexAction($name)
{
return new Response('<html><body>Ciao '.$name.'!</body></html>');
}
}
Internally Symfony2 (inside app/bootstrap.php.cache) calls the call user_func_array() PHP built-in function:
$arguments = $this->resolver->getArguments($request, $controller);
$response = call_user_func_array($controller, $arguments);
And the call to the getArguments() method returns an array of arguments to pass to the action method. But if the controller were:
// src/Acme/HelloBundle/Controller/HelloController.php
namespace Acme\HelloBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
class HelloController
{
public function indexAction($n)
{
return new Response('<html><body>Ciao '.$n.'!</body></html>');
}
}
Symfony would have complained with a RuntimeException because no param $n is set.
My question is: how Symfony controls this behaviour, I mean, if a route has a {name} param, why the controller must have an action method with a $name parameter and the parameter must be called $name?
Cause in plain PHP, this would work:
$name = 'Alex';
function indexAction($n) {
echo $n;
}
$f = 'indexAction';
$arguments = array($name);
call_user_func_array($f, $arguments);
Even if the functions signature accepts a param named as $n and not as $name.
I hope that this question is understandable, if not, please tell me and I will make an edit.
Thanks for the attention!
It's all done in the Controller Resolver HttpKernel/Controller/ControllerResolver.php via getArguments() & doArguments().
For a better understanding, you will find what you need in Getting The Controller Arguments
Edit: Answer comment.
Does Symfony use the ReflectionParameter class internally in order to keep track of the params of the method's signature and match them with the route params?
Yes, the ControllerResolver uses:
ReflectionMethod to keep track of the params of the method's signature if $controller is a method
ReflectionObject if $controller is an object
ReflectionFunction if $controller is an function
Here is how:
public function getArguments(Request $request, $controller)
{
if (is_array($controller)) {
$r = new \ReflectionMethod($controller[0], $controller[1]);
} elseif (is_object($controller) && !$controller instanceof \Closure) {
$r = new \ReflectionObject($controller);
$r = $r->getMethod('__invoke');
} else {
$r = new \ReflectionFunction($controller);
}
return $this->doGetArguments($request, $controller, $r->getParameters());
}
The request is handled by the Symfony front controller (e.g. app.php);
The Symfony core (i.e. Kernel) asks the router to inspect the request;
The router matches the incoming URL to a specific route and returns information about the route, including the controller that should be executed;
The Symfony Kernel executes the controller, which ultimately returns a Response object.
Resource
In your example you only have one parameter on your action, so it's obvious to us that it needs to be populated from the route.
To extend your example, if you added another parameter to the route such as:
# app/config/routing.yml
hello:
path: /hello/{name}/{surname}
defaults: { _controller: AcmeHelloBundle:Hello:index }
And changed your controller to:
// src/Acme/HelloBundle/Controller/HelloController.php
namespace Acme\HelloBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
class HelloController
{
public function indexAction($s, $n)
{
return new Response('<html><body>Ciao '.$n.' '.$s.'!</body></html>');
}
}
Symfony wouldn't know which variable $s or $n to populate with which route parameter.
If you change your action:
public function indexAction($surname, $name)
{
return new Response('<html><body>Ciao '.$name.' '.$surname.'!</body></html>');
}
Now, Symfony can look at your argument names and map them to the route parameters. It also means you can have your indexAction arguments in any order as long as their names match the route parameters. I believe Symfony internally uses the Reflection API to figure all this out.

Categories