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 mysql架构,按照姓名分表
  • ¥15 MATLAB实现区间[a,b]上的Gauss-Legendre积分
  • ¥15 Macbookpro 连接热点正常上网,连接不了Wi-Fi。
  • ¥15 delphi webbrowser组件网页下拉菜单自动选择问题
  • ¥15 linux驱动,linux应用,多线程
  • ¥20 我要一个分身加定位两个功能的安卓app
  • ¥15 基于FOC驱动器,如何实现卡丁车下坡无阻力的遛坡的效果
  • ¥15 IAR程序莫名变量多重定义
  • ¥15 (标签-UDP|关键词-client)
  • ¥15 关于库卡officelite无法与虚拟机通讯的问题