Tutorial ZF2 + Doctrine + ZfcUser + Los – Parte 5


Hoje vamos ver todo o sistema que preparamos nos posts anteriores em funcionamento.

No post anterior, nós definimos as permissões no sistema de forma que as telas de cadastro não possam ser vistas por quem não esteja logado no sistema nem por quem não tem a devida permissão.

Então vamos testar a configuração acessando diretamente a rota de cadastro de clientes: Continue lendo

Bug Z-Ray para ZF2 usando Doctrine

Bom dia!

Existe um bug no Z-Ray para ZF2 se você usa Entidades Doctrine numa Zend\Form (num select ou checkbox por exemplo):

You cannot serialize or unserialize PDO instances

File:

vendor/zendframework/zendframework/library/Zend/Form/View/Helper/Form.php:0

Trace:

Form/View/Helper/Form.php(112): PDO->__sleep()
Form/View/Helper/Form.php(112): Zend\Form\View\Helper\Form->openTag(Object(App\Form\MyForm))

A equipe do Z-Ray já está ciente do problema, claro, e está trabalhando para resolve-lo:

https://github.com/zend-server-extensions/Z-Ray-ZF2/issues/6#issuecomment-71366020

Enquanto isso, se você encontrar este problema, basta fechar o Z-Ray para a sessão e recarregar a página.

Abraços,

Leandro Silva

Tutorial ZF2 + Doctrine + ZfcUser + Los – Parte 2

Neste segundo post do tutorial vou falar sobre o Doctrine.

O que é Doctrine?

Praticamente todo sistema usa uma forma de armazenar os dados, principalmente banco de dados (MySql, Postgresql, Oracle, SqlServer, etc). Num projeto usando Zend Framework 2, podemos fazer nossa abstração dos dados usando o Zend\Db que é excelente, mas em todos meus projetos uso o Doctrine, em especial o Doctrine ORM.

Doctrine ORM é Object Relational Mapper, que traduzindo ao pé da letra significa, Mapeador de Objeto Relacional, que nada mais é que uma camada no nosso sistema que salva nossas entidades num banco de dados e lê de volta.

A equipe do Doctrine fez 2 módulos para facilitar o uso num projeto ZF2: DoctrineModule e DoctrineORMModule.

Configuração

No primeiro post, colocamos o doctrine-orm-module como dependência, e apenas ele é suficiente, pois o composer irá instalar os outros módulos necessários.

Vamos rever nosso config/autoload/global.php:

<?php
return array(
    'service_manager' => array(
        'invokables' => array(
            'Zend\Session\SessionManager' => 'Zend\Session\SessionManager',
        ),
        'aliases' => [
            'Zend\Authentication\AuthenticationService' => 'zfcuser_auth_service'
        ]
    ),
    'doctrine' => array(
        'connection' => array(
            'orm_default' => array(
                'driverClass' => 'Doctrine\DBAL\Driver\PDOMySql\Driver',
                'params' => array(
                    'host' => 'localhost',
                    'port' => '3306',
                    'user' => 'leandro',
                    'dbname' => 'tutorial',
                    'charset' => 'UTF8',
                    'driverOptions' => array(
                        'charset' => 'UTF8'
                    )
                )
            )
        ),
        'entitymanager' => array(
            'orm_default' => array(
                'connection' => 'orm_default',
                'configuration' => 'orm_default'
            )
        ),
        'configuration' => array(
            'orm_default' => array(
                'query_cache' => 'apc',
                'result_cache' => 'apc',
                'metadata_cache' => 'apc'
            )
        )
    ),
    'view_manager' => array(
        'template_map' => array(
            'error/403' => __DIR__ . '/../../module/Application/view/error/403.phtml',
        ),
    ),
);

A configuração do doctrine está destacada. Vamos falar das principais linhas.

'driverClass' => 'Doctrine\DBAL\Driver\PDOMySql\Driver',

Aqui definimos qual banco de dados vamos usar. Está configurado para o MySql, mas se estiver usando outro, basta consultar na documentação do doctrine qual a classe correta a ser usada.

'host'   => 'localhost',
'port'   => '3306',
'user'   => 'leandro',
'dbname' => 'tutorial',

Aqui informamos as configurações de acesso ao banco. Lembro que nunca se deve colocar senhas no global.php. Toda informação sensitiva ou que se altera de acordo com a instalação, deve ficar no local.php.

'configuration' => array(
    'orm_default' => array(
        'query_cache' => 'apc',
        'result_cache' => 'apc',
        'metadata_cache' => 'apc'
    )
)

