Symfony 3 - Outsourcing Controller Code into Service Layer - php

I am very new in Symfony 3 and I want to avoid
the business logic in my controllers.
What I have done so far is this:
<?php
namespace RestBundle\Controller;
use RestBundle\Entity\Attribute;
use RestBundle\Entity\DistributorProduct;
use RestBundle\Entity\AttributeValue;
use RestBundle\Entity\ProductToImage;
use Symfony\Component\HttpFoundation\Request;
use RestBundle\Entity\Product;
use FOS\RestBundle\Controller\FOSRestController;
/**
* Product controller.
*
*/
class ProductController extends FOSRestController
{
/**
* Creates a new Product entity.
*
*/
public function createProductAction(Request $request)
{
// Doctrine Manager
$em = $this->getDoctrine()->getManager();
// todo: get the logged in distributor object
$distributor = $em->getRepository('RestBundle:Distributor')->find(1);
// Main Product
$product = new Product();
$product->setEan($request->get('ean'));
$product->setAsin($request->get('asin'));
$em->persist($product);
// New Distributor Product
$distributorProduct = new DistributorProduct();
$distributorProduct->setDTitle($request->get('title'));
$distributorProduct->setDDescription($request->get('description'));
$distributorProduct->setDPrice($request->get('price'));
$distributorProduct->setDProductId($request->get('product_id'));
$distributorProduct->setDStock($request->get('stock'));
// Relate this distributorProduct to the distributor
$distributorProduct->setDistributor($distributor);
// Relate this distributorProduct to the product
$distributorProduct->setProduct($product);
$em->persist($distributorProduct);
// Save it
$em->flush();
$response = $em->getRepository('RestBundle:Product')->find($product->getUuid());
return array('product' => $response);
}
}
}
I know that this is not good code because all the business logic is in the controller.
But how and where can I put this code (set requests into model, persist and flush with doctrine, etc) into a service or use dependency injection for it? Or is service for this purpose not the right way?
I know this page and tutorial http://symfony.com/doc/current/best_practices/business-logic.html
but it is not clearify for me where to put CRUD Actions.
Do ONE service for save a whole project with all the related entities? And use the Symfony\Component\HttpFoundation\Request; in a service? So put the whole controller code where I get the request and assign to the models into a service?
Thanks

