Skip to content
This repository has been archived by the owner on Jun 14, 2023. It is now read-only.

Filters and DataTransformer #37

Closed
retuerto opened this issue Feb 28, 2013 · 5 comments
Closed

Filters and DataTransformer #37

retuerto opened this issue Feb 28, 2013 · 5 comments

Comments

@retuerto
Copy link

Hi,

I am using your Bundle for my application. I am migrating a symfony 1.4 website to a symfony 2.1 and I have the following problem with the translations, since the doctrine extensions for 1.4 and 2.1 have a different approach. I have two entities, ProductOption (ProductOptionTranslation to keep the translation) and ProductOptionValue (ProductOptionValueTranslation to do the same as before). The relationship between ProductOption and ProductOptionValue is 1:N (i.e Chocolate/Strawberry/Vanilla are Flavours)

What I want is to filter in the index view of the ProductOptionValue by ProductOption, but the name of the ProductOption is in ProductOptionTranslation entity. So I created a new choice type with a datatransformer. The filter form show the choice perfectly, but the filter is not applied because in the code above the form does not have any children since I am using a DataTransformer and not a filter_XXXX field.

protected function addFilters(FormInterface $form, $filterBuilder, $alias = null, array &$parts = array(), $expr = null)
{
/** @var $child FormInterface */
foreach ($form->all() as $child) {

Here is the code:

<?php

namespace Ecomm\Bundle\CatalogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * ProductOption
 *
 * @ORM\Entity(repositoryClass="Ecomm\Bundle\CatalogBundle\Entity\Manager\ProductOptionManager")
 * @ORM\Table(name="products_options")
 * @ORM\HasLifecycleCallbacks
 */
class ProductOption
{
    /**
     * @var integer
     *
     * @ORM\Id
     * @ORM\Column(name="product_option_id", type="integer", nullable=false)
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    protected $id;

    /**
     * @ORM\OneToMany(targetEntity="Ecomm\Bundle\CatalogBundle\Entity\Translation\ProductOptionTranslation", mappedBy="productOption", cascade={"persist", "remove"})
     * 
     */
    protected $translations;


    protected $name;


    /**
     * @ORM\OneToMany(targetEntity="Ecomm\Bundle\CatalogBundle\Entity\ProductOptionValue", mappedBy="productOption")
     **/
    protected $productOptionValues;


    /**
     * Constructor
     */
    public function __construct()
    {
        $this->resetTranslations();
        $this->productOptionValues = new \Doctrine\Common\Collections\ArrayCollection();
    }

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

    /**
     * Add translations
     *
     * @param \Ecomm\Bundle\CatalogBundle\Entity\Translation\ProductOptionTranslation $translations
     * @return ProductOption
     */
    public function addTranslation(\Ecomm\Bundle\CatalogBundle\Entity\Translation\ProductOptionTranslation $translation)
    {
        $this->translations[] = $translation;
        $translation->setProductOption($this);
        return $this;
    }

    /**
     * Remove translations
     *
     * @param \Ecomm\Bundle\CatalogBundle\Entity\Translation\ProductOptionTranslation $translations
     */
    public function removeTranslation(\Ecomm\Bundle\CatalogBundle\Entity\Translation\ProductOptionTranslation $translations)
    {
        $this->translations->removeElement($translations);
    }

    /**
     * Get translations
     *
     * @return \Doctrine\Common\Collections\Collection 
     */
    public function getTranslations()
    {
        return $this->translations;
    }

    public function resetTranslations()
    {
        $this->translations = new \Doctrine\Common\Collections\ArrayCollection();
    }

    public function getName()
    {
        return $this->name;
    }

    /**
     * 
     *  @ORM\PostLoad
     *
     */
    public function onPostLoad()
    {
        $this->name = array();

        if ($this->translations)
        {    

            foreach($this->translations as $translation)
            { 
                $locale = $translation->getLocale();
                $this->name[$locale] = $translation->getName();
            }
        }     
    }

    /**
     * Add productOptionValue
     *
     * @param \Ecomm\Bundle\CatalogBundle\Entity\ProductOptionValue $productOptionValue
     * @return ProductOption
     */
    public function addProductOptionValue(\Ecomm\Bundle\CatalogBundle\Entity\ProductOptionValue $productOptionValue)
    {
        $this->productOptionValues[] = $productOptionValue;
        return $this;
    }

    /**
     * Remove productOptionValue
     *
     * @param \Ecomm\Bundle\CatalogBundle\Entity\ProductOptionValue $productOptionValue
     */
    public function removeProductOptionValue(\Ecomm\Bundle\CatalogBundle\Entity\ProductOptionValue $productOptionValue)
    {
        $this->productOptionValues->removeElement($productOptionValue);
    }

    /**
     * Get productOptionValue
     *
     * @return \Doctrine\Common\Collections\Collection 
     */
    public function getProductOptionValues()
    {
        return $this->productOptionValues;
    }

}






<?php

namespace Ecomm\Bundle\CatalogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * ProductOptionValue
 *
 * @ORM\Entity(repositoryClass="Ecomm\Bundle\CatalogBundle\Entity\Manager\ProductOptionValueManager")
 * @ORM\Table(name="products_options_values")
 * @ORM\HasLifecycleCallbacks
 */
class ProductOptionValue
{
    /**
     * @var integer
     *
     * @ORM\Id
     * @ORM\Column(name="product_option_value_id", type="integer", nullable=false)
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    protected $id;

    /**
     * @ORM\OneToMany(targetEntity="Ecomm\Bundle\CatalogBundle\Entity\Translation\ProductOptionValueTranslation", mappedBy="productOptionValue", cascade={"persist", "remove"})
     * 
     */
    protected $translations;

    /**
     * @var \ProductsOptions
     *
     * @ORM\ManyToOne(targetEntity="Ecomm\Bundle\CatalogBundle\Entity\ProductOption", inversedBy="productOptionValues")
     * @ORM\JoinColumn(name="product_option_id", referencedColumnName="product_option_id", onDelete="RESTRICT")
     */
    protected $productOption;


    protected $name;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->resetTranslations();
    }


    public function __toString()
    {
        return "" . $this->name;
    }

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

    /**
     * Add translations
     *
     * @param \Ecomm\Bundle\CatalogBundle\Entity\Translation\ProductOptionValueTranslation $translations
     * @return ProductOptionValue
     */
    public function addTranslation(\Ecomm\Bundle\CatalogBundle\Entity\Translation\ProductOptionValueTranslation $translation)
    {
        $this->translations[] = $translation;
        $translation->setProductOptionValue($this);
        return $this;
    }

    /**
     * Remove translations
     *
     * @param \Ecomm\Bundle\CatalogBundle\Entity\Translation\ProductOptionValueTranslation $translations
     */
    public function removeTranslation(\Ecomm\Bundle\CatalogBundle\Entity\Translation\ProductOptionValueTranslation $translations)
    {
        $this->translations->removeElement($translations);
    }

    /**
     * Get translations
     *
     * @return \Doctrine\Common\Collections\Collection 
     */
    public function getTranslations()
    {
        return $this->translations;
    }

    public function resetTranslations()
    {
        $this->translations = new \Doctrine\Common\Collections\ArrayCollection();
    }

    public function getName()
    {
        return $this->name;
    }

    /**
     * 
     *  @ORM\PostLoad
     *
     */
    public function onPostLoad()
    {
        $this->name = array();

        if ($this->translations)
        {    

            foreach($this->translations as $translation)
            { 
                $locale = $translation->getLocale();
                $this->name[$locale] = $translation->getName();
            }
        }     
    }


    /**
     * Set productOption
     *
     * @param \Ecomm\Bundle\CatalogBundle\Entity\ProductOption $productOption
     * @return ProductOptionValue
     */
    public function setProductOption(\Ecomm\Bundle\CatalogBundle\Entity\ProductOption $productOption)
    {
        $this->productOption = $productOption;

        return $this;
    }

    /**
     * Get productOption
     *
     * @return \Ecomm\Bundle\CatalogBundle\Entity\ProductOption 
     */
    public function getProductOption()
    {
        return $this->productOption;
    }
}





<?php

namespace Ecomm\Bundle\CatalogBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormError;

use Lexik\Bundle\FormFilterBundle\Filter\ORM\Expr;
use Doctrine\ORM\QueryBuilder;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Translation\Translator;

class ProductOptionValueFilterType extends AbstractType
{
    /**
     * @var ServiceContainer
     */
    private $sc;

    protected $translator;

    public function __construct(ContainerInterface $container = null)
    {        
        $this->sc = $container;
        $this->translator = $this->sc->get('translator');
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', 'filter_text', array(
                'label' => $this->translator->trans('form.filter.label.name'),
                'apply_filter' => function (QueryBuilder $queryBuilder, Expr $expr, $field, array $values)
                {
                    if (!empty($values['value']))
                    {
                    // add  the join if you need it and it not already added
                        $alias = $queryBuilder->getRootAliases();
                        $queryBuilder->innerJoin($alias[0].'.translations', 'at');
                        $queryBuilder->andWhere('at.name = :name')
                        ->setParameter('name', $values['value']);
                    }
                },
            ))

            /*
            ->add('productOption', 'filter_entity', array(
                  'label' => $this->translator->trans('form.filter.label.option'),
                  'class' => 'EcommCatalogBundle:ProductOption',
                  'empty_value' => 'Choose a Product Option'
            ))
            */

            ->add('productOption', $this->sc->get('ecomm.type.product_option_filter_field'));


        ;

        $listener = function(FormEvent $event)
        {
            // Is data empty?
            foreach ($event->getData() as $data) {
                if(is_array($data)) {
                    foreach ($data as $subData) {
                        if(!empty($subData)) return;
                    }
                }
                else {
                    if(!empty($data)) return;
                }
            }

            $event->getForm()->addError(new FormError('Filter empty'));
        };
        $builder->addEventListener(FormEvents::POST_BIND, $listener);
    }

    public function getName()
    {
        return 'ecomm_bundle_catalogbundle_productoptionvaluefiltertype';
    }
}




