0

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

JavierCane
  • 2,324
  • 2
  • 22
  • 19

2 Answers2

3

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.

Onema
  • 7,331
  • 12
  • 66
  • 102
  • Thanks for your answer, I'll do it like you're suggesting instead of using the method described in my own answer. I only have 1 question about it: Is it better to inject the Request object and set the service scope to request than inject the Container object and then get the request service? – JavierCane Sep 18 '13 at 22:29
  • 1
    Symfony2 provides the Service Container as a way to manage the instantiation of services (objects), that way you don't have to re-write your code if you where to re-use your Custom Form Type elsewhere. Also note that you could inject the DI container to your Custom Form Type using this method see http://stackoverflow.com/questions/12056178/how-to-access-service-container-in-symfony2-global-helper-function-service. But try not to rely on having the DI container as it will make your code less reusable, see http://bit.ly/PKqPj7. – Onema Sep 18 '13 at 23:24
1

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:

  1. In the DataTransformer class (implementing the DataTransformerInterface):

    1. Define the new class attributes in order to hold the dependency injection:

      /**
       * @var EntityManager
       */
       private $entityManager;
      
      /**
      * @var \Symfony\Component\DependencyInjection\Container
      */
      private $container;
      
    2. Define the constructor like:

      public function __construct( EntityManager $entityManager, Container $container )
      {
          $this->entityManager = $entityManager;
          $this->container = $container;
      }
      
  2. In your form class (implementing the AbstractType)

    1. Add the following calls to the setDefaultOptions method:

      $resolver->setRequired( array( 'em', 'container' ) );
      
      $resolver->setAllowedTypes( array(
                                   'em'        => 'Doctrine\Common\Persistence\ObjectManager',
                                   'container' => 'appDevDebugProjectContainer',
                              ) );
      
    2. 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 );
      
  3. 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
                                                           ) );
    
  4. 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.

JavierCane
  • 2,324
  • 2
  • 22
  • 19