É sempre bom usar cache, e com o doctrine não é diferente. Essas 3 linhas configuram o doctrine para usar o APC como cache para as principais operações.

<?php
return array(
    'doctrine' => array(
        'configuration' => array(
            'orm_default' => array(
                'query_cache' => 'array',
                'result_cache' => 'array',
                'metadata_cache' => 'array'
            )
        ),
        'connection' => array(
            'orm_default' => array(
                'params' => array(
                    'password' => 'minhasenha'
                )
            )
        )
    )
);

No nosso local.php, configuramos a senha do banco de dados e, no caso do desenvolvimento, definimos o cache como array, ou seja, só usará o cache apenas naquela página. Isso é importante, pois como o cache armazena estrutura das tabelas e até as consultas ao banco, vai atrapalhar bastante o desenvolvimento.

Com tudo configurado, vamos testar o Doctrine. O Doctrine nos oferece algumas ferramentas de console:

$ php vendor/doctrine/doctrine-module/bin/doctrine-module.php 
DoctrineModule Command Line Interface version 0.8.0

Usage:
 [options] command [arguments]

Options:
 --help (-h)           Display this help message
 --quiet (-q)          Do not output any message
 --verbose (-v|vv|vvv) Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
 --version (-V)        Display this application version
 --ansi                Force ANSI output
 --no-ansi             Disable ANSI output
 --no-interaction (-n) Do not ask any interactive question

Available commands:
 help                             Displays help for a command
 list                             Lists commands
dbal
 dbal:import                      Import SQL file(s) directly to Database.
 dbal:run-sql                     Executes arbitrary SQL directly from the command line.
orm
 orm:clear-cache:metadata         Clear all metadata cache of the various cache drivers.
 orm:clear-cache:query            Clear all query cache of the various cache drivers.
 orm:clear-cache:result           Clear all result cache of the various cache drivers.
 orm:convert-d1-schema            Converts Doctrine 1.X schema into a Doctrine 2.X schema.
 orm:convert-mapping              Convert mapping information between supported formats.
 orm:convert:d1-schema            Converts Doctrine 1.X schema into a Doctrine 2.X schema.
 orm:convert:mapping              Convert mapping information between supported formats.
 orm:ensure-production-settings   Verify that Doctrine is properly configured for a production environment.
 orm:generate-entities            Generate entity classes and method stubs from your mapping information.
 orm:generate-proxies             Generates proxy classes for entity classes.
 orm:generate-repositories        Generate repository classes from your mapping information.
 orm:generate:entities            Generate entity classes and method stubs from your mapping information.
 orm:generate:proxies             Generates proxy classes for entity classes.
 orm:generate:repositories        Generate repository classes from your mapping information.
 orm:info                         Show basic information about all mapped entities
 orm:run-dql                      Executes arbitrary DQL directly from the command line.
 orm:schema-tool:create           Processes the schema and either create it directly on EntityManager Storage Connection or generate the SQL output.
 orm:schema-tool:drop             Drop the complete database schema of EntityManager Storage Connection or generate the corresponding SQL output.
 orm:schema-tool:update           Executes (or dumps) the SQL needed to update the database schema to match the current mapping metadata.
 orm:validate-schema              Validate the mapping files.

Um dos comandos que mais uso, é o ora:schema-tool:update. Ele varre nossas entidades e compara com a estrutura do banco de dados. Se houve uma nova entidade, será criada uma nova tabela. Se você adicionou ou alterou uma propriedade à uma entidade existente, ele reflete a adição ou alteração no banco. Uso tanto este comando que nos meus projetos, crio um diretório bin com 2 scripts:

$ php vendor/doctrine/doctrine-module/bin/doctrine-module.php orm:schema-tool:update --dump-sql
Nothing to update - your database is already in sync with the current entity metadata.

Se tudo correr bem, deve aparecer a mensagem acima. É normal que ainda não tenha nada para ser alterado, pois não criamos nossas entidades.

É importante criar a database antes de continuarmos, pois os comandos doctrine esperam que pelo menos a database esteja criada.

Entidade Cliente

Vamos começar criando um módulo chamado Cliente. Podemos criar um módulo de várias maneiras, manualmente, pela IDE (Zend Studio por exemplo), zftool ou, como vamos usar agora, o comando do LosBase:

$ php public/index.php create crud Cliente
The module Cliente has been created

Simples não é? E já cria todos os arquivos que vamos precisar:

Módulo ClienteVamos nos concentrar na nossa Entidade Cliente, que é o assunto desde post. Abrindo o arquivo module/Cliente/src/Cliente/Entity/Cliente.php:

