Handling Laminas Forms with Dependencies - the view results

I am developing a Laminas-Form that requires dependencies from a database table.

#My form looks like this:

namespace Application\Form;

use Laminas\Form\Form;

class CountryForm extends Form
{
    public function init()
    {
        parent::init();
	
        $this->add([
            'type' => CountryFieldset::class,
            'name' => 'country_id',
            'options' => [
                'use_as_base_fieldset' => true
            ]
        ]);
    }
}

#My Fieldset looks like this

namespace Application\Form;

use Application\Model\Table\CountriesTable;
use Laminas\Form\Element\Select;
use Laminas\Form\Fieldset;

class CountryFieldset extends Fieldset
{
    public function __construct(CountriesTable $countriesTable)
    {
        $countries = $countriesTable->fetchAllCountries();

        $selectField = new Select();
        $selectField->setName('country_id')
                    ->setOptions([
                        $selectField->setLabel('Countries'),
                        $selectField->setEmptyOption('Select...'),
                        $selectField->setValueOptions($countries);
                    ])
                    ->setAttributes([
                        'required' => true,
                        'class' => 'custom-select'
                     ]);

         $this->add($selectField);
    }
}

#Factory looks like this:

namespace Application\Form;

use Application\Model\Table\CountriesTable;
use Interop\Container\ContainerInterface;
use Laminas\ServiceManager\FactoryInterface;
use Laminas\ServiceManager\ServiceLocatorInterface;

class CountryFieldsetFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $name, array $options = null)
    {
        return new CountryFieldset($container->get(CountriesTable::class));
    }
}

#My Controller Factory looks like this:

namespace Application\Controller;

use Interop\Container\ContainerInterface;
use Application\Form\CountryForm;

class IndexControllerFactory
{
    public function __invoke(ContainerInterface $container)
    {
        $formManager = $container->get('FormElementManager');
        return new IndexController($formManager->get(CountryForm::class));
    }
}

#My controller looks like this:

namespace Application\Controller;

use Application\Form\MyForm;
use Laminas\Mvc\Controller\AbstractActionController;

class IndexController extends AbstractActionController
{
     private $countryForm;

    public function __construct(CountryForm $countryForm)
    {
        $this->countryForm = $countryForm;
    }

    public function indexAction()
    {
        return array('form' => $this->countryForm);
    }
}

#I mapped the fieldset factory to look like this in the module.config.php file:

return [
    'form_elements' => [
        'factories' => [
            Application\Form\CountryFieldset::class => Application\Form\CountryFieldsetFactory::class,
        ],
    ],
    'controllers' => [
        'factories' => [
            Application\Controller\IndexController::class => Application\Controller\IndexControllerFactory::class,
        ],
    ],
];

#My form view looks like

<?php
echo $this->form()->openTag($form);
echo $this->formCollection($form->get('country_id'));
echo $this->form()->closeTag();

As you can see everything works fine. The Select option values display as I would like them to.
The issue I am having is that in the views the the select form attribute name comes up as
‘country_id[country_id]’

 <fieldset>
    <label>
       <span>Countries</span>
          <select name="country_id[country_id]" required="required" class="custom-select">
              <option value="">Select...</option>
              <option value="1">Afghanistan</option>
                   .
                   .
                   .
          </select>
    </label>
</fieldset>  

as opposed to the expected attribute name of ‘country_id’:

<select name="country_id" required="required" class="custom-select">

Can anyone help me understand why that is the case?

Hey Pule,

That’s most likely because your field is inside a fieldset, and therefore its name is nested to dis-ambiguate with other (potential) fieldsets. If you don’t like the prefix, put the select element directly into your CountryForm, not in the CountryFieldset

Well thanks for the reply. My concern is about the name attribute not the arrangement of the form. That is i get attribute name of country_id[country_id] as opposed to the expected name of country_id. I am not sure if i am clear. My concern is this

name="country_id[country_id]"

I expect this - name=“country_id”.

Yeah, that’s exactly what I was referring to: the attribute name is using an array-like format because of how the element is placed under a nested fieldset.

Alright. I read that really fast and I misunderstood you. I got what you meant after I replied to you. I am working on the solution now. Thanks for nudging me in the right direction. I will be sure to post how I resolved this issue as soon as I am done. Cheers.

I resolved this issue by taking a rather ugly approach. This is how I did it:

namespace Application\Controller;

use Application\Model\Table\CountriesTable;
use Laminas\Mvc\Controller\AbstractActionController;
use Laminas\View\Model\ViewModel;

IndexController extends AbstractActionController
{
    private $countriesTable;
 
    public function __construct(CountriesTable $countriesTable)
    {
         $this->countriesTable = $countriesTable;
    }

    public function indexAction()
    {
        $countries = $this->countriesTable->fetchAllCountries();
        $selectField = new Select();
        $selectField->setName('country_id')
			->setOptions([
				$selectField->setLabel('Countries'),
				$selectField->setEmptyOption('Select...'),
				$selectField->setValueOptions($countries)
			])
			->setAttributes([
				'required' => true,
				'class' => 'custom-select'
			]);
		
	$createForm = new CreateForm();
	$createForm->add($selectField);

        $request = $this->getRequest();
        if ($request->isPost()) {
            #processing and saving data to the table goes here...
        }

       return new ViewModel(['form' => $createForm]);
    }
}

