0

I was not able to find any solution matching my case scenario, so I decided to ask here: Basically what I need to achieve is to render a form with several select boxes namely the Company, ProductsCategory, and Products. So depending on which category the user chooses, I want to filter and show only the products of that chosen category. I tried to follow the Symfony documentation as mentioned here , but I cannot get it to work. the front-end products select box remains blank even after a category has been set and also the ajax return with status 500 with error:

Return value of App\Entity\ProductsCategory::getProducts() must be an instance of App\Entity\Products or null, instance of Doctrine\ORM\PersistentCollection returned

here are the codes: My Exportables entity

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\ExportablesRepository")
 */

class Exportables
{
/**
 * @ORM\Id()
 * @ORM\GeneratedValue()
 * @ORM\Column(type="integer")
 */
private $id;

/**
 * @ORM\ManyToOne(targetEntity="App\Entity\ExportCompany", inversedBy="exportables")
 * @ORM\JoinColumn(nullable=false)
 */
private $company;

/**
 * @ORM\ManyToOne(targetEntity="App\Entity\Products", inversedBy="exportables")
 * @ORM\JoinColumn(nullable=false)
 */
private $product;

/**
 * @ORM\Column(type="boolean")
 */
private $isActive;

/**
 * @ORM\ManyToOne(targetEntity="App\Entity\ProductsCategory")
 * @ORM\JoinColumn(nullable=false)
 */
private $category;

public function getId(): ?int
{
    return $this->id;
}

public function getCompany(): ?ExportCompany
{
    return $this->company;
}

public function setCompany(?ExportCompany $company): self
{
    $this->company = $company;

    return $this;
}

public function getProduct(): ?Products
{
    return $this->product;
}

public function setProduct(?Products $product): self
{
    $this->product = $product;

    return $this;
}

public function getIsActive(): ?bool
{
    return $this->isActive;
}

public function setIsActive(bool $isActive): self
{
    $this->isActive = $isActive;

    return $this;
}

public function getCategory(): ?ProductsCategory
{
    return $this->category;
}

public function setCategory(?ProductsCategory $category): self
{
    $this->category = $category;

    return $this;
}
}

ProductsCategory entity:

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\ProductsCategoryRepository")
 */
class ProductsCategory
{
/**
 * @ORM\Id()
 * @ORM\GeneratedValue()
 * @ORM\Column(type="integer")
 */
private $id;

/**
 * @ORM\Column(type="string", length=50)
 */
private $categoryTitle;

/**
 * @ORM\Column(type="text", nullable=true)
 */
private $categoryDescription;

/**
 * @ORM\Column(type="boolean")
 */
private $isActive;

/**
 * @ORM\OneToMany(targetEntity="App\Entity\Products", mappedBy="category", cascade={"persist", "remove"})
 */
private $products;


public function __construct()
{
    $this->product = new ArrayCollection();
}

public function getId(): ?int
{
    return $this->id;
}

public function getCategoryTitle(): ?string
{
    return $this->categoryTitle;
}

public function setCategoryTitle(string $categoryTitle): self
{
    $this->categoryTitle = $categoryTitle;

    return $this;
}

public function getCategoryDescription(): ?string
{
    return $this->categoryDescription;
}

public function setCategoryDescription(?string $categoryDescription): self
{
    $this->categoryDescription = $categoryDescription;

    return $this;
}

public function getIsActive(): ?bool
{
    return $this->isActive;
}

public function setIsActive(bool $isActive): self
{
    $this->isActive = $isActive;

    return $this;
}

public function getProducts(): ?Products
{
    return $this->products;
}

public function setProducts(Products $products): self
{
    $this->products = $products;

    // set the owning side of the relation if necessary
    if ($products->getCategory() !== $this) {
        $products->setCategory($this);
    }

    return $this;
}

public function __toString()
{
    return $this->categoryTitle;
}
}

Products entity:

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\ProductsRepository")
 */

