ZF3 - 如何在字段集集合中填充 Select-元素

ZF3 - Howto fill a Select-Element insinde a Collection of Fieldsets

以下场景:
我想通过 Zend Framework 3 表单编辑由 3 个字段(id、用户名和域)组成的 table 'accounts'。可以从一组域名中选择字段'domain'(这里是静态数组以简化事情)

我有一个带有 getter 和 setter 的简单实体 'AccountModel.php':

namespace Project\Model;

class AccountModel {

    private $id;

    private $userName;

    private $domain;

    public function getUserName(){
        return $this->userName;
    }
    public function setUserName(string $userName){
        $this->userName = $userName;
        return $this;
    }
    public function getId() {
        return $this->id;
    }
    public function setId(int $id) {
        $this->id = $id;
        return $this;
    }
    public function getDomain() {
        return $this->domain;
    }
    public function setDomain($domain) {
        $this->domain = $domain;
        return $this;
    }
}

和相应的字段集'AccountFieldset.php':

namespace Project\Form;

use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;
use Zend\Form\Element\Text;
use Zend\Form\Element\Select;
use Project\Model\AccountModel;
use Zend\Hydrator\ClassMethods;


class AccountFieldset extends Fieldset implements 
    InputFilterProviderInterface {
    const NAME_ID = 'id';
    const NAME_USERNAME = 'username';
    const NAME_DOMAIN = 'domain';

    private $domainsOptionValues = [ ];

    public function __construct(array $domains, $name = null, $options = null) {
        parent::__construct ( isset ( $name ) ? $name : 'account-fieldset', $options );

        $this->setHydrator ( new ClassMethods ( false ) )->setObject ( new AccountModel () );
        $this->domainsOptionValues = $domains;
    }
    public function init() {
        $this->add ( [ 
            'name' => self::NAME_ID,
            'type' => Text::class,
            'options' => [ 
                    'label' => 'Id' 
            ],
            'attributes' => [ 
                    'required' => 'required',
                    'readonly' => true 
            ] 
        ] );
        $this->add ( [ 
                'name' => self::NAME_USERNAME,
                'type' => Text::class,
                'options' => [ 
                        'label' => 'Username' 
                ],
                'attributes' => [ 
                        'required' => 'required' 
                ] 
        ] );
        $this->add ( [ 
                'name' => self::NAME_DOMAIN,
                'type' => Select::class,
                'options' => [ 
                        'label' => 'Domains',
                        'value_options' => $this->domainsOptionValues 
                ],
                'attributes' => [ 
                        'required' => 'required' 
                ] 
        ] );
    }

    public function getInputFilterSpecification() {
        return [ 
                /**  some InputFilterSpecifications **/
        ];
    }
}

这个 Fieldset 将在另一个 Fieldset 中使用,它表示 table 'AccountTableFieldset.php':

namespace Project\Form;

use Project\Model\AccountsTableModel;
use Zend\Form\Fieldset;
use Zend\Hydrator\ClassMethods;
use Zend\InputFilter\InputFilterProviderInterface;

class AccountsTableFieldset extends Fieldset implements InputFilterProviderInterface {

    const NAME_ACCOUNTS = 'accounts';

    public function __construct($name = null, $options = []) {
        $name = isset ( $name ) ? $name : 'accounts-table';
        parent::__construct ( $name, $options );

        $this->setHydrator ( new ClassMethods ( false ) )->setObject ( new AccountsTableModel () );

        $this->add ( [ 
                'type' => 'Zend\Form\Element\Collection',
                'name' => 'accounts',
                'options' => [ 
                        'label' => 'Accounts',
                        'count' => 1,   
                        'should_create_template' => false,
                        'allow_add' => false,
                        'target_element' => [ 
                                'type' => AccountFieldset::class,
                        ] 
                ] 
        ] );
    }

    public function getInputFilterSpecification() {
        return [ 
                /** some InputFilterSpecification **/
        ];
    }
}

AccountsTableModel.php”没有什么特别之处:

namespace Project\Model;

class AccountsTableModel {

    private $accounts = [];

    /**
     * @return AccountModel[] 
     */
    public function getAccounts() : array {
        return $this->accounts;
    }
    public function setAccounts(array $accounts) {
        $this->accounts = $accounts;
        return $this;
    }
}

所以我的表格看起来像这样'AccountForm.php':

namespace Project\Form;

use Zend\Form\Form;
use Zend\Form\Element\Submit;

class AccountsForm extends Form {

    const NAME_TABLE = 'accounts-table';
    const NAME_SUBMIT= 'accounts-save';

    public function init(){

        $this->setName('accounts-form');

        $this->add([
            'name' => self::NAME_TABLE,
            'type' => AccountsTableFieldset::class,
            'attributes' => [
                    'class' => 'accounts',
                    'id' => 'accounts-table',
            ],
        ]);                

        $this->add([
                    'name' => self::NAME_SUBMIT,
                    'type' => Submit::class,
                    'attributes' => [
                            'class' => 'btn btn-success',
                            'value' => 'Save accounts',
                            'id' => 'accounts-save',
                    ],
        ]);
    }   
}

此表单通过“AccountsFormFactory.php”中的 FormElementManager 初始化:

namespace Project\Factory;

use Interop\Container\ContainerInterface;
use Project\Form\AccountsForm;
use Zend\ServiceManager\Factory\FactoryInterface;

class AccountsFormFactory implements FactoryInterface{

    public function __invoke(ContainerInterface $container){
        $accountsForm = $container->get('FormElementManager')->get(AccountsForm::class);
        $accountsForm->init();

        return $accountsForm;
    }
}

为了填充 AccountFieldset 中的 Select-元素,我创建了 '** AccountFieldsetFactory.php**':

namespace Project\Factory;

use Interop\Container\ContainerInterface;
use Project\Form\AccountFieldset;

class AccountFieldsetFactory {
    public function __invoke(ContainerInterface $container){
        $domains = [
            '1' => 'example1.com',
            '2' => 'example2.com',
            '3' => 'example3.com',
        ];
        $accountsFieldset = new AccountFieldset($domains);
        $accountsFieldset->init();

        // die(__FILE__ . ' #'. __LINE__);
        return $accountsFieldset;
    }
}

注意我让它死了。但不幸的是,这条线永远不会到达,因为 ElementFactory 直接调用了 AccountFieldset。此时我得到错误:

Uncaught TypeError: Argument 1 passed to Project\Form\AccountFieldset::__construct() must be of the type array, string given, called in /var/www/html/zf3.local/vendor/zendframework/zend-form/src/ElementFactory.php on line 70

为什么调用 ElementFactory 而不是我的 AccountFieldsetFactory?我将 'form_elements' 的 'factories' 配置如下 'ConfigProvider.php':

namespace MailManager;

use Zend\Db\Adapter;
use Project\Factory\FormElementManagerDelegatorFactory;

class ConfigProvider {
    public function __invoke(){

        return [
                'dependencies'  => $this->getDependencies(),
                'routes'        => $this->getRoutes(),
                'templates'     => $this->getTemplates(),
                'form_elements' => [
                     'factories' => [
                        Form\AccountFieldset::class => Factory\AccountFieldsetFactory::class,
                     ]
            ]
        ];
    }

    public function getDependencies(){
        return [
                'factories' => [
                        Action\AccountsAction::class => Factory\AccountsActionFactory::class,

                        Repository\AccountsRepositoryInterface::class => Factory\AccountsRepositoryFactory::class,

                        Storage\AccountsStorageInterface::class => Factory\AccountsStorageFactory::class,

                        Form\AccountsForm::class => Factory\AccountsFormFactory::class,
                        Form\NewAccountForm::class => Factory\NewAccountFormFactory::class,

                        Adapter\AdapterInterface::class => Adapter\AdapterServiceFactory::class,
                ],
                'delegators' => [
                        'FormElementManager' => [
                                FormElementManagerDelegatorFactory::class,
                        ],
                ],
        ];
    }

    public function getRoutes(){
        return [
                /** some routes **/
        ];
    }

    public function getTemplates(){
        return [
                'paths' => [
                    /** some paths **/
                ],
        ];
    }
}

感谢 Configure FormElementManager #387 中的建议,FormElementManagerDelegatorFactory 工作正常并且 AccountFieldsetFactory 显示在 FormElementManager 的工厂部分中。但遗憾的是(如前所述)从未调用过 AccountFieldsetFactory。我错过了什么?

问题是由一个简单的疏忽引起的:AccountsTableFieldset 直接在构造函数中添加了 AccountFieldsets 的集合。这应该由 init() 方法完成。所以改变我的'AccountTableFieldset.php':

namespace Project\Form;

use Project\Model\AccountsTableModel;
use Zend\Form\Fieldset;
use Zend\Hydrator\ClassMethods;
use Zend\InputFilter\InputFilterProviderInterface;

class AccountsTableFieldset extends Fieldset implements InputFilterProviderInterface {

    const NAME_ACCOUNTS = 'accounts';

    public function __construct($name = null, $options = []) {
        $name = isset ( $name ) ? $name : 'accounts-table';
        parent::__construct ( $name, $options );

        $this->setHydrator ( new ClassMethods ( false ) )->setObject ( new AccountsTableModel () );
    }

    public function init() {
        $this->add ( [ 
                'type' => 'Zend\Form\Element\Collection',
                'name' => 'accounts',
                'options' => [ 
                        'label' => 'Accounts',
                        'count' => 1,   
                        'should_create_template' => false,
                        'allow_add' => false,
                        'target_element' => [ 
                                'type' => AccountFieldset::class,
                        ] 
                ] 
        ] );
    }

    public function getInputFilterSpecification() {
        return [ 
                /** some InputFilterSpecification **/
        ];
    }
}