<?php

namespace Ecomm\Bundle\CatalogBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

use Ecomm\Bundle\CatalogBundle\Form\DataTransformer\ProductOptionFieldTransformer;

use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;


use Doctrine\ORM\QueryBuilder;

use Lexik\Bundle\FormFilterBundle\Filter\FilterBuilderExecuterInterface;
use Lexik\Bundle\FormFilterBundle\Filter\ORM\Expr;
use Lexik\Bundle\FormFilterBundle\Filter\Extension\Type\FilterTypeSharedableInterface;



class ProductOptionFilterFieldType extends AbstractType implements FilterTypeSharedableInterface
{

    /**
     * @var ServiceContainer
     */
    private $sc;


    public function __construct(ContainerInterface $container = null)
    {        
        $this->sc = $container;
    }


    public function buildForm(FormBuilderInterface $builder, array $options)
    {

        $transformer = new ProductOptionFieldTransformer($this->sc);        
        $builder->addModelTransformer($transformer);

    }


    /**
     * This method aim to add all joins you need
     */
    public function addShared(FilterBuilderExecuterInterface $qbe)
    {
        $closure = function(QueryBuilder $filterBuilder, $alias, $joinAlias, Expr $expr)
        {
            // add the join clause to the doctrine query builder
            // the where clause for the label and color fields will be added automatically with the right alias later by the Lexik\Filter\QueryBuilderUpdater
            $filterBuilder->innerJoin($alias . '.productOption', 'po');

        };

        // then use the query builder executor to define the join, the join's alias and things to do on the doctrine query builder.
        $qbe->addOnce($qbe->getAlias().'.productOption', 'po', $closure);
    }



    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'invalid_message' => 'The selected productOption does not exist',
            'empty_value' => 'Choose an option',
            'choices' => $this->buildData(),
        ));
    }

    private function buildData()
    {
        $em = $this->sc->get('doctrine')->getManager();
        $request = $this->sc->get('request');

        $choices = array();
        $productOptions = $em
            ->getRepository('EcommCatalogBundle:ProductOption')
            ->createQueryBuilder('c')
            ->select('c, ct')
            ->innerJoin('c.translations', 'ct')
            ->andWhere('ct.locale = :locale')
            ->setParameter('locale', $request->getLocale())
            ->orderBy('ct.name', 'ASC')
            ->getQuery()
            ->getResult();

        foreach ($productOptions as $productOption)
        {
            // I assume key is retrieved by getId
            $translation = $productOption->getTranslations();
            $choices[$productOption->getId()] = $translation[0]->getName();
        }

        return $choices;
    }

    public function getName()
    {
        return 'product_option_filter_field';
    }

    public function getParent()
    {
        return 'choice';
    }

}
@cedric-g
Copy link
Collaborator

