ZF2 multi-select form preselect values ManyToMany - php

I can't get to preselect values in a multiselect form element representing a many to many relation.
In my model $admin I have the proper data : an ArrayCollection containing the correct CampsTypes but in the form I can't get the multiselect to preselect the proper options.
Admins model
/**
* #var ArrayCollection CampsTypes $campstypes
*
* #ORM\ManyToMany(targetEntity="CampsTypes", inversedBy="admins", cascade={"persist"})
* #ORM\JoinTable(name="campstypes_admins",
* joinColumns={#ORM\JoinColumn(name="admins_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="campstypes_id", referencedColumnName="id")}
* )
*/
private $campstypes;
CampsType model
/**
* #var ArrayCollection Admins $admins
*
* #ORM\ManyToMany(targetEntity="Admins", mappedBy="campstypes", cascade={"persist"})
*/
private $admins;
Then I define my form select element as follow
[
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'name' => 'campTypes',
'required' => false,
'options' => [
'object_manager' => $this->getServiceLocator()->get(EntityManager::class),
'target_class' => CampsTypes::class,
'property' => 'title',
'label' => 'Type de camps autorisés',
'instructions' => 'Ne rien sélectionner si edition d\'un super admin',
],
'attributes' => [
'class' => '',
'multiple' => 'multiple',
]
],
And finally here is my action to receive the form
protected function saveAdmin(Admins &$admin, &$form, &$msg)
{
$em = $this->getEntityManager();
/** #var CampTypesService $serviceCampTypes */
$serviceCampTypes = $this->getServiceLocator()->get(CampTypesService::class);
$form->bind($admin);
if ($this->getRequest()->isPost()) {
$data = $this->getRequest()->getPost();
if (empty($data['password'])) {
$form->remove('password');
}
$form->setData($data);
if ($form->isValid()) {
if (isset($data['campTypes'])) {
$ids = $form->get('campTypes')->getValue();
$campsTypes = new ArrayCollection($serviceCampTypes->getCampTypesByIds(array_values($ids)));
foreach ($campsTypes as &$campsType) {
/** #var CampsTypes $campsType*/
$campsType->addAdmin($admin);
}
$admin->setCampTypes($campsTypes);
}
$em->persist($admin);
$em->flush();
$msg = 'Sauvegarde des données effectuée';
return;
}
}
return;
}
I'm getting out of solution to try.
Any idea what I am doing wrong ?

Have you read this ? I have the feeling you're looking for the ObjectMultiCheckbox instead of the ObjectSelect Form Element.
Examples from my own code
Usage for a single select (use case: set/change a default currency for some other entity)
$this->add([
'type' => ObjectSelect::class,
'required' => true,
'name' => 'defaultCurrency',
'options' => [
'object_manager' => $this->getEntityManager(),
'target_class' => Currency::class,
'property' => 'id',
// Use these commented lines if you wish to use a Repository function ('name' => 'repositoryFunctionName')
// 'is_method' => true,
// 'find_method' => [
// 'name' => 'getEnabledCurrencies',
// ],
'display_empty_item' => true,
'empty_item_label' => '---',
'label' => _('Default currency'),
'label_attributes' => [
'class' => '',
'title' => _('Default currency'),
],
'label_generator' => function ($targetEntity) {
/** #var Currency $targetEntity */
return $targetEntity->getName(); // Generates option text based on name property of Entity (Currency in this case)
},
],
]);
Usage for multiple select (Use case: add/remove (multiple) roles to/from user)
$this->add([
'name' => 'roles',
'required' => false,
'type' => ObjectMultiCheckbox::class,
'options' => [
'object_manager' => $this->getEntityManager(),
'target_class' => Role::class,
'property' => 'id',
'display_empty_item' => true,
'empty_item_label' => '---',
'label' => _('Roles'),
'label_generator' => function ($targetEntity) {
/** #var Role $targetEntity */
return $targetEntity->getName();
},
],
]);
As a side note: you should really use factories more. I see you using the ServiceLocator throughout your class code, you can avoid that by injecting your needs via a Factory.
If you need more information, I suggest you have a look at a bunch of my past questions as well. I had quite a few, starting out similar to what you're looking for. Managed to figure quite a few of them out on my own and have tried to describe the solutions in depth.

So I made it work !
basically my main problem is that I was not using proper Doctrine naming convention for the fields name of the form.
So I had to reverse engineer my DB into doctrine entities to compare with what I had done to understand where it was not working.
And also to set the model in both end (admins in camptype and camptypse in admin) for the whole shabang to work.
Here are the working classes :
Admin form:
[
'type' => ObjectSelect::class,
'name' => 'campstypes',
'required' => false,
'options' => [
'object_manager' => $this->getServiceLocator()->get(EntityManager::class),
'target_class' => CampsTypes::class,
'property' => 'title',
'label' => 'Type de camps autorisés',
'instructions' => 'Ne rien sélectionner si edition d\'un super admin',
],
'attributes' => [
'class' => '',
'multiple' => 'multiple',
]
],
Admin Controller:
protected function saveAdmin(Admins &$admin, &$form, &$msg)
{
$em = $this->getEntityManager();
/** #var CampTypesService $serviceCampTypes */
$serviceCampTypes = $this->getServiceLocator()->get(CampTypesService::class);
$form->bind($admin);
if ($this->getRequest()->isPost()) {
$data = $this->getRequest()->getPost();
if (empty($data['password'])) {
$form->remove('password');
}
$form->setData($data);
if ($form->isValid()) {
if (isset($data['campstypes'])) {
$ids = $form->get('campstypes')->getValue();
$campsTypes = new ArrayCollection($serviceCampTypes->getCampTypesByIds(array_values($ids)));
foreach ($campsTypes as &$campsType) {
/** #var CampsTypes $campsType*/
$campsType->addAdmin($admin);
}
$admin->setCampstypes($campsTypes);
}
$em->persist($admin);
$em->flush();
$msg = 'Sauvegarde des données effectuée';
return;
}
}
return;
}
So by renaming properly the fields of my form and models and setting the data in both end model of the relation I got it to work.

