[vc_row][vc_column][vc_column_text]Em 2014 eu escrevi um artigo sobre a ferramenta de migração de banco de dados (migrations) para o CakePHP 2. Hoje, trabalho majoritariamente com o Zend Framework em suas versões 2 e 3, e sempre acabo utilizando migrations em meus projetos. Migrations é uma maneira que possuímos, na camada de programação, de manter o banco de dados estruturalmente sincronizado. Ou seja, uma ferramenta para que não precisemos ficar passando dump da versão atual do banco para o colega de trabalho. Por cuidarem somente da estrutura do banco de dados (DDL - Data Definition Language ), comumente as ferramentas de migração não são backup.
composer create-project -s dev zendframework/skeleton-application path/to/install
composer serveO resultado é como da imagem a seguir. Agora acesse o endereço descrito em seu navegador. Ah, também funciona com https://www.andrebian.com ;) Observação: Tive um erro ao rodar desta forma e talvez ele ocorra pra você também. No arquivo composer.json que veio com o Zend Skeleton o script serve possuía a seguinte definição:
"serve": "php -S 0.0.0.0:8080 -t public public/index.php"Ao rodar pelo composer serve o seguinte erro era exibido: Após uma pequena correção no script, tudo funcionou. Veja o antes e depois.
# Antes "serve": "php -S 0.0.0.0:8080 -t public public/index.php" # Depois "serve": "php -S 0.0.0.0:8080 -t public"A partir deste ponto pode parar o server e fechar a aba referente a aplicação recém criada. Todos os exemplos que virão serão exclusivamente via linha de comando. Apenas lhe mostrei a aplicação rodando para que tenha certeza de que está tudo certo para prosseguirmos.
mysql -u root -p CREATE SCHEMA zf3_blog CHARACTER SET utf8 COLLATE utf8_unicode_ci;
composer require doctrine/doctrine-orm-moduleDurante a instalação você será questionado duas vezes se deseja incluir o módulo automaticamente no arquivo de configurações dos módulos. Digite 1 para adicionar no arquivo correto (neste cenário que estamos contruindo com o Skeleton Application). A primeira vez refere-se ao Zend Hydrator, a segunda sobre o DoctrineModule. Ao final o seu arquivo config/module.config.php deve conter o conteúdo semelhante à este:
<?php
/**
* @link http://github.com/zendframework/ZendSkeletonApplication for the canonical source repository
* @copyright Copyright (c) 2005-2016 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
/**
* List of enabled modules for this application.
*
* This should be an array of module namespaces used in the application.
*/
return [
'ZendCache',
'ZendForm',
'ZendInputFilter',
'ZendFilter',
'ZendPaginator',
'ZendHydrator',
'ZendRouter',
'ZendValidator',
'DoctrineModule',
'DoctrineORMModule',
'Application',
];
Caso não possua os valores 'ZendHydrator', 'DoctrineModule' e 'DoctrineORMModule', os adicione manualmente. Eles são necessários para nossa aplicação de exemplo.
Após finalizada a instalação, verifique se o DoctrineModule está presente e funcionando corretamente.
./vendor/bin/doctrine-module
Se tudo ocorreu bem, é para o comando acima gerar uma saída semelhante à esta:
./vendor/bin/doctrine-module orm:validate-schemaPara corrigir, na pasta config/autoload, crie um arquivo chamado doctrine_orm.local.php. Neste arquivo defina suas configurações do banco de dados, semelhante ao código a seguir.
<?php # config/autoload/doctrine_orm.local.php return [ 'doctrine' => [ 'connection' => [ 'orm_default' => [ 'driverClass' => 'DoctrineDBALDriverPDOMySqlDriver', 'params' => [ 'host' => 'localhost', 'port' => '3306', 'user' => 'root', 'password' => 'root', 'dbname' => 'zf3_blog', 'driverOptions' => [ PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'UTF8'" ] ] ] ] ], ];Rodando novamente o comando anterior, temos o problema da conexão resolvido.
composer require doctrine/migrationsDiferente do DoctrineModule e DoctrineORMModule, o Doctrine Migrations não precisa ser registrado nos módulos da aplicação. Após a instalação, execute o comando abaixo, e se está no mesmo estágio que eu, é para dar um erro.
./vendor/bin/doctrine-moduleO erro diz que a pasta necessária para gerar as migrações não existe, então, crie-a.
mkdir -p data/DoctrineORMModule/MigrationsEm seguida rode o comando ./vendor/bin/doctrine-module novamente. Desta vez vai aparecer em meio às demais opções, as funcionalidades de migração.
./vendor/bin/doctrine-module migrations:generate[caption id="attachment_6492" align="alignnone" width="995"] Resultado da inicialização das migrações[/caption] [caption id="attachment_6493" align="alignnone" width="828"] Primeiro arquivo de migrações[/caption] Já está pronto? Não! Até o dado momento nosso banco de dados não possui nenhuma tabela; [caption id="attachment_6494" align="alignnone" width="331"] Banco de dados ainda sem tabelas[/caption] Agora vamos utilizar nosso segundo comando, o migrate.
./vendor/bin/doctrine-module migrations:migrateAo ser questionado(a) se tem certeza que quer prosseguir, confirme. Note o aviso que é exibido, informando que dados podem ser perdidos. Por este motivo que em produção este comando deve ser utilizado com muita cautela. [caption id="attachment_6495" align="alignnone" width="1253"] Migrations sendo propagada no banco de dados[/caption] Agora o banco de dados já possui uma tabela chamada migrations para que seja adicionada cada uma das migrações executadas. [caption id="attachment_6496" align="alignnone" width="571"] Verficando o banco de dados[/caption] Note que os comandos da imagem anterior formam um passo a passo de algumas verificações. Verificam-se as tabelas, em seguida a estrutura e por último os dados. Recapitulando: Na primeira parte foi instalado o Zend Skeleton, o DoctrineModule, criado o banco de dados e configurada a conexão. Nesta segunda parte, foi instalado o Doctrine Migrations, realizados pequenos ajustes e inicializado migrations em nossa aplicação. Na terceira parte, veremos como é o funcionamento das migrações no dia a dia.
<?php # module/Application/src/Entity/Post.php namespace ApplicationEntity; use DateTime; use DoctrineORMMapping as ORM;Foi dado o apelido de ORM para simplificar a utilização posterior.
/** * Class Post * @package ApplicationEntity * * @ORMTable(name="posts") * @ORMEntity() */ class Post
/** * @ORMId * @ORMColumn(type="integer") * @ORMGeneratedValue(strategy="IDENTITY") * @var int */ private $id; /** * @var string * @ORMColumn(type="string", length=255, nullable=false) */ private $title; /** * @var string * @ORMColumn(type="text", nullable=false) */ private $content; /** * @var DateTime * @ORMColumn(type="datetime", nullable=false) */ private $created; /** * @var DateTime * @ORMColumn(type="datetime", nullable=false) */ private $modified;Perceba como é simples a definição das anotações, os tipos do Doctrine Annotations são muito semelhantes aos do PHP puro.
/** * @return int */ public function getId() { return $this->id; } /** * @param int $id * @return Post */ public function setId($id) { $this->id = $id; return $this; } /** * @return string */ public function getTitle() { return $this->title; } /** * @param string $title * @return Post */ public function setTitle($title) { $this->title = $title; return $this; } /** * @return string */ public function getContent() { return $this->content; } /** * @param string $content * @return Post */ public function setContent($content) { $this->content = $content; return $this; } /** * @return DateTime */ public function getCreated() { return $this->created; } /** * @param DateTime $created * @return Post */ public function setCreated($created) { $this->created = $created; return $this; } /** * @return DateTime */ public function getModified() { return $this->modified; } /** * @param DateTime $modified * @return Post */ public function setModified($modified) { $this->modified = $modified; return $this; }
# nos imports use ZendHydratorClassMethods;
/** * Extrai a entidade para um array * @return array */ public function toArray() { return (new ClassMethods(false))->extract($this); }
/** * Recebe um array e popula a entidade * @param array $data */ public function __construct($data = []) { $this->created = new DateTime(); $this->modified = new DateTime(); if (!empty($data)) { (new ClassMethods(false))->hydrate($data, $this); } }O parâmetro false ao instanciar a classe ClassMethods é necessário porque a lib possui retrocompatibilidade com o ZF1, onde os namespaces eram definidos com underscores ( _ ). Como no ZF2 e ZF3 os namespaces seguem a PSR-0 e PSR4, faz-se necessário indicar que não é pra utilizar o underscore como separador de namespace do ZF1. Um exemplo de população através do Hydrator é o seguinte:
$post = new Post([ 'title' => 'Post Teste', 'content' => 'teste de conteudo' ]); // output print_r($post); ApplicationEntityPost Object ( [id:ApplicationEntityPost:private] => [title:ApplicationEntityPost:private] => Teste [content:ApplicationEntityPost:private] => teste de onteúdo [created:ApplicationEntityPost:private] => DateTime Object ( [date] => 2018-01-18 02:48:13.088643 [timezone_type] => 3 [timezone] => America/Sao_Paulo ) [modified:ApplicationEntityPost:private] => DateTime Object ( [date] => 2018-01-18 02:48:13.088655 [timezone_type] => 3 [timezone] => America/Sao_Paulo ) )
<?php # module/Application/src/Entity/Post.php namespace ApplicationEntity; use DateTime; use DoctrineORMMapping as ORM; use ZendHydratorClassMethods; /** * Class Post * @package ApplicationEntity * * @ORMTable(name="posts") * @ORMEntity() */ class Post { /** * @ORMId * @ORMColumn(type="integer") * @ORMGeneratedValue(strategy="IDENTITY") * @var int */ private $id; /** * @var string * @ORMColumn(type="string", length=255, nullable=false) */ private $title; /** * @var string * @ORMColumn(type="text", nullable=false) */ private $content; /** * @var DateTime * @ORMColumn(type="datetime", nullable=false) */ private $created; /** * @var DateTime * @ORMColumn(type="datetime", nullable=false) */ private $modified; /** * @return int */ public function getId() { return $this->id; } /** * @param int $id * @return Post */ public function setId($id) { $this->id = $id; return $this; } /** * @return string */ public function getTitle() { return $this->title; } /** * @param string $title * @return Post */ public function setTitle($title) { $this->title = $title; return $this; } /** * @return string */ public function getContent() { return $this->content; } /** * @param string $content * @return Post */ public function setContent($content) { $this->content = $content; return $this; } /** * @return DateTime */ public function getCreated() { return $this->created; } /** * @param DateTime $created * @return Post */ public function setCreated($created) { $this->created = $created; return $this; } /** * @return DateTime */ public function getModified() { return $this->modified; } /** * @param DateTime $modified * @return Post */ public function setModified($modified) { $this->modified = $modified; return $this; } /** * Extrai a entidade para um array * @return array */ public function toArray() { return (new ClassMethods(false))->extract($this); } /** * Recebe um array e popula a entidade * @param array $data */ public function __construct($data = []) { $this->created = new DateTime(); $this->modified = new DateTime(); if (!empty($data)) { (new ClassMethods(false))->hydrate($data, $this); } } }[quads id=1] Agora vamos ao que interessa. Já temos tudo configurado para as migrações: inicializamos e no momento temos uma entidade que está mapeada mas não sincronizada com o banco de dados. Para verificar o sincronismo com o banco rodamos o comando ./vendor/bin/doctrine-module orm:v (preguiça né... o correto é orm:validate-schema, mas digitando apenas orm:v e dando enter já funciona). Poxa... diz que está ok, mas o que aconteceu? Afinal de contas já criamos uma entidade, deveria ter acusado que o banco de dados não está em sincronia. Isso ocorreu porque o nossa aplicação, que foi construída com o ZF3, ainda não tem conhecimento de onde estão as entidades. No arquivo module/Application/config/module.config.php adicione o seguinte trecho de código.
# nos imports use DoctrineORMMappingDriverAnnotationDriver; ... 'doctrine' => [ 'driver' => [ __NAMESPACE__ . '_driver' => [ 'class' => AnnotationDriver::class, 'cache' => 'array', // apc... 'paths' => [dirname(__DIR__) . '/src/Entity'] ], 'orm_default' => [ 'drivers' => [ __NAMESPACE__ . 'Entity' => __NAMESPACE__ . '_driver' ] ] ], ],Agora validando novamente nossa estrutura, temos a informação de que o mapeamento está correto (as entidades) mas o banco de dados não está sincronizado. Pronto, agora sim, estamos plenamente aptos a realizar nossa primeira migração no mundo real! [/vc_column_text][us_cta title="Interesse em TDD?" title_size="h3" color="custom" bg_color="#422b72" text_color="#ffffff" btn_link="url:https%3A%2F%2Ftddcomphp.com.br||target:%20_blank|" btn_label="Conheça meu livro" btn_size="20px" btn_style="4"]
./vendor/bin/doctrine-module migrations:diffAgora na pasta data/DoctrineORMModule/Migrations existe mais um arquivo de migração e o seu conteúdo: Vamos aos detalhes. Existem dois métodos, up e down, que servem para realizar uma migração e retornar ao estágio anterior em caso de rollback da migração. Outro ponto que você deve ter reparado é que a primeira coisa que é feita em cada um dos métodos é a verificação do banco de dados. Caso não seja o Mysql a migração é abortada. Tudo pronto? Não!!! O que fizemos até então foi apenas comparar o nosso mapeamento com a situação atual do banco de dados, gerando o resultado desta diferença em um arquivo que poderá ser utilizado posteriormente.
./vendor/bin/doctrine-module migrations:migrateO que aconteceu aqui é que o arquivo recém gerado pelo comando diff foi lido e executado o seu método up, fazendo assim alterações serem realizadas no banco de dados. Caso existissem diversos arquivos ainda não sincronizados, todos estes seriam processados, surtindo suas alterações no banco. E nosso banco de dados, como ficou? Perfeito, agora vou adicionar uma nova propriedade na entidade para criar mais um caso de migração.
/** * @var string * @ORMColumn(type="string", length=500, nullable=true) */ private $featuredImage; /** * @return string */ public function getFeaturedImage() { return $this->featuredImage; } /** * @param string $featuredImage * @return Post */ public function setFeaturedImage($featuredImage) { $this->featuredImage = $featuredImage; return $this; }Novamente rodando o comando diff e em seguida o comando migrate o novo campo é adicionado na tabela posts.
./vendor/bin/doctrine-module migrations:diff ./vendor/bin/doctrine-module migrations:migrate --no-interactionNote que no momento de rodar a migração foi adicionado um novo parâmetro, o "--no-interaction". Ele foi adicionado para não realizar perguntas, simplesmente executar a migração e pronto! No entanto friso: isso é arriscado, somente utilize desta forma se tiver absoluta certeza do que está fazendo, principalmente em produção.
./vendor/bin/doctrine-module migrations:execute YYYYMMDDHHMMSS --downVeja o exemplo: Simples não?!
# composer.json "scripts": { "diff-db": "doctrine-module migrations:diff", "migrate-db": "doctrine-module migrations:migrate --no-interaction" }Aí para criar um arquivo de migração, aquele que é criado quando uma nova entidade é adicionada ou algum atributo de uma entidade existente é alterado, roda-se o comando:
composer diff-dbPara atualizar a estrutura do banco de dados em conformidade com os arquivos de migração ainda não sincronizados, roda-se o comando:
composer migrate-db
# .git/hooks/post-receive cd project/path; ./vendor/bin/doctrine-module migrations:migrate --no-interactionAssim toda vez que vou colocar as alterações em homologação ou produção, a estrutura do banco de dados é automaticamente sincronizada. Somente envio algum dump do banco quando a alteração mexe com relacionamentos e estes acabam se quebrando. Se você chegou até aqui, caramba, parabéns! Como mencionei no início, este é um artigo extenso, por isso o dividi em partes. Agora pra compensar o tempo que você investiu na leitura, vou lhe dar um presente (não fique bravo(a)): Eu fiz um super screencast com exatamente o mesmo conteúdo deste artigo e disponibilizei no youtube. [/vc_column_text][/vc_column][/vc_row]