cedric-g commented Mar 1, 2013

Hi :) which version of the bundle are you using ?
The way to solve your problem may vary according to the bundle version.

@retuerto
Copy link
Author

retuerto commented Mar 1, 2013

I am using your bundle with the jordillonch crud generator:

"jordillonch/crud-generator": "2.1.x-dev",
"lexik/form-filter-bundle": "dev-master",

@retuerto
Copy link
Author

retuerto commented Mar 1, 2013

lexik/form-filter-bundle [dev-master 9c01c14]

@cedric-g
Copy link
Collaborator

cedric-g commented Mar 1, 2013

As ProductOptionFilterFieldType aim to represent a single field (which is a choice in your case) you don't need to implements FilterTypeSharedableInterface (so you can remove the addShared() method).

Then you can define a custom filter class to apply the filter conditions for a ProductOptionFilterFieldType, you can do some thing like :

<?php

namespace Your\Namespace;

use Doctrine\ORM\QueryBuilder;

use Lexik\Bundle\FormFilterBundle\Filter\ORM\Expr;
use Lexik\Bundle\FormFilterBundle\Filter\ORM\ORMFilter;

class ProductOptionFilter extends ORMFilter
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'product_option_filter_field'; // this is the same name as ProductOptionFilterFieldType::getName()
    }

    /**
     * {@inheritdoc}
     */
    protected function apply(QueryBuilder $queryBuilder, Expr $expr, $field, array $values)
    {
        if (!empty($values['value'])) {
            // add your conditions here
        }
    }
}

