Best way to use Composition instead of Inheritance for Laminas Form Elements

Right now we have inputs like this, which extend Select

namespace Admin\Form\Input;

use Admin\Enum\PeriodRange as PeriodRangeEnum;
use Laminas\Filter\ToString;
use Laminas\Form\Element\Select;
use Laminas\Validator\InArray;

use function Core\Utility\identity as translate;

class PeriodRange extends Select
{
    public function __construct($name = null, iterable $options = [])
    {
        $this->label = translate('Period Range');
        parent::__construct($name, $options);
    }

    /** @return array<value-of<PeriodRangeEnum>, string> */
    public function getValueOptions(): array
    {
        return [
            PeriodRangeEnum::All->value => translate('All'),
            PeriodRangeEnum::Day->value => translate('Day'),
            PeriodRangeEnum::Week->value => translate('Week'),
            PeriodRangeEnum::Month->value => translate('Month'),
            PeriodRangeEnum::Year->value => translate('Year'),
        ];
    }

    public function getInputSpecification(): array
    {
        return [
            'name' => $this->getName(),
            'required' => false,
            'filters' => [
                ['name' => ToString::class],
            ],
            'validators' => [
                [
                    'name' => InArray::class,
                    'options' => [
                        'haystack' => array_keys($this->getValueOptions()),
                        'message' => translate('Select a valid period range'),
                    ],
                ],
            ],
        ];
    }
}

But, as Select is marked final, is there a better way we should be doing things?

In most cases, you can use a factory to prepare the element. But you are right, your example is the standard procedure in many applications where the value options are set and the specifications for the input filter are defined.

The goal for the next major version, or perhaps even sooner, is to find ways to simplify the process without having to extend the standard form elements.

See also:

Some additional background/explanation: The way to extend the standard form elements was originally intended to be. Therefore it is very easy in the current versions because in a form the form element manager is used. The form element manager can fetch form elements, fieldsets and forms without registration!

Fetch a Custom Element without Registration

The form element manager allows fetching custom elements without prior registration with the manager.

The following example creates a custom element:

final class ExampleElement extends Laminas\Form\Element
{
    // …
}

The form element manager can create these custom elements by the related class name:

$element = $formElementManager->get(ExampleElement::class);

The manager uses the factory Laminas\Form\ElementFactory to instantiate the element, and will pass the name and options array:

$element = $formElementManager->get(
    ExampleElement::class,
    [
        'name'    => '…',
        'options' => [ 
            // …
        ],
    ]
);

Usage in a Form

final class ExampleForm extends Laminas\Form\Form
{
    public function init() : void
    {
        $this->add([
            'name'    => 'example',
            'type'    => ExampleElement::class,
            'options' => [
                'label' => 'Example',
            ],
        ]);

        // …
    }
}

It is not necessary to extend the configuration!


The challenge now is to maintain this simplicity without necessarily having to extend the standard elements.