<?php
namespace Cliente\Entity;

use Doctrine\ORM\Mapping as ORM;
use LosBase\Entity\AbstractEntity;

/**
 * @ORM\Entity
 * @ORM\Table(name="cliente")
 */
class Cliente extends AbstractEntity
{
}

Nossa entidade já está criada. Definimos o nome da tabela no banco de dados como “cliente”. Vamos adicionar 2 campos: nome e crédito:

<?php
namespace Cliente\Entity;

use Doctrine\ORM\Mapping as ORM;
use LosBase\Entity\AbstractEntity;

/**
 * @ORM\Entity
 * @ORM\Table(name="cliente")
 */
class Cliente extends AbstractEntity
{

    /**
     * @ORM\Column(type="string", length=250)
     */
    protected $nome;

    /**
     * @ORM\Column(type="brprice")
     */
    protected $credito;

    public function getNome()
    {
        return $this->nome;
    }

    public function setNome($nome)
    {
        $this->nome = $nome;
        return $this;
    }

    public function getCredito()
    {
        return $this->credito;
    }

    public function setCredito($credito)
    {
        $this->credito = $credito;
        return $this;
    }

    public function __toString()
    {
        return $this->nome;
    }
}

Neste post, estou retirando os Annotations do Form do ZF2 para mais clareza na parte do Doctrine.

O campo nome é uma string de 250 caracteres enquanto que o crédito é um brprice, um tipo de dado do LosBase que converte nosso formato de números (1.234,56) para o de banco de dados (1234.56) e vice-versa.

Neste tutorial estou usando Annotations para definir os campos e a form do ZF2. Existe uma parte da comunidade que não gosta de Annotations e prefere usar código PHP. Não vou entrar no mérito de qual opção é a melhor, mas acho que no fundo é mais uma escolha pessoal.

É muito importante ressaltar que o Doctrine usa as propriedades da classe como campo do banco, mas usa o getters e setters para alimentar a entidade quando as lê do banco. Então sempre crie os gets e sets.

Vamos voltar ao nosso comando doctrine e vamos ver o que acontece:

