Zend Framework 2/3 набор сбора пользовательских атрибутов

Судя по этим вопросам:

Zend framework 2 просматривает элементы коллекции элементов

Как передать аргумент экземпляру Zend Form Collection и как установить пользовательские метки Fieldset в ZF2?

Я полагаю, что нет хорошего способа настроить элементы коллекции.

Как пример, имея такую ​​коллекцию:

//path: MyModule\Form\MyFieldset

public function __construct($name = null) {

    parent::__construct('myFieldset');

    $this->add([
        'name'=>'test',
        'type' => Element\Collection::class,
        'options' => [
            'label' => 'MyCollection',
            'count' => 6,
            'should_create_template' => true,
            'target_element' => new Element\Text()
        ],
    ]);
}

Затем сделайте что-нибудь, чтобы определить (здесь, в текущем классе) настраиваемые атрибуты для каждого текстового элемента и / или метки с автонумерацией и затем вывести (просто вызывая помощник Zend FormCollection без какого-либо настраиваемого помощника вида):

<label>
   <span>text element n° 1</span>
   <input type="text" name="myFielset[test][0]" id='myId_0' alt='input 0' value="">
</label>

<label>
   <span>text element n° 2</span>
   <input type="text" name="myFielset[test][1]" id='myId_1' alt='input 1' value="">
</label>

[...]

Я ошибся?

(Я спрашиваю об этом, потому что я нашел хорошее решение, чтобы сделать это, и, возможно, может быть полезно опубликовать его)

3 ответа

Решение, которое я нашел, имеет что-то общее с решением Ричарда Парнаби-Кинга:

target_element должен ссылаться на набор полей.

Но вместо того, чтобы установить счетчик клонов, он расширяет Zend\Form\Fieldset method prepareElement

Основное применение:

namespace Module\Form;

use Zend\Form\Fieldset;
use Zend\Form\FormInterface; //needed in order to call prepareElement method

class MyFieldset extends Fieldset {


    public function __construct($name = null) {
        parent::__construct($name);

        $this->add([
            'name' => 'question',
            'type' => 'text',
            'attributes' => [
                'alt' => 'input',
            ],
            'options' => [
                'label' => 'Text',
            ],
        ]);

    }//construct

    public function prepareElement(FormInterface $form){

        parent::prepareElement($form);

        $name = $this->getName(); 
        //Do stuff related to this fieldset

        foreach ($this->iterator as $elementOrFieldset) {

           $elementName=$elementOrFieldset->getName()
           //Do stuff related to this fieldset children
        }

    }//prepareElement

}

Особенности:

  1. Автонумерация меток и / или атрибутов
  2. Позволяет условное присвоение атрибутов / меток
  3. Позволяет взаимодействие между различными элементами (например: передача идентификатора элемента A в элемент B в качестве цели)
  4. Работает с шаблонами


Поскольку это решение может быть разработано разными способами, я подготовил полную демонстрацию, готовую к запуску и изучению.

Примечание: эта демонстрация не лучшая реализация, а набор примеров, которые приводят к результату. :-)

Этот пример предназначен для запуска в модуле "Приложение" по умолчанию с использованием префикса "Bob", чтобы избежать конфликтов с другими файлами (я представлял, что у кого-то уже может быть файл с именем TestController, но, полагаю, ни у кого нет файла с именем BobController).

Тогда, если вы точно выполните следующие шаги, вы сможете без проблем запустить и исследовать демонстрацию.

Реализация prepareElement метод в BobFieldset класс может показаться массивным, но это просто вопрос комментариев, пробелов и примеров. Это может быть очень мало в зависимости от ваших потребностей.

ШАГ 1:

РЕДАКТИРОВАТЬ файл: Application \ config \ module.config.php

//add bob route to router

