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

1 Like

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.

1 Like

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.

2 Likes

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.