php vendor/doctrine/doctrine-module/bin/doctrine-module.php orm:schema-tool:update --dump-sql
CREATE TABLE cliente (id INT AUTO_INCREMENT NOT NULL, nome VARCHAR(250) NOT NULL, credito NUMERIC(9, 2) NOT NULL, created DATETIME NOT NULL, updated DATETIME NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;

Fácil não é mesmo? Ele já vai criar nossa tabela com o id do cliente, nome, crédito e 2 campos indicando quando aquele cliente foi criado e quando sofreu a última alteração. Mas de onde vieram os campos id, created e updated? Não está na classe Cliente. Na verdade está sim.

Como a classe Cliente estende da LosBase\Entity\AbstractEntity, o doctrine também usa os campos das classes estendidas, interfaces ou traits.

Vamos executar o mesmo comando, mas sem o –dump-sql para criar a tabela:

$ php vendor/doctrine/doctrine-module/bin/doctrine-module.php orm:schema-tool:update --force
Updating database schema...
Database schema updated successfully! "1" queries were executed

Pronto, nossa tabela está criada no MySql.

Mas acho que 250 caracteres para o nome do cliente muita coisa … vamos reduzir para 150:

/**
 * @ORM\Column(type="string", length=150)
 */
protected $nome;

E executar o dump-sql novamente:

$ php vendor/doctrine/doctrine-module/bin/doctrine-module.php orm:schema-tool:update --dump-sql
ALTER TABLE cliente CHANGE nome nome VARCHAR(150) NOT NULL, CHANGE credito credito NUMERIC(9, 2) NOT NULL;

E ele altera o tamanho do campo nome para varchar(150).

Um ponto importante de se notar, é que ele também mostra uma alteração no crédito mas para o mesmo campo, sem alterações. A ferramenta do doctrine possui essa característica: Se você usar um campo que você criou, ou fez uma alteração na definição da coluna, o comando sempre vai achar que foi feita uma alteração. Mas normalmente os bancos de dados são espertos o suficiente para não realizar a alteração se realmente não for necessário.

Entidade Usuário

Agora vamos criar nossa entidade usuário, que pertencerá a um Cliente e terá vários Acessos.

$ php public/index.php create crud Usuario
The module Usuario has been created

Vamos abrir o arquivo module/Usuario/src/Usuario/Entity/Usuario.php e adicionar os campos:

<?php
namespace Usuario\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use ZfcUser\Entity\UserInterface as ZfcUserInterface;
use ZfcRbac\Identity\IdentityInterface;
use LosBase\Entity\AbstractEntity;

/**
 * @ORM\Entity
 * @ORM\Table(name="usuario")
 */
class Usuario extends AbstractEntity implements ZfcUserInterface, IdentityInterface
{

    /**
     * @ORM\Column(type="string", length=250)
     */
    protected $nome;

    /**
     * @ORM\Column(type="string", length=255)
     */
    protected $email = '';

    /**
     * @ORM\ManyToOne(targetEntity="Cliente\Entity\Cliente", inversedBy="usuarios")
     * @ORM\JoinColumn(nullable=true, onDelete="CASCADE")
     * @ORM\OrderBy({"nome" = "ASC"})
     */
    protected $cliente;

    /**
     * @ORM\Column(type="string", length=32)
     * Possiveis: visitante, usuario, suporte, admin
     */
    protected $permissao = 'visitante';

    protected $username;

    /**
     * @ORM\Column(type="string", length=128)
     */
    protected $password = '';

    protected $confirmesenha;

    /**
     * @ORM\OneToMany(targetEntity="Usuario\Entity\Acesso", mappedBy="usuario")
     * @ORM\JoinColumn(nullable=false)
     */
    protected $acessos;

    public function __construct()
    {
        $this->created = new \DateTime('now');
        $this->updated = new \DateTime('now');
        $this->acessos = new ArrayCollection();
    }

    /**
     * @return string the $nome
     */
    public function getNome()
    {
        return $this->nome;
    }

    /**
     * @param string $nome
     */
    public function setNome($nome)
    {
        $this->nome = $nome;

        return $this;
    }

    /**
     * Retorna o campo $permissao
     * @return $permissao
     */
    public function getPermissao()
    {
        return $this->permissao;
    }

    /**
     * Seta o campo $permissao
     * @param field_type $permissao
     * @return $this
     */
    public function setPermissao($permissao)
    {
        $this->permissao = $permissao;

        return $this;
    }

    public function getRoles()
    {
        return array(
            $this->permissao
        );
    }

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }

    public function getId()
    {
        return $this->id;
    }

    public function setId($id)
    {
        $this->id = $id;
        return $this;
    }

    public function getUsername()
    {
        return $this->username;
    }

    public function setUsername($username)
    {
        $this->username = $username;
        return $this;
    }

    public function getDisplayName()
    {
        return $this->getNome();
    }

    public function setDisplayName($displayName)
    {}

    public function getPassword()
    {
        return $this->password;
    }

    public function setPassword($password)
    {
        if (! empty($password)) {
            $this->password = (string) $password;
        }
    }

    public function getState()
    {}

    public function setState($state)
    {}

    public function getConfirmesenha()
    {
        return $this->confirmesenha;
    }

    public function setConfirmesenha($confirmesenha)
    {
        $this->confirmesenha = $confirmesenha;
        return $this;
    }

    public function getCliente()
    {
        return $this->cliente;
    }

    public function setCliente($cliente)
    {
        $this->cliente = $cliente;
        return $this;
    }

    public function getAcessos()
    {
        return $this->acessos;
    }

    public function setAcessos($acessos)
    {
        $this->acessos = $acessos;
        return $this;
    }

    public function addAcessos(Collection $acessos)
    {
        foreach ($acessos as $acesso) {
            $acesso->setUsuario($this);
            $this->acessos->add($acesso);
        }
    }

    public function removeAcessos(Collection $acessos)
    {
        foreach ($acessos as $acesso) {
            $this->acessos->removeElement($acesso);
        }
    }

    public function addAcesso($acesso)
    {
        foreach ($this->acessos as $tok) {
            if ($tok->getId() == $acesso->getId()) {
                return $this;
            }
        }
        $this->acessos[] = $acesso;
        return $this;
    }

    public function __toString()
    {
        return $this->getDisplayName();
    }
}

Esta classe tem várias coisas novas, em especial do ZfcUser, ZfcRbac. No post 4 vou entrar em detalhes sobre esta parte. Vamos focar na parte do ORM.

Diferente da classe Cliente, a classe Usuário possui 2 relacionamentos com outras classes: cliente e acessos.

Um Usuário pertence a um único Cliente, por isso o relacionamento é o ManyToOne. Por outro lado, um Usuário possui vários Acessos, então é um OneToMany.

No relacionamento com Cliente, definimos com qual Classe é o relacionamento (Cliente\Entity\Cliente), dizemos que se um cliente for excluído, este usuário também será (onDelete: cascade).

Todo relacionamento OneToMany (entre outros) deve ser iniciado no __construct da entidade como um novo ArrayCollection (como na lista 62).

