Welcome to Abdul Malik Ikhsan's Blog

Zend Framework 2 : Using DoctrineModule\Form\Element\ObjectSelect and custom repository

Posted in Tutorial PHP, Zend Framework 2 by samsonasik on May 22, 2014

zf2-zendframework2When we are using Doctrine2 in Zend Framework 2 project, we can use existing module named DoctrineModule that can be used to easist our job. Now I will explain about how to use DoctrineModule\Form\Element\ObjectSelect in our Form to load data into select element with custom query instead of default one.
For example, we have data like this :
countries-continent
Now, we need to build a form element that collect continent data, that’s means, we need to ‘group by continent’ for query-ing the table data like this :
continent-grab
To make it work, we need to create custom repository. Ok, let’s start.
1. Prepare the Entity

//module/Tutorial/src/Tutorial/Entity/Countries.php
namespace Tutorial\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Countries
 *
 * @ORM\Table(name="countries")
 * @ORM\Entity(repositoryClass="Tutorial\Repository\CountriesRepository")
 */
class Countries
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="bigint", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="country", type="string", length=30, nullable=false)
     */
    private $country;

    /**
     * @var string
     *
     * @ORM\Column(name="continent", type="string", length=30, nullable=false)
     */
    private $continent;



    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set country
     *
     * @param string $country
     * @return Countries
     */
    public function setCountry($country)
    {
        $this->country = $country;

        return $this;
    }

    /**
     * Get country
     *
     * @return string 
     */
    public function getCountry()
    {
        return $this->country;
    }

    /**
     * Set continent
     *
     * @param string $continent
     * @return Countries
     */
    public function setContinent($continent)
    {
        $this->continent = $continent;

        return $this;
    }

    /**
     * Get continent
     *
     * @return string 
     */
    public function getContinent()
    {
        return $this->continent;
    }
}

Above entity is pretty generic, but we add new “repositoryClass” attribute in the @ORM\Entity annotation to linked with our custom repository.
2. Create a custom repository

//module/Tutorial/src/Tutorial/Repository/CountriesRepository.php
namespace Tutorial\Repository;

use Doctrine\ORM\EntityRepository;

class CountriesRepository extends EntityRepository
{
    public function getContinent()
    {
        $querybuilder = $this->createQueryBuilder('c');
        return $querybuilder->select('c')
                    ->groupBy('c.continent')
                    ->orderBy('c.id', 'ASC')
                    ->getQuery()->getResult();
    }
}

The getContinent grab the countries data group by continent.
3. Create form

//module/Tutorial/src/Tutorial/Form/CountriesForm.php
namespace Tutorial\Form;

use Zend\Form\Form;
use Zend\InputFilter\InputFilterProviderInterface;
use Doctrine\ORM\EntityManager;

class CountriesForm extends Form
    implements InputFilterProviderInterface
{
    protected $entityManager;
    
    public function __construct(EntityManager $entityManager)
    {
        parent::__construct();

        $this->entityManager = $entityManager;       
    }
    
    public function init()
    {
        $this->add(array(
           'name' => 'continent',
           'type' => 'DoctrineModule\Form\Element\ObjectSelect',
           'options' => array(
                'object_manager'     => $this->entityManager,
                'target_class'       => 'Tutorial\Entity\Countries',
                'property' => 'continent',
                'is_method' => true,
                'find_method'        => array(
                    'name'   => 'getContinent',
                ),
            ), 
        ));
    }
    
    public function getInputFilterSpecification()
    {
        return array(); // filter and validation here
    }
}

We need to inject form with Doctrine\ORM\EntityManager, and call the getContinent method that we already define at our CountriesRepository. So the Form need to be created via factory :

//module/Tutorial/src/Tutorial/Factory/Form/CountriesFormFactory.php
namespace Tutorial\Factory\Form;

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Tutorial\Form\CountriesForm;

class CountriesFormFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $services         = $serviceLocator->getServiceLocator();
        $entityManager    = $services->get('Doctrine\ORM\EntityManager');
        
        $form = new CountriesForm($entityManager);
        return $form;
    }
}

4. Create controller

