FOSRestBundle—create singular route - rest

I am creating a RESTFul application using the FOSRestBundle (2.0) for Symfony3. I have a Controller like so:
class GroupsController extends FOSRestController
{
public function getGroupAction($id) { … }
public function getGroupAddAction() { … }
}
running bin/console debug:router gives:
…
get_group GET ANY ajax.localhost.dev /groups/{id}
get_group_add GET ANY ajax.localhost.dev /group/add
…
But getting a single Group is the request for a single Item, so the route should be: [GET] /group/{id}, in my opinion.
Am I wrong with that? Should the correct route, to get a single group be plural?
So if my opinion is right, how can I change this single route manually? I have tried:
/**
* #NamePrefix("group")
*/
public function getGroupAction() { … }
But that results in an Error telling me, that I can use that annotation only for the Class, not the method.

Yes it should be plural.
If you want to have singular-routes, you can use this in your controller.
You can find it in the Symfony documentation.
/**
* #RouteResource("User", pluralize=false)
*/

Related

Symfony 4 setter injection in parent class

Just a quick question. I'm building some API. I was thinking about creating simple parent class that would deal with form requests.
So for example if you would like to easily handle form request you just extend this class and you get access to request object, request data extracted from that object and bunch of methods that do some things for you out of the box. It doesn't matter what and why exactly.
The problem is:
I send request through postman.
I try to use request object in class that extends parent class but instead of request I get null.
How do I set up the whole thing?:
Now in Symfony every controller is by default registered as a service so I override this definition like this:
#generic api form controller
App\Controller\Api\ApiFormController:
calls:
- [setDependencies, ['#request_stack', '#App\Service\Serialization\Serializer']]
So as you can see I am using setter injection.
I extend above class in my other class. Let's call it PostController. So:
<?php
namespace App\Controller\Api;
use Symfony\Component\HttpFoundation\RequestStack;
class ApiFormController
{
/**
* #var Request
*/
public $request;
/**
* #param RequestStack $requestStack
*/
public function setDependencies(
RequestStack $requestStack
) {
$this->request = $requestStack;
}
}
And now PostController:
public function get(int $post = null)
{
dump($this->request); exit;
}
I was expecting to get access like this and I think I understand why I don't have access to this object. I'm looking for some ideas how I could achieve this goal in cleanest possible way. I'm not expecting ready answers but hints.
I was thinking about using events to set it up in the background?
I also think it has something to do with the way I'm hooking up my controller as a service.
The core of it all: Symfony does not pick up service definition for subclasses. So if you define dependencies for a class and extend it in another class, you have to define the dependencies for this second class too.
The easiest way is to use the parent keyword for this, so your example would work in the following way:
App\Controller\Api\ApiFormController:
calls:
- [setDependencies, ['#request_stack', '#App\Service\Serialization\Serializer']]
PostController:
parent: App\Controller\Api\ApiFormController
If you are using autowiring, you can use #required to make Symfony call the setter automatically. https://symfony.com/doc/current/service_container/autowiring.html#autowiring-other-methods-e-g-setters
/**
* #param RequestStack $requestStack
* #required
*/
public function setDependencies(
RequestStack $requestStack
) {
$this->request = $requestStack;
}
This should do the trick.
I see several problems here.
If you want to inject dependencies in such a way you should define controller as service. You can read more here.
Routing should be something like this:
# config/routes.yaml
get_something:
path: /
defaults: { _controller: App\Controller\Api\PostController:get }
Also, you should define PostController as service, not ApiFormController.
You injected RequestStack but type hint for the attribute is Request.
Instead of:
$this->request = $requestStack;
You need to use:
$this->request = $requestStack->getMasterRequest();

Proper way to access the container from a class not in a slim controller