Outro ponto importante, é que sempre que você definir um relacionamento que é um ArrayCollection, como $acessos na nossa classe Usuario, então você precisa definir 4 métodos:

  • getAcessos()
  • setAcessos($acessos)
  • addAcessos($acessos)
  • removeAcessos($acessos)

Vamos criar nossa entidade Acesso, no mesmo diretório da Usuario:

<?php
namespace Usuario\Entity;

use Doctrine\ORM\Mapping as ORM;
use LosBase\Entity\AbstractEntity as AbstractEntity;

/**
 * @ORM\Entity
 * @ORM\Table(name="acesso")
 */
class Acesso extends AbstractEntity
{

    /**
     * @ORM\Column(type="string")
     */
    protected $ip;

    /**
     * @ORM\Column(type="string")
     */
    protected $agente;

    /**
     * @ORM\ManyToOne(targetEntity="Usuario\Entity\Usuario", inversedBy="acessos")
     * @ORM\JoinColumn(nullable=false, onDelete="RESTRICT")
     */
    protected $usuario;

    /**
     * Retorna o campo $ip
     * @return $ip
     */
    public function getIp()
    {
        return $this->ip;
    }

    /**
     * Seta o campo $ip
     * @param field_type $ip
     * @return Acesso
     */
    public function setIp($ip)
    {
        $this->ip = $ip;
        return $this;
    }

    /**
     * Retorna o campo $usuario
     * @return $usuario
     */
    public function getUsuario()
    {
        return $this->usuario;
    }

    /**
     * Seta o campo $usuario
     * @param field_type $usuario
     * @return Compra
     */
    public function setUsuario($usuario)
    {
        $this->usuario = $usuario;
        return $this;
    }

    public function getAgente()
    {
        return $this->agente;
    }

    public function setAgente($agente)
    {
        $this->agente = $agente;
        return $this;
    }
}

Como um Acesso pertence a um único Usuario, o relacionamento é representado pelo ManyToOne.

Vamos executar nosso dump:

$ php vendor/doctrine/doctrine-module/bin/doctrine-module.php orm:schema-tool:update --dump-sql

CREATE TABLE acesso (id INT AUTO_INCREMENT NOT NULL, usuario_id INT NOT NULL, ip VARCHAR(255) NOT NULL, agente VARCHAR(255) NOT NULL, created DATETIME NOT NULL, updated DATETIME NOT NULL, INDEX IDX_2FA8F705DB38439E (usuario_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;

CREATE TABLE usuario (id INT AUTO_INCREMENT NOT NULL, cliente_id INT DEFAULT NULL, nome VARCHAR(250) NOT NULL, email VARCHAR(255) NOT NULL, permissao VARCHAR(32) NOT NULL, password VARCHAR(128) NOT NULL, created DATETIME NOT NULL, updated DATETIME NOT NULL, INDEX IDX_2265B05DDE734E51 (cliente_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;

ALTER TABLE acesso ADD CONSTRAINT FK_2FA8F705DB38439E FOREIGN KEY (usuario_id) REFERENCES usuario (id) ON DELETE RESTRICT;

ALTER TABLE usuario ADD CONSTRAINT FK_2265B05DDE734E51 FOREIGN KEY (cliente_id) REFERENCES cliente (id) ON DELETE CASCADE;

ALTER TABLE cliente CHANGE nome nome VARCHAR(150) NOT NULL, CHANGE credito credito NUMERIC(9, 2) NOT NULL;

Bastante coisa desta vez. Ele cria a tabela acesso e usuario já com as chaves estrangeiras ligando as 3 tabelas, incluindo os casdades e restricts. Perfeito!

Vamos realizar as alterações no banco:

$ php vendor/doctrine/doctrine-module/bin/doctrine-module.php orm:schema-tool:update --force
Updating database schema...
Database schema updated successfully! "5" queries were executed

Realmente o Doctrine facilita muito nossa vida com o banco de dados.

Na parte 5 do nosso tutorial, vamos entrar no sistema, criar os usuários, clientes e ver o Doctrine realmente em ação.

O Doctrine é MUITO mais do que falo aqui. Me resumi ao que vamos usar neste tutorial, mas convido a todos darem uma olhada no site do doctrine, em especial na parte ORM e DBAL.

Referência:

Parte 1: Instalação e configuração

Parte 2: Banco de dados (Doctrine)

Parte 3: Visual e Layout (LosUi)

Parte 4: Autenticação e Autorização (ZfcUser e ZfcRbac)

Parte 5: Usando o sistema

 

Até a próxima!