Symfony 2.8 - ChoiceType требует строку, но для входа в систему требуется, чтобы роли были массивом

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

Кроме того, я заменил настоящее имя пакета на MyApp для этого поста.

Проблема: я пытаюсь заставить работать приложение Symfony. Я следовал документам Symfony 2.8, чтобы создать базу данных и таблицу, и процесс входа в систему работает нормально. После того, как я добавил форму для создания новых пользователей, я столкнулся с ошибками с ChoiceType и моей страницей входа.

В частности, когда я устанавливаю getRoles() в return array($this->roles); в Entity/User.php, вход в систему работает нормально, но я получаю следующие ошибки в форме создания пользователя.

GET /admin/users с несколькими =>false (посетить страницу)

Notice: Array to string conversion
500 Internal Server Error - ContextErrorException
Stack Trace

    in vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php at line 79  -
                if (null === $value && $this->castableToString($choices)) {
                    $value = function ($choice) {
                        return false === $choice ? '0' : (string) $choice;
                    };
                }
    at ErrorHandler ->handleError ('8', 'Array to string conversion', '/home/dev/MyApp/vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php', '79', array('choice' => array(null)))
    in vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php at line 79  +
    at ArrayChoiceList ->Symfony\Component\Form\ChoiceList\{closure} (array(null))
    at call_user_func (object(Closure), array(null))
    in vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php at line 164  +
    at ArrayChoiceList ->getValuesForChoices (array(array(null)))
    in vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php at line 32  +
    at ChoiceToValueTransformer ->transform (array(null))
    in vendor/symfony/symfony/src/Symfony/Component/Form/Form.php at line 1156  +
    at Form ->normToView (array(null))
    in vendor/symfony/symfony/src/Symfony/Component/Form/Form.php at line 351  +
    at Form ->setData (array(null))
    in vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php at line 49  +
    at PropertyPathMapper ->mapDataToForms (object(User), object(RecursiveIteratorIterator))
    in vendor/symfony/symfony/src/Symfony/Component/Form/Form.php at line 384  +
    at Form ->setData (object(User))
    in vendor/symfony/symfony/src/Symfony/Component/Form/Form.php at line 488  +
    at Form ->initialize ()
    in vendor/symfony/symfony/src/Symfony/Component/Form/FormBuilder.php at line 226  +
    at FormBuilder ->getForm ()
    in vendor/symfony/symfony/src/Symfony/Component/Form/FormFactory.php at line 33  +
    at FormFactory ->create ('MyAppBundle\Form\UserType', object(User), array())
    in vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php at line 282  +
    at Controller ->createForm ('MyAppBundle\Form\UserType', object(User))
    in src/MyAppBundle/Controller/AdminController.php at line 214  +
    at AdminController ->adminUsersAction (object(Request))
    in vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php at line 135  +
    at HttpKernel ->handleRaw (object(Request), '1')
    in vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php at line 57  +
    at HttpKernel ->handle (object(Request), '1', true)
    in vendor/symfony/symfony/src/Symfony/Component/HttpKernel/DependencyInjection/ContainerAwareHttpKernel.php at line 67  +
    at ContainerAwareHttpKernel ->handle (object(Request), '1', true)
    in vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php at line 183  +
    at Kernel ->handle (object(Request))
    in web/app_dev.php at line 30  +
    at require ('/home/dev/MyApp/web/app_dev.php')
    in vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Resources/config/router_dev.php at line 40  +

Я попытался установить multiple=>true, как было предложено в некоторых ответах (хотя мне не нужен множественный выбор). Это устраняет ошибку рендеринга, но то же самое появляется при отправке формы.

Опубликовать в /admin/users с multiple=>true (отправить форму пользователя)