You must define this class as a service with a kernel.event_listener tag :

<service id="my_custom_filter.product_option_filter" class="Your\Namespace\ProductOptionFilter">
    <tag name="kernel.event_listener" event="lexik_filter.get" method="onFilterGet" />
</service>

By doing like this the lexik_form_filter.query_builder_updater service will be able to find the filter class for a type named product_option_filter_field.

The FilterTypeSharedableInterface is usefull in case of you define some fields inside the type that implements FilterTypeSharedableInterface.

@retuerto
Copy link
Author

retuerto commented Mar 1, 2013

It works!! Very important to add the transformer_id:

$resolver->setDefaults(array(
            'invalid_message' => 'The selected productOption does not exist',
            'empty_value' => 'Choose an option',
            'choices' => $this->buildData(),
            'transformer_id' => 'lexik_form_filter.transformer.default',
        ))
                 ->setAllowedValues(array(
                'transformer_id' => array('lexik_form_filter.transformer.default'),
        ));

Here is the final code:

<?php

namespace Ecomm\Bundle\CatalogBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

use Ecomm\Bundle\CatalogBundle\Form\DataTransformer\ProductOptionFieldTransformer;

use Symfony\Component\DependencyInjection\ContainerInterface;



class ProductOptionFilterFieldType extends AbstractType
{

    /**
     * @var ServiceContainer
     */
    private $sc;


    public function __construct(ContainerInterface $container = null)
    {        
        $this->sc = $container;
    }


    public function buildForm(FormBuilderInterface $builder, array $options)
    {

        $transformer = new ProductOptionFieldTransformer($this->sc);        
        $builder->addModelTransformer($transformer);

    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'invalid_message' => 'The selected productOption does not exist',
            'empty_value' => 'Choose an option',
            'choices' => $this->buildData(),
            'transformer_id' => 'lexik_form_filter.transformer.default',
        ))
                 ->setAllowedValues(array(
                'transformer_id' => array('lexik_form_filter.transformer.default'),
        ));

    }

    private function buildData()
    {
        $em = $this->sc->get('doctrine')->getManager();
        $request = $this->sc->get('request');

        $choices = array();
        $productOptions = $em
            ->getRepository('EcommCatalogBundle:ProductOption')
            ->createQueryBuilder('c')
            ->select('c, ct')
            ->innerJoin('c.translations', 'ct')
            ->andWhere('ct.locale = :locale')
            ->setParameter('locale', $request->getLocale())
            ->orderBy('ct.name', 'ASC')
            ->getQuery()
            ->getResult();

        foreach ($productOptions as $productOption)
        {
            // I assume key is retrieved by getId
            $translation = $productOption->getTranslations();
            $choices[$productOption->getId()] = $translation[0]->getName();
        }

        return $choices;
    }

    public function getName()
    {
        return 'product_option_filter_field';
    }

    public function getParent()
    {
        return 'choice';
    }

}


<?php

namespace Ecomm\Bundle\CatalogBundle\Form;

use Doctrine\ORM\QueryBuilder;

use Lexik\Bundle\FormFilterBundle\Filter\ORM\Expr;
use Lexik\Bundle\FormFilterBundle\Filter\ORM\ORMFilter;

class ProductOptionFilter extends ORMFilter
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'product_option_filter_field'; // this is the same name as ProductOptionFilterFieldType::getName()
    }

    /**
     * {@inheritdoc}
     */
    protected function apply(QueryBuilder $queryBuilder, Expr $expr, $field, array $values)
    {

        if (!empty($values['value']))
        {
            // add  the join if you need it and it not already added
            $alias = $queryBuilder->getRootAliases();
            $queryBuilder->innerJoin($alias[0].'.productOption', 'po');
            $queryBuilder->andWhere('po.id = :id')
            ->setParameter('id', $values['value']);
        }

    }
}

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants