Frage Wie man Formulareingaben / Elemente in ZF2 erstellt


EDIT: Meine Hauptfrage ist jetzt geworden "Wie bekomme ich den ServiceManager mit dem Doktrin-Entity-Manager in die Hände meiner Form, Element und Input-Klassen auf eine saubere Art und Weise?" Lesen Sie weiter, um den vollständigen Beitrag zu sehen.

Ich werde versuchen, hier mit gutem Beispiel zu fragen, also ertragen Sie mit mir. Lass mich wissen, wo ich falsch liege oder wo ich mich verbessern könnte

Ich versuche ein Registrierungsformular zu erstellen. Ich könnte ZfcUser Modul verwenden, aber ich möchte das alleine machen. Ich benutze auch ZF2 mit Doctrine2, was mich ein wenig von diesem Modul wegführt.

Meine Strategie war das,

  1. Erstellen Sie eine Formularklasse namens Registrierungsformular

  2. Erstellen Sie separate Elementklassen für jedes Element, für das jedes Element eine Eingabespezifikation hat

  3. Da jedes Element eine separate Klasse von der Form ist, kann ich jede Einheit separat testen.

Alles schien in Ordnung zu sein, bis ich meinem Usernamen-Element einen Validator hinzufügen wollte, der prüfen würde, ob der Benutzername NOT ist.

Hier ist der Code soweit

namepsace My\Form;

use Zend\Form\Form,
    Zend\Form\Element,
    Zend\InputFilter\Input,
    Zend\InputFilter\InputFilter,

/**
 * Class name : Registration
 */
class Registration
    extends Form
{

    const USERNAME     = 'username';
    const EMAIL        = 'email';
    const PASSWORD     = 'password';
    const PASS_CONFIRM = 'passwordConfirm';
    const GENDER       = 'gender';
    const CAPTCHA      = 'captcha';
    const CSRF         = 'csrf';
    const SUBMIT       = 'submit';

    private $captcha = 'dumb';

    public function prepareForm()
    {
        $this->setName( 'registration' );

        $this->setAttributes( array(
            'method' => 'post'
        ) );

        $this->add( array(
            'name'       => self::USERNAME,
            'type'       => '\My\Form\Element\UsernameElement',
            'attributes' => array(
                'label'     => 'Username',
                'autofocus' => 'autofocus'
            )
            )
        );

        $this->add( array(
            'name'       => self::SUBMIT,
            'type'       => '\Zend\Form\Element\Submit',
            'attributes' => array(
                'value' => 'Submit'
            )
        ) );

    }

}

Ich habe viel entfernt, was ich nicht für notwendig halte. Hier ist mein Benutzername Element unten.

namespace My\Form\Registration;

use My\Validator\UsernameNotInUse;
use Zend\Form\Element\Text,
    Zend\InputFilter\InputProviderInterface,
    Zend\Validator\StringLength,
    Zend\Validator\NotEmpty,
    Zend\I18n\Validator\Alnum;

/**
 *
 */
class UsernameElement
    extends Text
    implements InputProviderInterface
{

    private $minLength = 3;
    private $maxLength = 128;

    public function getInputSpecification()
    {
        return array(
            'name'     => $this->getName(),
            'required' => true,
            'filters'  => array(
                array( 'name'       => 'StringTrim' )
            ),
            'validators' =>
            array(
                new NotEmpty(
                    array( 'mesages' =>
                        array(
                            NotEmpty::IS_EMPTY => 'The username you provided is blank.'
                        )
                    )
                ),
                new AlNum( array(
                    'messages' => array( Alnum::STRING_EMPTY => 'The username can only contain letters and numbers.' )
                    )
                ),
                new StringLength(
                    array(
                        'min'      => $this->getMinLength(),
                        'max'      => $this->getMaxLength(),
                        'messages' =>
                        array(
                            StringLength::TOO_LONG  => 'The username is too long. It cannot be longer than ' . $this->getMaxLength() . ' characters.',
                            StringLength::TOO_SHORT => 'The username is too short. It cannot be shorter than ' . $this->getMinLength() . ' characters.',
                            StringLength::INVALID   => 'The username is not valid.. It has to be between ' . $this->getMinLength() . ' and ' . $this->getMaxLength() . ' characters long.',
                        )
                    )
                ),
                array(
                    'name'    => '\My\Validator\UsernameNotInUse',
                    'options' => array(
                        'messages' => array(
                            UsernameNotInUse::ERROR_USERNAME_IN_USE => 'The usarname %value% is already being used by another user.'
                        )
                    )
                )
            )
        );
    }    
}

Jetzt ist hier mein Validator

namespace My\Validator;

use My\Entity\Helper\User as UserHelper,
    My\EntityRepository\User as UserRepository;
use Zend\Validator\AbstractValidator,
    Zend\ServiceManager\ServiceManagerAwareInterface,
    Zend\ServiceManager\ServiceLocatorAwareInterface,
    Zend\ServiceManager\ServiceManager;

/**
 *
 */