Notice: Array to string conversion
500 Internal Server Error - ContextErrorException
Stack Trace

    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php at line 67  -
            public function bindValue($param, $value, $type = \PDO::PARAM_STR)
            {
                try {
                    return parent::bindValue($param, $value, $type);
                } catch (\PDOException $exception) {
                    throw new PDOException($exception);
                }
    at ErrorHandler ->handleError ('8', 'Array to string conversion', '/home/dev/MyApp/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php', '67', array('param' => '5', 'value' => array('ROLE_USER'), 'type' => '2'))
    at PDOStatement ->bindValue ('5', array('ROLE_USER'), '2')
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php at line 67  +
    at PDOStatement ->bindValue ('5', array('ROLE_USER'), '2')
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Statement.php at line 120  +
    at Statement ->bindValue ('5', array('ROLE_USER'), object(StringType))
    in vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php at line 277  +
    at BasicEntityPersister ->executeInserts ()
    in vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php at line 1014  +
    at UnitOfWork ->executeInserts (object(ClassMetadata))
    in vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php at line 378  +
    at UnitOfWork ->commit (null)
    in vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php at line 356  +
    at EntityManager ->flush ()
    in src/MyAppBundle/Controller/AdminController.php at line 228  +
    at AdminController ->adminUsersAction (object(Request))
    in vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php at line 135  +
    at HttpKernel ->handleRaw (object(Request), '1')
    in vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php at line 57  +
    at HttpKernel ->handle (object(Request), '1', true)
    in vendor/symfony/symfony/src/Symfony/Component/HttpKernel/DependencyInjection/ContainerAwareHttpKernel.php at line 67  +
    at ContainerAwareHttpKernel ->handle (object(Request), '1', true)
    in vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php at line 183  +
    at Kernel ->handle (object(Request))
    in web/app_dev.php at line 30  +
    at require ('/home/dev/MyApp/web/app_dev.php')
    in vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Resources/config/router_dev.php at line 40  +

Изменение getRoles() на return $this->roles; действительно решает проблему рендеринга и публикации, но вызывает обратную проблему в функции входа в систему.

Запрос на вход в /login

Type error: Argument 4 passed to Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken::__construct() must be of the type array, string given, called in /home/dev/MyApp/vendor/symfony/security/Core/Authentication/Provider/UserAuthenticationProvider.php on line 96
500 Internal Server Error - FatalThrowableError
Stack Trace

    in vendor/symfony/security/Core/Authentication/Token/UsernamePasswordToken.php at line 36  -
            *
            * @throws \InvalidArgumentException
            */
            public function __construct($user, $credentials, $providerKey, array $roles = array())
            {
                parent::__construct($roles);
    at UsernamePasswordToken ->__construct (object(User), 'test', 'main', 'ROLE_USER')
    in vendor/symfony/security/Core/Authentication/Provider/UserAuthenticationProvider.php at line 96  +
    at UserAuthenticationProvider ->authenticate (object(UsernamePasswordToken))
    in app/cache/dev/classes.php at line 2749  +
    at AuthenticationProviderManager ->authenticate (object(UsernamePasswordToken))
    in vendor/symfony/security/Http/Firewall/UsernamePasswordFormAuthenticationListener.php at line 93  +
    at UsernamePasswordFormAuthenticationListener ->attemptAuthentication (object(Request))
    in vendor/symfony/security/Http/Firewall/AbstractAuthenticationListener.php at line 146  +
    at AbstractAuthenticationListener ->handle (object(GetResponseEvent))
    in app/cache/dev/classes.php at line 2664  +
    at Firewall ->onKernelRequest (object(GetResponseEvent), 'kernel.request', object(TraceableEventDispatcher))
    at call_user_func (array(object(Firewall), 'onKernelRequest'), object(GetResponseEvent), 'kernel.request', object(TraceableEventDispatcher))
    in vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php at line 61  +
    at WrappedListener ->__invoke (object(GetResponseEvent), 'kernel.request', object(ContainerAwareEventDispatcher))
    at call_user_func (object(WrappedListener), object(GetResponseEvent), 'kernel.request', object(ContainerAwareEventDispatcher))
    in app/cache/dev/classes.php at line 1888  +
    at EventDispatcher ->doDispatch (array(object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener)), 'kernel.request', object(GetResponseEvent))
    in app/cache/dev/classes.php at line 1803  +
    at EventDispatcher ->dispatch ('kernel.request', object(GetResponseEvent))
    in vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php at line 133  +
    at TraceableEventDispatcher ->dispatch ('kernel.request', object(GetResponseEvent))
    in vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php at line 116  +
    at HttpKernel ->handleRaw (object(Request), '1')
    in vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php at line 57  +
    at HttpKernel ->handle (object(Request), '1', true)
    in vendor/symfony/symfony/src/Symfony/Component/HttpKernel/DependencyInjection/ContainerAwareHttpKernel.php at line 67  +
    at ContainerAwareHttpKernel ->handle (object(Request), '1', true)
    in vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php at line 183  +
    at Kernel ->handle (object(Request))
    in web/app_dev.php at line 30  +
    at require ('/home/dev/MyApp/web/app_dev.php')
    in vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Resources/config/router_dev.php at line 40  +

Как обработать выбор ролей в пользовательской форме и логине, чтобы обоих устраивал формат $roles?

Исходный код для справки.

Форма/UserType.php

[...]
public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('email', EmailType::class)
        ->add('username', TextType::class)
        ->add('roles', ChoiceType::class, array(
            'multiple' => true,
            'expanded' => false,
            'choices'  => array(
                'User' => 'ROLE_USER',
                'Admin' => 'ROLE_ADMIN'),
            'choices_as_values' => true,
        ))
        ->add('plainPassword', RepeatedType::class, array(
            'type' => PasswordType::class,
            'first_options'  => array('label' => 'Password'),
            'second_options' => array('label' => 'Repeat Password'),
        )
    );
}

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => 'MyAppBundle\Entity\User',
    ));
}
[...]

Сущность/User.php

[...]
/**
* @ORM\Table(name="app_users")
* @ORM\Entity(repositoryClass="MyAppBundle\Entity\UserRepository")
*/
class User implements UserInterface, \Serializable
{
    /**
    * @ORM\Column(type="integer")
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="AUTO")
    */
    private $id;

    /**
    * @ORM\Column(type="string", length=25, unique=true)
    */
    private $username;