//module/Tutorial/src/Tutorial/Controller/CountriesController.php
namespace Tutorial\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\Form\FormInterface;
use Zend\View\Model\ViewModel;

class CountriesController extends AbstractActionController
{
    protected $countriesForm;
    
    public function __construct(FormInterface $countriesForm)
    {
        $this->countriesForm = $countriesForm;
    }
    
    public function indexAction()
    {
        return new ViewModel(array(
            'form' => $this->countriesForm, 
        ));
    }
}

We need to inject controller with the CountriesForm, so the Controller need to be created via factory :

//module/Tutorial/src/Tutorial/Factory/Controller/CountriesControllerFactory.php
namespace Tutorial\Factory\Controller;

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Tutorial\Controller\CountriesController;

class CountriesControllerFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $services       = $serviceLocator->getServiceLocator();
        $countryForm    = $services->get('FormElementManager')->get('Tutorial\Form\CountriesForm');
        $controller = new CountriesController($countryForm);

        return $controller;
    }
}

5. Register services

//module/Tutorial/config/module.config.php
return array(
    'doctrine' => array(
        'driver' => array(
            'Tutorial_Entities' => array(
                'class' =>'Doctrine\ORM\Mapping\Driver\AnnotationDriver',
                'cache' => 'array',
                'paths' => array(__DIR__ . '/../src/Tutorial/Entity')
            ),
            'orm_default' => array(
                'drivers' => array(
                    'Tutorial\Entity' => 'Tutorial_Entities'
                ),
            ),
        ),
    ),
    
    'controllers' => array(
        'factories' => array(
            'Tutorial\Controller\Countries' => 'Tutorial\Factory\Controller\CountriesControllerFactory',  
        ),
    ),
    
    'form_elements' => array(
        'factories' => array(
            'Tutorial\Form\CountriesForm' => 'Tutorial\Factory\Form\CountriesFormFactory',  
        ),
    ),
    
    'router' => array(
        'routes' => array(
            'countries' => array(
                'type'    => 'segment',
                'options' => array(
                    'route'    => '/countries[/:action]',
                    'constraints' => array(
                        'action' => '[a-zA-Z][a-zA-Z0-9_-]*',
                    ),
                    'defaults' => array(
                        'controller' => 'Tutorial\Controller\Countries',
                        'action'     => 'index',
                    ),
                ),
            ),
        ),
    ),
    
    'view_manager' => array(
        'template_path_stack' => array(
            'tutorial' => __DIR__ . '/../view',
        ),
    ),
    
);

6. Last but not least, build a view :

// module/Tutorial/view/tutorial/countries/index.phtml
$form = $this->form;
$form->prepare();

echo $this->form()->openTag($form);
echo $this->formCollection($form);
echo $this->form()->closeTag();

Ok, done 😉

35 Responses