class UsernameNotInUse
    extends AbstractValidator
    implements ServiceManagerAwareInterface
{

    const ERROR_USERNAME_IN_USE = 'usernameUsed';

    private $serviceManager;

    /**
     *
     * @var UserHelper
     */
    private $userHelper;
    protected $messageTemplates = array(
        UsernameNotInUse::ERROR_USERNAME_IN_USE => 'The username you specified is being used already.'
    );

    public function isValid( $value )
    {
        $inUse = $this->getUserHelper()->isUsernameInUse( $value );
        if( $inUse )
        {
            $this->error( UsernameNotInUse::ERROR_USERNAME_IN_USE, $value );
        }

        return !$inUse;
    }

    public function setUserHelper( UserHelper $mapper )
    {
        $this->userHelper = $mapper;
        return $this;
    }

    /**
     * @return My\EntityRepository\User
     */
    public function getUserHelper()
    {
        if( $this->userHelper == null )
        {
            $this->setUserHelper( $this->getServiceManager()->get( 'doctrine.entitymanager.orm_default' )->getObjectRepository( 'My\Entity\User') );
        }
        return $this->userHelper;
    }

    public function setServiceManager( ServiceManager $serviceManager )
    {
        echo get_class( $serviceManager );
        echo var_dump( $serviceManager );
        $this->serviceManager = $serviceManager;
        return $this;
    }

    /**
     *
     * @return ServiceManager
     */
    public function getServiceManager( )
    {
        return $this->serviceManager;
    }

}

Warum schien mir das eine gute Idee zu sein?

  1. Es schien mir eine gute Test- / Wiederverwendungswahl zu sein, da ich die Elemente bei Bedarf in meiner Anwendung separat wiederverwenden könnte.

  2. Ich könnte jeden von jedem Element generierten Input testen, um sicherzustellen, dass er Eingaben korrekt akzeptiert / ablehnt.

Dies ist das Beispiel meines Komponententests für das Element

public function testFactoryCreation()
{
    $fac = new Factory();

    $element = $fac->createElement( array(
        'type' => '\My\Form\Registration\UsernameElement'
        ) );
    /* @var $element \My\Form\Registration\UsernameElement  */

    $this->assertInstanceOf( '\My\Form\Registration\UsernameElement',
                             $element );

    $input      = $fac->getInputFilterFactory()->createInput( $element->getInputSpecification() );
    $validators = $input->getValidatorChain()->getValidators();
    /* @var $validators \Zend\Validator\ValidatorChain */

    $expectedValidators = array(
        'Zend\Validator\StringLength',
        'Zend\Validator\NotEmpty',
        'Zend\I18n\Validator\Alnum',
        'My\Validator\UsernameNotInUse'
    );

    foreach( $validators as $validator )
    {
        $actualClass = get_class( $validator['instance'] );
        $this->assertContains( $actualClass, $expectedValidators );

        switch( $actualClass )
        {
            case 'My\Validator\UsernameNotInUse':
                $helper = $validator['instance']->getUserHelper();
                //HAVING A PROBLEM HERE
                $this->assertNotNull( $helper );
                break;

            default:

                break;
        }
    }

}

Das Problem, das ich habe, ist, dass der Validator den UserHelper nicht richtig abrufen kann, was wirklich ein UserRepository von der Doktrin ist. Der Grund dafür ist, dass die Validatoren nur als ServiceManager Zugriff auf den ValidatorPluginManager erhalten und nicht auf den applikationsweiten ServiceManager zugreifen müssen.

Ich bekomme diesen Fehler für den Validator-Teil, obwohl, wenn ich die gleiche Get-Methode auf dem allgemeinen Service-Manager aufrufen, es ohne Probleme funktioniert.

1) Test\My\Form\Registration\UsernameElementTest::testFactoryCreation
Zend\ServiceManager\Exception\ServiceNotFoundException: Zend\ServiceManager\ServiceManager::get was unable to fetch or create an instance for doctrine.entitymanager.orm_default

Der var_dump ($ serviceManager) im Validator zeigt mir an, dass es sich um die Klasse ValidatorPluginManager handelt.

Ich habe versucht, eine Factory in den service_manager-Eintrag zu setzen

