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?
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:
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.
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.