Subscribe to comments with RSS.

  1. seyferx said, on May 22, 2014 at 12:15 pm

    I’m always made it via Form/Element/Select. Just inject Doctrine manager inside, make request and fill options with result.
    This way, what you showed like some binding. Thank.

  2. Webdevilopers (@webdevilopers) said, on May 22, 2014 at 2:34 pm

    Great work as always, @samsonasik!

    I didn’t take an In-Depth look at the hydration but regarding the label_generator option (https://github.com/doctrine/DoctrineModule/blob/master/docs/form-element.md#example-2–modifying-the-label) I wonder if Doctrine will always return an object hydrated result.

    Is array hydration possible? Because when you use a lot of this feature you may get a lot of overhead from the objects. Do you have any info about this?

  3. Webdevilopers (@webdevilopers) said, on May 22, 2014 at 7:56 pm

    BTW: I recognized that with the current version of Doctrine setting the object_manager on the formElement is obsolete when the Fieldset implements the ObjectManagerAwareInterface?

    • samsonasik said, on June 1, 2014 at 6:15 pm

      if you’re working with many forms, doing initializers is ok I think, but for one form, create one factory for it with contructor injection is not bad 🙂

  4. Igor Carvalho de Paula said, on June 11, 2014 at 12:29 am

    Great, and how i do with for annotations?

    • Jan Malte said, on September 1, 2014 at 3:25 pm

      I’m currently looking for a way to use doctrines ObjectSelect within a form build with annotations. This would absolutely increase the speed of rapid development.

      Does someone has a hint how to achieve it?

  5. Jan Malte said, on September 1, 2014 at 4:41 pm

    Using “DoctrineORMModule\Form\Annotation\AnnotationBuilder” instead of the “Zend\Form\Annotaion\AnnotationBuilder” will automatically resolve dependencies into ObjectSelect form elements.

  6. Tobi said, on January 7, 2015 at 9:25 am

    I am trying to follow your documentation but I am getting the error “Method “customFindBy” could not be found in repository “Doctrine\ORM\EntityRepository””. I am not populating the form via factories – does that have an effect? I don’t understand how the form knows how to look for your getContinent method.

    • samsonasik said, on January 7, 2015 at 12:24 pm

      the form injected with EntityManager which uses the entity that have repository, in your repository, you need to define your own “customFindBy” that return query result.

  7. Tobi said, on January 7, 2015 at 11:05 pm

    I do have “customFindBy” in my custom repository but it seems like Zend can’t find it. I have @ORM\Entity(repositoryClass=”Application\Repository\AssessmentRepository”) in my entity. Thank you for the quick reply, I really appreciate it!

    • samsonasik said, on January 8, 2015 at 1:21 am

      it should work as far as everything configured correctly. just need to be patient to check everything 😉

      • Tobi said, on January 8, 2015 at 2:04 am

        Okay, thanks again =D

  8. Akbar Adeeb said, on February 28, 2015 at 4:19 pm

    I want sql query of left join using doctrin in zf2. But don’t know how to achive this. I have lean about custom repositery in doctrin2. Can you please suggest me the best way to achive this in zf2

  9. George Istomin said, on March 4, 2015 at 7:25 am

    It makes sense to add the validation of the selected country in the class CountriesForm:

    public function getInputFilterSpecification()
    {
    return array(
    ‘continent’ => array(
    ‘validators’ => array(
    array(
    ‘name’ => ‘DoctrineModule\Validator\ObjectExists’,
    ‘options’ => array(
    ‘object_repository’ => $this->entityManager->getRepository(‘Tutorial\Entity\Countries’),
    ‘fields’ => ‘id’
    )
    )
    )
    )
    );
    }

    • samsonasik said, on March 4, 2015 at 9:09 pm

      yes, this is just a demo for custom repository in objectselect

  10. Akbar Adeeb said, on March 5, 2015 at 5:21 pm

    Hi i want to select few fields from table. The query ren successfull but it return result in array. i also try hydrate but it does not work my code is foulling

    $qb = $this->_em->getRepository($this->getEntityName())
    ->createQueryBuilder(‘p’);

    $qb->select(‘p.name,p.price,c.name as category’)
    ->leftJoin(‘\Application\Entity\Category’, ‘c’, ‘WITH’, ‘c.id = p.cid’);
    return $qb->getQuery()->getResult();

  11. Pat said, on July 8, 2015 at 10:26 pm

    Hi, how can I show the Continent and Country name in the option, instead of Continent only? For Example: China, Asia. Thx!

  12. Sergei Vakulenko said, on June 6, 2016 at 6:59 pm

    how to implement this in Zend expressive ?

  13. Akshaya said, on September 26, 2016 at 5:34 pm

    How to implement this in Zend framework 3

  14. David Mintz said, on February 16, 2017 at 9:29 pm

    is there a way to tell the objectselect element you want it to use result caching? It appears there is not. yes, certainly, you can create a custom repository and tell the objectselect element to use your method which in turn can use caching. I am wondering, however, if there’s another way — for the truly lazy 🙂

  15. Sami said, on January 27, 2021 at 12:17 pm

    Hello Samsonasik, you tuto is good. I would like to adapt it to my zf3 project and I do not use Doctrine.
    Can I pass the service manager in the constructor of my forms to access my repos ? I heard that pass the service Manager in a constructor is not suitable.
    Any advice from you will be great !

    • samsonasik said, on January 28, 2021 at 12:50 pm

      Hello, you can use a factory to inject real service to the service constructor.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: