Search code examples
phpdoctrine-ormzend-framework2zend-framework3fieldset

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:

Entity Model

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: Screen of system

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?


Solution

  • 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!