I have a regular php class outside of a controller, so it doesn't benefit from automatic injection of container. I need to access the response object from that class, and I guess I should get it from the container.
What's the proper way to access it ? Just pass it as argument so the outside class can use it ? Is there a better way ?
You need to use middleware for that because the response object is immutable so "changing" it will not update the response which will be used by slim.
$app->add(function($request, $response, $next) {
if($shouldRedirect === true) {
return $response->withRedirect('myurl'); // do not execute next middleware/route and redirect
}
return $next($request, $response); // execute next middleware/ the route
});
For more information about middleware have a look at this.
If you need to send a subrequest, Slim provides such functionality. Use it carefully though, as in some situations its result is not obvious.
<?php
class MySortOfOutsideClass
{
/**
* If you need to send a subrequest, you have to access application instance,
* so let's inject it here.
*/
public function __construct(\Slim\App $app)
{
$this->$app = $app;
}
/**
* Method that makes a subrequest, and returns the result of it.
*/
public function myMethod()
{
if ($subRequestIsRequired) {
return $this->app->subRequest('GET', '/hello');
}
}
}

Is splitting an index action into multiple ones a restful-friendly approach?

I need to display two different index pages to two different user groups. For example, a regular user should see one page, and a privileged user - another one. I see two ways of approaching this issue:
One index action with conditionals:
public function index()
{
// view for privileged users
if(request()->user()->hasRole('privileged')){
return view('index_privileged');
}
// view for regular users
if(request()->user()->hasRole('regular')){
return view('index_regular');
}
return redirect('/');
}
Multiple actions:
public function index_privileged()
{
return view('index_privileged');
}
public function index_regular()
{
return view('index_regular');
}
Which approach is more "restful-friendly" and generally better?
I'm a big fan of light controllers. This might be a little overboard for a simple problem but if something like this pops up again, you'd already have everything all setup.
With that said, it might be best to create a PrivilegedUser class and a RegularUser class and give them both an index method which returns their respective views. Code them both to an interface UserInterface and make sure they both implement that.
Here is what those looked like in my test.
class RegularUser implements UserInterface
{
public function index()
{
return view('index_regular');
}
}
class PrivilegedUser implements UserInterface
{
public function index()
{
return view('index_privileged');
}
}
interface UserInterface
{
public function index();
}
Then you can add a listener which should run for the event Illuminate\Auth\Events\Login. Laravel will fire this event for you automatically when someone logs in. This goes into the file EventServiceProvider.php.
protected $listen = [
'Illuminate\Auth\Events\Login' => [
'App\Listeners\AuthLoginListener',
],
];
Now you can run php artisan event:generate to generate the new listener. Here is what my listener looks like, it should work for you.
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use Illuminate\Foundation\Application;
class AuthLoginListener
{
/**
* Create the event listener.
*
* #param Application $app
*/
public function __construct(Application $app)
{
$this->app = $app;
}
/**
* Handle the event.
*
* #param Login $event
* #return void
*/
public function handle(Login $event)
{
if ($event->user->hasRole('privileged')) {
$this->app->bind('App\Repositories\UserInterface', 'App\Repositories\PrivilegedUser');
} else if ($event->user->hasRole('regular')) {
$this->app->bind('App\Repositories\UserInterface', 'App\Repositories\RegularUser');
}
}
}
Essentially what this is doing is telling Laravel to load up a certain class based on the type of user that just logged in. The User instance is available through the Login object which was automatically passed in by Laravel.
Now that everything is setup, we barely have to do anything in our controller and if you need to do more things that are different depending on the user, just add them to the RegularUser or PrivilegedUser class. If you get more types of users, simply write a new class for them that implements the interface, add an additional else if to your AuthLoginListener and you should be good to go.
To use this, in your controller, you'd do something like the following...
// Have Laravel make our user class
$userRepository = App::make('App\Repositories\UserInterface');
return $userRepository->index()->with('someData', $data);
Or even better, inject it as a dependency.
use App\Repositories\UserInterface;
class YourController extends Controller
{
public function index(UserInterface $user)
{
return $user->index();
}
}
Edit:
I just realized I forgot the part where you wanted to return redirect('/'); if no condition was met. You could create a new class GuestUser (I know this sounds like an oxymoron) which implements UserInterface but instead of using the AuthLoginListener, I'd bind it in a service provider when Laravel boots. This way Laravel will always have something to return when it needs an implementation of UserInterface in the event it needs this class if no one is logged in.
Well, its more like a refactoring "issue" than a rest-friendly issue. Check this guideline and you can see that most of the things that makes an api friendly is concerned to the url.
But, lets answer what you are asking. The thing you wanna do is a refactoring method but it is not only the move method but something like the extract variable.
The second option would make the code more readable, either ways are right but the second is more developer friendly. It enhances the code readability from any developer. I would recommend using the second option.
Refactoring is never enough, but read something like this, it will help you a lot writing more readable codes.

ManyToOne with FOSUSerBundle ignoring exclusion policy

Building a JSON response for an API type thing, to retrieve a specific set of data that includes a ManyToOne relationship in the entity for my entity that extends FOSUSerBundle's User entity (called Account in my case).
The problem is, the Account entity thats included as a field in the response, is wanted, but I dont want to include all of the password and role type stuff.
I've been browing the internet for a couple hours now, and I've followed many guides on this, and I've cleared my cache every single time, and to no avail; So here's where I ended up:
// app/config/config.yml
jms_serializer:
metadata:
auto_detection: true
directories:
FOSUserBundle:
namespace_prefix: "FOS\\UserBundle"
path: "%kernel.root_dir%/Resources/serializer/FOS"
I've for below I've tried User.Model.yml and Model.User.yml and User.Entity.yml as well in a vain thought that the file name actually matters
// app/Resources/serializer/FOS/Entity.User.yml
FOS\UserBundle\Model\User:
exclusion_policy: ALL
properties:
id:
expose: true
and what I get still looks like this:
{
"status":"ok",
"api_version":"1.0",
"code":200,
"data":{
"video":{
"id":1,
"published":true,
"visibility":true,
"title":"Megaman 2",
"slug":"megaman-2",
"summary":"A rap song about Megaman",
"description":"A rap song\r\nAbout megaman",
"youtube_id":"R6L9bUouDr8",
"date_published":"2014-07-02T14:09:26-0700",
"date_created":"2014-07-02T14:09:26-0700",
"date_updated":"2014-07-02T14:09:26-0700",
"author_id":3,
"author":{
"id":3,
"username":"kharrison",
"username_canonical":"kharrison",
"email":"(sorry private)",
"email_canonical":"(sorry, private)",
"enabled":true,
"salt":"(sorry, private)",
"password":"(sorry, private)",
"last_login":"2014-07-04T15:17:34-0700",
"locked":false,
"expired":false,
"roles":[
"ROLE_SUPER_ADMIN"
],
"credentials_expired":false,
"display_name":"Kyle Harrison",
"slug":"kyle-harrison",
"bio":"Test"
}
}
}
}
The "author" field, is my Account entity thats being run through the JMSSerializer
I want to exclude ALL of that, except the user ID, Display name, and slug.
And finally this is how the API works:
// My/Bundle/Controller/BaseAPIController.php
//......... other code
/**
* #param string $status
* #param integer $code
* #return Response
*/
public function render_api($status, $code)
{
$this->apiResponse->setStatus($status);
$this->apiResponse->setCode($code);
return new Response($this->apiResponse->serialize($this->get('jms_serializer')), $this->apiResponse->getCode(), ["Content-type"=>"application/json"]);
}
//............. other code
and finally, that calls this:
// My/Bundle/Models
class APIResponse {
protected $status;
protected $apiVersion;
protected $code;
protected $data;
public function __construct($apiVersion, $status = "OK", $code = 500)
{
$this->status = $status;
$this->code = $code;
$this->apiVersion = $apiVersion;
$this->data = [];
}
// ... getters and setters
/**
* #return mixed
*/
public function serialize($serializer) {
return $serializer->serialize($this, "json");
}
}
I've for below I've tried User.Model.yml and Model.User.yml and
User.Entity.yml as well in a vain thought that the file name actually
matters.
It does matter, actually. It's a concatenation of the namespace and class name. In this case, you're trying to configure the FOS\UserBundle\Model\User class, so the file name should be Model.User.yml. (FOS\UserBundle\ should be excluded from the file name, since you configured it as namespace_prefix in your config.yml)
Also make sure that your Account class doesn't re-declare (overwrite) the properties, as the serializer config only works if you configure it for the class that actually declares the properties.
Ok So, the actual answer, couldn't have been arrived to via the information I provided. But Nic's Answer did lead me towards the solution. The description of how the the serializer looks at and deciphers the config file lead me to the real problem at hand.
This is what I failed to show:
<?php
namespace [PRIVATE]\[PRIVATE]Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;
use JMS\Serializer\Annotation\Groups;
use JMS\Serializer\Annotation\VirtualProperty;
/**
* Account
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="[PRIVATE]\[PRIVATE]Bundle\Entity\AccountRepository")
*/
class Account extends BaseUser
{
The problem lays with the Alias I provided the FOS\UserBundle\Model\User namespace. I no longer remember why I wrote that that way. However, the moment I remove the Alias and rewrote the extends to resemble this instead:
<?php
namespace [PRIVATE]\[PRIVATE]Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;
use JMS\Serializer\Annotation\Groups;
use JMS\Serializer\Annotation\VirtualProperty;
/**
* Account
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="[PRIVATE]\[PRIVATE]Bundle\Entity\AccountRepository")
*/
class Account extends User
{
combined with the new correct filename from Nic's answer, the config based Exclusion policy for JMSSerializerBundle totally kicks in, and every instance of FOSUserBundle's items are now completely hidden, except for the fields I've now explicitly told it to expose.
This is exactly what I wanted :)
Thanks everyone for your help! Cheers
~k
I'm not sure it's the exact way you want it, more a way around:
way around 1: Select only the properties you want (via the entity manager) and then serialize the array obtained.
It's what I do with what I call my API (which is not a class as you but controllers)

Symfony 2.4.2 - Pass the entity manager between controllers in different bundles

I'm having a problem with passing the entity manager between two layers of controllers.
The system I'm building has the following structure:
2 Bundles:
Core Bundle (let's call it Backend Controller)
This is the bundle that contains all the Models (entities) and business rules/logic.
API Bundle (call it Frontend controller)
Is responsible for checking the permissions of passed in api keys and communicating with the Core bundle to get the info.
Here's an example with the User controllers and entities:
UserController.php in APIBundle:
<?php
namespace Acme\Bundle\APIBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Acme\Bundle\CoreBundle\Controller\UserController as User;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class UserController extends BaseController implements AuthenticatedController
{
public function readAction(Request $request) {
$user = new User($this->getDoctrine()->getManager());
$user->load(2);
return $this->response($user);
}
}
UserController.php in CoreBundle:
<?php
namespace Acme\Bundle\CoreBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Response;
use Acme\Bundle\CoreBundle\Entity\User;
class UserController extends BaseController
{
function __construct($em) {
parent::__construct($em);
$this->entity = new User();
}
/**
* Get userId
*
* #return integer
*/
public function getUserId()
{
return $this->entity->userId;
}
/**
* Set firstName
*
* #param string $firstName
* #return User
*/
public function setFirstName($firstName)
{
$this->entity->firstName = $firstName;
return $this;
}
// ...
public function load($id) {
if (!$this->entity instanceof \Acme\Bundle\CoreBundle\Entity\BaseEntity) {
throw new \Exception('invalid entity argument');
}
$this->entity = $this->em->getRepository('AcmeCoreBundle:User')->find($id);
}
}
Please, tell me if I'm doing this right. It seems strange to pass the entity manager between the controllers every time.
Maybe there's a better way of doing that?
Does the idea between the separation of the bundles make sense?
Thank you, every idea is greatly appreciated.
If CoreBundle UserController is never accessed through HTTP nor do its methods return instances of Symfony\Component\HttpFoundation\Response then it's not really a controller.
You should better define it as a service, as in CoreBundle\Service\User, and inject the EntityManager through the DI container.
sevices.yml
corebundle.userservice:
class: Acme\CoreBundle\Service\User
arguments: [#doctrine.orm.entity_manager]
It will then be available from Acme\Bundle\APIBundle\Controller\UserController with the following:
$user = $this->get('corebundle.userservice');
Of course, you can also define Acme\Bundle\APIBundle\Controller\UserController as a service on its own, then inject 'corebundle.userservice', for convenience.
I suggest you read the Symfony docs on Dependency Injection.
Search to get entity manager in Entity class is a wrong way !
In CoreBundle, you use the UserController.php same as an Entity class.
Read docs to understand how to properly use repository in symfony.
In UserController of APIBundle you must call a custom repository function. This repository is declare in your entity.