class Products
{

/**
 * @ORM\Id()
 * @ORM\GeneratedValue()
 * @ORM\Column(type="integer")
 */
private $id;

/**
 * @ORM\Column(type="string", length=50)
 */
private $productTitle;

/**
 * @ORM\Column(type="text")
 */
private $productDescription;

/**
 * @ORM\Column(type="boolean")
 */
private $isActive;

/**
 * @ORM\ManyToOne(targetEntity="App\Entity\ProductsCategory", inversedBy="products", cascade={"persist", "remove"})
 * @ORM\JoinColumn(nullable=false)
 */
private $category;

/**
 * @ORM\OneToMany(targetEntity="App\Entity\Exportables", mappedBy="product")
 */
private $exportables;

public function __construct()
{
    $this->exportables = new ArrayCollection();
}


public function getId(): ?int
{
    return $this->id;
}

public function getProductTitle(): ?string
{
    return $this->productTitle;
}

public function setProductTitle(string $productTitle): self
{
    $this->productTitle = $productTitle;

    return $this;
}

public function getProductDescription(): ?string
{
    return $this->productDescription;
}

public function setProductDescription(string $productDescription): self
{
    $this->productDescription = $productDescription;

    return $this;
}

public function getIsActive(): ?bool
{
    return $this->isActive;
}

public function setIsActive(bool $isActive): self
{
    $this->isActive = $isActive;

    return $this;
}

public function getCategory(): ?ProductsCategory
{
    return $this->category;
}

public function setCategory(ProductsCategory $category): self
{
    $this->category = $category;

    return $this;
}

/**
 * @return Collection|Exportables[]
 */
public function getExportables(): Collection
{
    return $this->exportables;
}

public function addExportable(Exportables $exportable): self
{
    if (!$this->exportables->contains($exportable)) {
        $this->exportables[] = $exportable;
        $exportable->setProduct($this);
    }

    return $this;
}

public function removeExportable(Exportables $exportable): self
{
    if ($this->exportables->contains($exportable)) {
        $this->exportables->removeElement($exportable);
        // set the owning side to null (unless already changed)
        if ($exportable->getProduct() === $this) {
            $exportable->setProduct(null);
        }
    }

    return $this;
}

public function __toString(){
    return $this->productTitle;
}
}

Exportables Type:

namespace App\Form;

use App\Entity\Products;
use App\Entity\Exportables;
use App\Entity\ProductsCategory;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;

class ExportablesType extends AbstractType

{

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

    $builder
        ->add('company')
        ->add('category', EntityType::class, array(
            'class' => ProductsCategory::class,
            'placeholder' => 'Select a Category...',
        ))
        ->add('isActive')
    ;

    $formModifier = function (FormInterface $form, ProductsCategory $cat = null) {
        $products = null === $cat ? [] : $cat->getProducts();
        dump($products);

        $form->add('product', EntityType::class, [
            'class' => 'App\Entity\Products',
            'placeholder' => '',
            'choices' => $products,
        ]);
    };

    $builder->addEventListener(
        FormEvents::PRE_SET_DATA,
        function (FormEvent $event) use ($formModifier) {
            // this would be your entity, i.e. SportMeetup
            $data = $event->getData();

            $formModifier($event->getForm(), $data->getCategory());
        }
    );

    $builder->get('category')->addEventListener(
        FormEvents::POST_SUBMIT,
        function (FormEvent $event) use ($formModifier) {
            // It's important here to fetch $event->getForm()->getData(), as
            // $event->getData() will get you the client data (that is, the ID)
            $cat = $event->getForm()->getData();

            // since we've added the listener to the child, we'll have to pass on
            // the parent to the callback functions!
            $formModifier($event->getForm()->getParent(), $cat);
        }
    );

}

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults([
        'data_class' => Exportables::class,
    ]);
}
}

ExportablesController:

namespace App\Controller;