UPDATE 2: I've extended this answer in a post. Be sure to check it!
UPDATE: use Symfony 3.3 (May 2017) with PSR-4 service autodiscovery and PHP 7.1 types.
I will show you how I lecture controller repository decoupling in companies.
There are 2 simple rules:
there are no signs about Doctrine in controller
there is no new in the controller (static, non-DI approach) (there is now also Sniff for that)
Let's apply this to your controller
Note: this is pseudo code, I haven't tried that, but the logic should be easy to understand. If this is too many change, just check the steps 3 and 4.
We decouple create and save process. For both entities.
This will lead us to 4 services:
# app/config/services.yml
services:
_defaults:
autowire: true
App\Domain\:
resource: ../../App/Domain
App\Repository\:
resource: ../../App/Repository
1. Product Factory to decouple create process
// ProductFactory.php
namespace App\Domain\Product;
final class ProductFactory
{
public function createFromRequest(Request $request): Product
{
$product = new Product();
$product->setEan($request->get('ean'));
$product->setAsin($request->get('asin'));
return $product;
}
}
2. Distributor Product Factory to decouple create process
// DistributorProductFactory.php
namespace App\Domain\Product;
final class DistributorProductFactory
{
public function createFromRequestProductAndDistributor(
Request $request,
Product $product,
Distributor $distributor
): DistributorProduct {
$distributorProduct = new DistributorProduct();
$distributorProduct->setDTitle($request->get('title'));
$distributorProduct->setDDescription($request->get('description'));
$distributorProduct->setDPrice($request->get('price'));
$distributorProduct->setDProductId($request->get('product_id'));
$distributorProduct->setDStock($request->get('stock'));
// Relate this distributorProduct to the product
$distributorProduct->setProduct($product);
// Relate this distributorProduct to the product
$distributorProduct->setDistributor($distributor);
return $distributorProduct;
}
}
3. Create own ProductRepository service
// ProductRepository.php
namespace App\Repository;
use RestBundle\Entity\Product;
use Doctrine\ORM\EntityManagerInterface;
final class ProductRepository
{
/**
* #var EntityManagerInterface
*/
private $entityManager;
public funtion __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function save(Product $product): void
{
$this->entityManager->persist($product);
$this->entityManager->flush();
}
}
4. Create own DistributorProductRepository service
// DistributorProductRepository.php
namespace App\Repository;
use RestBundle\Entity\DistributorProduct;
use Doctrine\ORM\EntityManagerInterface;
final class DistributorProductRepository
{
/**
* #var EntityManagerInterface
*/
private $entityManager;
public funtion __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function save(DistributorProduct $distributorProduct): void
{
$this->entityManager->persist($distributorProduct);
$this->entityManager->flush();
}
}
5. And we finish with nice and thin controller!
namespace RestBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use FOS\RestBundle\Controller\FOSRestController;
final class ProductController extends FOSRestController
{
// get here dependencies via constructor
public function createProductAction(Request $request): array
{
// todo: get the logged in distributor object
$distributor = $em->getRepository('RestBundle:Distributor')->find(1);
$product = $this->productFactory->createFromRequest($request);
$distributorProduct = $this->distributorProductFactory->createFromRequestProductAndDistributor(
$request,
$product,
$distributor
);
$this->productRepository->save($product);
$this->distributorProductRepository->save($product);
return [
'product' => $product
];
}
}
That's all!

Related

New alternative for getDoctrine() in Symfony 5.4 and up

As my IDE points out, the AbstractController::getDoctrine() method is now deprecated.
I haven't found any reference for this deprecation neither in the official documentation nor in the Github changelog.
What is the new alternative or workaround for this shortcut?
As mentioned here:
Instead of using those shortcuts, inject the related services in the constructor or the controller methods.
You need to use dependency injection.
For a given controller, simply inject ManagerRegistry on the controller's constructor.
use Doctrine\Persistence\ManagerRegistry;
class SomeController {
public function __construct(private ManagerRegistry $doctrine) {}
public function someAction(Request $request) {
// access Doctrine
$this->doctrine;
}
}
You can use EntityManagerInterface $entityManager:
public function delete(Request $request, Test $test, EntityManagerInterface $entityManager): Response
{
if ($this->isCsrfTokenValid('delete'.$test->getId(), $request->request->get('_token'))) {
$entityManager->remove($test);
$entityManager->flush();
}
return $this->redirectToRoute('test_index', [], Response::HTTP_SEE_OTHER);
}
As per the answer of #yivi and as mentionned in the documentation, you can also follow the example below by injecting Doctrine\Persistence\ManagerRegistry directly in the method you want:
// src/Controller/ProductController.php
namespace App\Controller;
// ...
use App\Entity\Product;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\HttpFoundation\Response;
class ProductController extends AbstractController
{
/**
* #Route("/product", name="create_product")
*/
public function createProduct(ManagerRegistry $doctrine): Response
{
$entityManager = $doctrine->getManager();
$product = new Product();
$product->setName('Keyboard');
$product->setPrice(1999);
$product->setDescription('Ergonomic and stylish!');
// tell Doctrine you want to (eventually) save the Product (no queries yet)
$entityManager->persist($product);
// actually executes the queries (i.e. the INSERT query)
$entityManager->flush();
return new Response('Saved new product with id '.$product->getId());
}
}
Add code in controller, and not change logic the controller
<?php
//...
use Doctrine\Persistence\ManagerRegistry;
//...
class AlsoController extends AbstractController
{
public static function getSubscribedServices(): array
{
return array_merge(parent::getSubscribedServices(), [
'doctrine' => '?'.ManagerRegistry::class,
]);
}
protected function getDoctrine(): ManagerRegistry
{
if (!$this->container->has('doctrine')) {
throw new \LogicException('The DoctrineBundle is not registered in your application. Try running "composer require symfony/orm-pack".');
}
return $this->container->get('doctrine');
}
...
}
read more https://symfony.com/doc/current/service_container/service_subscribers_locators.html#including-services
In my case, relying on constructor- or method-based autowiring is not flexible enough.
I have a trait used by a number of Controllers that define their own autowiring. The trait provides a method that fetches some numbers from the database. I didn't want to tightly couple the trait's functionality with the controller's autowiring setup.
I created yet another trait that I can include anywhere I need to get access to Doctrine. The bonus part? It's still a legit autowiring approach:
<?php
namespace App\Controller;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\ObjectManager;
use Symfony\Contracts\Service\Attribute\Required;
trait EntityManagerTrait
{
protected readonly ManagerRegistry $managerRegistry;
#[Required]
public function setManagerRegistry(ManagerRegistry $managerRegistry): void
{
// #phpstan-ignore-next-line PHPStan complains that the readonly property is assigned outside of the constructor.
$this->managerRegistry = $managerRegistry;
}
protected function getDoctrine(?string $name = null, ?string $forClass = null): ObjectManager
{
if ($forClass) {
return $this->managerRegistry->getManagerForClass($forClass);
}
return $this->managerRegistry->getManager($name);
}
}
and then
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Entity\Foobar;
class SomeController extends AbstractController
{
use EntityManagerTrait
public function someAction()
{
$result = $this->getDoctrine()->getRepository(Foobar::class)->doSomething();
// ...
}
}
If you have multiple managers like I do, you can use the getDoctrine() arguments to fetch the right one too.

symfony 5 - event not being dispatched

I use an event subscriber to handle some actions when my order form is submitted.
Problem my event is not being dispached but symfony is able to find him because he tells me that my OrderEvent::ORDER_CREATE is orphan.
I excpected that execution was stopped with die('Hello you from subscriber'); but it's not.
Controller
public function commanderPanierAction(Request $request, SelectionWeb $selectionWeb, TableLumineuse $tableLumineuse, EventDispatcherInterface $eventDispatcher)
{
// DO PREVIOUS STUFF
$Order = new Order();
$OrderForm = $this->createForm(OrderForm::class, $Order);
if ($request->isMethod('POST')) {
$OrderForm->handleRequest($request);
if ($OrderForm->isSubmitted() && $OrderForm->isValid()) {
// OrderForm is valid proceed
$eventDispatcher->dispatch(
new OrderEvent($Order),
OrderEvent::ORDER_CREATE
);
}
}
OrderEvent
<?php
namespace App\Event;
use App\Entity\Order;
use Symfony\Contracts\EventDispatcher\Event;
class OrderEvent extends Event
{
public const ORDER_CREATE = 'order.created';
protected $order;
public function __construct(Order $order)
{
$this->order= $order;
}
public function getOrder(): Order
{
return $this->order;
}
}
OrderSubscriber
<?php
namespace App\EventSubscriber;
use App\Event\CommandeWebEvent;
use App\Service\SelectionWeb\SelectionWeb;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class OrderSubscriber implements EventSubscriberInterface
{
private $entityManager;
private $selectionWeb;
public function __construct(EntityManagerInterface $entityManager, SelectionWeb $selectionWeb)
{
$this->entityManager = $entityManager;
$this->selectionWeb = $selectionWeb;
}
public static function getSubscribedEvents()
{
return [
OrderEvent::ORDER_CREATE => [
// The higher the number, the earlier the method is called.
['processOrder', 10],
['notifyOrder', -10]
]
];
}
public function processOrder(OrderEvent $event)
{
// TODO
die('Hello you from subscriber');
}
public function notifyOrder(OrderEvent $event)
{
// TODO
}
}
EDIT
The only workaround found (thx to #nikserg) is to inject subscriber into controller action (subscriber has dependencies) then register my subscriber as service in services.yaml finaly use $eventDispatcher->addSubscriber($subscriber); before $eventDispatcher->dispatch(new OrderEvent($Order),OrderEvent::ORDER_CREATE);
It seems all that stuff is really complex for a task as simple as that
EDIT2
I found an another way I'm able to execute my subscriber without usage of $eventDispatcher->addSubscriber($subscriber); and only with $eventDispatcher->dispatch(new OrderEvent($Order)); only if I configure my subscriber as service in services.yaml but why symfony does need this information in services.yaml ? Thought that everything in src/ is avaible to be used as service..
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
# If I add those It works
App\EventSubscriber\OrderSubscriber:
autowire: true
EDIT3
My OrderSubscriber is loaded into container so why I should set it explicitly to being execute ? I can't figure out what's going on
php bin/console debug:container
---------------------------------------- ---------------------------------------
Service ID Class name
---------------------------------------- ----------------------------------------
App\EventSubscriber\OrderSuscriber App\EventSubscriber\OrderSuscriber
EDIT 4
If I set my OrderSubscriber explicitly there is two instances of it into container.
Why symfony execute one set explicitly and not the one set with resource: '../src/*'
Symfony will autowire your subscriber as service, if you will require it as argument in action:
public function commanderPanierAction(Request $request, SelectionWeb $selectionWeb, TableLumineuse $tableLumineuse, OrderSubscriber $orderSubscriber)
Of course, if your subscriber is registered properly.
But let me advice you not to create subscribers as objects manually. The main good thing about subscribers is that you know nothing about them, when you fire event. There could be dozens of subscribers to this event, and all of them will proceed your event. That will keep your code nice and lower cohesion.
It's in docs: https://symfony.com/doc/current/event_dispatcher.html#creating-an-event-subscriber
First I want to thank you all for your time and let me apologize my problem was due to a typo I wrote OrderSuscriber instead of OrderSubscriber that's why there was 2 services into my container and why defined service explicitly was working.

Symfony 2.8 Services issue

Since the last 4 hours I'm trying to understand the logic of Symfony 2 services and how they integrate in the application...
Basically I'm trying to set my EntityManager via a service and use it in a controller
I have the following structure
Bundle1/Controller/Bundle1Controller.php
Bundle1/Services/EntityService.php
Bundle2/Controller/Bundle2Controller.php
Bundle3/Controller/Bundle3Controller.php
....
I'm trying to make a REST API with different entry points, that's why I use multiple bundles bundle2,bundle3....
The logic is the following:
A POST is fired to Bundle2/Controller/Bundle2Controller.php
Bundle2Controller.php instances a new() Bundle1Controller.php
Inside Bundle1Controller I want to access a service entity_service in order to get my EntityManager
I have 2 cases in which I manage to land...
In Bundle1/Controller/Bundle1Controller if I try $this->container or $this->get('entity_service') I get a null everytime
If I set the container in Bundle2/Controller/Bundle2Controller and try $this->get('entity_service') I get You have requested a non-existent service "entity_service"
I will place all the code below
Bundle1/Controller/Bundle1Controller
<?php
namespace Bundle1\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use EntityBundle\Entity\TestEntity;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
class Bundle1Controller extends Controller
{
/**
* #param $response
* #return array
*/
public function verifyWebHookRespone($response){
$em = $this->get('entity_service')->getEm();
$array = json_decode($response);
$mapping = $em->getRepository('EntityBundle:TestEntity')
->findBy(["phone" => $array['usernumber']]);
return $mapping;
}
}
Bundle2/Controller/Bundle2Controller.php
<?php
namespace Bundle2\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Bundle1\Controller\Bundle1Controller;
class Bundle2Controller extends Controller
{
public function webhookAction(Request $request)
{
$data = $request->request->get('messages');
$model = new Bundle1Controller();
$responseMessage = $model->verifyWebHookRespone($data);
return new Response($responseMessage, Response::HTTP_CREATED, ['X-My-Header' => 'My Value']);
}
}
Bundle1/Services/EntityService.php
<?php
namespace EntityBundle\Services;
use Doctrine\ORM\EntityManager;
use Symfony\Component\DependencyInjection\Container;
class EntityService
{
protected $em;
private $container;
public function __construct(EntityManager $entityManager, Container $container)
{
$this->em = $entityManager;
$this->container = $container;
}
/**
* #return EntityManager
*/
public function getEm()
{
return $this->em;
}
}
services.yml
services:
entity_service:
class: Bundle1\Services\EntityService
arguments: [ "#doctrine.orm.entity_manager" , "#service_container" ]
Can anyone please help me with something regarding this issue?
How can I register a service and call it from anywhere no matter the bundle or another service?
You should check where your services.yml is located and whether it is imported in the config.yml
You can't just instantiate a controller and expect it to work, you need to set the container.
But you can call EntityManager without needing any other service by using;
$this->get('doctrine.orm.entity_manager');
I can't understand your structure or what you are trying to achieve, but those are the options to go about if you want to keep this structure.

Access Container or securityContext or EntityManager from MenuBuilder through RequestVoter

I found this piece of code shared in a Gist (somewhere I lost the link) and I needed something like that so I started to use in my application but I have not yet fully understood and therefore I am having some problems.
I'm trying to create dynamic menus with KnpMenuBundle and dynamic means, at some point I must verify access permissions via database and would be ideal if I could read the routes from controllers but this is another task, perhaps creating an annotation I can do it but I will open another topic when that time comes.
Right now I need to access the SecurityContext to check if the user is logged or not but not know how.
I'm render the menu though RequestVoter (I think) and this is the code:
namespace PlantillaBundle\Menu;
use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\Voter\VoterInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\SecurityContextInterface;
class RequestVoter implements VoterInterface {
private $container;
private $securityContext;
public function __construct(ContainerInterface $container, SecurityContextInterface $securityContext)
{
$this->container = $container;
$this->securityContext = $securityContext;
}
public function matchItem(ItemInterface $item)
{
if ($item->getUri() === $this->container->get('request')->getRequestUri())
{
// URL's completely match
return true;
}
else if ($item->getUri() !== $this->container->get('request')->getBaseUrl() . '/' && (substr($this->container->get('request')->getRequestUri(), 0, strlen($item->getUri())) === $item->getUri()))
{
// URL isn't just "/" and the first part of the URL match
return true;
}
return null;
}
}
All the code related to securityContext was added by me in a attempt to work with it from the menuBuilder. Now this is the code where I'm making the menu:
namespace PlantillaBundle\Menu;
use Knp\Menu\FactoryInterface;
use Symfony\Component\DependencyInjection\ContainerAware;
class MenuBuilder extends ContainerAware {
public function mainMenu(FactoryInterface $factory, array $options)
{
// and here is where I need to access securityContext
// and in the near future EntityManger
$user = $this->securityContext->getToken()->getUser();
$logged_in = $this->securityContext->isGranted('IS_AUTHENTICATED_FULLY');
$menu = $factory->createItem('root');
$menu->setChildrenAttribute('class', 'nav');
if ($logged_in)
{
$menu->addChild('Home', array('route' => 'home'))->setAttribute('icon', 'fa fa-list');
}
else
{
$menu->addChild('Some Menu');
}
return $menu;
}
}
But this is complete wrong since I'm not passing securityContext to the method and I don't know how to and I'm getting this error:
An exception has been thrown during the rendering of a template
("Notice: Undefined property:
PlantillaBundle\Menu\MenuBuilder::$securityContext in
/var/www/html/src/PlantillaBundle/Menu/MenuBuilder.php line 12") in
/var/www/html/src/PlantillaBundle/Resources/views/menu.html.twig at
line 2.
The voter is defined in services.yml as follow:
plantilla.menu.voter.request:
class: PlantillaBundle\Menu\RequestVoter
arguments:
- #service_container
- #security.context
tags:
- { name: knp_menu.voter }
So, how I inject securityContext (I'll not ask for EntityManager since I asume will be the same procedure) and access it from the menuBuilder?
Update: refactorizing code
So, following #Cerad suggestion I made this changes:
services.yml
services:
plantilla.menu_builder:
class: PlantillaBundle\Menu\MenuBuilder
arguments: ["#knp_menu.factory", "#security.context"]
plantilla.frontend_menu_builder:
class: Knp\Menu\MenuItem # the service definition requires setting the class
factory_service: plantilla.menu_builder
factory_method: createMainMenu
arguments: ["#request_stack"]
tags:
- { name: knp_menu.menu, alias: frontend_menu }
MenuBuilder.php
namespace PlantillaBundle\Menu;
use Knp\Menu\FactoryInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class MenuBuilder {
/**
* #var Symfony\Component\Form\FormFactory $factory
*/
private $factory;
/**
* #var Symfony\Component\Security\Core\SecurityContext $securityContext
*/
private $securityContext;
/**
* #param FactoryInterface $factory
*/
public function __construct(FactoryInterface $factory, $securityContext)
{
$this->factory = $factory;
$this->securityContext = $securityContext;
}
public function createMainMenu(RequestStack $request)
{
$user = $this->securityContext->getToken()->getUser();
$logged_in = $this->securityContext->isGranted('IS_AUTHENTICATED_FULLY');
$menu = $this->factory->createItem('root');
$menu->setChildrenAttribute('class', 'nav');
if ($logged_in)
{
$menu->addChild('Home', array('route' => 'home'))->setAttribute('icon', 'fa fa-list');
}
else
{
$menu->addChild('Some Menu');
}
return $menu;
}
}
Abd ib my template just render the menu {{ knp_menu_render('frontend_menu') }} but now I loose the FontAwesome part and before it works, why?
Your menu builder is ContainerAware, so I guess that in it you should access the SecurityContext via $this->getContainer()->get('security.context').
And you haven't supplied any use cases for the voter class, so I'm guessing you're not using the matchItem method.
You should definitely try to restructure your services so that the dependencies are obvious.
Per your comment request, here is what your menu builder might look like:
namespace PlantillaBundle\Menu;
use Knp\Menu\FactoryInterface;
class MenuBuilder {
protected $securityContext;
public function __construct($securityContext)
{
$this->securityContext = $securityContext;
}
public function mainMenu(FactoryInterface $factory, array $options)
{
// and here is where I need to access securityContext
// and in the near future EntityManger
$user = $this->securityContext->getToken()->getUser();
...
// services.yml
plantilla.menu.builder:
class: PlantillaBundle\Menu\MenuBuilder
arguments:
- '#security.context'
// controller
$menuBuilder = $this->container->get('plantilla.menu.builder');
Notice that there is no need to make the builder container aware since you only need the security context service. You can of course inject the entity manager as well.
================================
With respect to the voter stuff, right now you are only checking to see if a user is logged in. So no real need for voters. But suppose that certain users (administrators etc) had access to additional menu items. You can move all the security checking logic to the voter. Your menu builder code might then look like:
if ($this->securityContext->isGranted('view','homeMenuItem')
{
$menu->addChild('Home', array('route' ...
In other words, you can get finer controller over who gets what menu item.
But get your MenuBuilder working first then add the voter stuff if needed.

Get request service from a DataTransformer class

Short story:
I need to get the Request service from a class that doesn't inherit from the Controller class (it's a DataTransformer which -obviously- implements the DataTransformerInterface).
Long story:
I have an embedded form that has an email field. If the user enters an email which doesn't exists in my users database table, I want to create a new user with this email.
In order to do that, I need to set its IP, so I followed the embedded forms tutorial and the data transformer recipe, but finally I have no idea where I'm able to inject the Request instance to my DataTransformer constructor or something else.
If I was in a class extending form the Controller one, it would be as simple as:
$this->container->get('request')->getClientIp()
You can do this by "Referencing (Injecting) Services". In your case you want to inject the Request which is a service from a narrower scope.
If you are using transformers, you are probably already using a Custom Form Type, and are instantiating the Data Transformer within your Form Type BuildForm Method, see here for more info.
You want to inject the Request object to the custom Form Type, then it can passed to the Data Transformer as a constructor parameter.
To do this modify the services.yml file with in your bundle, and add a constructor to the Custom Form Type and the Custom Data Transformer like this:
// src/Acme/HelloBundle/Resources/config/services.yml
parameters:
// ...
services:
acme.type.custom_type:
class: Acme\HelloBundle\Form\Type\CustomType
scope: request
arguments: ["#doctrine.odm.mongodb.document_manager", "#request"]
tags:
- { name: form.type, alias: custom_type }
The update the CustomType Class like this:
<?php
// src/Acme/HelloBundle/Form/Type/CustomType.php
namespace Acme\HelloBundle\Form\Type;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Doctrine\ODM\MongoDB\DocumentManager;
use Acme\HelloBundle\Form\DataTransformer\CustomDataTransformer;
class CustomType extends AbstractType
{
private $request;
private $dm;
public function __construct(DocumentManager $dm, Request $request) {
$this->dm = $dm;
$this->request = $request;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Create a new Data Transformer that will be able to use the Request Object!
$transformer = new CustomDataTransformer($this->dm, $this->request);
$builder->addModelTransformer($transformer);
}
// ...
}
and finally add a constructor to the transformer similar to the one added in the Form Type:
<?php
// src/Acme/HelloBundle/Form/DataTransformer/CustomDataTransformer.php
namespace Acme\HelloBundle\Form\DataTransformer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\DataTransformerInterface;
use Doctrine\ODM\MongoDB\DocumentManager;
class CustomDataTransformer implements DataTransformerInterface
{
private $request;
private $dm;
public function __construct(DocumentManager $dm, Request $request) {
$this->dm = $dm;
$this->request = $request;
}
// ...
}
Notice that along with the Request I have injected the MongoDB DocumentManager, this is to show that multiple objects can be injected.
Ok, that's simple:
In my question I was assuming that the DataTransformer will be "magically" invoked, but it's instanced while building the form, so if it helps to anyone, here it is:
In the DataTransformer class (implementing the DataTransformerInterface):
Define the new class attributes in order to hold the dependency injection:
/**
* #var EntityManager
*/
private $entityManager;
/**
* #var \Symfony\Component\DependencyInjection\Container
*/
private $container;
Define the constructor like:
public function __construct( EntityManager $entityManager, Container $container )
{
$this->entityManager = $entityManager;
$this->container = $container;
}
In your form class (implementing the AbstractType)
Add the following calls to the setDefaultOptions method:
$resolver->setRequired( array( 'em', 'container' ) );
$resolver->setAllowedTypes( array(
'em' => 'Doctrine\Common\Persistence\ObjectManager',
'container' => 'appDevDebugProjectContainer',
) );
In the buildForm method, apply the transformer as defined in the transformer recipe but instance it as:
$entityManager = $options['em'];
$container = $options['container'];
$transformer = new FantasticTransformer( $entityManager, $container );
In your controller, when you're calling to the createForm method, is it possible to inject the EntityManager and the Container instances simply adding them as follows:
$form = $this->createForm( 'your_form', $lookup, array(
'action' => $this->generateUrl( 'your_action_url' ),
'em' => $this->getDoctrine()->getManager(),
'container' => $this->container
) );
Now, you can finally get the client IP from the request service calling to the container defined in the constructor of your DataTransformer class:
$ip = $this->container->get('request')->getClientIp();
Note that we're injecting the container instead of the request instance, it's due to the Symfony scopes.

Categories