    /**
    * @ORM\Column(type="string", length=64)
    */
    private $password;

    /**
    * @ORM\Column(type="string", length=60, unique=true)
    */
    private $email;

    /**
    * @ORM\Column(name="is_active", type="boolean")
    */
    private $isActive;

    /**
    * @ORM\Column(name="roles", type="string")
    */
    private $roles;

    /**
    * @Assert\NotBlank()
    * @Assert\Length(max=4096)
    */
    private $plainPassword;


    public function __construct()
    {
        // default for new users
        $this->isActive = true;
        //$this->roles = "ROLE_USER";
    }
    [...]
    public function getRoles()
    {
        //return $this->roles;
        return array($this->roles);
    }

    /** @see \Serializable::serialize() */
    public function serialize()
    {
        return serialize(array(
            $this->id,
            $this->username,
            $this->password,
        ));
    }

    /** @see \Serializable::unserialize() */
    public function unserialize($serialized)
    {
        list (
            $this->id,
            $this->username,
            $this->password,
        ) = unserialize($serialized);
    }
    [...]
    /**
    * Set Roles
    *
    * @param string $roles
    *
    * @return User
    */
    public function setRoles($roles)
    {
        $this->roles = $roles;
        return $this;
    }
    [...]

Контроллер/AdminController.php

/**
* @Route("/admin/users")
*/
public function adminUsersAction(Request $request)
{
    // 
    $repository = $this->getDoctrine()
        ->getRepository('MyAppBundle:User');

    $users = $repository->findAll();

    // USER FORM
    // 1) build the form
    $user = new User();
    $form = $this->createForm(UserType::class, $user);

    // 2) handle the submit (will only happen on POST)
    $form->handleRequest($request);
    if ($form->isSubmitted() && $form->isValid()) {

        // 3) Encode the password (you could also do this via Doctrine listener)
        $password = $this->get('security.password_encoder')
            ->encodePassword($user, $user->getPlainPassword());
        $user->setPassword($password);

        // 4) save the User!
        $em = $this->getDoctrine()->getManager();
        $em->persist($user);
        $em->flush();

        return $this->redirectToRoute('dashboard');
    }

    return $this->render(
        'MyAppBundle:Admin:users.html.twig',
        array(
            "form"=>$form->createView(),
            "users"=>$users
            )
    );
}

Ресурсы/представления/Admin/users.html.twig

<h2>Add Users</h2>
{{ form_start(form) }}
    {{ form_row(form.username) }}
    {{ form_row(form.email) }}
    {{ form_row(form.roles) }}
    {{ form_row(form.plainPassword.first) }}
    {{ form_row(form.plainPassword.second) }}
    <button type="submit">Add User</button>
{{ form_end(form) }}

person mohrphium    schedule 30.04.2018    source источник


Ответы (1)


Похоже, ваши роли сохраняются в виде строки:

/**
* @ORM\Column(name="roles", type="string")
*/
private $roles;

Это будет хранить литеральную строку ROLE_USER вместо сериализованного массива a:1:{i:0;s:9:"ROLE_USER";}. Пытаться:

/**
* @ORM\Column(name="roles", type="array")
*/
private $roles;

Сеттер должен взять массив из ChoiceType с поддержкой множественного выбора:

/**
* @param array $roles
* @return User
*/
public function setRoles($roles)
{
    $this->roles = $roles;
    return $this;
}

Геттер должен возвращать свойство в виде массива без необходимости вручную превращать его в массив:

public function getRoles()
{
    return $this->roles;
}
person OK sure    schedule 30.04.2018
comment
Спасибо, я сделал это, но я все еще получаю сообщение об ошибке на странице входа, которое argument 4 [...] must be of type array. Поле roles в базе данных теперь содержит s:9:"ROLE_USER"; вместо ROLE_USER. - person mohrphium; 30.04.2018
comment
хм, ну, это должно было превратить его обратно в массив, но я могу ошибаться. Я обновлю ответ с исправлением для этого. - person OK sure; 30.04.2018
comment
unserialize($this->roles) приводит к Notice: unserialize(): Error at offset 0 of 9 bytes при входе в систему. - person mohrphium; 30.04.2018
comment
Это больше похоже на проблему с типом/кодировкой столбца — в каком типе столбца вы храните роли? Объясняет, почему геттер возвращает строку, если она не может быть десериализована. Вероятно, следует хранить как тип BLOB - person OK sure; 30.04.2018
comment
Но также он должен хранить массив, а не только сериализованную строку: a:1:{i:0;s:9:"ROLE_USER";}, s:9:"ROLE_USER"; - person OK sure; 30.04.2018
comment
setRoles тоже должен принимать массив, а не строку - person OK sure; 30.04.2018
comment
Последний исправил это для меня. Роли БД содержат a:1:{i:0;s:9:"ROLE_USER";}, setRoles теперь $this->roles = array($roles); return $this;, а getRoles возвращается к return $this->roles;. Благодаря тонну! - person mohrphium; 30.04.2018