Related

Doctrine Table inheritance with zf3 fieldset

I'm working on a project using Zend Framework 3 and Doctrine 2, using for DcotrineModule integration, the following is the Entity modeling I'm having problems with:
To work with this modeling with the doctrine I'm using #InheritanceType, below are the relevant excerpts from Entities:
Pessoa Entity:
/**
* Abstração de Pessoa
*
* #author Rodrigo Teixeira Andreotti <ro.andriotti#gmail.com>
*
* #Entity
* #InheritanceType("JOINED")
* #DiscriminatorColumn(name="tipo", type="string")
* #DiscriminatorMap( { "pessoa" = "Pessoa",
* "pessoa_fisica" = "PessoaFisica",
* "pessoa_juridica" = "PessoaJuridica" } )
* #Table(name="pessoa")
*/
abstract class Pessoa implements JsonSerializable, PessoaInterface
{
use JsonSerializeTrait;
/**
* #Id
* #GeneratedValue(strategy="IDENTITY")
* #Column(type="integer", length=32, unique=true, nullable=false, name="id_pessoa")
* #var integer
*/
protected $idPessoa;
/**
* Usuário
* #OneToOne(targetEntity="User\Entity\User", inversedBy="pessoa", cascade={"persist"})
* #JoinColumn(name="usuario", referencedColumnName="id")
*
* #var User
*/
protected $usuario;
/**
* #OneToOne(targetEntity="EnderecoPessoa", mappedBy="pessoa", cascade={"persist"})
* #var EnderecoPessoa
*/
protected $endereco;
/**
* Contatos da pessoa
* #OneToMany(targetEntity="ContatoPessoa", mappedBy="pessoa", cascade={"persist"}, orphanRemoval=true)
* #var ArrayCollection|array
*/
protected $contatos;
const PESSOA_FISICA = "pessoa_fisica", PESSOA_JURIDICA = "pessoa_juridica";
public function __construct()
{
$this->contatos = new ArrayCollection();
}
}
PessoaFisica Entity:
/**
* Abstração da pessoa física
*
* #Entity
* #Table(name="pessoa_fisica")
* #author Rodrigo Teixeira Andreotti <ro.andriotti#gmail.com>
*/
class PessoaFisica extends Pessoa implements JsonSerializable {
use JsonSerializeTrait;
/**
* Nome da pessoa física
* #Column(type="string", length=14)
* #var string
*/
private $nome;
/**
* Número do CPF da pessoa (quando brasileiro)
* #Column(type="string", length=14)
* #var string
*/
private $cpf;
/**
* Número do RG (quando brasileiro)
* #Column(type="string", length=13)
* #var string
*/
private $rg;
/**
* Data de nascimento
* #Column(type="date", name="data_nascimento")
* #var DateTime
*/
private $dataNascimento;
}
PessoaJuridica Entity:
/**
* Abstração de Pessoa Jurídica
*
* #Entity
* #Table(name="pessoa_juridica")
* #InheritanceType("JOINED")
* #author Rodrigo Teixeira Andreotti <ro.andriotti#gmail.com>
*/
class PessoaJuridica extends Pessoa implements JsonSerializable {
use JsonSerializeTrait;
/**
* #Id
* #GeneratedValue(strategy="IDENTITY")
* #Column(type="integer", length=32, unique=true, nullable=false, name="id_pessoa")
* #var integer
*/
protected $idPessoa;
/**
* Nome fantasia
* #Column(type="string", length=32, name="nome_fantasia")
* #var String
*/
protected $nomeFantasia;
/**
* Número do CNPJ
* #Column(type="string", length=14, unique=true, name="cnpj")
* #var string
*/
protected $cnpj;
/**
* Razão social da empresa
* #Column(type="string", length=32, name="razao_social")
* #var string Razão social da empresa, quando necessário
*/
protected $razaoSocial;
}
So far everything works perfectly, the problem is when I need to generate a form for this information, I'm currently working on the "Customer" module, basically what I did for it was:
Create a form with client ID + Pessoa Fieldset
In the Pessoa Fieldset, I created the fieldsets for shared information (user, address, contacts etc)
In the Pessoa Fieldset, it also includes two other Fieldsets, one for each Pessoa's child class (PessoaFisica and PessoaJuridica) - and here come's the problem.
In the screen below you can see my registration form:
This form displays or hides the fieldset of PessoaJuridica or PessoaFisica according to the selected type using javascript, however as they are different fieldsets within the form, when zend hydrates them they are hydrated as different objects as well, ie the inheritance is not applied to the Person object, which should be selected according to the type.
Basically what, in my point of view, would need to happen, would be that there is a way for zend not to render the fieldsets referring to the child classes of the Person class as separate objects, at the moment the form is rendered with these fields so (for example) :
person [fsPeople] [name]
person [fsPessoaJuridica] [nameFantasica]
And this causes the zend not to generate the correct class to be saved in the database.
What would be the correct way to do this implementation of the form?
Well, the response from the #rkeet helped me a lot to understand where the problem was, which is not really a problem =]
Due to the usage of inheritance, you've created separate Entities.
However, the form you initially create in the back-end works with a
single Entity. The front-end you've modified to handle 2. So your
front-end does not match your back-end. As, due to the inheritance,
you now have 2 separate Entities, you should create 2 separate forms,
using different fieldsets (PessoaJuridica or PessoaFisica) as the base
fieldsets.
I'll leave the path I followed here, it might help someone with the same doubt as me.
First, following the logic explained in his comment, I created an abstract fieldset for the PessoaEntity with the information shared between the two types of person, and extended it into two child classes PessoaFisicaFieldset and PessoaJuridicaFieldset, which I describe below:
/**
* Fieldset com dados para a pessoa
*
* #author Rodrigo Teixeira Andreotti <ro.andriotti#gmail.com>
*/
abstract class PessoaFieldset extends Fieldset implements InputFilterProviderInterface
{
private $em;
private $userFs;
private $enderecoFs;
private $contatoFs;
public function __construct(ObjectManager $em,
UserFieldset $userFs,
PessoaEnderecoFieldset $enderecoFs,
ContatoFieldset $contatoFs)
{
parent::__construct('pessoa');
$this->em = $em;
$this->userFs = $userFs;
$this->enderecoFs = $enderecoFs;
$this->contatoFs = $contatoFs;
$this->init();
}
protected function getEm()
{
return $this->em;
}
public function init()
{
$this
->setHydrator(new DoctrineObject($this->getEm()));
$this->add(array(
'type' => 'Hidden',
'name' => 'id_pessoa',
'attributes' => array(
'id' => 'txtId'
)
));
$this->add(array(
'type' => 'hidden',
'name' => 'tipo',
));
$this->add($this->userFs);
$this->add($this->enderecoFs);
$elCollection = new Collection;
$elCollection
->setName('contatos')
->setLabel('Informações de Contato')
->setCount(1)
->setShouldCreateTemplate(true)
->setAllowAdd(true)
->setAllowRemove(true)
->setTargetElement($this->contatoFs);
$this->add($elCollection);
$this->add(array(
'type' => 'Button',
'name' => 'btAddContato',
'options' => array(
'label' => '<i class="fa fa-fw fa-plus"></i> Adicionar',
'label_options' => array(
'disable_html_escape' => true
)
),
'attributes' => array(
'class' => 'btn btn-info',
'id' => 'btAddContato'
)
));
}
public function getInputFilterSpecification(): array
{
return array(
'id_pessoa' => array(
'required' => false,
'filters' => array(
['name'=>'Int']
)
),
'tipo' => array(
'required' => true,
)
);
}
}
This is my PessoaFisicaFieldset class.
/**
* Fieldset com dados para a pessoa Física
*
* #author Rodrigo Teixeira Andreotti <ro.andriotti#gmail.com>
*/
class PessoaFisicaFieldset extends PessoaFieldset implements InputFilterProviderInterface
{
private $em;
public function __construct(ObjectManager $em,
\User\Form\UserFieldset $userFs,
PessoaEnderecoFieldset $enderecoFs,
\Common\Form\ContatoFieldset $contatoFs)
{
parent::__construct($em, $userFs, $enderecoFs, $contatoFs);
$this->init();
}
public function init()
{
parent::init();
$this
->setObject(new PessoaFisica());
$this->get('tipo')->setValue(\Pessoa\Entity\Pessoa::PESSOA_FISICA);
$this->add(array(
'type' => 'Text',
'name' => 'cpf',
'options' => array(
'label' => 'CPF',
'label_attributes' => array(
'class' => 'col-sm-12'
)
),
'attributes' => array(
'class' => 'form-control form-control-line',
'id' => 'txtCpf'
)
));
$this->add(array(
'type' => 'Text',
'name' => 'nome',
'options' => array(
'label' => 'Nome',
'label_attributes' => array(
'class' => 'col-sm-12'
)
),
'attributes' => array(
'class' => 'form-control form-control-line',
'id' => 'txtNome'
)
));
$this->add(array(
'type' => 'Text',
'name' => 'rg',
'options' => array(
'label' => 'RG',
'label_attributes' => array(
'class' => 'col-sm-12'
)
),
'attributes' => array(
'class' => 'form-control form-control-line',
'id' => 'txtRazaoSocial'
)
));
$this->add(array(
'type' => 'DateTime',
'name' => 'dataNascimento',
'options' => array(
'format' => 'd/m/Y',
'label' => 'Data de Nascimento',
'label_attributes' => array(
'class' => 'col-sm-12'
)
),
'attributes' => array(
'class' => 'form-control form-control-line data',
)
));
}
public function getInputFilterSpecification(): array
{
return array(
'nome' => array(
'required' => true,
'filters' => array(
['name' => 'StripTags'],
['name' => 'StringTrim']
)
),
'rg' => array(
'required' => false,
'filters' => array(
['name' => 'StripTags'],
['name' => 'StringTrim']
)
),
'cpf' => array(
'required' => false,
'filters' => array(
['name' => 'StripTags'],
['name' => 'StringTrim']
),
'validators' => array(
['name' => CpfValidator::class]
)
),
'dataNascimento' => array(
'required' => true,
'filters' => array(
array(
'name' => 'Zend\Filter\DatetimeFormatter',
'options' => array (
'format' => 'd/m/Y',
),
),
),
'validators' => array(
array(
'name' => Date::class,
'options' => array(
'format' => 'd/m/Y'
)
)
)
)
);
}
}
And here is my PessoaJuridicaFieldset
/**
* Fieldset com dados específicos para a pessoa jurídica
*
* #author Rodrigo Teixeira Andreotti <ro.andriotti#gmail.com>
*/
class PessoaJuridicaFieldset extends PessoaFieldset implements InputFilterProviderInterface
{
public function __construct(ObjectManager $em,
\User\Form\UserFieldset $userFs, PessoaEnderecoFieldset $enderecoFs,
\Common\Form\ContatoFieldset $contatoFs)
{
parent::__construct($em, $userFs, $enderecoFs, $contatoFs);
$this->init();
}
public function init()
{
parent::init();
$this
->setObject(new PessoaJuridica());
$this->get('tipo')->setValue(\Pessoa\Entity\Pessoa::PESSOA_JURIDICA);
$this->add(array(
'type' => 'Text',
'name' => 'cnpj',
'options' => array(
'label' => 'CNPJ',
'label_attributes' => array(
'class' => 'col-sm-12'
)
),
'attributes' => array(
'class' => 'form-control form-control-line',
'id' => 'txtCnpj'
)
));
$this->add(array(
'type' => 'Text',
'name' => 'razaoSocial',
'options' => array(
'label' => 'Razão Social',
'label_attributes' => array(
'class' => 'col-sm-12'
)
),
'attributes' => array(
'class' => 'form-control form-control-line',
'id' => 'txtRazaoSocial'
)
));
$this->add(array(
'type' => 'Text',
'name' => 'nomeFantasia',
'options' => array(
'label' => 'Nome Fantasia',
'label_attributes' => array(
'class' => 'col-sm-12'
)
),
'attributes' => array(
'class' => 'form-control form-control-line',
'id' => 'txtNomeFantasia'
)
));
}
public function getInputFilterSpecification(): array
{
return array(
'razaoSocial' => array(
'required' => true,
'filters' => array(
['name' => 'StripTags'],
['name' => 'StringTrim']
)
),
'nomeFantasia' => array(
'required' => true,
'filters' => array(
['name' => 'StripTags'],
['name' => 'StringTrim']
)
),
'cnpj' => array(
'required' => true,
'filters' => array(
['name' => 'StripTags'],
['name' => 'StringTrim']
),
'validators' => array(
['name' => CnpjValidator::class]
)
)
);
}
}
And to complete I did the entity type treatment on the Controller that will load this form, as below: (only relevant parts)
//...
if ($id) {
$cliente = $this->repository->getById($id);
$form->remove('pessoa');
// loads form according to the type loaded from the database
if (!$request->isXmlHttpRequest()) {
if ($cliente->getPessoa() instanceof \Pessoa\Entity\PessoaFisica) {
$form->add($this->pessoaFisicaFieldset);
} elseif ($cliente->getPessoa() instanceof \Pessoa\Entity\PessoaJuridica) {
$form->add($this->pessoaJuridicaFieldset);
}
var_dump($cliente->getPessoa());
}
$form->bind($cliente);
}
if ($request->isPost()) {
$form->remove('pessoa');
// loads form according to the type selected in the post
if ($request->getPost('tipo') == \Pessoa\Entity\Pessoa::PESSOA_FISICA) {
$form->add($this->pessoaFisicaFieldset);
} elseif ($request->getPost('tipo') == \Pessoa\Entity\Pessoa::PESSOA_JURIDICA) {
$form->add($this->pessoaJuridicaFieldset);
}
$form->get('tipo')->setValue($request->getPost('tipo'));
$form->setData($request->getPost());
if(!$request->isXmlHttpRequest()) {
if ($form->isValid()) {
$cliente = $form->getObject();
if ($cliente->getId() != 0) {
$cliente->getPessoa()->setCadastradoEm(new \DateTime);
}
// ...
}
}
}
//...
Again, thanks #rkeet!

