Zend Framework 2 : Using DoctrineModule\Form\Element\ObjectSelect and custom repository
When 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 :
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 :
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 😉
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.
you’re welcome 😉
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?
Thanks, I don’t have idea about it yet
I posed a question on git, maybe we’ll get an answer! 🙂
https://github.com/doctrine/DoctrineModule/issues/415
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?
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 🙂
Great, and how i do with for annotations?
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?
Using “DoctrineORMModule\Form\Annotation\AnnotationBuilder” instead of the “Zend\Form\Annotaion\AnnotationBuilder” will automatically resolve dependencies into ObjectSelect form elements.
Thank you very much for sharing 😉
OMG! that is life-changing. 🙂 why is this such a well-kept secret?
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.
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.
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!
it should work as far as everything configured correctly. just need to be patient to check everything 😉
Okay, thanks again =D
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
actually, doctine 2 left join itself is nothing to do with zf2, so, you can read the docs http://doctrine-orm.readthedocs.org/en/latest/reference/query-builder.html#high-level-api-methods
I leanr this but i’m not able to understand in which class we have to put this query builder
in the repositoryClass. try, debug, do effort 😉
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’
)
)
)
)
);
}
yes, this is just a demo for custom repository in objectselect
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();
please read doctrine doc for partial object syntax http://docs.doctrine-project.org/en/latest/reference/dql-doctrine-query-language.html#partial-object-syntax
Hi, how can I show the Continent and Country name in the option, instead of Continent only? For Example: China, Asia. Thx!
you should can do with ‘label_generator’, https://github.com/doctrine/DoctrineModule/blob/master/docs/form-element.md#example-2–modifying-the-label
how to implement this in Zend expressive ?
this may help: https://xtreamwayz.com/blog/2015-12-12-setup-doctrine-for-zend-expressive
How to implement this in Zend framework 3
there is a workaround for it as the PRs are not merged yet: https://github.com/doctrine/DoctrineORMModule/issues/491#issuecomment-242222677
working just fine with ZF3 these days — February 2017 – -and has been for some time.
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 🙂
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 !
Hello, you can use a factory to inject real service to the service constructor.