Symfony 4 setter injection in parent class - forms

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();

Related

Symfony controllers

I have the task of implementing an Api with FOSRestBundle, but a have told explicitly I can't use inheritance on my controllers, and also i can't use the service container(It's supposed to make the API lighter). Without the extend on the controllers I can't access the AbstractController methods and without the service container my controllers are practically isolated. Is this a viable choice or is an insane? Any advise?
Not crazy though I would have expected that the same people who told you how not to do things would have also offered some advice on how to do them.
One approach is to break up the ControllerTrait (aka AbstractController) into smaller traits.
For example:
trait RouterTrait
{
/** #var RouterInterface */
protected $router;
/** #required NOTE THIS */
public function setRouter(RouterInterface $router)
{
$this->router = $router;
}
protected function generateUrl(
string $route,
array $parameters = array(),
int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string
{
return $this->router->generate($route, $parameters, $referenceType);
}
protected function redirect($url, $status = 302) : RedirectResponse ...
protected function redirectToRoute($route, array $parameters = array(), $status = 302) : RedirectResponse ...
The #required before the setRouter method is a little used annotation which causes the router service to be automatically injected by the container.
At this point, any controller or any other service that uses the router trait will have access to the router helper functions.
For example:
class UserCreateAction implements ActionInterface
{
use GuidTrait;
use RouterTrait;
...
public function __invoke(Request $request)
{
...
return $this->redirectToRoute('reg_person_register');
Composition over inheritance can be a good thing.
One final off-topic piece of advice. There is little or no reason to use FOSRestBundle anymore unless it is to support legacy projects. The Symfony based API Platform is really the goto solution for new projects.

FOSRestBundle—create singular route

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)
*/

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)

Add custom property to serialized object

I'm developing RESTful API for a web service. And I need to expose some properties that do not belong to an entity itself.
For example I have a Pizza entity object, it has it's own size and name properties. I'm outputting it in JSON format with FOSRestBundle and JMSSerializer. I've setup properties annotations for this entity to expose needed properties via serialization groups and it's working great.
But I need to add some properties that do not belong to the entity itself. For example I want my pizza to have property: isFresh that is determined by some PizzaService::isFresh(Pizza $pizza) service. How do I do this?
Should I inject some additional logic to serialization process (if so how)?
Should I create a wrapper entity with properties that I want to expose from original entity plus additional external properties?
Should I add property isFresh to the original Pizza entity and populate in in the controller before serialization?
Should I return additional data independent of entity data (in a sibling JSON properties for example)?
In other words: what are the best practices around this issue? Could you provide examples? Thank you.
I think you can do that with the VirtualProperty annotation :
/**
* #JMS\VirtualProperty
* #return boolean
*/
public function isFresh (){
...
}
Edit : another solution with the Accessor annotation
/** #Accessor(getter="getIsFresh",setter="setIsFresh") */
private $isFresh;
// ...
public function getIsFresh()
{
return $this->isFresh;
}
public function setIsFresh($isFresh)
{
$this->isFresh= $isFresh;
}
In your controller, you call the setIsFresh method
(See http://jmsyst.com/libs/serializer/master/reference/annotation)
I've decided to create my own class to serialize an entity.
Here's the example:
class PizzaSerializer implements ObjectSerializerInterface
{
/** #var PizzaService */
protected $pizzaService;
/**
* #param PizzaService $pizzaService
*/
public function __construct(PizzaService $pizzaService)
{
$this->pizzaService = $pizzaService;
}
/**
* #param Pizza $pizza
* #return array
*/
public function serialize(Pizza $pizza)
{
return [
'id' => $pizza->getId(),
'size' => $pizza->getSize(),
'name' => $pizza->getName(),
'isFresh' => $this->pizzaService->isFresh($pizza),
];
}
}
You just have to configure DC to inject PizzaService into the object serializer and then just call it like this from the controller:
$pizza = getPizzaFromSomewhere();
$pizzaSerializer = $this->get('serializer.pizza');
return $pizzaSerializer->serialize($pizza);
The object serializer will return an array that can be easily converted to JSON, XML, YAML or any other format by using real serializer like JMS Serializer. FOSRestBundle will do this automatically if you configured it so.

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.