ZendFramework 2 - removing InputFilter causes misbehavior in custom filter

I have an registration form User\UserForm which contains a fieldset User\UserFieldset. In User\UserFieldset I put an field called "passwordVerify" which should be identical with another field in the fieldset called "password".
This works fine.
However, if an admin wants to modify an user account within the Admin\UserForm, which also contains the User\UserFieldset, the field "passwordVerify" in the fieldset User\UserFieldset should be removed. Therefore I call the following within the Admin\UserForm:
$this->get('user')->remove('passwordVerify');
$this->getInputFilter()->get('user')->remove('passwordVerify')
As expected, the form lacks the field "passwordVerify" now.
If I save the form after editing some stuff, my custom filter "PasswordFilter" cannot retrieve the bound object of the fieldset anymore ($this->getOption('object'); returns an User-object) - but all properties of the bound object are nulled. If I use the Admin\UserForm without removing "passwordVerify"-field and "passwordVerify"-inputfilter everything works fine and the bound object is passed to "PasswordFilter" with populated properties (in respect of values, inserted by the user in the Admin\UserForm). The line which breaks everything is $this->getInputFilter()->get('user')->remove('passwordVerify'). So this leads to my assumption, that by removing an inputfilter, the hydrated object gets somehow nulled / emptied. Below are my some excerpts of my code, if needed I can provide more information about factories, etc.
Admin\UserForm:
class UserForm extends Form
{
/**
* #var EntityManager
*/
protected $entityManager = null;
/**
* #var Translator
*/
protected $translator = null;
public function __construct(EntityManager $entityManager, Translator $translator)
{
$this->entityManager = $entityManager;
$this->translator = $translator;
parent::__construct("userForm");
$this->setHydrator(new DoctrineHydrator($entityManager));
}
public function init()
{
// Adding UserFieldset
$this->add(array(
'name' => 'user',
'type' => \User\Form\UserFieldset::class,
'options' => array(
'use_as_base_fieldset' => true,
),
));
$this->get('user')->remove('passwordVerify');
$this->getInputFilter()->get('user')->remove('passwordVerify');
$this->add(array(
'type' => 'Zend\Form\Element\Csrf',
'name' => 'csrf',
));
$this->add(array(
'type' => 'submit',
'name' => 'submit',
'options' => array(
'label' => $this->translator->translate('Btn.submit.user', 'Form')
),
));
}
}
User\UserFieldset:
class UserFieldset extends Fieldset implements InputFilterProviderInterface
{
/**
* #var EntityManager
*/
protected $entityManager = null;
/**
* #var Translator
*/
protected $translator = null;
public function __construct(EntityManager $entityManager, Translator $translator)
{
$this->entityManager = $entityManager;
$this->translator = $translator;
parent::__construct("userFieldset");
$this->setHydrator(new DoctrineHydrator($entityManager))->setObject(new User());
}
public function init()
{
$this->add(array(
'type' => 'text',
'name' => 'firstName',
'options' => array(
'label' => $this->translator->translate('label.firstName', 'Form'),
'label_attributes' => array(
'class' => 'col-sm-3',
),
'column-size' => 'sm-5',
),
'attributes' => array(
'id' => 'firstName',
),
));
/* ... */
$this->add(array(
'type' => 'text',
'name' => 'password',
'options' => array(
'label' => $this->translator->translate('label.password', 'Form'),
'label_attributes' => array(
'class' => 'col-sm-3',
),
'column-size' => 'sm-5',
),
'attributes' => array(
'id' => 'password',
),
));
$this->add(array(
'type' => 'password',
'name' => 'passwordVerify',
'options' => array(
'label_attributes' => array(
'class' => 'col-sm-3 control-label'
),
'label' => $this->translator->translate('label.verifyPassword', 'Form'),
'column-size' => 'sm-8',
),
'attributes' => array(
'class' => 'form-control',
'id' => 'password'),
));
/* ... */
// Adding AddressFieldset
$this->add(array(
'name' => 'address',
'type' => \User\Form\AddressFieldset::class,
));
/* ... */
$this->add(array(
'type' => 'datetime',
'name' => 'created',
'options' => array(
'label' => $this->translator->translate('label.created', 'Form'),
'format' => 'd.m.Y H:i',
'label_attributes' => array(
'class' => 'col-sm-3',
),
'column-size' => 'sm-5',
),
'attributes' => array(
'id' => 'created',
),
));
}
public function getInputFilterSpecification()
{
return array(
'firstName' => array(
'required' => true,
'filters' => array(
array('name' => 'StringTrim'),
array('name' => 'StripTags'),
),
'validators' => array(),
),
/* ... */
'password' => array(
'required' => true,
'filters' => array(
array('name' => 'StringTrim'),
array('name' => 'StripTags'),
[
'name' => PasswordFilter::class,
'options' => [
'object' => $this->getObject(),
'field' => 'password'
]
]
),
'validators' => array(),
),
'passwordVerify' => array(
'required' => true,
'filters' => [
[
'name' => PasswordFilter::class,
'options' => [
'object' => $this->getObject(),
'field' => 'password'
]
]
],
'validators' => array(
array(
'name' => 'StringLength',
'options' => array(
'min' => 6
)
),
array(
'name' => 'Identical',
'options' => array(
'token' => 'password'
)
)
)
),
/* ... */
'created' => array(
'required' => false,
'filters' => array(
array('name' => 'StringTrim'),
),
'validators' => array(
array(
'name' => 'Date',
'options' => array('format' => 'd.m.Y H:i')
),
),
)
);
}
}
PasswordFilter:
class PasswordFilter extends AbstractFilter
{
/** #var EntityManager */
protected $entityManager;
/** #var PasswordInterface */
protected $passwordManager;
/**
* PasswordFilter constructor.
* #param EntityManager $entityManager
* #param PasswordInterface $passwordManager
* #param array $options
*/
public function __construct(EntityManager $entityManager, PasswordInterface $passwordManager, $options = [])
{
$this->entityManager = $entityManager;
$this->passwordManager = $passwordManager;
$this->options = $options;
}
public function filter($value)
{
$object = $this->getOption('object');
$field = $this->getOption('field');
$getter = 'get'.ucfirst($field);
if (!$object || !$field) {
throw new \Exception('Options "object" and "field" are required.');
}
if ($object->getId()) {
$dbObject = $this->entityManager->getRepository(get_class($object))->find($object->getId());
if ($value === $dbObject->{$getter}()) {
return $value;
}
}
// hash password here...
return $this->passwordManager->create($value);
}
private function getOption($option)
{
if (array_key_exists($option, $this->options)) {
return $this->options[$option];
}
return false;
}
}
Any clues? Do I call remove inputfilter of "passwordVerify" to early in the process of instantiation?
I also tested to remove the inputFilter and field after "$this->form->bind($user)" in my controller, which also works. Why it does not work then if I remove it in Admin\UserForm, which is in my opinion the cleaner way of managing the "passwordVerify"-stuff?
If you call $this->getInputFilter(), the InputProviderInterface::getInputSpecification method in your UserForm is being called.
If you did not attached the object already, it cannot retrieve it.
But I dont get why you would even need that. You are hashing the password if the value does not fit to the database value, but obviously the database value seems to be plain text as the input or why would you compare it?
IMHO you just should hash the password, no matter what the current value in your database is.
If you are using doctrine and the password wont change, it wont execute an UPDATE query anyway.