'router' => [
        'routes' => [

            'bob' => [
                'type' => Literal::class,
                'options' => [
                    'route'    => '/bob',
                    'defaults' => [
                        'controller' => Controller\BobController::class,
                        'action'     => 'index',
                    ],
                ],
            ],

            [...]

//add BobController

 'controllers' => [
        'factories' => [
            [...]
            Controller\BobController::class => InvokableFactory::class,
        ],
    ],

ШАГ 2:

СОЗДАТЬ файл: Application\src\ Controller \BobController.php

<?php

namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

use Application\Form\BobForm;

class BobController extends AbstractActionController
{
    public function __construct(){}

    public function indexAction()
    {

     $form = new BobForm('album');

     $request = $this->getRequest();


     if( $request->isPost()){

       $form->setInputFilter($form->getInputFilter());
       $form->setData($request->getPost());

        if (! $form->isValid()) {
            return ['form' => $form];
        }
     }
     return ['form' => $form];
    }
}

ШАГ 3:

СОЗДАТЬ файл: Application\src\Form\BobForm.php

 <?php

namespace Application\Form;

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

class BobForm extends Form
{
   private $inputFilter;
    public function __construct($name = null)
    {
        parent::__construct($name);

        $this->setLabel('formBaseFieldset');

        $this->add([
            'name' => 'select',
            'type' => 'select',
            'options' => [
                'label' => 'random element',
                'value_options' => [
                             0 => null,
                             1 => 'someThing',
                             2 => 'someThingElse',
                     ],
            ],
            'attributes' => [
            'value'  => 0,
            ],
        ]);

        $this->add([
            'name' => 'answer',
            'type' => Element\Collection::class,
            'options' => [
                'label'=>'bobFieldset',
                'count' =>3,
                'should_create_template' => true,
                'target_element' => new \Application\Form\BobFieldset ,
            ],
            'attributes' => [
            'id'=>'bob',
            ],
        ]);
        $this->add(array(
             'name' => 'addNewRow',
             'type' => 'button',
             'options' => array(
                 'label' => 'Add a new Row',
             ),
             'attributes' => [
             'onclick'=>'return add_category()',

             ]
         ));

}//construct

    public function getInputFilterSpecification() {
               return array(

            'select' => [
                'validators' => [
                    ['name' => 'NotEmpty'],
                ],  
            ],
        );
    }

}

ШАГ 4:

СОЗДАТЬ файл: Application\src\Form\BobFieldset.php

<?php

namespace Application\Form;

use Zend\Form\Fieldset;
use Zend\Form\FormInterface; //needed in order to call prepareElement method
use Zend\InputFilter\InputFilterProviderInterface;

class BobFieldset extends Fieldset  implements InputFilterProviderInterface 
{

   private $inputFilter;

    public function __construct($name = null) {

        parent::__construct($name);

        $this->setLabel('bobFieldset: Answer __num__');

        $this->add(array(
             'name' => 'text',
             'type' => 'text',
             'options' => array(
                 'label' => 'Text __num__',
             ),
             'attributes' => [
             'customAttribute'=>' -> ', //see below

             ]
         ));

        $this->add(array(
             'name' => 'optionsButton',
             'type' => 'button',
             'options' => array(
                 'label' => 'Options',
             ),
             'attributes' => [
             'data-dialog-target'=>'options', //sub fieldset name
             'class'=>'options',
             ]
         ));

        $this->add( new \Application\Form\BobSubFieldset('options'));
    }

   public function prepareElement(FormInterface $form)
    {

        /*--->here we're looping throug the collection target_element(instance of BobFieldset)<---*/

        //Leave untouched the default naming strategy
        parent::prepareElement($form);



        //output: (string) 'answer[$i]' -> BobFieldset's clone name attribute
        //Note: $i corresponds to the instance number
        $name = $this->getName();   //var_dump($name);

        //output: array(0=>'answer',1=>$i)
        $sections = $this->splitArrayString($name); //var_dump($sections);

        //output: (string) $i ->When the collection's option 'should_create_template' is setted to true, the last one will be: (string) '__index__'
        $key=end($sections);  //var_dump($key);

        //output (string) 'answer_$i' -> I guess this could be the easyest way to handle ids (easy to manipulate, see below)
        $string=implode('_',$sections); //var_dump($string);


        //Just because a label like 'answer number 0' is ugly ;-)
        $keyPlus=(is_numeric($key)) ? $key+1 : $key;  //var_dump($keyPlus);

        //Since we're using different placeholders:
        //Predefined __index__: used for names ($key)
        //__num__: used for labels ($keyplus)
        //Then we need this control to avoid replacements between placeholders(check below)
        $isTemplate=($keyPlus==$key) ? true : false;


        if(!$isTemplate){
          //get the label of the current element(BobFieldset clone) and replace the placeholder __num__ (defined above) with the current key (+1)
          $label = str_replace('__num__',($keyPlus),$this->getLabel());
          $this->setLabel($label); //var_dump($this->getLabel());
        }
        /*--->From here we're looping throug the target_element (BobFieldset) children<---*/
        foreach ($this->iterator as $elementOrFieldset) {

            //output: (string) 'answer[$i][elementName]'
            //answer[0][text]
            //answer[0][optionsButton]
            //answer[0][options]
            //answer[1][text]
            //...
            $elementName=$elementOrFieldset->getName();//var_dump($elementName);

            //Example: get specific element and OVERWRITE an autonumbered label
            $sections = $this->splitArrayString($elementName);
            $trueName=end($sections);
            if($trueName=='text' && !$isTemplate){
              $elementOrFieldset->setLabel('Input '.$keyPlus);
            }

           //Example2: get specific element via custom attribute
           //Note: when an attribute isn't listed into the Zend\Form\View\Helper\AbstractHelper's $validGlobalAttributes (array) it will be automatically removed on render
           //global attributes data-? will be rendered
           if($target=$elementOrFieldset->getAttribute('customAttribute')){
             $label=$elementOrFieldset->getLabel();
             $elementOrFieldset->setLabel($label.$target);
             }

           //Reference another element as target for a javascript function
           //button 'optionsButton' will have an attribute containing the id of the relative element 'options' (BobSubFieldset)
           //Alternatives:
           //1) work only with javascript & DOM 
           //2) set a javascript call directly: $elementOrFieldset->setAttribute('onclick','return doSomething();'); check BobForm 'addNewRow' button
           if($target=$elementOrFieldset->getAttribute('data-dialog-target')){
             $elementOrFieldset->setAttribute('data-dialog-target',$string.'_'.$target);
           }

           //set id for jqueryui dialog function. This id corresponds to the target setted above
           //The attribute data-transform will be used as jquery selector to create the dialogs
           if($elementOrFieldset->getAttribute('data-transform')=='dialog'){
             $id = str_replace(['[', ']'],['_', ''],$elementName);
             $elementOrFieldset->setAttribute('id',$id);

             //Set and autonumbering the dialog title
             if(!$isTemplate){
               $title = str_replace('__num__',($keyPlus),$elementOrFieldset->getAttribute('title'));
               $elementOrFieldset->setAttribute('title',$title);
             }
           }


        }//foreach

    }

    public function splitArrayString($string){

    return preg_split('/\h*[][]/', $string, -1, PREG_SPLIT_NO_EMPTY);
    }


    public function getInputFilterSpecification() {
               return array(

            'text' => [
                'validators' => [
                    ['name' => 'NotEmpty'],
                ],  
            ],
        );
    }
 }

ШАГ 5:

СОЗДАТЬ файл: Application\src\Form\BobSubFieldset.php

<?php

namespace Application\Form;

use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;

class BobSubFieldset extends Fieldset  implements 
InputFilterProviderInterface {

   private $inputFilter;

    public function __construct($name = null) {
         parent::__construct($name);

              $this->setLabel('bobSubFieldset');
              $this->setattribute('data-transform','dialog');
              $this->setattribute('title','Options for answer __num__');


         $this->add(array(
             'name' => 'first',
             'type' => 'text',
             'options' => array(
                 'label' => 'firstOption',
             ),
             'attributes' => [

             ]
         ));
         $this->add(array(
             'name' => 'second',
             'type' => 'text',
             'options' => array(
                 'label' => 'secondOption',
             ),
             'attributes' => [

             ]
         ));
                $this->add(array(
             'name' => 'third',
             'type' => 'text',
             'options' => array(
                 'label' => 'thirdOption',
             ),
             'attributes' => [

             ]
         ));

    }

    public function getInputFilterSpecification() {
        return array();
    }
}

ШАГ 6 (последний):

СОЗДАТЬ файл: Application\view\application\bob\index.phtml

Примечание: здесь я добавил все внешние js/css, которые я использовал, возможно, они уже есть в вашем макете.

<?php

$script=
"$(document).ready(function(){

 $( '#bobContainer' ).on('click','button.options', function () {

    //Retrieve the target setted before...
    id=$(this).attr('data-dialog-target');
     $('#'+id).dialog('open');


    return false;
  });

 //We need a custo event in order to detect html changes when a new element is added dynamically
 $( '#bobContainer' ).on( 'loadContent', function() {

     //We need this because by default the dialogs are appended to the body (outside the form)
     $('fieldset[data-transform=dialog]').each(function (index) {

     $(this).dialog({
        autoOpen: false,
        appendTo:$(this).parent(),
        modal: true,
        height: 250,
        width: 450,
        buttons: {

        Ok: function() {
          $(this).dialog( 'close' );
        }
      },



    });
    });
  });


 $( '#bobContainer' ).trigger( 'loadContent');
}); //doc/ready