I wouldn’t advise building your form that way within an action. I would use the previous suggestion of getting rid of the fieldset entirely and just putting the select element directly in your form.

I understood that suggestion. The issue is if I place the Select field in the CountryForm how would I fetch the database dependencies with the form set to init(). This is what I was running away from. I tried using the FactoryInterface in the CountryForm but did not get far with that.

You could use the technique shown in the docs here to create your form in a way that will allow you to inject dependencies with a factory: https://docs.laminas.dev/laminas-form/quick-start/#factory-backed-form-extension

Thank you @ ddelrio1986. It worked the way I wanted it to. I will put the final solution below so that others who may have the same issue as me may benefit. Once again thank you.

Final Working Solution

#My CountryForm now looks as follows:

namespace Application\Form;

use Laminas\Form\Element;
use Laminas\Form\Form;

class CountryForm extends Form
{
    public function __construct(CountriesTable $countriesTable)
    {
        parent::__construct();
        $this->setAttribute('method', 'post');

        $this->add([
            'type' => Element\Select::class,
            'name' => 'country_id',
            'options' => [
                'label' => 'Countries',
                'value_options' => $categories->fetchAllCountries()
            ],
            'attributes' => [
                'required' => true,
                'class' => 'custom-select'
            ]
        ]);
    }
}

#My Application Module.php file is now updated to look thus:

namespace Application;

use Application\Form\CountryForm;
use Application\Model\Table\CountriesTable;
use Laminas\Db\Adapter\Adapter;

class Module
{
   public functon getConfig(): array{...}
   
   public function getServiceConfig()
   {
       return [
           'factories' => [
               CountriesTable::class => function($sm) {
                   $dbAdapter = $sm->get(Adapter::class);
                   return new CountriesTable($dbAdapter);
               }
           ]
       ];    
   }

   public function getFormElementConfig()
   {
       return [
           'factories' => [
               CountryForm::class => function($sm) {
                   $countriesTable = $sm->get(CountriesTable::class);
                   return new CountryForm($countriesTable);
               }
           ]
       ];
   }
}

#My Controller hasn’t changed

namespace Application\Controller;

use Application\Form\CountryForm;
use Laminas\Mvc\Controller\AbstractActionController;
use Laminas\View\Model\ViewModel;

class IndexController extends AbstractActionController
{
    protected $countryForm;

    public function __construct(CountryForm $countryForm)
    {
        $this->countryForm = $countryForm;
    }

    public function indexAction()
    {
        return new ViewModel(['form' => $this->countryForm]);
    }
}

#IndexControllerFactory looks like

namespace Application\Controller\Factory;

use Application\Controller\IndexController;
use Application\Form\CountryForm;
use Interop\Container\ContainerInterface;

class IndexControllerFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
	$formManager = $container->get('FormElementManager');
        return new IndexController(
            $formManager->get(CountryForm::class)
        );
    }
}

#My view looks like this:

<?php
echo $this->form()->openTag($form);
echo $this->formSelect($form->get('country_id'));
echo $this->form()->closeTag();

#My module.config.php file looks like this

return [
    'form_elements' => [
        'factories' => [
            Application\Form\CountryFieldset::class => Application\Form\CountryFieldsetFactory::class,
        ],
    ],
    'controllers' => [
        'factories' => [
            Application\Controller\IndexController::class => Application\Controller\IndexControllerFactory::class,
        ],
    ],
];

Hope this helps someone out there. Thanks to everyone who helped steer me in the right direction.
Cheers.

If only the select element needs entries from a database then you can create a separate form element with the required dependencies.

  1. Create the element:

    class CountrySelectElement extends Laminas\Form\Element\Select
    {
        // …
    
        public function init()
        {
            $this->setName('country_id');
            $this->setLabel('Countries');
            $this->setValueOptions($categories->fetchAllCountries());
            // …
        }
    }
    
  2. Create a factory for the element.

  3. Register the new element:

    return [
        'form_elements' => [
            'factories' => [
                CountrySelectElement::class => CountrySelectElementFactory::class,
            ],
        ],
        // …
    ];
    
  4. Usage in a form:

    class MyForm extends Laminas\Form\Form
    {
        public function init()
        {
            $this->add(['type' => CountrySelectElement::class]);
        }
    }
    

This allows the element to be reused without factories for forms.

I am wondering about the MyForm code there. Wouldn’t you get an error because the name has not been defined. I had an issue where I wrote the code like you did in the MyForm and got an error regarding a name not found or something along those lines.

The same applies here as for your fieldset, you must register the form element in your config under form_elements.

I added the missing steps in my previous comment.