Search code examples
phpzend-frameworkzend-formzend-framework3

Zend3 Form Filter - ParamConverter could not generate valid form


I'm really confused about my Form Filter.

My Test-Project contains 2 Models.

class Category extends AbstractEntity 
{
    use Nameable; // just property name and getter and setter

    /**
     * @var boolean
     * @ORM\Column(name="issue", type="boolean")
     */
    private $issue;

    /**
     * @var Collection|ArrayCollection|Entry[]
     *
     * @ORM\OneToMany(targetEntity="CashJournal\Model\Entry", mappedBy="category", fetch="EAGER", orphanRemoval=true, cascade={"persist", "remove"})
     */
    private $entries;
}

the entry

class Entry extends AbstractEntity
{
    use Nameable;

    /**
     * @var null|float
     *
     * @ORM\Column(name="amount", type="decimal")
     */
    private $amount;

    /**
     * @var null|Category
     *
     * @ORM\ManyToOne(targetEntity="CashJournal\Model\Category", inversedBy="entries", fetch="EAGER")
     * @ORM\JoinColumn(name="category_id", referencedColumnName="id", nullable=false)
     */
    protected $category;

    /**
     * @var null|DateTime
     *
     * @ORM\Column(name="date_of_entry", type="datetime")
     */
    private $dateOfEntry;
}

And if someone needed the AbstractEntity

abstract class AbstractEntity implements EntityInterface
{
    /**
     * @var int
     * @ORM\Id
     * @ORM\Column(name="id", type="integer")
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    protected $id;
}

Every Category can have many Entries. I'm using Doctrine for this relation. And this works fine.

I have a Form based on this FieldSet:

$this->add([
    'name' => 'id',
    'type' => Hidden::class
]);

$this->add([
    'name' => 'name',
    'type' => Text::class,
    'options' => [
        'label' => 'Name'
    ]
]);

$this->add([
    'name' => 'amount',
    'type' => Number::class,
    'options' => [
        'label' => 'Summe'
    ]
]);

$this->add([
    'name' => 'date_of_entry',
    'type' => Date::class,
    'options' => [
        'label' => 'Datum'
    ]
]);

$this->add([
    'name' => 'category',
    'type' => ObjectSelect::class,
    'options' => [
        'target_class' => Category::class,
    ]
]);

So my Form displays a dropdown with my categories. Yeah fine.

To load the Category for my Entry Entity i use a filter.

$this->add([
    'name' => 'category',
    'required' => true,
    'filters' => [
        [
            'name' => Callback::class,
            'options' => [
                'callback' => [$this, 'loadCategory']
            ]
        ]
    ]
]);

And the callback:

public function loadCategory(string $categoryId)
{
    return $this->mapper->find($categoryId);
}

The mapper loads the category fine. great. But the form is invalid because:

Object of class CashJournal\Model\Category could not be converted to int

Ok, so i'm removing the Filter, but now it failed to set the attributes to the Entry Entity, because the setter needs a Category. The Form error says:

The input is not a valid step

In Symfony i can create a ParamConverter, which converts the category_id to an valid Category Entity.

Question How i can use the filter as my ParamConver?

Update Also when i cast the category_id to int, i will get the error from the form.

Update 2 I changed my FieldSet to:

class EntryFieldSet extends Fieldset implements ObjectManagerAwareInterface
{
    use ObjectManagerTrait;

    /**
     * {@inheritDoc}
     */
    public function init()
    {
        $this->add([
            'name' => 'id',
            'type' => Hidden::class
        ]);

        $this->add([
            'name' => 'name',
            'type' => Text::class,
            'options' => [
                'label' => 'Name'
            ]
        ]);

        $this->add([
            'name' => 'amount',
            'type' => Number::class,
            'options' => [
                'label' => 'Summe'
            ]
        ]);

        $this->add([
            'name' => 'date_of_entry',
            'type' => Date::class,
            'options' => [
                'label' => 'Datum'
            ]
        ]);

        $this->add([
            'name' => 'category',
            'required' => false,
            'type' => ObjectSelect::class,
            'options' => [
                'target_class' => Category::class,
                'object_manager' => $this->getObjectManager(),
                'property'       => 'id',
                'display_empty_item' => true,
                'empty_item_label'   => '---',
                'label_generator' => function ($targetEntity) {
                    return $targetEntity->getName();
                },
            ]
        ]);

        parent::init();
    }
} 

But this will be quit with the error message:

