ZF3 Forms + Doctrine, can't bind objects in factories?

Hey guys. Meant to ask about this for a long time. I also posted in Stack Overflow if you want internet points: php - Zend Framework 3, why can't I bind doctrine entities to forms in the factory? - Stack Overflow

Why is it, that we can’t bind objects to a form in its Factory and have form elements properly populated, but can do so down the chain (e.g., in the Controller)? Anyone else run into this?

Cheers.
Alex

Unfortunately, there is nothing to see from your form, so I can only guess.

In the controller, you use the form element manager which initialises your form but in the factory, the initialisation is missing.
Without initialisation there are no elements and nothing can be populated from the object to the form.

init() is there in the code, I left out the ornery stuff. There are fields, whose names match the doctrine entity and so forth. You can repeat this problem with the most basic of wirings. Time permitting, I’ll post a simple scaffold on Github to demonstrate the problem.

But you bind an object before the form is initialised. The init method is normally called by the form element manager during the fetch of the form via the get method:

$formElementManager->get(ExampleForm::class);
1 Like

I see what you are saying, I’d completely ignored that. Init is called by the factory on the way out.

Would be nice to have a means to move this type of pattern over to the factory, but seems it is euchred by the FormElementManager’s behavior?

The idea of the form element manager and factory pattern is to facilitate extension and ease the process of form creation. Creating and ensuring that all dependencies are injected does not include binding objects which populates data to form. The binding is done afterwards.

Besides the fact that the form in your factory is without elements, the data-binding feature does not work conceptually, because there is no binding, only an initial filling with data.

Data-binding means:

// Bind object
$form->bind($album);

// Set request data to post
$form->setData($_POST);

// Validation and bind values to the bound object
$form->isValid();

$repository->save($album);

So you break the basic concept and therefore it doesn’t work as you expected.


I would suggest to simplify the whole thing: set your configuration object as custom option in the constructor or via setOption and use it in the init method after all elements have been added. The populateValues method is suitable for this.

class ExampleForm extends Laminas\Form\Form
{
    public function init(): void
    {
        // Add elements
        $this->add(['type' => Laminas\Form\Element\Text::class]);

        // …
        
        // Populate values of attached elements
        $this->populateValues($this->getOption(TermConfig::class));
    }

It did have elements, just as written, I’d omitted the ornery basics.

Binding does go beyond saving, though, which is what I feel your last post would intimate.

When provided a Hydrator (e.g., Doctrine situation), binding can be a real convenience with repeating hydrated object data in forms. Bind a user to your user form, and the user’s hydrated data will automatically populate the form. This saves a ton of boilerplate.

For reference Form::bind() calls Fieldset::setObject and Fieldset::extract() that lets the Hydrator do its work.

Using option is an interesting approach, but I think I’ll wire something a bit more unbreakable for my team; you know how it goes - give an inch…

Again - appreciate the convo, and thanks again for mentioning init earlier today. That was the piece my senility had ejected. Shoulda had more coffee.

I’m sorry if it was misleading: I was only talking about binding objects in factories because that does not belong in a factory.

And please note when you call the init method in your factory, the same is done later in the form element manager again!

And please update your answer on Stackoverflow because the problem is not the form element manager. This works as it should.

Thanks in advance!

There, I do disagree a bit.

It’s not a problem per se, but it is an intricacy that is not noted. That the Form object behaves differently, silently, without warning is the problem. Bind should have a consistent behavior, but here, the contract on bind changes without warning.

Throwing opinion out there:

  • A call to trigger_error with E_USER_NOTICE if you call bind before init would be a nice prevention to beat guys like me into submission.
  • Another option, saving bound objects for post-init would be a nice behavior adjustment. If you call bind() before init occurs, it short circuits and resumes on init. Disclaimer: I haven’t researched if there’s a valid motive to binding before init.

Maybe if time permits, I’ll try to formulate the ideas as a PR. I think it’d make sense if the FEM always calls init().

The form element manager calls always the init method but after the form is backed by a factory and delegators to ensure that all dependencies are injected so that the elements can be created.

(The “Initializers” of laminas-servicemanager are used here.)