'service_manager' => array(
                'factories' => array(
                    'My\Validator\UsernameNotInUse' => function( $sm )
                    {
                        $validator = new \My\Validator\UsernameNotInUse();
                        $em        = $serviceManager->get( 'doctrine.entitymanager.orm_default' );
                        /* @var $em \Doctrine\ORM\EntityManager */
                        $validator->setUserHelper( $em->getRepository( '\My\Entity\User' ) );

                        return $validator;
                    }
                )

aber das hat nicht funktioniert, weil es den Service Manager der Anwendungsebene nicht konsultiert.

Also, insgesamt, hier sind meine Fragen:

  1. Ist diese Strategie, Form und Elemente zu trennen, eine gute? Soll ich diesen Weg weitergehen? Was sind Alternativen? (Ich wollte Dinge aus Gründen der Testbarkeit auflösen) Ich wollte NUR die Form selbst testen, ursprünglich mit einer Kombination ALLER Eingaben, aber es schien, als würde ich zu viel versuchen.

  2. Wie behebe ich das Problem, das ich oben habe?

  3. Sollte ich die Form / Element / Input-Teile von Zend auf eine andere Weise benutzen, die ich nicht sehe?


11
2017-09-09 19:22


Ursprung


Antworten:


Dies ist mein Validator, der eine statische Methode verwendet, um den EntityManager zu injizieren und mit einer beliebigen Doctine Entity zu arbeiten.

<?php

namespace Base\Validator;

use Traversable;
use Zend\Stdlib\ArrayUtils;
use Zend\Validator\AbstractValidator;
use Doctrine\ORM\EntityManager;

class EntityUnique extends AbstractValidator
{
    const EXISTS = 'exists';

    protected $messageTemplates = array(
        self::EXISTS => "A %entity% record already exists with %attribute% %value%",
    );

    protected $messageVariables = array(
        'entity' => '_entity',
        'attribute' => '_attribute',
    );


    protected $_entity;
    protected $_attribute;
    protected $_exclude;

    protected static $_entityManager;

    public static function setEntityManager(EntityManager $em) {

        self::$_entityManager = $em;
    }

    public function getEntityManager() {

        if (!self::$_entityManager) {

            throw new \Exception('No entitymanager present');
        }

        return self::$_entityManager;
    }

    public function __construct($options = null)
    {
        if ($options instanceof Traversable) {
            $options = ArrayUtils::iteratorToArray($token);
        }

        if (is_array($options)) {

            if (array_key_exists('entity', $options)) {

                $this->_entity = $options['entity'];
            }

            if (array_key_exists('attribute', $options)) {

                $this->_attribute = $options['attribute'];
            }

            if (array_key_exists('exclude', $options)) {

                if (!is_array($options['exclude']) ||
                    !array_key_exists('attribute', $options['exclude']) ||
                    !array_key_exists('value', $options['exclude'])) {

                    throw new \Exception('exclude option must contain attribute and value keys');
                }

                $this->_exclude = $options['exclude'];
            }
        }

        parent::__construct(is_array($options) ? $options : null);
    }

    public function isValid($value, $context = null)
    {
        $this->setValue($value);

        $queryBuilder = $this->getEntityManager()
            ->createQueryBuilder()
            ->from($this->_entity, 'e')
            ->select('COUNT(e)')
            ->where('e.'. $this->_attribute . ' = :value')
            ->setParameter('value', $this->getValue());

        if ($this->_exclude) {

            $queryBuilder = $queryBuilder->andWhere('e.'. $this->_exclude['attribute'] . ' != :exclude')
                ->setParameter('exclude', $this->_exclude['value']);
        }

        $query = $queryBuilder->getQuery();        
        if ((integer)$query->getSingleScalarResult() !== 0) {

            $this->error(self::EXISTS);
            return false;
        }

        return true;
    }
}

dh. Ich verwende es für diese Formelemente, die auch getestet werden und gut funktionieren:

<?php

namespace User\Form\Element;

use Zend\Form\Element\Text;
use Zend\InputFilter\InputProviderInterface;

class Username extends Text implements InputProviderInterface
{
    public function __construct() {

        parent::__construct('username');
        $this->setLabel('Benutzername');
        $this->setAttribute('id', 'username');
    }

    public function getInputSpecification() {

        return array(
            'name' => $this->getName(),
            'required' => true,
            'filters'  => array(
                array(
                    'name' => 'StringTrim'
                ),
            ),
            'validators' => array(
                array(
                    'name' => 'NotEmpty',
                    'break_chain_on_failure' => true,
                    'options' => array(
                        'messages' => array(
                            'isEmpty' => 'Bitte geben Sie einen Benutzernamen ein.',
                        ),
                    ),
                ),
            ),
        );
    }
}

Beim Erstellen eines neuen Benutzers

<?php

namespace User\Form\Element;

use Zend\InputFilter\InputProviderInterface;
use User\Form\Element\Username;

class CreateUsername extends Username implements InputProviderInterface
{
    public function getInputSpecification() {

        $spec = parent::getInputSpecification();
        $spec['validators'][] = array(
            'name' => 'Base\Validator\EntityUnique',
            'options' => array(
                'message' => 'Der name %value% ist bereits vergeben.',
                'entity' => 'User\Entity\User',
                'attribute' => 'username',  
            ),    
        );

        return $spec;
    }
}

beim Bearbeiten eines vorhandenen Benutzers

<?php

namespace User\Form\Element;

use Zend\InputFilter\InputProviderInterface;
use User\Form\Element\Username;

class EditUsername extends Username implements InputProviderInterface
{
    protected $_userId;

    public function __construct($userId) {

        parent::__construct();
        $this->_userId = (integer)$userId;
    }

    public function getInputSpecification() {

        $spec = parent::getInputSpecification();
        $spec['validators'][] = array(
            'name' => 'Base\Validator\EntityUnique',
            'options' => array(
                'message' => 'Der name %value% ist bereits vergeben.',
                'entity' => 'User\Entity\User',
                'attribute' => 'username',
                'exclude' => array(
                    'attribute' => 'id',
                    'value' => $this->_userId,  
                ),
            ),    
        );

        return $spec;
    }
}

8
2017-09-13 19:34