[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.
Numa situação sem migrations, Marcos entraria em contato com José para entender o que tem que fazer pra corrigir o erro e aguardaria a resposta.
Por estarem trabalhando com ZF3 e o ORM Doctrine , utilizaram desde o começo o Doctrine Migrations para trabalhar em conjunto. Ao perceber o erro informando que falta um campo na tabela equipamentos, Marcos instantaneamente sabe que precisa rodar o migrations. Executando um simples comando a estrutura do seu banco fica semelhante a de José e o erro anterior não mais existe.
Este foi apenas um cenário hipotético, mas muito comum no dia a dia de desenvolvedores trabalhando em equipe. A partir de agora você conhecerá mais detalhes de como o Marcos e o José trabalham com migrations.
[quads id=1]
composer create-project -s dev zendframework/skeleton-application path/to/install
Esta é uma estrutura mínima possível com o Zend Skeleton Application. Perceba que existe uma pasta chamada module e nela um módulo apenas chamado Application. Não criaremos outro módulo neste exemplo, faremos tudo dentro do Application mesmo.
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-schema
Para 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-module
O 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:diff
Agora 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:migrate
O 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-interaction
Note 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]