function add_category() {

var currentCount = $('#bob > fieldset').length;
var template = $('#bob > span').data('template');

template = template.replace(/__index__/g, currentCount);
template = template.replace(/__num__/g, (currentCount+1));

$('#bob').append(template).trigger( 'loadContent');

return false;
}
";
$this->headScript()
            ->appendFile("https://code.jquery.com/jquery-3.3.1.min.js",'text/javascript',array('integrity' => 'sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=','crossorigin'=>'anonymous'))
            ->appendFile("https://code.jquery.com/ui/1.12.1/jquery-ui.js")
            ->appendScript($script, $type = 'text/javascript', $attrs = array());
$this->headLink()
            ->appendStylesheet('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css')
            ->appendStylesheet('https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css');


echo $this->headScript();
echo $this->headLink();

$title = 'Custom Collection Attributes';
$this->headTitle($title);

echo'<h1>'.$this->escapeHtml($title).'</h1>';
echo'<div class="container" id="bobContainer">';


$form->prepare();
echo $this->form()->openTag($form);
echo $this->formCollection($form);

$form->add([
            'name' => 'submit',
            'type' => 'submit',
            'attributes' => [
                'value' => 'Go',
                'id'    => 'submitbutton',
                'class'=>'btn btn-success',
            ],
        ]);