Entry::setDateOfEntry() must be an instance of DateTime, string given


Solution

  • Have you checked the documentation for ObjectSelect? You appear to be missing a few options, namely which hydrator (EntityManager) and identifying property (id) to use. Have a look here.

    Example:

    $this->add([
        'type'    => ObjectSelect::class,
        'name'    => 'category',                           // Name of property, 'category' in your question
        'options' => [
            'object_manager' => $this->getObjectManager(), // Make sure you provided the EntityManager to this Fieldset/Form
            'target_class'   => Category::class,           // Entity to target
            'property'       => 'id',                      // Identifying property
        ],
    ]);
    

    To validate selected Element, add in your InputFilter:

    $this->add([
        'name'     => 'category',
        'required' => true,
    ]);
    

    No more is needed for the InputFilter. A Category already exist and as such has been validated before. So, you should just be able to select it.

    You'd only need additional filters/validators if you have special requirements, for example: "A Category may only be used once in Entries", making it so that you need to use a NoObjectExists validator. But that does not seem to be the case here.


    UPDATE BASED ON COMMENTS & PAST QUESTIONS

    I think you're over complicating a lot of things in what you're trying to do. It seems you want to simply populate a Form before you load it client-side. On receiving a POST (from client) you wish to put the received data in the Form, validate it and store it. Correct?

    Based on that, please find a complete controller for User that I have in one of my projects. Hope you find it helpful. Providing it because updates are veering away from your original question and this might help you out.

    I've removed some additional checking and error throwing, but otherwise is in complete working fashion.

    (Please note that I'm using my own abstract controller, make sure to replace it with your own and/or recreate and match requirements)

    I've also placed additional comments throughout this code to help you out

    <?php
    
    namespace User\Controller\User;
    
    use Doctrine\Common\Persistence\ObjectManager;
    use Doctrine\ORM\ORMException;
    use Exception;
    use Keet\Mvc\Controller\AbstractDoctrineActionController;
    use User\Entity\User;
    use User\Form\UserForm;
    use Zend\Http\Request;
    use Zend\Http\Response;
    
    class EditController extends AbstractDoctrineActionController
    {
        /**
         * @var UserForm
         */
        protected $userEditForm; // Provide this
    
        public function __construct(ObjectManager $objectManager, UserForm $userEditForm)
        {
            parent::__construct($objectManager); // Require this in this class or your own abstract class
    
            $this->setUserEditForm($userEditForm);
        }
    
        /**
         * @return array|Response
         * @throws ORMException|Exception
         */
        public function editAction()
        {
            $id = $this->params()->fromRoute('id', null);
            // check if id set -> else error/redirect
    
            /** @var User $entity */
            $entity = $this->getObjectManager()->getRepository(User::class)->find($id);
            // check if entity -> else error/redirect
    
    
            /** @var UserForm $form */
            $form = $this->getUserEditForm();  // GET THE FORM
            $form->bind($entity);              // Bind the Entity (object) on the Form
    
            // Only go into the belof if() on POST, else return Form. Above the data is set on the Form, so good to go (pre-filled with existing data)
    
            /** @var Request $request */
            $request = $this->getRequest();
            if ($request->isPost()) {
                $form->setData($request->getPost());       // Set received POST data on Form
    
                if ($form->isValid()) {                    // Validates Form. This also updates the Entity (object) with the received POST data
                    /** @var User $user */
                    $user = $form->getObject();            // Gets updated Entity (User object)
    
                    $this->getObjectManager()->persist($user); // Persist it
    
                    try {
                        $this->getObjectManager()->flush();    // Store in DB
                    } catch (Exception $e) {
    
                        throw new Exception('Could not save. Error was thrown, details: ', $e->getMessage());
                    }
    
                    return $this->redirectToRoute('users/view', ['id' => $user->getId()]);
                }
            }
    
            // Returns the Form with bound Entity (object). 
            // Print magically in view with `<?= $this->form($form) ?>` (prints whole Form!!!)
    
            return [
                'form' => $form,
            ];
        }
    
        /**
         * @return UserForm
         */
        public function getUserEditForm() : UserForm
        {
            return $this->userEditForm;
        }
    
        /**
         * @param UserForm $userEditForm
         *
         * @return EditController
         */
        public function setUserEditForm(UserForm $userEditForm) : EditController
        {
            $this->userEditForm = $userEditForm;
    
            return $this;
        }
    }
    

    Hope that helps...