use App\Entity\Exportables;
use App\Form\ExportablesType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class ExportablesController extends AbstractController
{

/**
 * @Route("/admin-panel/exportables/new", name="exportables_create")
 * @Route("/admin-panel/exportables/{id}/edit", name="exportables_edit")
 */
public function exportables_create_and_edit(Request $request, EntityManagerInterface $em, Exportables $exportables = null)
{
    if(!$exportables){
        $exportables = new Exportables();
    }
    $form = $this->createForm(ExportablesType::class, $exportables);
    $form->handleRequest($request);

    if($form->isSubmitted() && $form->isValid()){
        $em->persist($exportables);
        $em->flush();
    }
    return $this->render('/admin-panel/exportables_create.html.twig', [
        'exForm' => $form->createView(),
        'editMode' => $exportables->getId() !== null
    ]);
}   
}

Finally, the twig file rendering the form:

{% extends '/admin-panel/base-admin.html.twig' %}
{%  block body %}
{{ form_start(exForm) }}
<div class="form-group">
    {{ form_row(exForm.company, {'attr':{'class':"form-control"}}) }}
</div>
   <div class="form-group">
       {{ form_row(exForm.category, {'attr':{'class':"form-control"}}) }}
   </div>
{% if exForm.product is defined %}
   <div class="form-group">
       {{ form_row(exForm.product, {'label': "Product..",  'attr':{'class':"form-control"}}) }}
   </div>
{% endif %}

<div class="form-group">
    <div class="custom-control custom-checkbox">
        {{ form_widget(exForm.isActive, {'attr': {'class': "custom-control-input", 'checked': "checked"}}) }}
        <label class="custom-control-label" for="exportables_isActive">Visible on the website?</label>
    </div>
</div>
<button type="submit" class="btn btn-success">Create</button>

 {{ form_end(exForm) }}
{% endblock %}

{% block javascripts %}
<script>
    {# //for some reasons this line doesnt work  $(document).ready(function() { #}
    jQuery(document).ready(function($){
        var $cat = $('#exportables_category');
        // When cat gets selected ...
        $cat.change(function() {
            // ... retrieve the corresponding form.
            var $form = $(this).closest('form');
            // Simulate form data, but only include the selected cat value.
            var data = {};
            data[$cat.attr('name')] = $cat.val();
        console.log("cat val " + $cat.val());
        //console.log($form.attr('method'));
        const url = "{{ path('exportables_create')|escape('js') }}";
        //console.log(data);
        //console.log(url);
        //why undefined?? console.log($form.attr('action'));
            // Submit data via AJAX to the form's action path.
            $.ajax({
                //url : $form.attr('action'),
                url : url,
                type: $form.attr('method'),
                data : data,
                success: function(html) {
                // Replace current position field ...
                $('#exportables_product').replaceWith(
                    // ... with the returned one from the AJAX response.
                    //$(html).find('#exportables_product')
                    array(1,2,3)
                );
                // Position field now displays the appropriate positions.
                }
            });
        });
    });
    </script>
{% endblock %}

Any help is greatly appreciated.

1 Answer 1

1

I didn't take the time to analyse all your code, so I'm not sure it will solve the whole problem, but it may help.

I guess there is something weird inside the ProductsCategory entity class. Indeed the products property is annotated as a OneToMany relation:

/**
 * @ORM\OneToMany(targetEntity="App\Entity\Products", mappedBy="category", cascade={"persist", "remove"})
 */
private $products;

This implies that this property refers to a collection (one category can have many products). But the getters/setters for this property are defined later as if it was a OneToOne relation:

public function getProducts(): ?Products
{
    return $this->products;
}

public function setProducts(Products $products): self
{
    $this->products = $products;

    // set the owning side of the relation if necessary
    if ($products->getCategory() !== $this) {
        $products->setCategory($this);
    }

    return $this;
}

It looks like you modified the annotation without modifying the getters/setters, which typically should offer the following methods for a OneToMany relation:

public function getProducts(): Collection;
public function addProducts(Product $product): self;
public function removeProducts(Product $product): self;

A last point: you should rename your entity Products to Product, it will greatly improve the readability of your code: indeed, the entity Products actually represent only one product, not several ones.

Sign up to request clarification or add additional context in comments.

1 Comment

as you mentioned, correcting my ProductsCategory entity solved the issue. Thanks a lot.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.