$submit = $form->get('submit');

echo '<br>'.$this->formSubmit($submit);

echo $this->form()->closeTag();

echo'</div>';

ВЫХОД:

Screenshot1 screenshot2

screenshot3


РЕДАКТИРОВАТЬ Я заметил небольшую проблему: когда элемент создается из шаблона, диалоги старых элементов становятся недоступными. Это связано с опцией диалога jquery modal: true, Вероятно, есть исправление, но поскольку основной аргумент касается Zend... просто удалите эту опцию.

Я только что понял, что есть еще одно хорошее и несколько более гибкое решение: расширение элемента коллекции (почему я не подумал об этом раньше?).

Основным преимуществом этого подхода является то, что нет необходимости разделять имя элемента: "номер клона" ([0],[1],...) напрямую доступен.

Особенности:

  1. Автонумерация меток и / или атрибутов
  2. Позволяет условное присвоение атрибутов / меток
  3. Позволяет взаимодействие между различными элементами (ограничено, см. Вопросы ниже)
  4. Работает с шаблонами (используя заполнители -> подробнее), и нет необходимости проверять, является ли индекс числом (это была проблема моего другого решения)
  5. Target_element может быть простым элементом, нет необходимости реализовывать Zend/Form/Fieldset

Вопросы:

  1. Установить идентификаторы может быть проблематично, так как (2)

  2. из расширенного сценария нет способа получить доступ к имени конечного элемента (например:fieldset[subfieldset][0][elementName]), поскольку это будет иерархически построено позже.


Как это устроено:

1. Расширенная коллекция

//file: Application\src\Form\Element\ExtendedCollection.php
<?php

namespace Application\Form\Element;

use Zend\Form\Element\Collection;

class ExtendedCollection extends Collection
{
    protected $autonumbering_callback = false;
    protected $autonumbering_callback_options = [];  

    public function setOptions($options)
    {
        parent::setOptions($options);

        if (isset($options['autonumbering_callback'])) {
            $this->autonumbering_callback=(isset($options['autonumbering_callback'][0])) ? $options['autonumbering_callback'][0] : $options['autonumbering_callback'];
            $this->autonumbering_callback_options=(isset($options['autonumbering_callback'][1])) ? $options['autonumbering_callback'][1] : [];
        }

        return $this;
    }

    protected function addNewTargetElementInstance($key)
    {

        //Original instructions
        $this->shouldCreateChildrenOnPrepareElement = false;

        $elementOrFieldset = $this->createNewTargetElementInstance();
        $elementOrFieldset->setName($key);

        $this->add($elementOrFieldset);

        if (! $this->allowAdd && $this->count() > $this->count) {
            throw new Exception\DomainException(sprintf(
                'There are more elements than specified in the collection (%s). Either set the allow_add option ' .
                'to true, or re-submit the form.',
                get_class($this)
            ));
        }

        //Callback
        if ($this->autonumbering_callback && method_exists(...$this->autonumbering_callback) && is_callable($this->autonumbering_callback)){
          call_user_func_array($this->autonumbering_callback,[$elementOrFieldset,$key,$this->autonumbering_callback_options]);
        }

        return $elementOrFieldset;
    }

}

2. Целевой элемент (здесь это набор полей, но может быть простым элементом)

//file: Application\src\Form\BobFieldset.php
<?php

namespace Application\Form;

use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;

class BobFieldset extends Fieldset  implements InputFilterProviderInterface {

   private $inputFilter;

