douweidao3882 2015-11-04 21:58
浏览 40
已采纳

与Zend \ Form和Doctrine 2实现多对多关系

I have a Zend\Form and corresponding Doctrine entity class where the entity as a ManyToMany relationship with another entity. More precisely, the user needs to be able to choose one or more names from a data table containing 12,000 names -- far too many for a normal SELECT element.

In an earlier iteration of this project, which used ZF1, I had a MultiSelect element, with zero options, that I simply never rendered. Instead I made an autocompletion text field with JQueryUI for dynamically inserting the human-readable names and the ids as hidden elements. Worked great.

I have looked at Zend\Form\Element\Collection but the docs say you can't update it with fewer elements than you started out with -- that is, if at update-form hydration time you have 2 whatevers, you have to submit at least 2 whatevers. That won't do.

Elsewhere I am happily using DoctrineModule\Form\Element\ObjectSelect but it doesn't seem like the right choice for this case.

Before I go off and try to use the same technique I used with ZF1 I'd be delighted if anyone could give me a better idea.

  • 写回答

2条回答 默认 最新

  • dsgd4654674 2015-12-07 16:28
    关注

    My answer: there is no exotic, secret knowledge. Use autocompletion and a database as you would with any other tools/libraries.

    To describe my use-case a little: users submit requests for an interpreter for a future court proceeding. They have to provide the name(s) of the defendant(s), among other data (e.g., language). There are over 12K such names already in our database and we re-use recurring names (IOW, the entity represents a person's proper name rather than a person).

    Now for some code excerpts. In the front end, the view script:

    <?php 
    $this->headScript()->appendFile('/dev-requests/js/jquery-ui.min.js');
    $this->headScript()->appendFile('/dev-requests/js/defts.js');
    // stuff omitted...
    // within the form:
    // $element is a \Zend\Form\Element\Select with attribute 'multiple' => 'multiple', zero options,
    // and it's hidden via css because no one needs to see it
    <div class="form-group">
        <label class="control-label col-sm-3" for="<?=$element->getName()?>"><?=$element->getLabel()?></label>
        <div class="col-sm-9"><?= $this->formElement($element)?><?= $this->formElementErrors($element) ?>
        <?php if ($this->defendants): 
        // if we are an update (as opposed to create) action, our controller's updateAction 
        // will have set $this->defendants to a (possibly empty) array of 'defendantName' entities
        foreach($this->defendants as $deft): ?>
            <div id="deft-div-<?=$deft->getDeftId()?>"><span class="remove-div"><a href="#">[x]</a></span> 
                <?=$deft->getFullname()?>
                <input value="<?=$deft->getDeftId()?>" name="request-fieldset[defendantNames][]" type="hidden">
            </div>
            <?php
                endforeach;
            endif;
            ?>
        </div>
    </div>
    

    And some Javascript from defts.js, inside our document.ready() callback:

    // the autocomplete textfield itself
    $('#deft-select').after(
        $('<input>').attr({id:'deftname-autocomplete',size:25})
    );
    // for deleting a name from the form
    ($('form').on('click','span.remove-div',function(event){
        event.preventDefault();
        $(this).parent().slideUp(function(){$(this).remove();});
    }));
    
    $('#deftname-autocomplete').autocomplete({
    
        source : '/dev-requests/defendants/autocomplete',
        select: function( event, ui ) { 
            // add a human-readable label and hidden form element
            $(this).val('');
            var elementName = $('#deft-select').attr('name');
            var deftName = ui.item.label;
            var deft_id  = ui.item.value;
            if ($( '#deft-div-'+ deft_id ).length) {
                return false; // already exists
            }
            var div = $(this).closest('div');
            div.append(
                $('<div/>').attr({id: "deft-div-"+ deft_id})
                    .html([
                        '<span class="remove-div"><a href="#">[x]</a></span> ' + deftName,
                        $('<input/>').attr({type:'hidden',name:elementName}).val(deft_id)
                    ])
            );
            return false;
        }
    
    });
    

    In our controller:

     public function autocompleteAction()
    {
    
        $term = $this->getRequest()->getQuery('term');
        if (! $term) { return false; }
    
        /**
         * @var $em Doctrine\ORM\EntityManager
         */
        $em = $this->getServiceLocator()->get('entity-manager');
        /**
         * @var $repo Application\Entity\DefendantNameRepository
         */
        $repo = $em->getRepository('Application\Entity\DefendantName');
    
        $data = json_encode($repo->autocomplete($term));
        $response = $this->getResponse();
        $response->getHeaders()->addHeaders(['Content-type'=>'application/json;charset=UTF-8']);
        return $response->setContent($data);
    }
    

    In our custom Doctrine repository, Application\Entity\DefendantNameRepository:

    /**
    * return array of value/label for autocompletion via xhr
    * @param string $term name 
    * @param int limit max number of records to return
    *  
    * $term is expected to be proper name in the format la[stname][,f[irstname]] 
    */
    
    public function autocomplete($term, $limit = 20) {
    
        /**
         * @var $connection Doctrine\DBAL\Connection
         */
        $connection = $this->getEntityManager()->getConnection();
        list($lastname,$firstname) = $this->parseName($term);
        if (! strstr($lastname,'-')) {
            $where = 'lastname LIKE '.$connection->quote("$lastname%");
        } else {
            // they frequently insert gratuitous hyphens between the 
            // paternal and maternal surnames of Spanish-speaking people
            $lastname = str_replace('-','( |-)',$lastname);
            $where = 'lastname REGEXP '.$connection->quote("^$lastname");
        }
        if ($firstname) {
            $where .= " AND firstname LIKE ".$connection->quote("$firstname%");
        } else {
            $where .= " AND firstname <> '' "; // some old records have no firstname, but we don't like that
        }
        $sql = 'SELECT CONCAT(lastname, ", ",firstname) AS label, deft_id AS value 
                FROM deft_names WHERE '.$where . " ORDER BY lastname, firstname LIMIT $limit ";
        return $connection->fetchAll($sql);
    }
    

    ...and this little helper, elsewhere in our repository:

    /**
     * parses first and last names out of $name. expected format is
     * la[stname][,f[irstname]] 
     * @param string $name
     * @return array ($lastname, $firstname)
     */
    public function parseName($name) {
    
        $name = preg_split('/ *, */',trim($name),2,PREG_SPLIT_NO_EMPTY);
        if (2 == sizeof($name)) {
            list($last, $first) = $name;
        } else {
            $last = $name[0];
            $first = false;
        }
        return array($last,$first);
    }
    

    The class definition Application\Entity\DefendantName is straightforward and omitted for brevity.

    Still to do: add a search button thingy to the right of our autocompleting text element for them to click when they get no autocompletion matches so we can tell them "sorry, no matching records found." And -- though this is tangential to the original question -- provide a way for them to submit names we've never heard of.

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论
查看更多回答(1条)

报告相同问题?

悬赏问题

  • ¥20 求各位懂行的人,注册表能不能看到usb使用得具体信息,干了什么,传输了什么数据
  • ¥15 个人网站被恶意大量访问,怎么办
  • ¥15 Vue3 大型图片数据拖动排序
  • ¥15 Centos / PETGEM
  • ¥15 划分vlan后不通了
  • ¥20 用雷电模拟器安装百达屋apk一直闪退
  • ¥15 算能科技20240506咨询(拒绝大模型回答)
  • ¥15 自适应 AR 模型 参数估计Matlab程序
  • ¥100 角动量包络面如何用MATLAB绘制
  • ¥15 merge函数占用内存过大