ZF2 + Doctrine2 - Fieldset in Fieldset of a Collection in Fieldset does not validate properly

I asked a similar question a while ago, which came down to the structuring of the Forms, Fieldsets and InputFilters.
I've been thoroughly applying the principle of separation of concerns to split up Fieldsets from InputFilters as the modules they're created in will also be used in an API (Apigility based), so I would need only Entities and InputFilters.
However, I now have a problem that when I have a Fieldset, used by a Fieldset, used in a Collection in a Fieldset, that the inner-most Fieldset does not validate.
Let me elaborate with examples, and code!
The situation is that I want to be able to create a Location. A Location consists of a property name and a OneToMany ArrayCollection|Address[] association. This is because a Location could have multiple addresses (such as a visitors address and a delivery address).
An Address consists of a few properties (street, number, city, Country, etc.) and a OneToOne Coordinates association.
Now, Address has the below Fieldset:
class AddressFieldset extends AbstractFieldset
{
public function init()
{
parent::init();
// More properties, but you get the idea
$this->add([
'name' => 'street',
'required' => false,
'type' => Text::class,
'options' => [
'label' => _('Street'),
],
]);
$this->add([
'name' => 'country',
'required' => false,
'type' => ObjectSelect::class,
'options' => [
'object_manager' => $this->getEntityManager(),
'target_class' => Country::class,
'property' => 'id',
'is_method' => true,
'find_method' => [
'name' => 'getEnabledCountries',
],
'display_empty_item' => true,
'empty_item_label' => '---',
'label' => _('Country'),
'label_generator' => function ($targetEntity) {
return $targetEntity->getName();
},
],
]);
$this->add([
'type' => CoordinatesFieldset::class,
'required' => false,
'name' => 'coordinates',
'options' => [
'use_as_base_fieldset' => false,
],
]);
}
}
As you can see, details for the Address Entity must entered, a Country must be selected and Coordinates could (not required) be provided.
The above is validated using the InputFilter below.
class AddressFieldsetInputFilter extends AbstractFieldsetInputFilter
{
/** #var CoordinatesFieldsetInputFilter $coordinatesFieldsetInputFilter */
protected $coordinatesFieldsetInputFilter;
public function __construct(
CoordinatesFieldsetInputFilter $filter,
EntityManager $objectManager,
Translator $translator
) {
$this->coordinatesFieldsetInputFilter = $filter;
parent::__construct([
'object_manager' => $objectManager,
'object_repository' => $objectManager->getRepository(Address::class),
'translator' => $translator,
]);
}
/**
* Sets AddressFieldset Element validation
*/
public function init()
{
parent::init();
$this->add($this->coordinatesFieldsetInputFilter, 'coordinates');
$this->add([
'name' => 'street',
'required' => false,
'filters' => [
['name' => StringTrim::class],
['name' => StripTags::class],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'min' => 3,
'max' => 255,
],
],
],
]);
$this->add([
'name' => 'country',
'required' => false,
]);
}
}
As you can see, the AddressFieldsetInputFilter required a few things, one of which is the CoordinatesFieldsetInputFilter. In the init() function this is then added with the name corresponding to that given in the Fieldset.
Now, all of the above works, no problem. Addresses with Coordinates everywhere. It's great.
The problem arises when we go another level further and have the LocationFieldset, as below, with it's LocationFieldsetInputFilter.
class LocationFieldset extends AbstractFieldset
{
public function init()
{
parent::init();
$this->add([
'name' => 'name',
'required' => true,
'type' => Text::class,
'options' => [
'label' => _('Name'),
],
]);
$this->add([
'type' => Collection::class,
'name' => 'addresses',
'options' => [
'label' => _('Addresses'),
'count' => 1,
'allow_add' => true,
'allow_remove' => true,
'should_create_template' => true,
'target_element' => $this->getFormFactory()->getFormElementManager()->get(AddressFieldset::class),
],
]);
}
}
In the class below, you might notice a bunch of commented out lines, these have been different attempts to modify the DI and/or setup of the InputFilter so that it works.
class LocationFieldsetInputFilter extends AbstractFieldsetInputFilter
{
/** #var AddressFieldsetInputFilter $addressFieldsetInputFilter */
protected $addressFieldsetInputFilter;
// /** #var CoordinatesFieldsetInputFilter $coordinatesFieldsetInputFilter */
// protected $coordinatesFieldsetInputFilter;
public function __construct(
AddressFieldsetInputFilter $filter,
// CoordinatesFieldsetInputFilter $coordinatesFieldsetInputFilter,
EntityManager $objectManager,
Translator $translator
) {
$this->addressFieldsetInputFilter = $filter;
// $this->coordinatesFieldsetInputFilter = $coordinatesFieldsetInputFilter;
parent::__construct([
'object_manager' => $objectManager,
'object_repository' => $objectManager->getRepository(Location::class),
'translator' => $translator,
]);
}
/**
* Sets LocationFieldset Element validation
*/
public function init()
{
parent::init();
$this->add($this->addressFieldsetInputFilter, 'addresses');
// $this->get('addresses')->add($this->coordinatesFieldsetInputFilter, 'coordinates');
$this->add([
'name' => 'name',
'required' => true,
'filters' => [
['name' => StringTrim::class],
['name' => StripTags::class],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'min' => 3,
'max' => 255,
],
],
],
]);
}
}
You might have noticed that the LocationFieldset and LocationFieldsetInputFilter make use of the existing AddressFieldset and `AddressFieldsetInputFilter.
Seeing as how they work, I cannot figure out why it's going wrong.
But what goes wrong?
Well, to create a Location, it appears that entering Coordinates is always required. If you look in the AddressFieldset (at the top), you'll notice a 'required' => false,, so this makes no sense.
However, when I DO enter values in the inputs, they do not get validated. When debugging, I get into the \Zend\InputFilter\BaseInputFilter, line #262 where it specifically validates the input, and I notice that it has lost it's data along the way of validation.
I've confirmed the presence of the data at the start, and during the validation, up until it tries to validate the Coordinates Entity, where it seems to lose it (haven't found out why).
If someone could point me in the right direction to clean this up, that help would be greatly appreciated. Have been banging against this issue for way too many hours now.
EDIT
Added in view partial code to show method for printing, in case that should/could help:
address-form.phtml
<?php
/** #var \Address\Form\AddressForm $form */
$form->prepare();
echo $this->form()->openTag($form);
echo $this->formRow($form->get('csrf'));
echo $this->formRow($form->get('address')->get('id'));
echo $this->formRow($form->get('address')->get('street'));
echo $this->formRow($form->get('address')->get('city'));
echo $this->formRow($form->get('address')->get('country'));
echo $this->formCollection($form->get('address')->get('coordinates'));
echo $this->formRow($form->get('submit'));
echo $this->form()->closeTag($form);
location-form.phtml
<?php
/** #var \Location\Form\LocationForm $form */
$form->prepare();
echo $this->form()->openTag($form);
echo $this->formRow($form->get('csrf'));
echo $this->formRow($form->get('location')->get('id'));
echo $this->formRow($form->get('location')->get('name'));
//echo $this->formCollection($form->get('location')->get('addresses'));
$addresses = $form->get('location')->get('addresses');
foreach ($addresses as $address) {
echo $this->formCollection($address);
}
echo $this->formRow($form->get('submit'));
echo $this->form()->closeTag($form);
And just in case it makes it all even more clear: a debug picture to help out
After another day of debugging (and swearing), I found the answer!
This SO question helped me out by pointing me towards the Zend CollectionInputFilter.
Because the AddressFieldset is added to the LocationFieldset within a Collection, it must be validated using a CollectionInputFilter which has the specific InputFilter for the Fieldset specified.
To fix my application I had to modify both the LocationFieldsetInputFilter and the LocationFieldsetInputFilterFactory. Below the updated code, with the old code in comments.
LocationFieldsetInputFilterFactory.php
class LocationFieldsetInputFilterFactory extends AbstractFieldsetInputFilterFactory
{
/**
* #param ServiceLocatorInterface|ControllerManager $serviceLocator
* #return InputFilter
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
parent::setupRequirements($serviceLocator, Location::class);
/** #var AddressFieldsetInputFilter $addressFieldsetInputFilter */
$addressFieldsetInputFilter = $this->getServiceManager()->get('InputFilterManager')
->get(AddressFieldsetInputFilter::class);
$collectionInputFilter = new CollectionInputFilter();
$collectionInputFilter->setInputFilter($addressFieldsetInputFilter); // Make sure to add the FieldsetInputFilter that is to be used for the Entities!
return new LocationFieldsetInputFilter(
$collectionInputFilter, // New
// $addressFieldsetInputFilter, // Removed
$this->getEntityManager(),
$this->getTranslator()
);
}
}
LocationFieldsetInputFilter.php
class LocationFieldsetInputFilter extends AbstractFieldsetInputFilter
{
// Removed
// /** #var AddressFieldsetInputFilter $addressFieldsetInputFilter */
// protected $addressFieldsetInputFilter ;
// New
/** #var CollectionInputFilter $addressCollectionInputFilter */
protected $addressCollectionInputFilter;
public function __construct(
CollectionInputFilter $addressCollectionInputFilter, // New
// AddressFieldsetInputFilter $filter, // Removed
EntityManager $objectManager,
Translator $translator
) {
// $this->addressFieldsetInputFilter = $filter; // Removed
$this->addressCollectionInputFilter = $addressCollectionInputFilter; // New
parent::__construct([
'object_manager' => $objectManager,
'object_repository' => $objectManager->getRepository(Location::class),
'translator' => $translator,
]);
}
/**
* Sets LocationFieldset Element validation
*/
public function init()
{
parent::init();
// $this->add($this->addressFieldsetInputFilter, 'addresses'); // Removed
$this->add($this->addressCollectionInputFilter, 'addresses'); // New
$this->add([
'name' => 'name',
'required' => true,
'filters' => [
['name' => StringTrim::class],
['name' => StripTags::class],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'min' => 3,
'max' => 255,
],
],
],
]);
}
}
The way this works is that, during the validation of the data, it will apply the singular AddressFieldsetInputFilter to every "element" received from the client-side. Because a Collection, from the client, may be 0 or more of these elements (as adding/removing them is done using JavaScript).
Now that I've figured that out, it does actually make perfect sense.

Symfony Sonata admin find by some field

I install SonataAdminBundle and create controller extends Admin and function configureFormFields, configureDatagridFilters, configureListFields. And in field list I use field image but I see only url for image, my image live in amazon S3 I want see image in table. How I do this? And I have filter for find by colums, my entity developer have array skill and by one skill find good but how find for two or many skill ?
And I add for admin can do upload avatar for developer but in my action(not extends Admin) I upload like this for(field image in Developer = string and I set just url for S3)
$url = sprintf(
'%s%s',
$this->container->getParameter('acme_storage.amazon_s3.base_url'),
$this->getPhotoUploader()->upload($request->files->get('file'), $user_company_name)
);
$user->setImage($url);
how I can do for sonata, reload controller? How I do this ?
this my action:
class DeveloperAdmin extends Admin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('firstName', null, array('label' => 'Developer\'s First Name', 'max_length' => 255))
->add('lastName', null, array('label' => 'Developer\'s Last Name', 'max_length' => 255))
->add('qualification', 'choice', array('label' => 'Speciality',
'choices' => array('Frontend' => 'Frontend', 'Backend' => 'Backend', 'Full stack' => 'Full stack'),'attr'=> array('class'=>'qualif'), 'required' => false))
->add('level', 'choice', array('label' => 'Professional Level', 'max_length' => 255,
'choices' => array('Junior' => 'Junior', 'Middle' => 'Middle', 'Senior' => 'Senior')))
->add('tags', 'tags', array('label' => 'Tags','required' => false))
->add('main_skill', 'mainSkill', array('label' => 'Main Skill', 'required' => true, 'mapped' => true, 'attr' => array('placeholder' => 'Select your skills ...', 'class'=>'main_skill') ))
->add('skills', 'skills', array('label' => 'Skills','required' => false))
->add('english', 'choice', array('label' => 'English Level', 'max_length' => 255,
'choices' => array('Basic' => 'Basic', 'Intermediate' => 'Intermediate', 'Advanced' => 'Advanced')))
->add('rate', null, array('label' => 'Rate $/h', 'max_length' => 255));
$image = $this->getSubject();
$fileFieldOptions = array('required' => false);
if ($image && ($webPath = $image->getImage())) {
dump($image);exit; //I have all user and field image local url /temp/sdgsdg
$container = $this->getConfigurationPool()->getContainer();
$fullPath = $container->get('request')->getBasePath().'/'.$webPath;
$fileFieldOptions['help'] = '<img src="'.$fullPath.'" class="admin-preview" />';
}
$formMapper
->add('image', 'file', $fileFieldOptions)
}
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('firstName')
->add('lastName')
->add('main_skill')
->add('skills')
;
}
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('id')
->add('username')
->add('firstName')
->add('lastName')
->add('main_skill')
->add('skills')
->add('image', 'string', array('template' => 'SonataMediaBundle:MediaAdmin:list_image.html.twig'))
->add('_action', 'actions', array(
'actions' => array(
'show' => array(),
'edit' => array(),
)
))
;
}
}
How find by two or many skill ????
this is my entity:
class Developer extends CustomUser
{
/**
* #var string
*
* #ORM\Column(name="email", type="string", length=255, unique=false, nullable=true)
* #Assert\Length(min=3, max=255)
*/
protected $email;
////
/**
* #var string
*
* #ORM\Column(name="skills", type="array")
*/
private $skills = array();
and in table for my developer in colum skill I see:
[0 => SOAP] [1 => Cisco] [2 => PHP] [3 => Sugar Crm] [4 => Hibernate] [5 => Java ME]
but when I add developer I use my service for skill and I see norm skill:
xPHP, xJava
How can fix this problem, reload template or controller ? Help please
For this task use https://sonata-project.org/bundles/media/2-2/doc/index.html . It's very easy

How should I handle Symfony2/Doctrine exceptions

Maybe this is a hot topic and some others talk about this but I don't find a good solution yet to this problem. Take this error for UNIQUE fields as example. When I try to insert the same values to the database I get this error:
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry
'j1234567' for key 'UNIQ_FC3A5A1592FC23A8'
Of course this happens on app_dev.php (development) enviroment but I don't know how to deal with this in order to show an error page to users instead of this ugly error. I test the same code at production then the ugly error disappear but I get this instead:
ERROR: INTERNAL SERVER ERROR
Paths, I though, are more than one, for example I could check the existence of the record before I insert or before I send the request trough AJAX but I want to learn how to achieve this by using Symfony2 and Doctrine2 asserts. I have already added this code to my entities:
<?php
....
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
* SysPerfil
*
* #ORM\Entity
* #ORM\Table(name="sys_perfil")
* #UniqueEntity(fields={"rif"}, message="Este RIF ya existe en nuestra base de datos")
* #UniqueEntity(fields={"ci"}, message="Este CI ya existe en nuestra base de datos")
* #UniqueEntity(fields={"nombre"}, message="Este nombre ya existe en nuestra base de datos")
*/
class SysPerfil
{
....
But it's not working since I get the error mentioned above, so what is the best way to handle this? Any ideas? Advices? Docs?
Add form types
Yes, I send the data trough a form type, see below:
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder
->add('email', 'email', array(
'required' => true,
'label' => 'Email',
'trim' => true
))
->add('password', 'password', array(
'required' => true,
'label' => 'Contraseña',
'always_empty' => true
))
->add('confirm', 'password', array(
'required' => true,
'mapped' => false,
'label' => 'Verificar contraseña',
'always_empty' => true
))
->add('enabled', 'checkbox', array(
'required' => true,
'label' => 'Activo?',
'data' => true
))
->add('perfil', new AdminPerfilType());
}
And AdminPerfilType.php:
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder
->add('persJuridica', 'choice', array(
'choices' => RifType::getChoices(),
'required' => true,
'label' => 'RIF',
'trim' => true,
'attr' => array(
'class' => 'persJuridica'
)
))
->add('roleType', 'choice', array(
'choices' => AdminRoleType::getChoices(),
'required' => true,
'label' => "Tipo de Usuario",
'trim' => true
))
->add('rif', 'text', array(
'required' => true,
'label' => false,
'trim' => true,
'attr' => array(
'class' => "numeric",
'maxlength' => 15
)
))
->add('ci', 'text', array(
'label' => 'CI',
'trim' => true,
'attr' => array(
'class' => "numeric ci",
'disabled' => 'disabled'
)
))
->add('nombre', 'text', array(
'required' => true,
'label' => 'Nombre',
'trim' => true
))
->add('apellido', 'text', array(
'required' => true,
'label' => 'Apellidos',
'trim' => true
));
}
If you're looking for validation rules inside the form then I haven't since I though that Doctrine/Symfony2 handle that part already
I guess your error is because you have a Parent -> Child Entities with One-To-One mapping, your form validation is checking the parent entity validation rules without checking the child validation rules because you are not using Assert\Valid
Example from Symfony Documentation http://symfony.com/doc/current/reference/constraints/Valid.html:
// src/Acme/HelloBundle/Entity/Address.php
namespace Acme\HelloBundle\Entity;
use Symfony\Component\Validator\Constraints as Assert;
class Address
{
/**
* #Assert\NotBlank()
*/
protected $street;
/**
* #Assert\NotBlank
* #Assert\Length(max = "5")
*/
protected $zipCode;
}
// src/Acme/HelloBundle/Entity/Author.php
namespace Acme\HelloBundle\Entity;
class Author
{
/**
* #Assert\NotBlank
* #Assert\Length(min = "4")
*/
protected $firstName;
/**
* #Assert\NotBlank
*/
protected $lastName;
//without this Symfony won't check if the inserted address is satisfying the validation rules or not
/**
* #Assert\Valid
*/
protected $address;
}

Categories