    public function __construct($name = null) {

        parent::__construct($name);

        $this->setLabel('Answer __num__');
        $this->setAttributes([
                             'title'=>'title no __num__',
                             'data-something'=>'custom attribute no __num__',
                             ]);


        $this->add(array(
             'name' => 'text',
             'type' => 'text',
             'options' => array(
                 'label' => 'Text',
             ),
         ));

         $this->add(array(
             'name' => 'text2',
             'type' => 'text',
             'options' => array(
                 'label' => 'Text',
             ),
         ));

         $this->add(array(
             'name' => 'text3',
             'type' => 'text',
             'options' => array(
                 'label' => 'Text',
             ),
         ));
    }

    public function getInputFilterSpecification() {
        return array(/*...*/);
    }

}//class

3. Форма (с некоторым примером обратного вызова)

//file: Application\src\Form\BobForm.php
<?php

namespace Application\Form;

use Zend\Form\Form;
use Zend\Form\Fieldset; //needed for myCallback3
use Application\Form\Element\ExtendedCollection;

class BobForm extends Form
{
   private $inputFilter;
    public function __construct($name = null)
    {
        parent::__construct($name);


        $this->add([
            'name' => 'answer',
            'type' => ExtendedCollection::class,
            'options' => [
                'count' =>3,
                'should_create_template' => true,
                'target_element' => new \Application\Form\BobFieldset2 ,
                'autonumbering_callback'=>[
                                           [$this,'myCallback'],
                                           ['attributes'=>['title','data-something'],'whateverYouWant'=>'something',]
                                          ],
                ],
        ]);
    }

    public function myCallback($elementOrFieldset, $key, $params){

      foreach($params['attributes'] as $attr){
        $autoNumAttr=str_replace('__num__',($key),$elementOrFieldset->getAttribute($attr));
        $elementOrFieldset->setAttribute($attr,$autoNumAttr);
      }//foreach

      $label = str_replace('__num__',($key+1),$elementOrFieldset->getLabel());
      $elementOrFieldset->setLabel($label);
    }

    public function myCallback2($elementOrFieldset, $key, $params){

      $char='a';
      foreach(range(1,$key) as $i) {
        if($key>0){$char++;}
      }
      $elementOrFieldset->setLabel('Answer '.$char);
    }

    public function myCallback3($elementOrFieldset, $key, $params, $isChild=null){

      if(!$isChild){$elementOrFieldset->setLabel('Answer '.($key+1));}
      else{$elementOrFieldset->setLabel($key);}

      //don't forget: use Zend\Form\Fieldset;
      if($elementOrFieldset instanceof Fieldset && !$isChild){
        $char='a';
        foreach($elementOrFieldset as $item){
          $this->myCallback3($item,($key+1 .$char++.') '),null,1);
        }
      }
    }
}

ВЫХОД

без autonumbering_callback опция: без опции autonumbering_callback

с помощью myCallback: используя myCallback

с помощью myCallback2: используя myCallback2

с помощью myCallback3: используя myCallback3

target_element должен ссылаться на набор полей. Это может быть либо новый экземпляр в форме, где находится коллекция, либо имя класса.

Например:

$fieldset = new Fieldset();
$fieldset->add([
    'name' => 'some_field_name',
    'type' => 'text',
]);
$this->add([
    'name'=>'test',
    'type' => Element\Collection::class,
    'options' => [
        'label' => 'MyCollection',
        'count' => 6,
        'should_create_template' => true,
        'target_element' => $fieldset
    ],
]);

или же

$this->add([
    'name'=>'test',
    'type' => Element\Collection::class,
    'options' => [
        'label' => 'MyCollection',
        'count' => 6,
        'should_create_template' => true,
        'target_element' => '\Namespace\Form\MyTextFieldset',
    ],
]);

Что касается настройки метки для каждого ввода, я еще не нашел способ сделать это.

Не слишком уверен во внутренней работе того, как работает коллекция, но я подозреваю, что она создает столько же новых экземпляров target_element как требуется. С точки зрения простого добавления числа к метке (или произвольному атрибуту), вы можете создать класс fieldset со статическим свойством, которое начинается с 1добавить его к метке и увеличить его значение?

Например:

namespace Module\Form;
use Zend\Form\Fieldset;

class MyFieldset extends Fieldset {
    public static $instance_count = 1;

    public function __construct() {
        parent::__construct();

        $this->add([
            'name' => 'question',
            'type' => 'text',
            'attributes' => [
                'alt' => 'input' . MyFieldset::$instance_count,
            ],
            'options' => [
                'label' => 'Text element No ' . MyFieldset::$instance_count,
            ],
        ]);
        MyFieldset::$instance_count++;
    }
}