Initial commit: apps.supersam24.ru

This commit is contained in:
root committed 2026-10-22 00:04:27 +03:00
commit e955e1da36
6566 files changed
+1271095

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
# Basic docker based environment
# Necessary to trick dokku into building the documentation
# using dockerfile instead of herokuish
FROM ubuntu:17.04
# Add basic tools
RUN apt-get update && \
apt-get install -y build-essential \
software-properties-common \
curl \
git \
libxml2 \
libffi-dev \
libssl-dev && \
LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php && \
apt-get update && \
apt-get install -y php7.2-cli php7.2-mbstring php7.2-xml php7.2-zip php7.2-intl php7.2-opcache php7.2-sqlite &&\
apt-get clean &&\
rm -rf /var/lib/apt/lists/*
WORKDIR /code
VOLUME ["/code"]
CMD [ '/bin/bash' ]
+28
View File
@@ -0,0 +1,28 @@
The MIT License
CakePHP(tm) : The Rapid Development PHP Framework (http://cakephp.org)
Copyright (c) 2005-present, Cake Software Foundation, Inc.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
Cake Software Foundation, Inc.
1785 E. Sahara Avenue,
Suite 490-204
Las Vegas, Nevada 89104,
United States of America.
+46
View File
@@ -0,0 +1,46 @@
# Migrations plugin for CakePHP
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.txt)
[![Build Status](https://img.shields.io/travis/cakephp/migrations/master.svg?style=flat-square)](https://travis-ci.org/cakephp/migrations)
[![Coverage Status](https://img.shields.io/codecov/c/github/cakephp/migrations/master.svg?style=flat-square)](https://codecov.io/github/cakephp/migrations?branch=master)
[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/migrations.svg?style=flat-square)](https://packagist.org/packages/cakephp/migrations)
This is a Database Migrations system for CakePHP 3.
The plugin consists of a CakePHP shell wrapper for the [Phinx](http://phinx.org) migrations library.
## Installation
You can install this plugin into your CakePHP application using [Composer](http://getcomposer.org).
Run the following command
```sh
composer require cakephp/migrations
```
## Configuration
You can load the plugin using the shell command:
```
bin/cake plugin load Migrations
```
Or you can manually add the loading statement in the **src/Application.php** file of your application:
```php
public function bootstrap()
{
parent::bootstrap();
$this->addPlugin('Migrations');
}
```
Prior to 3.6.0
```php
Plugin::load('Migrations');
```
Additionally, you will need to configure the ``default`` database configuration in your **config/app.php** file.
## Documentation
Full documentation of the plugin can be found on the [CakePHP Cookbook](https://book.cakephp.org/migrations/2/).
+63
View File
@@ -0,0 +1,63 @@
{
"name": "cakephp/migrations",
"description": "Database Migration plugin for CakePHP based on Phinx",
"type": "cakephp-plugin",
"keywords": ["cakephp", "migrations"],
"homepage": "https://github.com/cakephp/migrations",
"license": "MIT",
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/migrations/graphs/contributors"
}
],
"support": {
"issues": "https://github.com/cakephp/migrations/issues",
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"source": "https://github.com/cakephp/migrations"
},
"require": {
"php": ">=5.6.0",
"robmorgan/phinx": "^0.10.3|^0.11.1",
"cakephp/orm": "^3.6.0",
"cakephp/cache": "^3.6.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7.14|^6.0",
"cakephp/cakephp": "^3.6.0",
"cakephp/bake": "^1.7.0",
"cakephp/cakephp-codesniffer": "^3.0"
},
"autoload": {
"psr-4": {
"Migrations\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Cake\\Test\\": "vendor/cakephp/cakephp/tests/",
"Migrations\\PHPStan\\": "tests/PHPStan/",
"Migrations\\Test\\": "tests/",
"TestApp\\": "tests/test_app/App/",
"TestBlog\\": "tests/test_app/Plugin/TestBlog/src/"
}
},
"suggest": {
"cakephp/bake": "If you want to generate migrations."
},
"minimum-stability": "dev",
"prefer-stable": true,
"scripts": {
"check": [
"@cs-check",
"@test"
],
"cs-check": "phpcs --colors -p src/ tests/",
"cs-fix": "phpcbf --colors -p src/ tests/",
"phpstan": "phpstan analyse src/",
"phpstan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan-shim:^0.11 && mv composer.backup composer.json",
"test": "phpunit",
"test-coverage": "phpunit --coverage-clover=clover.xml"
}
}
+20
View File
@@ -0,0 +1,20 @@
# Generate the HTML output.
FROM markstory/cakephp-docs-builder as builder
COPY docs /data/docs
RUN cd /data/docs-builder && \
# In the future repeat website for each version
make website LANGS="en fr ja pt ru" SOURCE=/data/docs DEST=/data/website/
# Build a small nginx container with just the static site in it.
FROM nginx:1.15-alpine
COPY --from=builder /data/website /data/website
COPY --from=builder /data/docs-builder/nginx.conf /etc/nginx/conf.d/default.conf
# Move each version into place
RUN cp -R /data/website/html/* /usr/share/nginx/html \
&& rm -rf /data/website/
RUN ln -s /usr/share/nginx/html /usr/share/nginx/html/2.x
View File
Whitespace-only changes.
+48
View File
@@ -0,0 +1,48 @@
# Global configuration information used across all the
# translations of documentation.
#
# Import the base theme configuration
from cakephpsphinx.config.all import *
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The full version, including alpha/beta/rc tags.
release = '2.x'
# The search index version.
search_version = 'migrations-2'
# The marketing display name for the book.
version_name = ''
# Project name shown in the black header bar
project = 'CakePHP Migrations'
# Other versions that display in the version picker menu.
version_list = [
{'name': '2.x', 'number': '/migrations/2.x', 'title': '2.x', 'current': True},
]
# Languages available.
languages = ['en', 'fr', 'ja', 'pt', 'ru']
# The GitHub branch name for this version of the docs
# for edit links to point at.
branch = 'master'
# Current version being built
version = '2.x'
# Language in use for this directory.
language = 'en'
show_root_link = True
repository = 'cakephp/migrations'
source_path = 'docs/'
hide_page_contents = ('search', '404', 'contents')
+9
View File
@@ -0,0 +1,9 @@
import sys, os
# Append the top level directory of the docs, so we can import from the config dir.
sys.path.insert(0, os.path.abspath('..'))
# Pull in all the configuration options defined in the global config file..
from config.all import *
language = 'en'
+5
View File
@@ -0,0 +1,5 @@
.. toctree::
:maxdepth: 2
:caption: CakePHP Migrations
/index
File diff suppressed because it is too large. Load diff
+9
View File
@@ -0,0 +1,9 @@
import sys, os
# Append the top level directory of the docs, so we can import from the config dir.
sys.path.insert(0, os.path.abspath('..'))
# Pull in all the configuration options defined in the global config file..
from config.all import *
language = 'fr'
+5
View File
@@ -0,0 +1,5 @@
.. toctree::
:maxdepth: 2
:caption: CakePHP Migrations
/index
File diff suppressed because it is too large. Load diff
+9
View File
@@ -0,0 +1,9 @@
import sys, os
# Append the top level directory of the docs, so we can import from the config dir.
sys.path.insert(0, os.path.abspath('..'))
# Pull in all the configuration options defined in the global config file..
from config.all import *
language = 'ja'
+5
View File
@@ -0,0 +1,5 @@
.. toctree::
:maxdepth: 2
:caption: CakePHP Migrations
/index
File diff suppressed because it is too large. Load diff
+9
View File
@@ -0,0 +1,9 @@
import sys, os
# Append the top level directory of the docs, so we can import from the config dir.
sys.path.insert(0, os.path.abspath('..'))
# Pull in all the configuration options defined in the global config file..
from config.all import *
language = 'pt'
+5
View File
@@ -0,0 +1,5 @@
.. toctree::
:maxdepth: 2
:caption: CakePHP Migrations
/index
+940
View File
@@ -0,0 +1,940 @@
Migrations
##########
Migrations é um plugin suportado pela equipe oficial do CakePHP que ajuda você a
fazer mudanças no **schema** do banco de dados utilizando arquivos PHP,
que podem ser versionados utilizando um sistema de controle de versão.
Ele permite que você atualize suas tabelas ao longo do tempo. Ao invés de
escrever modificações de **schema** via SQL, este plugin permite que você
utilize um conjunto intuitivo de métodos para fazer mudanças no seu banco de
dados.
Esse plugin é um **wrapper** para a biblioteca `Phinx <https://phinx.org/>`_.
Instalação
==========
Por padrão o plugin é instalado junto com o esqueleto da aplicação.
Se você o removeu e quer reinstalá-lo, execute o comando a seguir a partir do
diretório **ROOT** da sua aplicação
(onde o arquivo composer.json está localizado):
.. code-block:: bash
$ php composer.phar require cakephp/migrations "@stable"
# Or if composer is installed globally
$ composer require cakephp/migrations "@stable"
Para usar o plugin você precisa carregá-lo no arquivo **config/bootstrap.php**
da sua aplicação. Você pode usar o
:ref:`shell de plugins do CakePHP <https://book.cakephp.org/3.0/pt/console-and-shells/plugin-shell.html>` para carregar e descarregar
plugins do seu arquivo **config/bootstrap.php**::
$ bin/cake plugin load Migrations
Ou você pode carregar o plugin editando seu arquivo **config/bootstrap.php** e
adicionando a linha::
Plugin::load('Migrations');
Adicionalmente, você precisará configurar o banco de dados padrão da sua
aplicação, no arquivo **config/app.php** como explicado na seção
:ref:`Configuração de banco de dados <https://book.cakephp.org/3.0/pt/orm/database-basics.html#database-configuration>`.
Visão Geral
===========
Uma migração é basicamente um arquivo PHP que descreve as mudanças a
serem feitas no banco de dados. Um arquivo de migração pode criar ou excluir
tabelas, adicionar ou remover colunas, criar índices e até mesmo inserir
dados em seu banco de dados.
Aqui segue um exemplo de migração::
<?php
use Migrations\AbstractMigration;
class CreateProducts extends AbstractMigration
{
/**
* Change Method.
*
* More information on this method is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
* @return void
*/
public function change()
{
$table = $this->table('products');
$table->addColumn('name', 'string', [
'default' => null,
'limit' => 255,
'null' => false,
]);
$table->addColumn('description', 'text', [
'default' => null,
'null' => false,
]);
$table->addColumn('created', 'datetime', [
'default' => null,
'null' => false,
]);
$table->addColumn('modified', 'datetime', [
'default' => null,
'null' => false,
]);
$table->create();
}
}
Essa migração irá adicionar uma tabela chamada ``products`` ao banco de dados
com as seguintes colunas:
- ``id`` coluna do tipo ``integer`` como chave primária
- ``name`` coluna do tipo ``string``
- ``description`` coluna do tipo ``text``
- ``created`` coluna do tipo ``datetime``
- ``modified`` coluna do tipo ``datetime``
.. tip::
A coluna de chave primária ``id`` será adicionada **implicitamente**.
.. note::
Note que este arquivo descreve como o banco de dados deve ser **após** a
aplicação da migração. Neste ponto, a tabela ``products``ainda não existe
no banco de dados, nós apenas criamos um arquivo que é capaz de criar a
tabela ``products`` com seus devidos campos ou excluir a tabela caso uma
operação rollback seja executada.
Com o arquivo criado na pasta **config/MIgrations**, você será capaz de executar
o comando abaixo para executar as migrações no seu banco de dados::
bin/cake migrations migrate
O comando seguinte irá executar um **rollback** na migração e irá excluir a
tabela recém criada::
bin/cake migrations rollback
Criando migrations
==================
Arquivos de migração são armazeados no diretório **config/Migrations** da
sua aplicação. O nome dos arquivos de migração têm como prefixo a data
em que foram criados, no formato **YYYYMMDDHHMMSS_MigrationName.php**. Aqui
estão exemplos de arquivos de migração:
* 20160121163850_CreateProducts.php
* 20160210133047_AddRatingToProducts.php
A maneira mais fácil de criar um arquivo de migrações é usando o
:doc:`/bake/usage` a linha de comando.
Por favor, leia a `documentação do Phinx <https://book.cakephp.org/phinx/0/en/migrations.html>`
a fim de conhecer a lista completa dos métodos que você pode usar para escrever
os arquivos de migração.
.. note::
Ao gerar as migrações através do ``bake`` você ainda pode alterá-las antes
da sua execução, caso seja necessário.
Sintaxe
-------
A sintaxe do ``bake`` para a geração de migrações segue o formato abaixo::
$ bin/cake bake migration CreateProducts name:string description:text created modified
Quando utilizar o ``bake`` para criar as migrações, você normalmente precisará
informar os seguintes dados::
* o nome da migração que você irá gerar (``CreateProducts`` por exemplo)
* as colunas da tabela que serão adicionadas ou removidas na migração
(``name:string description:text created modified`` no nosso caso)
Devido às convenções, nem todas as alterações de schema podem ser realizadas
através destes comandos.
Além disso, você pode criar um arquivo de migração vazio caso deseje ter um
controle total do que precisa ser executado. Para isto, apenas omita a definição
das colunas::
$ bin/cake migrations create MyCustomMigration
Nomenclatura de migrations
~~~~~~~~~~~~~~~~~~~~~~~~~~
A nomenclatura das migrações pode seguir qualquer um dos padrões apresentados a
seguir:
* (``/^(Create)(.*)/``) Cria a tabela especificada.
* (``/^(Drop)(.*)/``) Exclui a tabela especificada.
Ignora campos especificados nos argumentos
* (``/^(Add).*(?:To)(.*)/``) Adiciona campos a
tabela especificada
* (``/^(Remove).*(?:From)(.*)/``) Remove campos de uma
tabela específica
* (``/^(Alter)(.*)/``) Altera a tabela especificada. Um apelido para
um CreateTable seguido de um AlterTable
* (``/^(Alter).*(?:On)(.*)/``) Alterar os campos da tabela especificada
Você também pode usar ``underscore_form`` como nome das suas **migrations**.
Ex.: ``create_products``.
.. versionadded:: cakephp/migrations 1.5.2
A partir da versão 1.5.2 do `plugin migrations <https://github.com/cakephp/migrations/>`_,
o nome dos arquivos de migrações são colocados automaticamente no padrão
**camel case**.
Esta versão do plugin está disponível apenas a partir da versão 3.1 do
CakePHP.
Antes disto, o padrão de nomes do plugin migrations utilizava a nomenclatura
baseada em **underlines**, ex.: ``20160121164955_create_products.php``.
.. warning::
O nome das migrações são usados como nomes de classe, e podem colidir com
outras migrações se o nome das classes não forem únicos. Neste caso, pode
ser necessário sobreescrever manualmente os nomes mais tarde ou simplesmente
mudar os nomes que você está especificando.
Definição de colunas
~~~~~~~~~~~~~~~~~~~~
Quando utilizar colunas na linha de comando, pode ser útil lembrar que eles seguem o
seguinte padrão::
fieldName:fieldType[length]:indexType:indexName
Por exemplo, veja formas válidas de especificar um campo de e-mail:
* ``email:string:unique``
* ``email:string:unique:EMAIL_INDEX``
* ``email:string[120]:unique:EMAIL_INDEX``
O parâmetro ``length`` para o ``fieldType`` é opcional e deve sempre ser
escrito entre colchetes
Os campos ``created`` e ``modified`` serão automaticamente definidos
como ``datetime``.
Os tipos de campos são genericamente disponibilizados pela biblioteca ``Phinx``.
Eles podem ser:
* string
* text
* integer
* biginteger
* float
* decimal
* datetime
* timestamp
* time
* date
* binary
* boolean
* uuid
Há algumas heurísticas para a escolha de tipos de campos que não são especificados
ou são definidos com valor inválido. O tipo de campo padrão é ``string``;
* id: integer
* created, modified, updated: datetime
Criando uma tabela
------------------
Você pode utilizar o ``bake`` para criar uma tabela::
$ bin/cake bake migration CreateProducts name:string description:text created modified
A linha de comando acima irá gerar um arquivo de migração parecido com este::
<?php
use Migrations\AbstractMigration;
class CreateProducts extends AbstractMigration
{
/**
* Change Method.
*
* More information on this method is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
* @return void
*/
public function change()
{
$table = $this->table('products');
$table->addColumn('name', 'string', [
'default' => null,
'limit' => 255,
'null' => false,
]);
$table->addColumn('description', 'text', [
'default' => null,
'null' => false,
]);
$table->addColumn('created', 'datetime', [
'default' => null,
'null' => false,
]);
$table->addColumn('modified', 'datetime', [
'default' => null,
'null' => false,
]);
$table->create();
}
}
Adicionando colunas a uma tabela existente
------------------------------------------
Se o nome da migração na linha de comando estiver na forma "AddXXXToYYY" e
for seguido por uma lista de nomes de colunas e tipos, então o arquivo de
migração com o código para criar as colunas será gerado::
$ bin/cake bake migration AddPriceToProducts price:decimal
A linha de comando acima irá gerar um arquivo com o seguinte conteúdo::
<?php
use Migrations\AbstractMigration;
class AddPriceToProducts extends AbstractMigration
{
public function change()
{
$table = $this->table('products');
$table->addColumn('price', 'decimal')
->update();
}
}
Adicionando uma coluna como indice a uma tabela
-----------------------------------------------
Também é possível adicionar índices a colunas::
$ bin/cake bake migration AddNameIndexToProducts name:string:index
irá gerar::
<?php
use Migrations\AbstractMigration;
class AddNameIndexToProducts extends AbstractMigration
{
public function change()
{
$table = $this->table('products');
$table->addColumn('name', 'string')
->addIndex(['name'])
->update();
}
}
Especificando o tamanho do campo
--------------------------------
.. versionadded:: cakephp/migrations 1.4
Se você precisar especificar o tamanho do campo, você pode fazer isto entre
colchetes logo após o tipo do campo, ex.::
$ bin/cake bake migration AddFullDescriptionToProducts full_description:string[60]
Executar o comando acima irá gerar::
<?php
use Migrations\AbstractMigration;
class AddFullDescriptionToProducts extends AbstractMigration
{
public function change()
{
$table = $this->table('products');
$table->addColumn('full_description', 'string', [
'default' => null,
'limit' => 60,
'null' => false,
])
->update();
}
}
Se o tamanho não for especificado, os seguintes padrões serão utilizados:
* string: 255
* integer: 11
* biginteger: 20
Alterar uma coluna de uma tabela
-----------------------------------
Da mesma maneira, você pode gerar uma migração para alterar uma coluna usando a
linha de comando, se o nome da migração estiver no formato "X""AlterXXXOnYYY":
.. code-block:: bash
bin/cake bake migration AlterPriceOnProducts name:float
Cria o arquivo::
<?php
use Migrations\AbstractMigration;
class AlterPriceFromProducts extends AbstractMigration
{
public function change()
{
$table = $this->table('products');
$table->changeColumn('name', 'float');
$table->update();
}
}
Removendo uma coluna de uma tabela
----------------------------------
Da mesma forma, você pode gerar uma migração para remover uma coluna
utilizando a linha de comando, se o nome da migração estiver na forma
"RemoveXXXFromYYY"::
$ bin/cake bake migration RemovePriceFromProducts price
Cria o arquivo::
<?php
use Migrations\AbstractMigration;
class RemovePriceFromProducts extends AbstractMigration
{
public function change()
{
$table = $this->table('products');
$table->removeColumn('price');
}
}
Gerando migrações a partir de uma base de dados existente
=========================================================
Se você está trabalhando com um banco de dados pré-existente e quer começar
a usar migrações, ou para versionar o schema inicial da base de dados da sua
aplicação, você pode executar o comando ``migration_snapshot``::
$ bin/cake bake migration_snapshot Initial
Isto irá gerar um arquivo de migração chamado **YYYYMMDDHHMMSS_Initial.php**
contendo todas as instruções CREATE para todas as tabelas no seu banco de dados.
Por padrão, o snapshot será criado a partir da conexão ``default`` definida na
configuração.
Se você precisar fazer o bake de um snapshot de uma fonte de dados diferente,
você pode utilizar a opção ``--connection``::
$ bin/cake bake migration_snapshot Initial --connection my_other_connection
Você também pode definir que o snapshot inclua apenas as tabelas para as quais
você tenha definido models correspendentes, utilizando a flag
``require-table``::
$ bin/cake bake migration_snapshot Initial --require-table
Quando utilizar a flag ``--require-table``, o shell irá olhar através das
classes do diretório ``Table`` da sua aplicação e apenas irá adicionar no
snapshot as tabelas lá definidas.
A mesma lógica será aplicada implicitamente se você quiser fazer o bake de um
snapshot para um plugin. Para fazer isso, você precisa usar a opção
``--plugin``, veja a seguir::
$ bin/cake bake migration_snapshot Initial --plugin MyPlugin
Apenas as tabelas que tiverem um objeto ``Table`` definido serão adicionadas
ao snapshot do seu plugin.
.. note::
Quando fizer o bake de um snapshot para um plugin, os arquivos de migrações
serão criados no diretório **config/Migrations** do seu plugin.
Fique atento que quando você faz o bake de um snapshot, ele é automaticamente
adicionado ao log do phinx como migrado.
Gerando um *diff* entre dois estados da base de dados
=====================================================
.. versionadded:: cakephp/migrations 1.6.0
Você pode gerar um arquivo de migração que agrupará todas as diferenças entre
dois estados de uma base de dados usando ``migration_diff``. Para fazê-lo,
você pode usar o seguinte comando::
$ bin/cake bake migration_diff NomeDasMigrações
De forma a ter um ponto de comparação do estado atual da sua base de dados, a
*shell* de ``migrations`` gerará um arquivo de *dump* após cada chamada de
``migrate`` ou ``rollback``. O arquivo de *dump* é um arquivo contendo o
estado completo do esquema da sua base de dados em um determinado instante no
tempo.
Uma vez gerado o arquivo de *dump*, cada modificação que você fizer
diretamente no seu sistema de gerenciamento da base de dados será adicionada
quando você chamar o comando ``bake migration_diff``.
Por padrão, o *diff* será criado através de uma conexão com a base de dados
definida na configuração de conexão ``default``.
Se você precisar criar um *diff* de uma fonte de dados diferente, você pode
usar a opção ``--connection``::
$ bin/cake bake migration_diff NomeDasMigrações --connection minha_outra_conexão
Se você quiser usar a funcionalidade de *diff* em uma aplicação que já possui
um histórico de migrações, você precisará criar manualmente o arquivo de
*dump* a ser usado como base da comparação::
$ bin/cake migrations dump
O estado da base de dados deve ser o mesmo que você teria caso você tivesse
migrado todas as suas migrações antes de criar o arquivo de *dump*.
Uma vez que o arquivo de *dump* for gerado, você pode começar a fazer
modificações na sua base de dados e usar o comando ``bake migration_diff``
sempre que desejar.
.. note::
A *shell* de migrações não é capaz de detectar colunas renomeadas.
Os Comandos
===========
``migrate`` : Aplicando migrações
---------------------------------
Depois de ter gerado ou escrito seu arquivo de migração, você precisa executar
o seguinte comando para aplicar as mudanças a sua base de dados::
# Executa todas as migrações
$ bin/cake migrations migrate
# Execute uma migração específica utilizando a opção ``--target`` ou ``-t``
# O valor é um timestamp que serve como prefixo para cada migração::
$ bin/cake migrations migrate -t 20150103081132
# Por padrão, as migrações ficam no diretório **config/Migrations**. Você
# pode especificar um diretório utilizando a opção ``--source`` ou ``-s``.
# O comando abaixo executa as migrações no diretório **config/Alternate**
$ bin/cake migrations migrate -s Alternate
# Você pode executar as migrações de uma conexão diferente da ``default``
# utilizando a opção ``--connection`` ou ``-c``.
$ bin/cake migrations migrate -c my_custom_connection
# Migrações também podem ser executadas para plugins. Simplesmente utilize
# a opção ``--plugin`` ou ``-p``
$ bin/cake migrations migrate -p MyAwesomePlugin
``rollback`` : Revertendo migrações
-----------------------------------
O comando rollback é utilizado para desfazer migrações realizadas anteriormente
pelo plugin Migrations. É o inverso do comando ``migrate``::
# Você pode desfazer uma migração anterior utilizando o
# comando ``rollback``::
$ bin/cake migrations rollback
# Você também pode passar a versão da migração para voltar
# para uma versão específica::
$ bin/cake migrations rollback -t 20150103081132
Você também pode utilizar as opções ``--source``, ``--connection`` e
``--plugin`` exatamente como no comando ``migrate``.
``status`` : Status da migração
-------------------------------
O comando status exibe uma lista de todas as migrações juntamente com seu
status. Você pode utilizar este comando para ver quais migrações foram
executadas::
$ bin/cake migrations status
Você também pode ver os resultados como JSON utilizando a opção
``--format`` (ou ``-f``)::
$ bin/cake migrations status --format json
Você também pode utilizar as opções ``--source``, ``--connection`` e
``--plugin`` exatamente como no comando ``migrate``.
``mark_migrated`` : Marcando uma migração como migrada
------------------------------------------------------
.. versionadded:: 1.4.0
Algumas vezes pode ser útil marcar uma lista de migrações como migrada sem
efetivamente executá-las.
Para fazer isto, você pode usar o comando ``mark_migrated``. O comando é
bastante semelhante aos outros comandos.
Você pode marcar todas as migrações como migradas utilizando este comando::
$ bin/cake migrations mark_migrated
Você também pode marcar todas as migrações de uma versão específica
utilizando a opção ``--target``::
$ bin/cake migrations mark_migrated --target=20151016204000
Se você não quer marcar a migração alvo como migrada durante o processo, você
pode utilizar a opção ``--exclude``::
$ bin/cake migrations mark_migrated --target=20151016204000 --exclude
Finalmente, se você deseja marcar somente a migração alvo como migrada,
você pode utilizar a opção ``--only``::
$ bin/cake migrations mark_migrated --target=20151016204000 --only
Você também pode utilizar as opções ``--source``, ``--connection`` e
``--plugin`` exatamente como no comando ``migrate``.
.. note::
Quando você criar um snapshot utilizando o bake com o comando
``cake bake migration_snapshot``, a migração criada será automaticamente
marcada como migrada.
.. deprecated:: 1.4.0
A seguinte maneira de utilizar o comando foi depreciada. Use somente se
você estiver utilizando uma versão do plugin inferior a 1.4.0.
Este comando espera um número de versão de migração como argumento::
$ bin/cake migrations mark_migrated
Se você deseja marcar todas as migrações como migradas, você pode utilizar
o valor especial ``all``. Se você o utilizar, ele irá marcar todas as migrações
como migradas::
$ bin/cake migrations mark_migrated all
``seed`` : Populando seu banco de dados
---------------------------------------
A partir da versão 1.5.5, você pode usar a **shell** de ``migrations`` para
popular seu banco de dados. Essa função é oferecida graças ao
`recurso de seed da biblioteca Phinx <https://book.cakephp.org/phinx/0/en/seeding.html>`_.
Por padrão, arquivos **seed** ficarão no diretório ``config/Seeds`` de sua
aplicação. Por favor, tenha certeza de seguir as
`instruções do Phinx para construir seus arquivos de seed <https://book.cakephp.org/phinx/0/en/seeding.html#creating-a-new-seed-class>`_.
Assim como nos **migrations**, uma interface do ```bake`` é oferecida para gerar
arquivos de **seed**::
# This will create a ArticlesSeed.php file in the directory config/Seeds of your application
# By default, the table the seed will try to alter is the "tableized" version of the seed filename
$ bin/cake bake seed Articles
# You specify the name of the table the seed files will alter by using the ``--table`` option
$ bin/cake bake seed Articles --table my_articles_table
# You can specify a plugin to bake into
$ bin/cake bake seed Articles --plugin PluginName
# You can specify an alternative connection when generating a seeder.
$ bin/cake bake seed Articles --connection connection
.. versionadded:: cakephp/migrations 1.6.4
As opções ``--data``, ``--limit`` e ``--fields`` foram adicionadas para
exportar dados da sua base de dados.
A partir da versão 16.4, o comando ``bake seed`` permite que você crie um
arquivo de *seed* com dados exportados da sua base de dados com o uso da
*flag* ``--data``::
$ bin/cake bake seed --data Articles
Por padrão, esse comando exportará todas as linhas encontradas na sua
tabela. Você pode limitar o número de linhas a exportar usando a opção
``--limit``::
# Exportará apenas as 10 primeiras linhas encontradas
$ bin/cake bake seed --data --limit 10 Articles
Se você deseja incluir apenas uma seleção dos campos da tabela no seu
arquivo de *seed*, você pode usar a opção ``--fields``. Ela recebe a
lista de campos a incluir na forma de uma *string* separada por
vírgulas::
# Exportará apenas os campos `id`, `title` e `excerpt`
$ bin/cake bake seed --data --fields id,title,excerpt Articles
.. tip::
Você pode utilizar ambas as opções ``--limit`` e ``--fields``
simultaneamente em uma mesma chamada.
Para popular seu banco de dados, você pode usar o subcomando ``seed``::
# Without parameters, the seed subcommand will run all available seeders
# in the target directory, in alphabetical order.
$ bin/cake migrations seed
# You can specify only one seeder to be run using the `--seed` option
$ bin/cake migrations seed --seed ArticlesSeed
# You can run seeders from an alternative directory
$ bin/cake migrations seed --source AlternativeSeeds
# You can run seeders from a plugin
$ bin/cake migrations seed --plugin PluginName
# You can run seeders from a specific connection
$ bin/cake migrations seed --connection connection
Esteja ciente que, ao oposto das **migrations**, **seeders** não são
versionados, o que significa que o mesmo **seeder** pode ser aplicado diversas
vezes.
Usando migrations em plugins
============================
**Plugins** também podem oferecer **migrations**. Isso faz com que **plugins**
que são planejados para serem distribuídos tornem-se muito mais práticos e
fáceis de instalar. Todos os comandos do plugin **Migrations** suportam a opção
``--plugin`` ou ``-p``, que por sua vez vai delegar a execução da tarefa ao
escopo relativo a um determinado **plugin**::
$ bin/cake migrations status -p PluginName
$ bin/cake migrations migrate -p PluginName
Executando migrations em ambientes fora da linha de comando
===========================================================
.. versionadded:: cakephp/migrations 1.2.0
Desde o lançamento da versão 1.2 do plugin, você pode executar **migrations**
fora da linha de comando, diretamente de uma aplicação, ao usar a nova classe
``Migrations``. Isso pode ser muito útil caso você esteja desenvolvendo um
instalador de **plugins** para um CMS, para exemplificar.
A classe ``Migrations`` permite que você execute os seguintes comandos
disponíveis na **shell**:
* migrate
* rollback
* markMigrated
* status
* seed
Cada um desses comandos tem um método definido na classe ``Migrations``.
Veja como usá-la::
use Migrations\Migrations;
$migrations = new Migrations();
// Retornará um array de todos migrations e seus status
$status = $migrations->status();
// Retornará true se bem sucedido. Se um erro ocorrer, uma exceção será lançada
$migrate = $migrations->migrate();
// Retornará true se bem sucedido. Se um erro ocorrer, uma exceção será lançada
$rollback = $migrations->rollback();
// Retornará true se bem sucedido. Se um erro ocorrer, uma exceção será lançada
$markMigrated = $migrations->markMigrated(20150804222900);
// Retornará true se bem sucedido. Se um erro ocorrer, uma exceção será lançada
$seeded = $migrations->seed();
Os métodos aceitam um **array** de parâmetros que devem combinar com as opções
dos comandos::
use Migrations\Migrations;
$migrations = new Migrations();
// Retornará um array de todos migrations e seus status
$status = $migrations->status(['connection' => 'custom', 'source' => 'MyMigrationsFolder']);
Você pode passar qualquer opção que esteja disponível pelos comandos **shell**.
A única exceção é o comando ``markMigrated`` que espera um número de versão a
ser marcado como migrado, como primeiro argumento. Passe o **array** de
parâmetros como segundo argumento nesse caso.
Opcionalmente, você pode passar esses parâmetros pelo construtor da classe.
Eles serão usados como padrão evitando que você tenha que passá-los em cada
chamada do método::
use Migrations\Migrations;
$migrations = new Migrations(['connection' => 'custom', 'source' => 'MyMigrationsFolder']);
// Todas as chamadas de métodos serão executadas usando os parâmetros passados pelo construtor da classe
$status = $migrations->status();
$migrate = $migrations->migrate();
Se você precisar sobrescrever um ou mais parâmetros definidos previamente, você
pode passá-los para um método::
use Migrations\Migrations;
$migrations = new Migrations(['connection' => 'custom', 'source' => 'MyMigrationsFolder']);
// Essa chamada será feita com a conexão "custom"
$status = $migrations->status();
// Essa chamada será feita com a conexão "default"
$migrate = $migrations->migrate(['connection' => 'default']);
Dicas e truques
===============
Criando chaves primárias customizadas
-------------------------------------
Se você precisa evitar a criação automática da chave primária ``id`` ao
adicioanr novas tabelas ao banco de dados, é possível usar o segundo argumento
do método ``table()``::
<?php
use Migrations\AbstractMigration;
class CreateProductsTable extends AbstractMigration
{
public function change()
{
$table = $this->table('products', ['id' => false, 'primary_key' => ['id']]);
$table
->addColumn('id', 'uuid')
->addColumn('name', 'string')
->addColumn('description', 'text')
->create();
}
}
O código acima vai criar uma coluna ``CHAR(36)`` ``id`` que também é a chave
primária.
.. note::
Ao especificar chaves primárias customizadas pela linha de comando, você
deve apontá-las como chave primária no campo id, caso contrário você pode
receber um erro apontando campos diplicados, i.e.::
$ bin/cake bake migration CreateProducts id:uuid:primary name:string description:text created modified
Adicionalmente, desde a versão 1.3, uma novo meio de lidar com chaves primárias
foi introduzido. Para tal, sua classe de migração deve estender a nova classe
``Migrations\AbstractMigration``.
Você pode especificar uma propriedade ``autoId`` na sua classe e defini-la como
``false``, o quê desabilitará a geração automática da coluna ``id``. Você
vai precisar criar manualmente a coluna que será usada como chave primária e
adicioná-la à declaração da tabela::
<?php
use Migrations\AbstractMigration;
class CreateProductsTable extends AbstractMigration
{
public $autoId = false;
public function up()
{
$table = $this->table('products');
$table
->addColumn('id', 'integer', [
'autoIncrement' => true,
'limit' => 11
])
->addPrimaryKey('id')
->addColumn('name', 'string')
->addColumn('description', 'text')
->create();
}
}
Comparado ao método apresentado anteriormente de lidar com chaves primárias,
nesse método, temos a possibilidade de ter maior controle sobre as definições
da coluna da chave primária:
unsigned, limit, comentários, etc.
Todas as migrations e snapshots criadas pelo bake vão usar essa nova forma
quando necessário.
.. warning::
Lidar com chaves primárias só é possível no momento de criação de tabelas.
Isso é devido a algumas limitações de alguns servidores de banco de dados
que o plugin suporta.
Colações
--------
Se você precisar criar uma tabela com colação diferente do padrão do banco de
dados, você pode defini-la pelo método ``table()``, como uma opção::
<?php
use Migrations\AbstractMigration;
class CreateCategoriesTable extends AbstractMigration
{
public function change()
{
$table = $this
->table('categories', [
'collation' => 'latin1_german1_ci'
])
->addColumn('title', 'string', [
'default' => null,
'limit' => 255,
'null' => false,
])
->create();
}
}
Note que isso só pode ser feito na criação da tabela : não há atualmente uma
forma de adicionar uma coluna a uma tabela existente com uma colação diferente
do padrão da tabela, ou mesmo do banco de dados.
Apenas ``MySQL`` e ``SqlServer`` suportam essa chave de configuração.
Atualizando nome de colunas e usando objetos de tabela
------------------------------------------------------
Se você usa um objeto ORM Table do CakePHP para manipular valores do seu banco
de dados, renomeando ou removendo uma coluna, certifique-se de criar uma nova
instância do seu objeto depois da chamada do ``update()``. O registro do objeto
é limpo depois da chamada do ``update()`` para atualizar o **schema** que é
refletido e armazenado no objeto ``Table`` paralelo à instanciação.
Migrations e Deployment
-----------------------
Se você usa o plugin ao fazer o **deploy** de sua aplicação, garanta que o cache
ORM seja limpo para renovar os metadados das colunas de suas tabelas.
Caso contrário, você pode acabar recebendo erros relativos a colunas
inexistentes ao criar operações nessas mesmas colunas.
O **core** do CakePHP possui uma :doc:`Schema Cache Shell <https://book.cakephp.org/3.0/en/console-and-shells/schema-cache.html>`
que você pode usar para realizar essas operação::
$ bin/cake schema_cache clear
Leia a seção :doc:`Schema Cache Shell <https://book.cakephp.org/3.0/en/console-and-shells/schema-cache.html>` do cookbook
se você quiser conhecer mais sobre essa **shell**.
+9
View File
@@ -0,0 +1,9 @@
import sys, os
# Append the top level directory of the docs, so we can import from the config dir.
sys.path.insert(0, os.path.abspath('..'))
# Pull in all the configuration options defined in the global config file..
from config.all import *
language = 'ru'
+5
View File
@@ -0,0 +1,5 @@
.. toctree::
:maxdepth: 2
:caption: CakePHP Migrations
/index
File diff suppressed because it is too large. Load diff
+44
View File
@@ -0,0 +1,44 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations;
use Phinx\Migration\AbstractMigration as BaseAbstractMigration;
class AbstractMigration extends BaseAbstractMigration
{
/**
* Whether the tables created in this migration
* should auto-create an `id` field or not
*
* This option is global for all tables created in the migration file.
* If you set it to false, you have to manually add the primary keys for your
* tables using the Migrations\Table::addPrimaryKey() method
*
* @var bool
*/
public $autoId = true;
/**
* {@inheritdoc}
*
* @return \Migrations\Table
*/
public function table($tableName, $options = [])
{
if ($this->autoId === false) {
$options['id'] = false;
}
return new Table($tableName, $options, $this->getAdapter());
}
}
+124
View File
@@ -0,0 +1,124 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations;
use Migrations\Command\Seed;
use Phinx\Migration\Manager;
use Phinx\Seed\AbstractSeed as BaseAbstractSeed;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputInterface;
/**
* Class AbstractSeed
* Extends Phinx base AbstractSeed class in order to extend the features the seed class
* offers.
*/
abstract class AbstractSeed extends BaseAbstractSeed
{
/**
* InputInterface this Seed class is being used with.
*
* @var \Symfony\Component\Console\Input\InputInterface
*/
protected $input;
/**
* Gives the ability to a seeder to call another seeder.
* This is particularly useful if you need to run the seeders of your applications in a specific sequences,
* for instance to respect foreign key constraints.
*
* @param string $seeder Name of the seeder to call from the current seed
* @return void
*/
public function call($seeder)
{
$this->getOutput()->writeln('');
$this->getOutput()->writeln(
' ====' .
' <info>' . $seeder . ':</info>' .
' <comment>seeding</comment>'
);
$start = microtime(true);
$this->runCall($seeder);
$end = microtime(true);
$this->getOutput()->writeln(
' ====' .
' <info>' . $seeder . ':</info>' .
' <comment>seeded' .
' ' . sprintf('%.4fs', $end - $start) . '</comment>'
);
$this->getOutput()->writeln('');
}
/**
* Calls another seeder from this seeder.
* It will load the Seed class you are calling and run it.
*
* @param string $seeder Name of the seeder to call from the current seed
* @return void
*/
protected function runCall($seeder)
{
list($pluginName, $seeder) = pluginSplit($seeder);
$argv = [
'seed',
'--seed',
$seeder,
];
$plugin = $pluginName ?: $this->input->getOption('plugin');
if ($plugin !== null) {
$argv[] = '--plugin';
$argv[] = $plugin;
}
$connection = $this->input->getOption('connection');
if ($connection !== null) {
$argv[] = '--connection';
$argv[] = $connection;
}
$source = $this->input->getOption('source');
if ($source !== null) {
$argv[] = '--source';
$argv[] = $source;
}
$seedCommand = new Seed();
$input = new ArgvInput($argv, $seedCommand->getDefinition());
$seedCommand->setInput($input);
$config = $seedCommand->getConfig();
$seedPaths = $config->getSeedPaths();
require_once(array_pop($seedPaths) . DS . $seeder . '.php');
$seeder = new $seeder();
$seeder->setOutput($this->getOutput());
$seeder->setAdapter($this->getAdapter());
$seeder->setInput($this->input);
$seeder->run();
}
/**
* Sets the InputInterface this Seed class is being used with.
*
* @param InputInterface $input Input object.
* @return void
*/
public function setInput(InputInterface $input)
{
$this->input = $input;
}
}
+100
View File
@@ -0,0 +1,100 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations;
use Cake\Database\Connection;
use Cake\Database\Driver\Postgres;
use PDO;
use Phinx\Db\Adapter\AdapterInterface;
use Phinx\Db\Adapter\AdapterWrapper;
use Phinx\Db\Table as PhinxTable;
use Phinx\Db\Table\Column;
use Phinx\Db\Table\ForeignKey;
use Phinx\Db\Table\Index;
use Phinx\Migration\MigrationInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Decorates an AdapterInterface in order to proxy some method to the actual
* connection object.
*/
class CakeAdapter extends AdapterWrapper
{
/**
* Database connection
*
* @var \Cake\Database\Connection
*/
protected $connection;
/**
* Constructor
*
* @param \Phinx\Db\Adapter\AdapterInterface $adapter The original adapter to decorate.
* @param \Cake\Database\Connection|null $connection The connection to actually use.
*/
public function __construct(AdapterInterface $adapter, Connection $connection = null)
{
if ($connection === null) {
throw new \InvalidArgumentException("The cake connection cannot be null");
}
parent::__construct($adapter);
$this->connection = $connection;
$pdo = $adapter->getConnection();
if ($pdo->getAttribute(PDO::ATTR_ERRMODE) !== PDO::ERRMODE_EXCEPTION) {
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
$connection->cacheMetadata(false);
if ($connection->getDriver() instanceof Postgres) {
$config = $connection->config();
$schema = empty($config['schema']) ? 'public' : $config['schema'];
$pdo->exec('SET search_path TO ' . $schema);
}
$connection->getDriver()->setConnection($pdo);
}
/**
* Gets the CakePHP Connection object.
*
* @return \Cake\Database\Connection
*/
public function getCakeConnection()
{
return $this->connection;
}
/**
* Returns a new Query object
*
* @return \Cake\Database\Query
*/
public function getQueryBuilder()
{
return $this->getCakeConnection()->newQuery();
}
/**
* Returns the adapter type name, for example mysql
*
* @return string
*/
public function getAdapterType()
{
return $this->getAdapter()->getAdapterType();
}
}
+370
View File
@@ -0,0 +1,370 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations;
use Phinx\Migration\Manager;
use Symfony\Component\Console\Input\InputInterface;
/**
* Overrides Phinx Manager class in order to provide an interface
* for running migrations within an app
*/
class CakeManager extends Manager
{
public $maxNameLength = 0;
/**
* Instance of InputInterface the Manager is dealing with for the current shell call
*
* @var \Symfony\Component\Console\Input\InputInterface
*/
protected $input;
/**
* Reset the migrations stored in the object
*
* @return void
*/
public function resetMigrations()
{
$this->migrations = null;
}
/**
* Reset the seeds stored in the object
*
* @return void
*/
public function resetSeeds()
{
$this->seeds = null;
}
/**
* Prints the specified environment's migration status.
*
* @param string $environment Environment name.
* @param null|string $format Format (`json` or `array`).
* @return array|string Array of migrations or json string.
*/
public function printStatus($environment, $format = null)
{
$migrations = [];
$isJson = $format === 'json';
if (count($this->getMigrations('default'))) {
$env = $this->getEnvironment($environment);
$versions = $env->getVersionLog();
$this->maxNameLength = $versions ? max(array_map(function ($version) {
return strlen($version['migration_name']);
}, $versions)) : 0;
foreach ($this->getMigrations('default') as $migration) {
if (array_key_exists($migration->getVersion(), $versions)) {
$status = 'up';
unset($versions[$migration->getVersion()]);
} else {
$status = 'down';
}
$version = $migration->getVersion();
$migrationParams = [
'status' => $status,
'id' => $migration->getVersion(),
'name' => $migration->getName(),
];
$migrations[$version] = $migrationParams;
}
foreach ($versions as $missing) {
$version = $missing['version'];
$migrationParams = [
'status' => 'up',
'id' => $version,
'name' => $missing['migration_name'],
];
if (!$isJson) {
$migrationParams = array_merge($migrationParams, [
'missing' => true,
]);
}
$migrations[$version] = $migrationParams;
}
}
ksort($migrations);
$migrations = array_values($migrations);
if ($isJson) {
$migrations = json_encode($migrations);
}
return $migrations;
}
/**
* {@inheritdoc}
*/
public function migrateToDateTime($environment, \DateTime $dateTime, $fake = false)
{
$versions = array_keys($this->getMigrations('default'));
$dateString = $dateTime->format('Ymdhis');
$versionToMigrate = null;
foreach ($versions as $version) {
if ($dateString > $version) {
$versionToMigrate = $version;
}
}
if ($versionToMigrate === null) {
$this->getOutput()->writeln(
'No migrations to run'
);
return;
}
$this->getOutput()->writeln(
'Migrating to version ' . $versionToMigrate
);
$this->migrate($environment, $versionToMigrate, $fake);
}
/**
* {@inheritdoc}
*/
public function rollbackToDateTime($environment, \DateTime $dateTime, $force = false)
{
$env = $this->getEnvironment($environment);
$versions = $env->getVersions();
$dateString = $dateTime->format('Ymdhis');
sort($versions);
$versions = array_reverse($versions);
if (empty($versions) || $dateString > $versions[0]) {
$this->getOutput()->writeln('No migrations to rollback');
return;
}
if ($dateString < end($versions)) {
$this->getOutput()->writeln('Rolling back all migrations');
$this->rollback($environment, 0);
return;
}
$index = 0;
foreach ($versions as $index => $version) {
if ($dateString > $version) {
break;
}
}
$versionToRollback = $versions[$index];
$this->getOutput()->writeln('Rolling back to version ' . $versionToRollback);
$this->rollback($environment, $versionToRollback, $force);
}
/**
* Checks if the migration with version number $version as already been mark migrated
*
* @param int|string $version Version number of the migration to check
* @return bool
*/
public function isMigrated($version)
{
$adapter = $this->getEnvironment('default')->getAdapter();
$versions = array_flip($adapter->getVersions());
return isset($versions[$version]);
}
/**
* Marks migration with version number $version migrated
*
* @param int|string $version Version number of the migration to check
* @param string $path Path where the migration file is located
* @return bool True if success
*/
public function markMigrated($version, $path)
{
$adapter = $this->getEnvironment('default')->getAdapter();
$migrationFile = glob($path . DS . $version . '*');
if (empty($migrationFile)) {
throw new \RuntimeException(
sprintf('A migration file matching version number `%s` could not be found', $version)
);
}
$migrationFile = $migrationFile[0];
$className = $this->getMigrationClassName($migrationFile);
require_once $migrationFile;
$Migration = new $className('default', $version);
$time = date('Y-m-d H:i:s', time());
$adapter->migrated($Migration, 'up', $time, $time);
return true;
}
/**
* Decides which versions it should mark as migrated
*
* @param \Symfony\Component\Console\Input\InputInterface $input Input interface from which argument and options
* will be extracted to determine which versions to be marked as migrated
* @return array Array of versions that should be marked as migrated
* @throws \InvalidArgumentException If the `--exclude` or `--only` options are used without `--target`
* or version not found
*/
public function getVersionsToMark($input)
{
$migrations = $this->getMigrations('default');
$versions = array_keys($migrations);
$versionArg = $input->getArgument('version');
$targetArg = $input->getOption('target');
$hasAllVersion = in_array($versionArg, ['all', '*']);
if ((empty($versionArg) && empty($targetArg)) || $hasAllVersion) {
return $versions;
}
$version = $targetArg ?: $versionArg;
if ($input->getOption('only') || !empty($versionArg)) {
if (!in_array($version, $versions)) {
throw new \InvalidArgumentException("Migration `$version` was not found !");
}
return [$version];
}
$lengthIncrease = $input->getOption('exclude') ? 0 : 1;
$index = array_search($version, $versions);
if ($index === false) {
throw new \InvalidArgumentException("Migration `$version` was not found !");
}
return array_slice($versions, 0, $index + $lengthIncrease);
}
/**
* Mark all migrations in $versions array found in $path as migrated
*
* It will start a transaction and rollback in case one of the operation raises an exception
*
* @param string $path Path where to look for migrations
* @param array $versions Versions which should be marked
* @param \Symfony\Component\Console\Output\OutputInterface $output OutputInterface used to store
* the command output
* @return void
*/
public function markVersionsAsMigrated($path, $versions, $output)
{
$adapter = $this->getEnvironment('default')->getAdapter();
if (empty($versions)) {
$output->writeln('<info>No migrations were found. Nothing to mark as migrated.</info>');
return;
}
$adapter->beginTransaction();
foreach ($versions as $version) {
if ($this->isMigrated($version)) {
$output->writeln(sprintf('<info>Skipping migration `%s` (already migrated).</info>', $version));
continue;
}
try {
$this->markMigrated($version, $path);
$output->writeln(
sprintf('<info>Migration `%s` successfully marked migrated !</info>', $version)
);
} catch (\Exception $e) {
$adapter->rollbackTransaction();
$output->writeln(
sprintf(
'<error>An error occurred while marking migration `%s` as migrated : %s</error>',
$version,
$e->getMessage()
)
);
$output->writeln('<error>All marked migrations during this process were unmarked.</error>');
return;
}
}
$adapter->commitTransaction();
}
/**
* Resolves a migration class name based on $path
*
* @param string $path Path to the migration file of which we want the class name
* @return string Migration class name
*/
protected function getMigrationClassName($path)
{
$class = preg_replace('/^[0-9]+_/', '', basename($path));
$class = str_replace('_', ' ', $class);
$class = ucwords($class);
$class = str_replace(' ', '', $class);
if (strpos($class, '.') !== false) {
$class = substr($class, 0, strpos($class, '.'));
}
return $class;
}
/**
* Sets the InputInterface the Manager is dealing with for the current shell call
*
* @param \Symfony\Component\Console\Input\InputInterface $input Instance of InputInterface
* @return void
*/
public function setInput(InputInterface $input)
{
$this->input = $input;
}
/**
* Overload the basic behavior to add an instance of the InputInterface the shell call is
* using in order to gives the ability to the AbstractSeed::call() method to propagate options
* to the other MigrationsDispatcher it is generating.
*
* {@inheritdoc}
*/
public function getSeeds()
{
parent::getSeeds();
if (empty($this->seeds)) {
return $this->seeds;
}
foreach ($this->seeds as $class => $instance) {
if ($instance instanceof AbstractSeed) {
$instance->setInput($this->input);
}
}
return $this->seeds;
}
}
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace Migrations\Command;
use Symfony\Component\Console\Command\Command;
abstract class BaseCommand extends Command
{
const CODE_SUCCESS = 0;
const CODE_ERROR = 1;
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace Migrations\Command;
use Migrations\Util\SchemaTrait;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class CacheBuild extends BaseCommand
{
use SchemaTrait;
/**
* {@inheritDoc}
*/
protected function configure()
{
$this
->setName('orm-cache-build')
->setDescription('Build all metadata caches for the connection. If a table name is provided, only that table will be cached.')
->addOption(
'connection',
null,
InputOption::VALUE_OPTIONAL,
'The connection to build/clear metadata cache data for.',
'default'
)
->addArgument(
'name',
InputArgument::OPTIONAL,
'A specific table you want to clear/refresh cached data for.'
);
}
/**
* {@inheritDoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$name = $input->getArgument('name');
$schema = $this->_getSchema($input, $output);
if (!$schema) {
return static::CODE_ERROR;
}
$tables = [$name];
if (empty($name)) {
$tables = $schema->listTables();
}
foreach ($tables as $table) {
$output->writeln('Building metadata cache for ' . $table);
$schema->describe($table, ['forceRefresh' => true]);
}
$output->writeln('<info>Cache build complete</info>');
return static::CODE_SUCCESS;
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace Migrations\Command;
use Cake\Cache\Cache;
use Migrations\Util\SchemaTrait;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class CacheClear extends BaseCommand
{
use SchemaTrait;
/**
* {@inheritDoc}
*/
protected function configure()
{
$this
->setName('orm-cache-clear')
->setDescription('Clear all metadata caches for the connection. If a table name is provided, only that table will be removed.')
->addOption(
'connection',
null,
InputOption::VALUE_OPTIONAL,
'The connection to build/clear metadata cache data for.',
'default'
)
->addArgument(
'name',
InputArgument::OPTIONAL,
'A specific table you want to clear/refresh cached data for.'
);
}
/**
* {@inheritDoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$schema = $this->_getSchema($input, $output);
$name = $input->getArgument('name');
if (!$schema) {
return static::CODE_ERROR;
}
$tables = [$name];
if (empty($name)) {
$tables = $schema->listTables();
}
$configName = $schema->getCacheMetadata();
foreach ($tables as $table) {
$output->writeln(sprintf(
'Clearing metadata cache from "%s" for %s',
$configName,
$table
));
$key = $schema->cacheKey($table);
Cache::delete($key, $configName);
}
$output->writeln('<info>Cache clear complete<info>');
return static::CODE_SUCCESS;
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Command;
use Cake\Datasource\ConnectionManager;
use Migrations\CakeAdapter;
use Migrations\CakeManager;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
trait CommandTrait
{
/**
* Overrides the action execute method in order to vanish the idea of environments
* from phinx. CakePHP does not believe in the idea of having in-app environments
*
* @param \Symfony\Component\Console\Input\InputInterface $input the input object
* @param \Symfony\Component\Console\Output\OutputInterface $output the output object
* @return int|null
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->beforeExecute($input, $output);
return parent::execute($input, $output);
}
/**
* Overrides the action execute method in order to vanish the idea of environments
* from phinx. CakePHP does not believe in the idea of having in-app environments
*
* @param \Symfony\Component\Console\Input\InputInterface $input the input object
* @param \Symfony\Component\Console\Output\OutputInterface $output the output object
* @return void
*/
protected function beforeExecute(InputInterface $input, OutputInterface $output)
{
$this->setInput($input);
$this->addOption('--environment', '-e', InputArgument::OPTIONAL);
$input->setOption('environment', 'default');
}
/**
* A callback method that is used to inject the PDO object created from phinx into
* the CakePHP connection. This is needed in case the user decides to use tables
* from the ORM and executes queries.
*
* @param \Symfony\Component\Console\Input\InputInterface $input the input object
* @param \Symfony\Component\Console\Output\OutputInterface $output the output object
* @return void
*/
public function bootstrap(InputInterface $input, OutputInterface $output)
{
parent::bootstrap($input, $output);
$name = $this->getConnectionName($input);
$this->connection = $name;
ConnectionManager::alias($name, 'default');
$connection = ConnectionManager::get($name);
$manager = $this->getManager();
if (!$manager instanceof CakeManager) {
$this->setManager(new CakeManager($this->getConfig(), $input, $output));
}
$env = $this->getManager()->getEnvironment('default');
$adapter = $env->getAdapter();
if (!$adapter instanceof CakeAdapter) {
$env->setAdapter(new CakeAdapter($adapter, $connection));
}
}
/**
* Sets the input object that should be used for the command class. This object
* is used to inspect the extra options that are needed for CakePHP apps.
*
* @param \Symfony\Component\Console\Input\InputInterface $input the input object
* @return void
*/
public function setInput(InputInterface $input)
{
$this->input = $input;
}
}
+99
View File
@@ -0,0 +1,99 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Command;
use Cake\Utility\Inflector;
use Migrations\ConfigurationTrait;
use Phinx\Console\Command\Create as CreateCommand;
use Phinx\Util\Util;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class Create extends CreateCommand
{
use CommandTrait {
execute as parentExecute;
}
use ConfigurationTrait;
/**
* {@inheritdoc}
*/
protected function configure()
{
$this->setName('create')
->setDescription('Create a new migration')
->addArgument('name', InputArgument::REQUIRED, 'What is the name of the migration?')
->setHelp(sprintf(
'%sCreates a new database migration file%s',
PHP_EOL,
PHP_EOL
))
->addOption('plugin', 'p', InputOption::VALUE_REQUIRED, 'The plugin the file should be created for')
->addOption('connection', 'c', InputOption::VALUE_REQUIRED, 'The datasource connection to use')
->addOption('source', 's', InputOption::VALUE_REQUIRED, 'The folder where migrations are in')
->addOption('template', 't', InputOption::VALUE_REQUIRED, 'Use an alternative template')
->addOption(
'class',
'l',
InputOption::VALUE_REQUIRED,
'Use a class implementing "' . parent::CREATION_INTERFACE . '" to generate the template'
)
->addOption(
'path',
null,
InputOption::VALUE_REQUIRED,
'Specify the path in which to create this migration'
);
}
/**
* Overrides the action execute method in order to vanish the idea of environments
* from phinx. CakePHP does not believe in the idea of having in-app environments
*
* @param \Symfony\Component\Console\Input\InputInterface $input the input object
* @param \Symfony\Component\Console\Output\OutputInterface $output the output object
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$result = $this->parentExecute($input, $output);
$output->writeln('<info>renaming file in CamelCase to follow CakePHP convention...</info>');
$migrationPaths = $this->getConfig()->getMigrationPaths();
$migrationPath = array_pop($migrationPaths) . DS;
$name = $input->getArgument('name');
list($phinxTimestamp, $phinxName) = explode('_', Util::mapClassNameToFileName($name), 2);
$migrationFilename = glob($migrationPath . '*' . $phinxName);
if (empty($migrationFilename)) {
$output->writeln(sprintf('<info>An error occurred while renaming file</info>'));
} else {
$migrationFilename = $migrationFilename[0];
$path = dirname($migrationFilename) . DS;
$name = Inflector::camelize($name);
$newPath = $path . Util::getCurrentTimestamp() . '_' . $name . '.php';
$output->writeln('<info>renaming file in CamelCase to follow CakePHP convention...</info>');
if (rename($migrationFilename, $newPath)) {
$output->writeln(sprintf('<info>File successfully renamed to %s</info>', $newPath));
} else {
$output->writeln(sprintf('<info>An error occurred while renaming file to %s</info>', $newPath));
}
}
return $result !== null ? $result : BaseCommand::CODE_SUCCESS;
}
}
+117
View File
@@ -0,0 +1,117 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Command;
use Cake\Datasource\ConnectionManager;
use Migrations\ConfigurationTrait;
use Migrations\TableFinderTrait;
use Phinx\Console\Command\AbstractCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Dump command class.
* A "dump" is a snapshot of a database at a given point in time. It is stored in a
* .lock file in the same folder as migrations files.
*/
class Dump extends AbstractCommand
{
use CommandTrait;
use ConfigurationTrait;
use TableFinderTrait;
/**
* Output object.
*
* @var \Symfony\Component\Console\Output\OutputInterface
*/
protected $output;
/**
* {@inheritdoc}
*/
protected function configure()
{
$this->setName('dump')
->setDescription('Dumps the current schema of the database to be used while baking a diff')
->setHelp(sprintf(
'%sDumps the current schema of the database to be used while baking a diff%s',
PHP_EOL,
PHP_EOL
))
->addOption('plugin', 'p', InputOption::VALUE_REQUIRED, 'The plugin the file should be created for')
->addOption('connection', 'c', InputOption::VALUE_REQUIRED, 'The datasource connection to use')
->addOption('source', 's', InputOption::VALUE_REQUIRED, 'The folder where migrations are in');
}
/**
* @param \Symfony\Component\Console\Output\OutputInterface $output The output object.
* @return mixed
*/
public function output(OutputInterface $output = null)
{
if ($output !== null) {
$this->output = $output;
}
return $this->output;
}
/**
* Dumps the current schema to be used when baking a diff
*
* @param \Symfony\Component\Console\Input\InputInterface $input the input object
* @param \Symfony\Component\Console\Output\OutputInterface $output the output object
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->setInput($input);
$this->bootstrap($input, $output);
$this->output($output);
$path = $this->getOperationsPath($input);
$connectionName = $input->getOption('connection') ?: 'default';
$connection = ConnectionManager::get($connectionName);
$collection = $connection->getSchemaCollection();
$options = [
'require-table' => false,
'plugin' => $this->getPlugin($input),
];
$tables = $this->getTablesToBake($collection, $options);
$dump = [];
if ($tables) {
foreach ($tables as $table) {
$schema = $collection->describe($table);
$dump[$table] = $schema;
}
}
$filePath = $path . DS . 'schema-dump-' . $connectionName . '.lock';
$output->writeln(sprintf('<info>Writing dump file `%s`...</info>', $filePath));
if (file_put_contents($filePath, serialize($dump))) {
$output->writeln(sprintf('<info>Dump file `%s` was successfully written</info>', $filePath));
return BaseCommand::CODE_SUCCESS;
}
$output->writeln(sprintf(
'<error>An error occurred while writing dump file `%s`</error>',
$filePath
));
return BaseCommand::CODE_ERROR;
}
}
+220
View File
@@ -0,0 +1,220 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Command;
use InvalidArgumentException;
use Migrations\ConfigurationTrait;
use Phinx\Console\Command\AbstractCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class MarkMigrated extends AbstractCommand
{
use CommandTrait;
use ConfigurationTrait;
/**
* The console output instance
*
* @var \Symfony\Component\Console\Output\OutputInterface
*/
protected $output;
/**
* @param \Symfony\Component\Console\Output\OutputInterface $output The output object.
* @return mixed
*/
public function output(OutputInterface $output = null)
{
if ($output !== null) {
$this->output = $output;
}
return $this->output;
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this->setName('mark_migrated')
->setDescription('Mark a migration as migrated')
->addArgument(
'version',
InputArgument::OPTIONAL,
'DEPRECATED: use `bin/cake migrations mark_migrated --target=VERSION --only` instead'
)
->setHelp(sprintf(
'%sMark migrations as migrated%s',
PHP_EOL,
PHP_EOL
))
->addOption('plugin', 'p', InputOption::VALUE_REQUIRED, 'The plugin the file should be created for')
->addOption('connection', 'c', InputOption::VALUE_REQUIRED, 'The datasource connection to use')
->addOption('source', 's', InputOption::VALUE_REQUIRED, 'The folder where migrations are in')
->addOption(
'target',
't',
InputOption::VALUE_REQUIRED,
'It will mark migrations from beginning to the given version'
)
->addOption(
'exclude',
'x',
InputOption::VALUE_NONE,
'If present it will mark migrations from beginning until the given version, excluding it'
)
->addOption(
'only',
'o',
InputOption::VALUE_NONE,
'If present it will only mark the given migration version'
);
}
/**
* Mark migrations as migrated
*
* `bin/cake migrations mark_migrated` mark every migrations as migrated
* `bin/cake migrations mark_migrated all` DEPRECATED: the same effect as above
* `bin/cake migrations mark_migrated --target=VERSION` mark migrations as migrated up to the VERSION param
* `bin/cake migrations mark_migrated --target=20150417223600 --exclude` mark migrations as migrated up to
* and except the VERSION param
* `bin/cake migrations mark_migrated --target=20150417223600 --only` mark only the VERSION migration as migrated
* `bin/cake migrations mark_migrated 20150417223600` DEPRECATED: the same effect as above
*
* @param \Symfony\Component\Console\Input\InputInterface $input the input object
* @param \Symfony\Component\Console\Output\OutputInterface $output the output object
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->setInput($input);
$this->bootstrap($input, $output);
$this->output($output);
$migrationPaths = $this->getConfig()->getMigrationPaths();
$path = array_pop($migrationPaths);
if ($this->invalidOnlyOrExclude()) {
$output->writeln(
"<error>You should use `--exclude` OR `--only` (not both) along with a `--target` !</error>"
);
return BaseCommand::CODE_ERROR;
}
if ($this->isUsingDeprecatedAll()) {
$this->outputDeprecatedAllMessage();
}
if ($this->isUsingDeprecatedVersion()) {
$this->outputDeprecatedVersionMessage();
}
try {
$versions = $this->getManager()->getVersionsToMark($input);
} catch (InvalidArgumentException $e) {
$output->writeln(sprintf("<error>%s</error>", $e->getMessage()));
return BaseCommand::CODE_ERROR;
}
$this->getManager()->markVersionsAsMigrated($path, $versions, $output);
return BaseCommand::CODE_SUCCESS;
}
/**
* Checks if the version is using the deprecated `all`
*
* @return bool Returns true if it is using the deprecated `all` otherwise false
*/
protected function isUsingDeprecatedAll()
{
$version = $this->input->getArgument('version');
return $version === 'all' || $version === '*';
}
/**
* Checks if the input has the `--exclude` option
*
* @return bool Returns true if `--exclude` option gets passed in otherwise false
*/
protected function hasExclude()
{
return $this->input->getOption('exclude');
}
/**
* Checks if the input has the `--only` option
*
* @return bool Returns true if `--only` option gets passed in otherwise false
*/
protected function hasOnly()
{
return $this->input->getOption('only');
}
/**
* Checks for the usage of deprecated VERSION as argument when not `all`
*
* @return bool True if it is using VERSION argument otherwise false
*/
protected function isUsingDeprecatedVersion()
{
$version = $this->input->getArgument('version');
return $version && $version !== 'all' && $version !== '*';
}
/**
* Checks for an invalid use of `--exclude` or `--only`
*
* @return bool Returns true when it is an invalid use of `--exclude` or `--only` otherwise false
*/
protected function invalidOnlyOrExclude()
{
return ($this->hasExclude() && $this->hasOnly()) ||
($this->hasExclude() || $this->hasOnly()) &&
$this->input->getOption('target') === null;
}
/**
* Outputs the deprecated message for the `all` or `*` usage
*
* @return void Just outputs the message
*/
protected function outputDeprecatedAllMessage()
{
$msg = "DEPRECATED: `all` or `*` as version is deprecated. Use `bin/cake migrations mark_migrated` instead";
$output = $this->output();
$output->writeln(sprintf("<comment>%s</comment>", $msg));
}
/**
* Outputs the deprecated message for the usage of VERSION as argument
*
* @return void Just outputs the message
*/
protected function outputDeprecatedVersionMessage()
{
$msg = 'DEPRECATED: VERSION as argument is deprecated. Use: ' .
'`bin/cake migrations mark_migrated --target=VERSION --only`';
$output = $this->output();
$output->writeln(sprintf("<comment>%s</comment>", $msg));
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Command;
use Cake\Event\EventDispatcherTrait;
use Migrations\ConfigurationTrait;
use Phinx\Console\Command\Migrate as MigrateCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class Migrate extends MigrateCommand
{
use CommandTrait {
execute as parentExecute;
}
use ConfigurationTrait;
use EventDispatcherTrait;
/**
* {@inheritdoc}
*/
protected function configure()
{
$this->setName('migrate')
->setDescription('Migrate the database')
->setHelp('runs all available migrations, optionally up to a specific version')
->addOption('--target', '-t', InputOption::VALUE_REQUIRED, 'The version number to migrate to')
->addOption('--date', '-d', InputOption::VALUE_REQUIRED, 'The date to migrate to')
->addOption('--dry-run', '-x', InputOption::VALUE_NONE, 'Dump queries to standard output instead of executing it')
->addOption('--plugin', '-p', InputOption::VALUE_REQUIRED, 'The plugin containing the migrations')
->addOption('--connection', '-c', InputOption::VALUE_REQUIRED, 'The datasource connection to use')
->addOption('--source', '-s', InputOption::VALUE_REQUIRED, 'The folder where migrations are in')
->addOption('--fake', null, InputOption::VALUE_NONE, "Mark any migrations selected as run, but don't actually execute them")
->addOption(
'--no-lock',
null,
InputOption::VALUE_NONE,
'If present, no lock file will be generated after migrating'
);
}
/**
* Overrides the action execute method in order to vanish the idea of environments
* from phinx. CakePHP does not believe in the idea of having in-app environments
*
* @param \Symfony\Component\Console\Input\InputInterface $input the input object
* @param \Symfony\Component\Console\Output\OutputInterface $output the output object
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$event = $this->dispatchEvent('Migration.beforeMigrate');
if ($event->isStopped()) {
return $event->result ? BaseCommand::CODE_SUCCESS : BaseCommand::CODE_ERROR;
}
$result = $this->parentExecute($input, $output);
$this->dispatchEvent('Migration.afterMigrate');
return $result !== null ? $result : BaseCommand::CODE_SUCCESS;
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Command;
use Cake\Event\EventDispatcherTrait;
use Migrations\ConfigurationTrait;
use Phinx\Console\Command\Rollback as RollbackCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class Rollback extends RollbackCommand
{
use CommandTrait {
execute as parentExecute;
}
use ConfigurationTrait;
use EventDispatcherTrait;
/**
* {@inheritDoc}
*/
protected function configure()
{
$this->setName('rollback')
->setDescription('Rollback the last or to a specific migration')
->setHelp('reverts the last migration, or optionally up to a specific version')
->addOption('--target', '-t', InputOption::VALUE_REQUIRED, 'The version number to rollback to')
->addOption('--date', '-d', InputOption::VALUE_REQUIRED, 'The date to migrate to')
->addOption('--plugin', '-p', InputOption::VALUE_REQUIRED, 'The plugin containing the migrations')
->addOption('--connection', '-c', InputOption::VALUE_REQUIRED, 'The datasource connection to use')
->addOption('--source', '-s', InputOption::VALUE_REQUIRED, 'The folder where migrations are in')
->addOption('--force', '-f', InputOption::VALUE_NONE, 'Force rollback to ignore breakpoints')
->addOption('--dry-run', '-x', InputOption::VALUE_NONE, 'Dump queries to standard output instead of executing it')
->addOption('--fake', null, InputOption::VALUE_NONE, "Mark any rollbacks selected as run, but don't actually execute them")
->addOption(
'--no-lock',
null,
InputOption::VALUE_NONE,
'Whether a lock file should be generated after rolling back'
);
}
/**
* Overrides the action execute method in order to vanish the idea of environments
* from phinx. CakePHP does not believe in the idea of having in-app environments
*
* @param \Symfony\Component\Console\Input\InputInterface $input the input object
* @param \Symfony\Component\Console\Output\OutputInterface $output the output object
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$event = $this->dispatchEvent('Migration.beforeRollback');
if ($event->isStopped()) {
return $event->result ? BaseCommand::CODE_SUCCESS : BaseCommand::CODE_ERROR;
}
$result = $this->parentExecute($input, $output);
$this->dispatchEvent('Migration.afterRollback');
return $result !== null ? $result : BaseCommand::CODE_SUCCESS;
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Command;
use Cake\Event\EventDispatcherTrait;
use Migrations\ConfigurationTrait;
use Phinx\Console\Command\SeedRun;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class Seed extends SeedRun
{
use CommandTrait {
execute as parentExecute;
}
use ConfigurationTrait;
use EventDispatcherTrait;
/**
* {@inheritdoc}
*/
protected function configure()
{
$this->setName('seed')
->setDescription('Seed the database with data')
->setHelp('runs all available migrations, optionally up to a specific version')
->addOption(
'--seed',
null,
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
'What is the name of the seeder?'
)
->addOption('--plugin', '-p', InputOption::VALUE_REQUIRED, 'The plugin containing the migrations')
->addOption('--connection', '-c', InputOption::VALUE_REQUIRED, 'The datasource connection to use')
->addOption('--source', '-s', InputOption::VALUE_REQUIRED, 'The folder where migrations are in');
}
/**
* Overrides the action execute method in order to vanish the idea of environments
* from phinx. CakePHP does not believe in the idea of having in-app environments
*
* @param \Symfony\Component\Console\Input\InputInterface $input the input object
* @param \Symfony\Component\Console\Output\OutputInterface $output the output object
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$event = $this->dispatchEvent('Migration.beforeSeed');
if ($event->isStopped()) {
return $event->result ? BaseCommand::CODE_SUCCESS : BaseCommand::CODE_ERROR;
}
$seed = $input->getOption('seed');
if (!empty($seed) && !is_array($seed)) {
$input->setOption('seed', [$seed]);
}
$this->setInput($input);
$this->bootstrap($input, $output);
$this->getManager()->setInput($input);
$result = $this->parentExecute($input, $output);
$this->dispatchEvent('Migration.afterSeed');
return $result !== null ? $result : BaseCommand::CODE_SUCCESS;
}
}
+127
View File
@@ -0,0 +1,127 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Command;
use Migrations\ConfigurationTrait;
use Phinx\Console\Command\Status as StatusCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class Status extends StatusCommand
{
use CommandTrait;
use ConfigurationTrait;
/**
* {@inheritDoc}
*/
protected function configure()
{
$this->setName('status')
->setDescription('Show migration status')
->addOption(
'--format',
'-f',
InputOption::VALUE_REQUIRED,
'The output format: text or json. Defaults to text.'
)
->setHelp('prints a list of all migrations, along with their current status')
->addOption('--plugin', '-p', InputOption::VALUE_REQUIRED, 'The plugin containing the migrations')
->addOption('--connection', '-c', InputOption::VALUE_REQUIRED, 'The datasource connection to use')
->addOption('--source', '-s', InputOption::VALUE_REQUIRED, 'The folder where migrations are in');
}
/**
* Show the migration status.
*
* @param \Symfony\Component\Console\Input\InputInterface $input the input object
* @param \Symfony\Component\Console\Output\OutputInterface $output the output object
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->beforeExecute($input, $output);
$this->bootstrap($input, $output);
$environment = $input->getOption('environment');
$format = $input->getOption('format');
if (null === $environment) {
$environment = $this->getManager()->getConfig()->getDefaultEnvironment();
$output->writeln('<comment>warning</comment> no environment specified, defaulting to: ' . $environment);
} else {
$output->writeln('<info>using environment</info> ' . $environment);
}
if (null !== $format) {
$output->writeln('<info>using format</info> ' . $format);
}
// print the status
$migrations = $this->getManager()->printStatus($environment, $format);
switch ($format) {
case 'json':
$this->getManager()->getOutput()->writeln($migrations);
break;
default:
$this->display($migrations);
break;
}
return BaseCommand::CODE_SUCCESS;
}
/**
* Will output the status of the migrations
*
* @param array $migrations Migrations array.
* @return void
*/
protected function display(array $migrations)
{
$output = $this->getManager()->getOutput();
if (!empty($migrations)) {
$output->writeln('');
$output->writeln(' Status Migration ID Migration Name ');
$output->writeln('-----------------------------------------');
foreach ($migrations as $migration) {
$status = $migration['status'] === 'up' ? ' <info>up</info> ' : ' <error>down</error> ';
$maxNameLength = $this->getManager()->maxNameLength;
$name = $migration['name'] !== false ?
' <comment>' . str_pad($migration['name'], $maxNameLength, ' ') . ' </comment>' :
' <error>** MISSING **</error>';
$missingComment = '';
if (!empty($migration['missing'])) {
$missingComment = ' <error>** MISSING **</error>';
}
$output->writeln(
$status .
sprintf(' %14.0f ', $migration['id']) .
$name .
$missingComment
);
}
$output->writeln('');
} else {
$msg = 'There are no available migrations. Try creating one using the <info>create</info> command.';
$output->writeln('');
$output->writeln($msg);
$output->writeln('');
}
}
}
+174
View File
@@ -0,0 +1,174 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations;
use Cake\Core\Plugin as CorePlugin;
use Cake\Datasource\ConnectionManager;
use Migrations\Util\UtilTrait;
use Phinx\Config\Config;
use Symfony\Component\Console\Input\InputInterface;
/**
* Contains a set of methods designed as overrides for
* the methods in phinx that are responsible for reading the project configuration.
* This is needed so that we can use the application configuration instead of having
* a configuration yaml file.
*/
trait ConfigurationTrait
{
use UtilTrait;
/**
* The configuration object that phinx uses for connecting to the database
*
* @var \Phinx\Config\Config
*/
protected $configuration;
/**
* Connection name to be used for this request
*
* @var string
*/
protected $connection;
/**
* The console input instance
*
* @var \Symfony\Component\Console\Input\Input
*/
protected $input;
/**
* Overrides the original method from phinx in order to return a tailored
* Config object containing the connection details for the database.
*
* @param bool $forceRefresh Refresh config.
* @return \Phinx\Config\Config
*/
public function getConfig($forceRefresh = false)
{
if ($this->configuration && $forceRefresh === false) {
return $this->configuration;
}
$migrationsPath = $this->getOperationsPath($this->input);
$seedsPath = $this->getOperationsPath($this->input, 'Seeds');
$plugin = $this->getPlugin($this->input);
if (!is_dir($migrationsPath)) {
mkdir($migrationsPath, 0777, true);
}
if (!is_dir($seedsPath)) {
mkdir($seedsPath, 0777, true);
}
$phinxTable = $this->getPhinxTable($plugin);
$connection = $this->getConnectionName($this->input);
$connectionConfig = ConnectionManager::getConfig($connection);
$adapterName = $this->getAdapterName($connectionConfig['driver']);
$templatePath = __DIR__ . DS . 'Template' . DS;
$config = [
'paths' => [
'migrations' => $migrationsPath,
'seeds' => $seedsPath,
],
'templates' => [
'file' => $templatePath . 'Phinx' . DS . 'create.php.template',
],
'migration_base_class' => 'Migrations\AbstractMigration',
'environments' => [
'default_migration_table' => $phinxTable,
'default_database' => 'default',
'default' => [
'adapter' => $adapterName,
'host' => isset($connectionConfig['host']) ? $connectionConfig['host'] : null,
'user' => isset($connectionConfig['username']) ? $connectionConfig['username'] : null,
'pass' => isset($connectionConfig['password']) ? $connectionConfig['password'] : null,
'port' => isset($connectionConfig['port']) ? $connectionConfig['port'] : null,
'name' => $connectionConfig['database'],
'charset' => isset($connectionConfig['encoding']) ? $connectionConfig['encoding'] : null,
'unix_socket' => isset($connectionConfig['unix_socket']) ? $connectionConfig['unix_socket'] : null,
'suffix' => '',
],
],
];
if ($adapterName === 'pgsql') {
if (!empty($connectionConfig['schema'])) {
$config['environments']['default']['schema'] = $connectionConfig['schema'];
}
}
if ($adapterName === 'mysql') {
if (!empty($connectionConfig['ssl_key']) && !empty($connectionConfig['ssl_cert'])) {
$config['environments']['default']['mysql_attr_ssl_key'] = $connectionConfig['ssl_key'];
$config['environments']['default']['mysql_attr_ssl_cert'] = $connectionConfig['ssl_cert'];
}
if (!empty($connectionConfig['ssl_ca'])) {
$config['environments']['default']['mysql_attr_ssl_ca'] = $connectionConfig['ssl_ca'];
}
}
return $this->configuration = new Config($config);
}
/**
* Returns the correct driver name to use in phinx based on the driver class
* that was configured for the configuration.
*
* @param string $driver The driver name as configured for the CakePHP app.
* @return string Name of the adapter.
* @throws \InvalidArgumentException when it was not possible to infer the information
* out of the provided database configuration
*/
public function getAdapterName($driver)
{
switch ($driver) {
case 'Cake\Database\Driver\Mysql':
case is_subclass_of($driver, 'Cake\Database\Driver\Mysql'):
return 'mysql';
case 'Cake\Database\Driver\Postgres':
case is_subclass_of($driver, 'Cake\Database\Driver\Postgres'):
return 'pgsql';
case 'Cake\Database\Driver\Sqlite':
case is_subclass_of($driver, 'Cake\Database\Driver\Sqlite'):
return 'sqlite';
case 'Cake\Database\Driver\Sqlserver':
case is_subclass_of($driver, 'Cake\Database\Driver\Sqlserver'):
return 'sqlsrv';
}
throw new \InvalidArgumentException('Could not infer database type from driver');
}
/**
* Returns the connection name that should be used for the migrations.
*
* @param \Symfony\Component\Console\Input\InputInterface $input the input object
* @return string
*/
protected function getConnectionName(InputInterface $input)
{
$connection = 'default';
if ($input->getOption('connection')) {
$connection = $input->getOption('connection');
}
return $connection;
}
}
+433
View File
@@ -0,0 +1,433 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations;
use Cake\Datasource\ConnectionManager;
use Phinx\Config\Config;
use Phinx\Config\ConfigInterface;
use Phinx\Db\Adapter\WrapperInterface;
use Phinx\Migration\Manager;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\NullOutput;
/**
* The Migrations class is responsible for handling migrations command
* within an none-shell application.
*/
class Migrations
{
use ConfigurationTrait;
/**
* The OutputInterface.
* Should be a \Symfony\Component\Console\Output\NullOutput instance
*
* @var \Symfony\Component\Console\Output\OutputInterface
*/
protected $output;
/**
* CakeManager instance
*
* @var \Migrations\CakeManager
*/
protected $manager;
/**
* Default options to use
*
* @var array
*/
protected $default = [];
/**
* Current command being run.
* Useful if some logic needs to be applied in the ConfigurationTrait depending
* on the command
*
* @var array
*/
protected $command;
/**
* Stub input to feed the manager class since we might not have an input ready when we get the Manager using
* the `getManager()` method
*
* @var \Symfony\Component\Console\Input\ArrayInput
*/
protected $stubInput;
/**
* Constructor
* @param array $default Default option to be used when calling a method.
* Available options are :
* - `connection` The datasource connection to use
* - `source` The folder where migrations are in
* - `plugin` The plugin containing the migrations
*/
public function __construct(array $default = [])
{
$this->output = new NullOutput();
$this->stubInput = new ArrayInput([]);
if ($default) {
$this->default = $default;
}
}
/**
* Sets the command
*
* @param string $command Command name to store.
* @return self
*/
public function setCommand($command)
{
$this->command = $command;
return $this;
}
/**
* Sets the input object that should be used for the command class. This object
* is used to inspect the extra options that are needed for CakePHP apps.
*
* @param \Symfony\Component\Console\Input\InputInterface $input the input object
* @return void
*/
public function setInput(InputInterface $input)
{
$this->input = $input;
}
/**
* Gets the command
*
* @return string Command name
*/
public function getCommand()
{
return $this->command;
}
/**
* Returns the status of each migrations based on the options passed
*
* @param array $options Options to pass to the command
* Available options are :
*
* - `format` Format to output the response. Can be 'json'
* - `connection` The datasource connection to use
* - `source` The folder where migrations are in
* - `plugin` The plugin containing the migrations
*
* @return array The migrations list and their statuses
*/
public function status($options = [])
{
$this->setCommand('status');
$input = $this->getInput('Status', [], $options);
$params = ['default', $input->getOption('format')];
return $this->run('printStatus', $params, $input);
}
/**
* Migrates available migrations
*
* @param array $options Options to pass to the command
* Available options are :
*
* - `target` The version number to migrate to. If not provided, will migrate
* everything it can
* - `connection` The datasource connection to use
* - `source` The folder where migrations are in
* - `plugin` The plugin containing the migrations
* - `date` The date to migrate to
*
* @return bool Success
*/
public function migrate($options = [])
{
$this->setCommand('migrate');
$input = $this->getInput('Migrate', [], $options);
$method = 'migrate';
$params = ['default', $input->getOption('target')];
if ($input->getOption('date')) {
$method = 'migrateToDateTime';
$params[1] = new \DateTime($input->getOption('date'));
}
$this->run($method, $params, $input);
return true;
}
/**
* Rollbacks migrations
*
* @param array $options Options to pass to the command
* Available options are :
*
* - `target` The version number to migrate to. If not provided, will only migrate
* the last migrations registered in the phinx log
* - `connection` The datasource connection to use
* - `source` The folder where migrations are in
* - `plugin` The plugin containing the migrations
* - `date` The date to rollback to
*
* @return bool Success
*/
public function rollback($options = [])
{
$this->setCommand('rollback');
$input = $this->getInput('Rollback', [], $options);
$method = 'rollback';
$params = ['default', $input->getOption('target')];
if ($input->getOption('date')) {
$method = 'rollbackToDateTime';
$params[1] = new \DateTime($input->getOption('date'));
}
$this->run($method, $params, $input);
return true;
}
/**
* Marks a migration as migrated
*
* @param int $version The version number of the migration to mark as migrated
* @param array $options Options to pass to the command
* Available options are :
*
* - `connection` The datasource connection to use
* - `source` The folder where migrations are in
* - `plugin` The plugin containing the migrations
*
* @return bool Success
*/
public function markMigrated($version = null, $options = [])
{
$this->setCommand('mark_migrated');
if (
isset($options['target']) &&
isset($options['exclude']) &&
isset($options['only'])
) {
$exceptionMessage = 'You should use `exclude` OR `only` (not both) along with a `target` argument';
throw new \InvalidArgumentException($exceptionMessage);
}
$input = $this->getInput('MarkMigrated', ['version' => $version], $options);
$this->setInput($input);
$migrationPaths = $this->getConfig()->getMigrationPaths();
$params = [
array_pop($migrationPaths),
$this->getManager()->getVersionsToMark($input),
$this->output,
];
$this->run('markVersionsAsMigrated', $params, $input);
return true;
}
/**
* Seed the database using a seed file
*
* @param array $options Options to pass to the command
* Available options are :
*
* - `connection` The datasource connection to use
* - `source` The folder where migrations are in
* - `plugin` The plugin containing the migrations
* - `seed` The seed file to use
*
* @return bool Success
*/
public function seed($options = [])
{
$this->setCommand('seed');
$input = $this->getInput('Seed', [], $options);
$seed = $input->getOption('seed');
if (!$seed) {
$seed = null;
}
$params = ['default', $seed];
$this->run('seed', $params, $input);
return true;
}
/**
* Runs the method needed to execute and return
*
* @param string $method Manager method to call
* @param array $params Manager params to pass
* @param \Symfony\Component\Console\Input\InputInterface $input InputInterface needed for the
* Manager to properly run
*
* @return mixed The result of the CakeManager::$method() call
*/
protected function run($method, $params, $input)
{
if ($this->configuration instanceof Config) {
$migrationPaths = $this->getConfig()->getMigrationPaths();
$migrationPath = array_pop($migrationPaths);
$seedPaths = $this->getConfig()->getSeedPaths();
$seedPath = array_pop($seedPaths);
}
$pdo = null;
if ($this->manager instanceof Manager) {
$pdo = $this->manager->getEnvironment('default')
->getAdapter()
->getConnection();
}
$this->setInput($input);
$newConfig = $this->getConfig(true);
$manager = $this->getManager($newConfig);
$manager->setInput($input);
if ($pdo !== null) {
$adapter = $this->manager->getEnvironment('default')->getAdapter();
while ($adapter instanceof WrapperInterface) {
$adapter = $adapter->getAdapter();
}
$adapter->setConnection($pdo);
}
$newMigrationPaths = $newConfig->getMigrationPaths();
if (isset($migrationPath) && array_pop($newMigrationPaths) !== $migrationPath) {
$manager->resetMigrations();
}
$newSeedPaths = $newConfig->getSeedPaths();
if (isset($seedPath) && array_pop($newSeedPaths) !== $seedPath) {
$manager->resetSeeds();
}
return call_user_func_array([$manager, $method], $params);
}
/**
* Returns an instance of CakeManager
*
* @param \Phinx\Config\ConfigInterface|null $config ConfigInterface the Manager needs to run
* @return \Migrations\CakeManager Instance of CakeManager
*/
public function getManager($config = null)
{
if (!($this->manager instanceof CakeManager)) {
if (!($config instanceof ConfigInterface)) {
throw new \RuntimeException(
'You need to pass a ConfigInterface object for your first getManager() call'
);
}
$input = $this->input ?: $this->stubInput;
$this->manager = new CakeManager($config, $input, $this->output);
} elseif ($config !== null) {
$defaultEnvironment = $config->getEnvironment('default');
try {
$environment = $this->manager->getEnvironment('default');
$oldConfig = $environment->getOptions();
unset($oldConfig['connection']);
if ($oldConfig == $defaultEnvironment) {
$defaultEnvironment['connection'] = $environment
->getAdapter()
->getConnection();
}
} catch (\InvalidArgumentException $e) {
}
$config['environments'] = ['default' => $defaultEnvironment];
$this->manager->setEnvironments([]);
$this->manager->setConfig($config);
}
$this->setAdapter();
return $this->manager;
}
/**
* Sets the adapter the manager is going to need to operate on the DB
* This will make sure the adapter instance is a \Migrations\CakeAdapter instance
*
* @return void
*/
public function setAdapter()
{
if ($this->input !== null) {
$connectionName = 'default';
if ($this->input->getOption('connection')) {
$connectionName = $this->input->getOption('connection');
}
$connection = ConnectionManager::get($connectionName);
$env = $this->manager->getEnvironment('default');
$adapter = $env->getAdapter();
if (!$adapter instanceof CakeAdapter) {
$env->setAdapter(new CakeAdapter($adapter, $connection));
}
}
}
/**
* Get the input needed for each commands to be run
*
* @param string $command Command name for which we need the InputInterface
* @param array $arguments Simple key/values array representing the command arguments
* to pass to the InputInterface
* @param array $options Simple key/values array representing the command options
* to pass to the InputInterface
* @return \Symfony\Component\Console\Input\InputInterface InputInterface needed for the
* Manager to properly run
*/
public function getInput($command, $arguments, $options)
{
$className = '\Migrations\Command\\' . $command;
$options = $arguments + $this->prepareOptions($options);
$definition = (new $className())->getDefinition();
return new ArrayInput($options, $definition);
}
/**
* Prepares the option to pass on to the InputInterface
*
* @param array $options Simple key-values array to pass to the InputInterface
* @return array Prepared $options
*/
protected function prepareOptions($options = [])
{
$options = array_merge($this->default, $options);
if (!$options) {
return $options;
}
foreach ($options as $name => $value) {
$options['--' . $name] = $value;
unset($options[$name]);
}
return $options;
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations;
use Migrations\Command;
use Symfony\Component\Console\Application;
/**
* Used to register all supported subcommand in order to make
* them executable by the Symfony Console component
*/
class MigrationsDispatcher extends Application
{
/**
* Class Constructor.
*
* Initialize the Phinx console application.
*
* @param string $version The Application Version
*/
public function __construct($version)
{
parent::__construct('Migrations plugin, based on Phinx by Rob Morgan.', $version);
$this->add(new Command\Create());
$this->add(new Command\Dump());
$this->add(new Command\MarkMigrated());
$this->add(new Command\Migrate());
$this->add(new Command\Rollback());
$this->add(new Command\Seed());
$this->add(new Command\Status());
$this->add(new Command\CacheBuild());
$this->add(new Command\CacheClear());
$this->setCatchExceptions(false);
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations;
use Cake\Core\BasePlugin;
/**
* Plugin class for migrations
*/
class Plugin extends BasePlugin
{
/**
* Plugin name.
*
* @var string
*/
protected $name = 'Migrations';
/**
* Don't try to load routes.
*
* @var bool
*/
protected $routesEnabled = false;
}
+192
View File
@@ -0,0 +1,192 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Shell;
use Cake\Console\Shell;
use Migrations\MigrationsDispatcher;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
/**
* A wrapper shell for phinx migrations, used to inject our own
* console actions so that database configuration already defined
* for the application can be reused.
*
* @property \Migrations\Shell\Task\CreateTask $Create
* @property \Migrations\Shell\Task\DumpTask $Dump
* @property \Migrations\Shell\Task\MarkMigratedTask $MarkMigrated
* @property \Migrations\Shell\Task\MigrateTask $Migrate
* @property \Migrations\Shell\Task\RollbackTask $Rollback
* @property \Migrations\Shell\Task\StatusTask $Status
*/
class MigrationsShell extends Shell
{
/**
* {@inheritDoc}
*/
public $tasks = [
'Migrations.Create',
'Migrations.Dump',
'Migrations.MarkMigrated',
'Migrations.Migrate',
'Migrations.Rollback',
'Migrations.Status',
];
/**
* Array of arguments to run the shell with.
*
* @var array
*/
public $argv = [];
/**
* Defines what options can be passed to the shell.
* This is required because CakePHP validates the passed options
* and would complain if something not configured here is present
*
* @return \Cake\Console\ConsoleOptionParser
*/
public function getOptionParser()
{
return parent::getOptionParser()
->addOption('plugin', ['short' => 'p'])
->addOption('target', ['short' => 't'])
->addOption('connection', ['short' => 'c'])
->addOption('source', ['short' => 's'])
->addOption('seed')
->addOption('ansi')
->addOption('no-ansi')
->addOption('no-lock', ['boolean' => true])
->addOption('force', ['boolean' => true])
->addOption('version', ['short' => 'V'])
->addOption('no-interaction', ['short' => 'n'])
->addOption('template', ['short' => 't'])
->addOption('format', ['short' => 'f'])
->addOption('only', ['short' => 'o'])
->addOption('dry-run', ['short' => 'x'])
->addOption('exclude', ['short' => 'e']);
}
/**
* Defines constants that are required by phinx to get running
*
* @return void
*/
public function initialize()
{
if (!defined('PHINX_VERSION')) {
define('PHINX_VERSION', 'UNKNOWN');
}
parent::initialize();
}
/**
* This acts as a front-controller for phinx. It just instantiates the classes
* responsible for parsing the command line from phinx and gives full control of
* the rest of the flow to it.
*
* The input parameter of the ``MigrationDispatcher::run()`` method is manually built
* in case a MigrationsShell is dispatched using ``Shell::dispatch()``.
*
* @return bool Success of the call.
*/
public function main()
{
$app = $this->getApp();
$input = new ArgvInput($this->argv);
$app->setAutoExit(false);
$exitCode = $app->run($input, $this->getOutput());
if (in_array('-h', $this->argv) || in_array('--help', $this->argv)) {
return $exitCode === 0;
}
if (
isset($this->argv[1]) && in_array($this->argv[1], ['migrate', 'rollback']) &&
!$this->params['no-lock'] &&
$exitCode === 0
) {
$dispatchCommand = 'migrations dump';
if (!empty($this->params['connection'])) {
$dispatchCommand .= ' -c ' . $this->params['connection'];
}
if (!empty($this->params['plugin'])) {
$dispatchCommand .= ' -p ' . $this->params['plugin'];
}
$dumpExitCode = $this->dispatchShell($dispatchCommand);
}
if (isset($dumpExitCode) && $exitCode === 0 && $dumpExitCode !== 0) {
$exitCode = 1;
}
return $exitCode === 0;
}
/**
* Returns the MigrationsDispatcher the Shell will have to use
*
* @return \Migrations\MigrationsDispatcher
*/
protected function getApp()
{
return new MigrationsDispatcher(PHINX_VERSION);
}
/**
* Returns the instance of OutputInterface the MigrationsDispatcher will have to use.
*
* @return \Symfony\Component\Console\Output\ConsoleOutput
*/
protected function getOutput()
{
return new ConsoleOutput();
}
/**
* Override the default behavior to save the command called
* in order to pass it to the command dispatcher
*
* {@inheritDoc}
*/
public function runCommand($argv, $autoMethod = false, $extra = [])
{
array_unshift($argv, 'migrations');
$this->argv = $argv;
return parent::runCommand($argv, $autoMethod, $extra);
}
/**
* Display the help in the correct format
*
* @param string $command The command to get help for.
* @return int|bool|null Exit code or number of bytes written to stdout
*/
protected function displayHelp($command)
{
return $this->main();
}
/**
* {@inheritDoc}
*/
// @codingStandardsIgnoreStart
protected function _displayHelp($command)
{
// @codingStandardsIgnoreEnd
return $this->displayHelp($command);
}
}
@@ -0,0 +1,54 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Shell\Task;
use Cake\Console\Shell;
/**
* Base Task that Migrations subcommands Task class should extend.
* It implements the getOptionParser method that defines the common options
* for all subcommands.
*/
class CommandTask extends Shell
{
/**
* {@inheritDoc}
*/
public function getOptionParser()
{
$parser = parent::getOptionParser();
$parser
->addOption('plugin', [
'short' => 'p',
'help' => 'The plugin the command should be applied to',
'required' => false,
])
->addOption('connection', [
'short' => 'c',
'help' => 'The datasource connection to use',
'required' => false,
])
->addOption('source', [
'short' => 's',
'help' => 'The name of the folder where migrations are stored',
'required' => false,
])
->addOption('ansi')
->addOption('no-ansi')
->addOption('no-interaction', ['short' => 'n']);
return $parser;
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Shell\Task;
use Phinx\Console\Command\Create;
/**
* This task class is needed in order to provide a correct autocompletion feature
* when using the CakePHP migrations shell plugin. It has no effect on the
* migrations process.
*/
class CreateTask extends CommandTask
{
/**
* {@inheritDoc}
*/
public function getOptionParser()
{
$parser = parent::getOptionParser();
$parser
->addArgument('name', [
'help' => 'What is the name of the migration?',
])
->addOption('template', [
'short' => 't',
'help' => 'Use an alternative template',
'required' => false,
])
->addOption('class', [
'short' => 'l',
'help' => 'Use a class implementing "' . Create::CREATION_INTERFACE . '" to generate the template',
'required' => false,
]);
return $parser;
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Shell\Task;
/**
* This task class is needed in order to provide a correct autocompletion feature
* when using the CakePHP migrations shell plugin. It has no effect on the
* migrations process.
*/
class DumpTask extends CommandTask
{
/**
* {@inheritDoc}
*/
public function getOptionParser()
{
$parser = parent::getOptionParser();
$parser
->setDescription('Dumps the current schema of the database to be used while baking a diff');
return $parser;
}
}
@@ -0,0 +1,47 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Shell\Task;
/**
* This task class is needed in order to provide a correct autocompletion feature
* when using the CakePHP migrations shell plugin. It has no effect on the
* migrations process.
*/
class MarkMigratedTask extends CommandTask
{
/**
* {@inheritDoc}
*/
public function getOptionParser()
{
$parser = parent::getOptionParser();
$parser
->addArgument('version', [
'help' => 'What is the version of the migration?',
])
->addOption('exclude', [
'short' => 'x',
'help' => 'If present it will mark migrations from beginning until the given version, excluding it',
'required' => false,
])
->addOption('only', [
'short' => 'o',
'help' => 'If present it will only mark the given migration version',
'required' => false,
]);
return $parser;
}
}
@@ -0,0 +1,50 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Shell\Task;
/**
* This task class is needed in order to provide a correct autocompletion feature
* when using the CakePHP migrations shell plugin. It has no effect on the
* migrations process.
*/
class MigrateTask extends CommandTask
{
/**
* {@inheritDoc}
*/
public function getOptionParser()
{
$parser = parent::getOptionParser();
$parser
->addOption('target', [
'short' => 't',
'help' => 'The version number to migrate to',
'required' => false,
])
->addOption('date', [
'short' => 'd',
'help' => 'The date to migrate to',
'required' => false,
])
->addOption('dry-run', ['short' => 'x'])
->addOption('fake', ['boolean' => true])
->addOption('no-lock', [
'help' => 'If present, no lock file will be generated after migrating',
'boolean' => true,
]);
return $parser;
}
}
@@ -0,0 +1,561 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Shell\Task;
use Cake\Core\Configure;
use Cake\Database\Schema\Table;
use Cake\Datasource\ConnectionManager;
use Cake\Event\Event;
use Cake\Event\EventManager;
use Migrations\Util\UtilTrait;
use Symfony\Component\Console\Input\ArrayInput;
/**
* Task class for generating migration diff files.
*
* @property \Bake\Shell\Task\BakeTemplateTask $BakeTemplate
* @property \Bake\Shell\Task\TestTask $Test
*/
class MigrationDiffTask extends SimpleMigrationTask
{
use SnapshotTrait;
use UtilTrait;
/**
* Array of migrations that have already been migrated
*
* @var array
*/
protected $migratedItems = [];
/**
* Path to the migration files
*
* @var string
*/
protected $migrationsPath;
/**
* Migration files that are stored in the self::migrationsPath
*
* @var array
*/
protected $migrationsFiles = [];
/**
* Name of the phinx log table
*
* @var string
*/
protected $phinxTable;
/**
* List the tables the connection currently holds
*
* @var array
*/
protected $tables = [];
/**
* Array of \Cake\Database\Schema\Table objects from the dump file which
* represents the state of the database after the last migrate / rollback command
*
* @var array
*/
protected $dumpSchema;
/**
* Array of \Cake\Database\Schema\Table objects from the current state of the database
*
* @var array
*/
protected $currentSchema;
/**
* List of the tables that are commonly found in the dump schema and the current schema
*
* @var array
*/
protected $commonTables;
/**
* {@inheritDoc}
*/
protected $templateData = [];
/**
* {@inheritDoc}
*/
public function bake($name)
{
$this->setup();
if (!$this->checkSync()) {
$this->abort('Your migrations history is not in sync with your migrations files. ' .
'Make sure all your migrations have been migrated before baking a diff.');
return 1;
}
if (empty($this->migrationsFiles) && empty($this->migratedItems)) {
return $this->bakeSnapshot($name);
}
$collection = $this->getCollection($this->connection);
EventManager::instance()->on('Bake.initialize', function (Event $event) use ($collection) {
$event->getSubject()->loadHelper('Migrations.Migration', [
'collection' => $collection,
]);
});
return parent::bake($name);
}
/**
* Sets up everything the baking process needs
*
* @return void
*/
public function setup()
{
$this->migrationsPath = $this->getPath();
$this->migrationsFiles = glob($this->migrationsPath . '*.php');
$this->phinxTable = $this->getPhinxTable($this->plugin);
$connection = ConnectionManager::get($this->connection);
$this->tables = $connection->getSchemaCollection()->listTables();
$tableExists = in_array($this->phinxTable, $this->tables);
$migratedItems = [];
if ($tableExists) {
$query = $connection->newQuery();
$migratedItems = $query
->select(['version'])
->from($this->phinxTable)
->order(['version DESC'])
->execute()->fetchAll('assoc');
}
$this->migratedItems = $migratedItems;
}
/**
* Get a collection from a database.
*
* @param string $connection Database connection name.
* @return \Cake\Database\Schema\Collection
*/
public function getCollection($connection)
{
$connection = ConnectionManager::get($connection);
return $connection->getSchemaCollection();
}
/**
* Process and prepare the data needed for the bake template to be generated.
*
* @return array
*/
public function templateData()
{
$this->dumpSchema = $this->getDumpSchema();
$this->currentSchema = $this->getCurrentSchema();
$this->commonTables = array_intersect_key($this->currentSchema, $this->dumpSchema);
$this->calculateDiff();
return [
'data' => $this->templateData,
'dumpSchema' => $this->dumpSchema,
'currentSchema' => $this->currentSchema,
];
}
/**
* This methods runs the various methods needed to calculate a diff between the current
* state of the database and the schema dump file.
*
* @return void
*/
protected function calculateDiff()
{
$this->getConstraints();
$this->getIndexes();
$this->getColumns();
$this->getTables();
}
/**
* Calculate the diff between the current state of the database and the schema dump
* by returning an array containing the full \Cake\Database\Schema\Table definitions
* of tables to be created and removed in the diff file.
*
* The method directly sets the diff in a property of the class.
*
* @return void
*/
protected function getTables()
{
$this->templateData['fullTables'] = [
'add' => array_diff_key($this->currentSchema, $this->dumpSchema),
'remove' => array_diff_key($this->dumpSchema, $this->currentSchema),
];
}
/**
* Calculate the diff between columns in existing tables.
* This will look for columns addition, columns removal and changes in columns metadata
* such as change of types or property such as length.
*
* Note that the method is not able to detect columns name change.
* The method directly sets the diff in a property of the class.
*
* @return void
*/
protected function getColumns()
{
foreach ($this->commonTables as $table => $currentSchema) {
$currentColumns = $currentSchema->columns();
$oldColumns = $this->dumpSchema[$table]->columns();
// brand new columns
$addedColumns = array_diff($currentColumns, $oldColumns);
foreach ($addedColumns as $columnName) {
$column = $currentSchema->getColumn($columnName);
$key = array_search($columnName, $currentColumns);
if ($key > 0) {
$column['after'] = $currentColumns[$key - 1];
}
if (isset($column['unsigned'])) {
$column['signed'] = !$column['unsigned'];
unset($column['unsigned']);
}
$this->templateData[$table]['columns']['add'][$columnName] = $column;
}
// changes in columns meta-data
foreach ($currentColumns as $columnName) {
$column = $currentSchema->getColumn($columnName);
$oldColumn = $this->dumpSchema[$table]->getColumn($columnName);
unset($column['collate']);
unset($oldColumn['collate']);
if (
in_array($columnName, $oldColumns) &&
$column !== $oldColumn
) {
$changedAttributes = array_diff_assoc($column, $oldColumn);
foreach (['type', 'length', 'null', 'default'] as $attribute) {
$phinxAttributeName = $attribute;
if ($attribute == 'length') {
$phinxAttributeName = 'limit';
}
if (!isset($changedAttributes[$phinxAttributeName])) {
$changedAttributes[$phinxAttributeName] = $column[$attribute];
}
}
if (isset($changedAttributes['unsigned'])) {
$changedAttributes['signed'] = !$changedAttributes['unsigned'];
unset($changedAttributes['unsigned']);
} else {
// badish hack
if (isset($column['unsigned']) && $column['unsigned'] === true) {
$changedAttributes['signed'] = false;
}
}
if (isset($changedAttributes['length'])) {
if (!isset($changedAttributes['limit'])) {
$changedAttributes['limit'] = $changedAttributes['length'];
}
unset($changedAttributes['length']);
}
$this->templateData[$table]['columns']['changed'][$columnName] = $changedAttributes;
}
}
// columns deletion
if (!isset($this->templateData[$table]['columns']['remove'])) {
$this->templateData[$table]['columns']['remove'] = [];
}
$removedColumns = array_diff($oldColumns, $currentColumns);
if (!empty($removedColumns)) {
foreach ($removedColumns as $columnName) {
$column = $this->dumpSchema[$table]->getColumn($columnName);
$key = array_search($columnName, $oldColumns);
if ($key > 0) {
$column['after'] = $oldColumns[$key - 1];
}
$this->templateData[$table]['columns']['remove'][$columnName] = $column;
}
}
}
}
/**
* Calculate the diff between contraints in existing tables.
* This will look for contraints addition, contraints removal and changes in contraints metadata
* such as change of referenced columns if the old constraints and the new one have the same name.
*
* The method directly sets the diff in a property of the class.
*
* @return void
*/
protected function getConstraints()
{
foreach ($this->commonTables as $table => $currentSchema) {
$currentConstraints = $currentSchema->constraints();
$oldConstraints = $this->dumpSchema[$table]->constraints();
// brand new constraints
$addedConstraints = array_diff($currentConstraints, $oldConstraints);
foreach ($addedConstraints as $constraintName) {
$this->templateData[$table]['constraints']['add'][$constraintName] =
$currentSchema->getConstraint($constraintName);
$constraint = $currentSchema->getConstraint($constraintName);
if ($constraint['type'] === Table::CONSTRAINT_FOREIGN) {
$this->templateData[$table]['constraints']['add'][$constraintName] = $constraint;
} else {
$this->templateData[$table]['indexes']['add'][$constraintName] = $constraint;
}
}
// constraints having the same name between new and old schema
// if present in both, check if they are the same : if not, remove the old one and add the new one
foreach ($currentConstraints as $constraintName) {
$constraint = $currentSchema->getConstraint($constraintName);
if (
in_array($constraintName, $oldConstraints) &&
$constraint !== $this->dumpSchema[$table]->getConstraint($constraintName)
) {
$this->templateData[$table]['constraints']['remove'][$constraintName] =
$this->dumpSchema[$table]->getConstraint($constraintName);
$this->templateData[$table]['constraints']['add'][$constraintName] =
$constraint;
}
}
// removed constraints
$removedConstraints = array_diff($oldConstraints, $currentConstraints);
foreach ($removedConstraints as $constraintName) {
$constraint = $this->dumpSchema[$table]->getConstraint($constraintName);
if ($constraint['type'] === Table::CONSTRAINT_FOREIGN) {
$this->templateData[$table]['constraints']['remove'][$constraintName] = $constraint;
} else {
$this->templateData[$table]['indexes']['remove'][$constraintName] = $constraint;
}
}
}
}
/**
* Calculate the diff between indexes in existing tables.
* This will look for indexes addition, indexes removal and changes in indexes metadata
* such as change of referenced columns if the old indexes and the new one have the same name.
*
* The method directly sets the diff in a property of the class.
*
* @return void
*/
protected function getIndexes()
{
foreach ($this->commonTables as $table => $currentSchema) {
$currentIndexes = $currentSchema->indexes();
$oldIndexes = $this->dumpSchema[$table]->indexes();
sort($currentIndexes);
sort($oldIndexes);
// brand new indexes
$addedIndexes = array_diff($currentIndexes, $oldIndexes);
foreach ($addedIndexes as $indexName) {
$this->templateData[$table]['indexes']['add'][$indexName] = $currentSchema->getIndex($indexName);
}
// indexes having the same name between new and old schema
// if present in both, check if they are the same : if not, remove the old one and add the new one
foreach ($currentIndexes as $indexName) {
$index = $currentSchema->getIndex($indexName);
if (
in_array($indexName, $oldIndexes) &&
$index !== $this->dumpSchema[$table]->getIndex($indexName)
) {
$this->templateData[$table]['indexes']['remove'][$indexName] =
$this->dumpSchema[$table]->getIndex($indexName);
$this->templateData[$table]['indexes']['add'][$indexName] = $index;
}
}
// indexes deletion
if (!isset($this->templateData[$table]['indexes']['remove'])) {
$this->templateData[$table]['indexes']['remove'] = [];
}
$removedIndexes = array_diff($oldIndexes, $currentIndexes);
$parts = [];
if (!empty($removedIndexes)) {
foreach ($removedIndexes as $index) {
$parts[$index] = $this->dumpSchema[$table]->getIndex($index);
}
}
$this->templateData[$table]['indexes']['remove'] = array_merge(
$this->templateData[$table]['indexes']['remove'],
$parts
);
}
}
/**
* Checks that the migrations history is in sync with the migrations files
*
* @return bool Whether migrations history is sync or not
*/
protected function checkSync()
{
if (empty($this->migrationsFiles) && empty($this->migratedItems)) {
return true;
}
if (!empty($this->migratedItems)) {
$lastVersion = $this->migratedItems[0]['version'];
$lastFile = end($this->migrationsFiles);
return (bool)strpos($lastFile, (string)$lastVersion);
}
return false;
}
/**
* Fallback method called to bake a snapshot when the phinx log history is empty and
* there are no migration files.
*
* @param string $name Name.
* @return int Value of the snapshot baking dispatch process
*/
protected function bakeSnapshot($name)
{
$this->out('Your migrations history is empty and you do not have any migrations files.');
$this->out('Falling back to baking a snapshot...');
$dispatchCommand = 'bake migration_snapshot ' . $name;
if (!empty($this->params['connection'])) {
$dispatchCommand .= ' -c ' . $this->params['connection'];
}
if (!empty($this->params['plugin'])) {
$dispatchCommand .= ' -p ' . $this->params['plugin'];
}
$dispatch = $this->dispatchShell([
'command' => $dispatchCommand,
]);
if ($dispatch === 1) {
$this->abort('Something went wrong during the snapshot baking. Please try again.');
}
return $dispatch;
}
/**
* Fetch the correct schema dump based on the arguments and options passed to the shell call
* and returns it as an array
*
* @return array Full database schema : the key is the name of the table and the value is
* an instance of \Cake\Database\Schema\Table.
*/
protected function getDumpSchema()
{
$inputArgs = [];
$connectionName = 'default';
if (!empty($this->params['connection'])) {
$connectionName = $inputArgs['--connection'] = $this->params['connection'];
}
if (!empty($this->params['plugin'])) {
$inputArgs['--plugin'] = $this->params['plugin'];
}
$className = '\Migrations\Command\Dump';
$definition = (new $className())->getDefinition();
$input = new ArrayInput($inputArgs, $definition);
$path = $this->getOperationsPath($input) . DS . 'schema-dump-' . $connectionName . '.lock';
if (!file_exists($path)) {
$msg = 'Unable to retrieve the schema dump file. You can create a dump file using ' .
'the `cake migrations dump` command';
$this->abort($msg);
}
return unserialize(file_get_contents($path));
}
/**
* Reflects the current database schema.
*
* @return array Full database schema : the key is the name of the table and the value is
* an instance of \Cake\Database\Schema\Table.
*/
protected function getCurrentSchema()
{
$schema = [];
if (empty($this->tables)) {
return $schema;
}
$collection = ConnectionManager::get($this->connection)->getSchemaCollection();
foreach ($this->tables as $table) {
if (preg_match("/^.*phinxlog$/", $table) === 1) {
continue;
}
$schema[$table] = $collection->describe($table);
}
return $schema;
}
/**
* {@inheritDoc}
*/
public function template()
{
return 'Migrations.config/diff';
}
/**
* Gets the option parser instance and configures it.
*
* @return \Cake\Console\ConsoleOptionParser
*/
public function getOptionParser()
{
$parser = parent::getOptionParser();
$parser->addArgument('name', [
'help' => 'Name of the migration to bake. Can use Plugin.name to bake migration files into plugins.',
'required' => true,
]);
return $parser;
}
}
@@ -0,0 +1,155 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Shell\Task;
use Cake\Core\Configure;
use Cake\Datasource\ConnectionManager;
use Cake\Event\Event;
use Cake\Event\EventManager;
use Migrations\TableFinderTrait;
use Migrations\Util\UtilTrait;
/**
* Task class for generating migration snapshot files.
*
* @property \Bake\Shell\Task\BakeTemplateTask $BakeTemplate
* @property \Bake\Shell\Task\TestTask $Test
*/
class MigrationSnapshotTask extends SimpleMigrationTask
{
use SnapshotTrait;
use TableFinderTrait;
use UtilTrait;
/**
* {@inheritDoc}
*/
public function bake($name)
{
$collection = $this->getCollection($this->connection);
EventManager::instance()->on('Bake.initialize', function (Event $event) use ($collection) {
$event->getSubject()->loadHelper('Migrations.Migration', [
'collection' => $collection,
]);
});
return parent::bake($name);
}
/**
* {@inheritDoc}
*/
public function template()
{
return 'Migrations.config/snapshot';
}
/**
* {@inheritDoc}
*/
public function templateData()
{
$namespace = Configure::read('App.namespace');
$pluginPath = '';
if ($this->plugin) {
$namespace = $this->_pluginNamespace($this->plugin);
$pluginPath = $this->plugin . '.';
}
$collection = $this->getCollection($this->connection);
$options = [
'require-table' => $this->params['require-table'],
'plugin' => $this->plugin,
];
$tables = $this->getTablesToBake($collection, $options);
sort($tables, SORT_NATURAL);
$tables = array_combine($tables, $tables);
$autoId = true;
if (isset($this->params['disable-autoid'])) {
$autoId = !$this->params['disable-autoid'];
}
return [
'plugin' => $this->plugin,
'pluginPath' => $pluginPath,
'namespace' => $namespace,
'collection' => $collection,
'tables' => $tables,
'action' => 'create_table',
'name' => $this->BakeTemplate->viewVars['name'],
'autoId' => $autoId,
];
}
/**
* Get a collection from a database
*
* @param string $connection Database connection name.
* @return \Cake\Database\Schema\Collection
*/
public function getCollection($connection)
{
$connection = ConnectionManager::get($connection);
return $connection->getSchemaCollection();
}
/**
* To check if a Table Model is to be added in the migration file
*
* @param string $tableName Table name in underscore case.
* @param string|null $pluginName Plugin name if exists.
* @deprecated Will be removed in the next version
* @return bool True if the model is to be added.
*/
public function tableToAdd($tableName, $pluginName = null)
{
return true;
}
/**
* Gets the option parser instance and configures it.
*
* @return \Cake\Console\ConsoleOptionParser
*/
public function getOptionParser()
{
$parser = parent::getOptionParser();
$parser->setDescription(
'Bake migration snapshot class.'
)->addArgument('name', [
'help' => 'Name of the migration to bake. Can use Plugin.name to bake migration files into plugins.',
'required' => true,
])
->addOption('require-table', [
'boolean' => true,
'default' => false,
'help' => 'If require-table is set to true, check also that the table class exists.',
])->addOption('disable-autoid', [
'boolean' => true,
'default' => false,
'help' => 'Disable phinx behavior of automatically adding an id field.',
])
->addOption('no-lock', [
'help' => 'If present, no lock file will be generated after baking',
'boolean' => true,
]);
return $parser;
}
}
@@ -0,0 +1,134 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Shell\Task;
use Cake\Core\Configure;
use Cake\Event\Event;
use Cake\Event\EventManager;
use Cake\Utility\Inflector;
use Migrations\Util\ColumnParser;
/**
* Task class for generating migration snapshot files.
*
* @property \Bake\Shell\Task\BakeTemplateTask $BakeTemplate
* @property \Bake\Shell\Task\TestTask $Test
*/
class MigrationTask extends SimpleMigrationTask
{
/**
* {@inheritDoc}
*/
public function bake($name)
{
EventManager::instance()->on('Bake.initialize', function (Event $event) {
$event->getSubject()->loadHelper('Migrations.Migration');
});
return parent::bake($name);
}
/**
* {@inheritDoc}
*/
public function template()
{
return 'Migrations.config/skeleton';
}
/**
* {@inheritdoc}
*/
public function templateData()
{
$className = $this->BakeTemplate->viewVars['name'];
$namespace = Configure::read('App.namespace');
$pluginPath = '';
if ($this->plugin) {
$namespace = $this->_pluginNamespace($this->plugin);
$pluginPath = $this->plugin . '.';
}
$action = $this->detectAction($className);
if (empty($action)) {
return [
'plugin' => $this->plugin,
'pluginPath' => $pluginPath,
'namespace' => $namespace,
'tables' => [],
'action' => null,
'name' => $className,
];
}
$arguments = $this->args;
unset($arguments[0]);
$columnParser = new ColumnParser();
$fields = $columnParser->parseFields($arguments);
$indexes = $columnParser->parseIndexes($arguments);
$primaryKey = $columnParser->parsePrimaryKey($arguments);
if (in_array($action[0], ['alter_table', 'add_field']) && !empty($primaryKey)) {
$this->abort('Adding a primary key to an already existing table is not supported.');
}
list($action, $table) = $action;
return [
'plugin' => $this->plugin,
'pluginPath' => $pluginPath,
'namespace' => $namespace,
'tables' => [$table],
'action' => $action,
'columns' => [
'fields' => $fields,
'indexes' => $indexes,
'primaryKey' => $primaryKey,
],
'name' => $className,
];
}
/**
* Detects the action and table from the name of a migration
*
* @param string $name Name of migration
* @return array
**/
public function detectAction($name)
{
if (preg_match('/^(Create|Drop)(.*)/', $name, $matches)) {
$action = strtolower($matches[1]) . '_table';
$table = Inflector::underscore($matches[2]);
} elseif (preg_match('/^(Add).+?(?:To)(.*)/', $name, $matches)) {
$action = 'add_field';
$table = Inflector::underscore($matches[2]);
} elseif (preg_match('/^(Remove).+?(?:From)(.*)/', $name, $matches)) {
$action = 'drop_field';
$table = Inflector::underscore($matches[2]);
} elseif (preg_match('/^(Alter).+?(?:On)(.*)/', $name, $matches)) {
$action = 'alter_field';
$table = Inflector::underscore($matches[2]);
} elseif (preg_match('/^(Alter)(.*)/', $name, $matches)) {
$action = 'alter_table';
$table = Inflector::underscore($matches[2]);
} else {
return [];
}
return [$action, $table];
}
}
@@ -0,0 +1,50 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Shell\Task;
/**
* This task class is needed in order to provide a correct autocompletion feature
* when using the CakePHP migrations shell plugin. It has no effect on the
* migrations process.
*/
class RollbackTask extends CommandTask
{
/**
* {@inheritDoc}
*/
public function getOptionParser()
{
$parser = parent::getOptionParser();
$parser
->addOption('target', [
'short' => 't',
'help' => 'The version number to migrate to',
'required' => false,
])
->addOption('date', [
'short' => 'd',
'help' => 'The date to migrate to',
'required' => false,
])
->addOption('dry-run', ['short' => 'x'])
->addOption('fake', ['boolean' => true])
->addOption('no-lock', [
'help' => 'If present, no lock file will be generated after rolling back',
'boolean' => true,
]);
return $parser;
}
}
+258
View File
@@ -0,0 +1,258 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Shell\Task;
use Bake\Shell\Task\SimpleBakeTask;
use Cake\Console\ConsoleOptionParser;
use Cake\Core\Configure;
use Cake\Core\Plugin as CorePlugin;
use Cake\Datasource\ConnectionManager;
use Cake\Utility\Inflector;
/**
* Task class for generating seed files.
*
* @property \Bake\Shell\Task\BakeTemplateTask $BakeTemplate
* @property \Bake\Shell\Task\TestTask $Test
*/
class SeedTask extends SimpleBakeTask
{
/**
* path to Migration directory
*
* @var string
*/
public $pathFragment = 'config/Seeds/';
/**
* {@inheritDoc}
*/
public function name()
{
return 'seed';
}
/**
* {@inheritDoc}
*/
public function fileName($name)
{
return Inflector::camelize($name) . 'Seed.php';
}
/**
* {@inheritDoc}
*/
public function getPath()
{
$path = ROOT . DS . $this->pathFragment;
if (isset($this->plugin)) {
$path = $this->_pluginPath($this->plugin) . $this->pathFragment;
}
return str_replace('/', DS, $path);
}
/**
* {@inheritDoc}
*/
public function template()
{
return 'Migrations.Seed/seed';
}
/**
* Get template data.
*
* @return array
*/
public function templateData()
{
$namespace = Configure::read('App.namespace');
if ($this->plugin) {
$namespace = $this->_pluginNamespace($this->plugin);
}
$table = Inflector::tableize($this->args[0]);
if (!empty($this->params['table'])) {
$table = $this->params['table'];
}
$records = false;
if ($this->param('data')) {
$limit = (int)$this->param('limit');
$fields = $this->param('fields') ?: '*';
if ($fields !== '*') {
$fields = explode(',', $fields);
}
$connection = ConnectionManager::get($this->connection);
$query = $connection->newQuery()
->from($table)
->select($fields);
if ($limit) {
$query->limit($limit);
}
$records = $connection->execute($query)->fetchAll('assoc');
$records = $this->prettifyArray($records);
}
return [
'className' => $this->BakeTemplate->viewVars['name'],
'namespace' => $namespace,
'records' => $records,
'table' => $table,
];
}
/**
* {@inheritDoc}
*/
public function bake($name)
{
$this->params['no-test'] = true;
return parent::bake($name);
}
/**
* Gets the option parser instance and configures it.
*
* @return \Cake\Console\ConsoleOptionParser
*/
public function getOptionParser()
{
$name = ($this->plugin ? $this->plugin . '.' : '') . $this->name;
$parser = new ConsoleOptionParser($name);
$bakeThemes = [];
foreach (CorePlugin::loaded() as $plugin) {
$path = CorePlugin::classPath($plugin);
if (is_dir($path . 'Template' . DS . 'Bake')) {
$bakeThemes[] = $plugin;
}
}
$parser->setDescription(
'Bake seed class.'
)->addOption('plugin', [
'short' => 'p',
'help' => 'Plugin to bake into.',
])->addOption('force', [
'short' => 'f',
'boolean' => true,
'help' => 'Force overwriting existing files without prompting.',
])->addOption('connection', [
'short' => 'c',
'default' => 'default',
'help' => 'The datasource connection to get data from.',
])->addOption('table', [
'help' => 'The database table to use.',
])->addOption('theme', [
'short' => 't',
'help' => 'The theme to use when baking code.',
'choices' => $bakeThemes,
])->addArgument('name', [
'help' => 'Name of the seed to bake. Can use Plugin.name to bake plugin models.',
])->addOption('data', [
'boolean' => true,
'help' => 'Include data from the table to the seed',
])->addOption('fields', [
'default' => '*',
'help' => 'If including data, comma separated list of fields to select (all fields by default)',
])->addOption('limit', [
'short' => 'l',
'help' => 'If including data, max number of rows to select',
]);
return $parser;
}
/**
* Prettify var_export of an array output
*
* @param array $array Array to prettify
* @param int $tabCount Initial tab count
* @param string $indentCharacter Desired indent for the code.
* @return string
*/
protected function prettifyArray($array, $tabCount = 3, $indentCharacter = " ")
{
$content = var_export($array, true);
$lines = explode("\n", $content);
$inString = false;
foreach ($lines as $k => &$line) {
if ($k === 0) {
// First row
$line = '[';
continue;
}
if ($k === count($lines) - 1) {
// Last row
$line = str_repeat($indentCharacter, --$tabCount) . ']';
continue;
}
$line = ltrim($line);
if (!$inString) {
if ($line === '),') {
//Check for closing bracket
$line = '],';
$tabCount--;
} elseif (preg_match("/^\d+\s\=\>\s$/", $line)) {
// Mark '0 =>' kind of lines to remove
$line = false;
continue;
}
//Insert tab count
$line = str_repeat($indentCharacter, $tabCount) . $line;
}
$length = strlen($line);
for ($j = 0; $j < $length; $j++) {
if ($line[$j] === '\\') {
//skip character right after an escape \
$j++;
} elseif ($line[$j] === '\'') {
//check string open/end
$inString = !$inString;
}
}
//check for opening bracket
if (!$inString && trim($line) === 'array (') {
$line = str_replace('array (', '[', $line);
$tabCount++;
}
}
unset($line);
// Remove marked lines
$lines = array_filter($lines, function ($line) {
return $line !== false;
});
return implode("\n", $lines);
}
}
@@ -0,0 +1,165 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Shell\Task;
use Bake\Shell\Task\SimpleBakeTask;
use Cake\Console\ConsoleOptionParser;
use Cake\Core\Plugin as CorePlugin;
use Cake\Utility\Inflector;
use Phinx\Util\Util;
/**
* Task class for generating migration snapshot files.
*/
abstract class SimpleMigrationTask extends SimpleBakeTask
{
/**
* path to Migration directory
*
* @var string
*/
public $pathFragment = 'config/Migrations/';
/**
* {@inheritDoc}
*/
public function name()
{
return 'migration';
}
/**
* {@inheritDoc}
*/
public function fileName($name)
{
$name = $this->getMigrationName($name);
return Util::getCurrentTimestamp() . '_' . Inflector::camelize($name) . '.php';
}
/**
* {@inheritDoc}
*/
public function getPath()
{
$path = ROOT . DS . $this->pathFragment;
if (isset($this->plugin)) {
$path = $this->_pluginPath($this->plugin) . $this->pathFragment;
}
return str_replace('/', DS, $path);
}
/**
* {@inheritDoc}
*/
public function bake($name)
{
$migrationWithSameName = glob($this->getPath() . '*_' . $name . '.php');
if (!empty($migrationWithSameName)) {
$force = $this->param('force');
if (!$force) {
$this->abort(
sprintf(
'A migration with the name `%s` already exists. Please use a different name.',
$name
)
);
}
$this->info(sprintf('A migration with the name `%s` already exists, it will be deleted.', $name));
foreach ($migrationWithSameName as $migration) {
$this->info(sprintf('Deleting migration file `%s`...', $migration));
if (unlink($migration)) {
$this->success(sprintf('Deleted `%s`', $migration));
} else {
$this->err(sprintf('An error occurred while deleting `%s`', $migration));
}
}
}
$this->params['no-test'] = true;
return parent::bake($name);
}
/**
* Returns a class name for the migration class
*
* If the name is invalid, the task will exit
*
* @param string|null $name Name for the generated migration
* @return string|null Name of the migration file or null if empty
*/
protected function getMigrationName($name = null)
{
if (empty($name)) {
$this->abort('Choose a migration name to bake in CamelCase format');
}
$name = $this->_getName($name);
$name = Inflector::camelize($name);
if (!preg_match('/^[A-Z]{1}[a-zA-Z0-9]+$/', $name)) {
$this->abort('The className is not correct. The className can only contain "A-Z" and "0-9".');
}
return $name;
}
/**
* Gets the option parser instance and configures it.
*
* @return \Cake\Console\ConsoleOptionParser
*/
public function getOptionParser()
{
$name = ($this->plugin ? $this->plugin . '.' : '') . $this->name;
$parser = new ConsoleOptionParser($name);
$bakeThemes = [];
foreach (CorePlugin::loaded() as $plugin) {
$path = CorePlugin::classPath($plugin);
if (is_dir($path . 'Template' . DS . 'Bake')) {
$bakeThemes[] = $plugin;
}
}
$parser->setDescription(
'Bake migration class.'
)
->addOption('plugin', [
'short' => 'p',
'help' => 'Plugin to bake into.',
])
->addOption('force', [
'short' => 'f',
'boolean' => true,
'help' => 'Force overwriting existing file if a migration already exists with the same name.',
])
->addOption('connection', [
'short' => 'c',
'default' => 'default',
'help' => 'The datasource connection to get data from.',
])
->addOption('theme', [
'short' => 't',
'help' => 'The theme to use when baking code.',
'choices' => $bakeThemes,
]);
return $parser;
}
}
@@ -0,0 +1,97 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Shell\Task;
use Cake\Core\App;
use Cake\Core\Plugin;
use Cake\Database\Schema\Collection;
use Cake\Datasource\ConnectionManager;
use Cake\Filesystem\Folder;
use Cake\ORM\TableRegistry;
use ReflectionClass;
/**
* Trait needed for all "snapshot" type of bake operations.
* Snapshot type operations are : baking a snapshot and baking a diff.
*/
trait SnapshotTrait
{
/**
* After a file has been successfully created, we mark the newly
* created migration as applied
*
* @param string $path Where to put the file.
* @param string $contents Content to put in the file.
* @return bool Success
*/
public function createFile($path, $contents)
{
$createFile = parent::createFile($path, $contents);
if ($createFile) {
$this->markSnapshotApplied($path);
if (!isset($this->params['no-lock']) || !$this->params['no-lock']) {
$this->refreshDump();
}
}
return $createFile;
}
/**
* Will mark a snapshot created, the snapshot being identified by its
* full file path.
*
* @param string $path Path to the newly created snapshot
* @return void
*/
protected function markSnapshotApplied($path)
{
$fileName = pathinfo($path, PATHINFO_FILENAME);
list($version, ) = explode('_', $fileName, 2);
$dispatchCommand = 'migrations mark_migrated -t ' . $version . ' -o';
if (!empty($this->params['connection'])) {
$dispatchCommand .= ' -c ' . $this->params['connection'];
}
if (!empty($this->params['plugin'])) {
$dispatchCommand .= ' -p ' . $this->params['plugin'];
}
$this->_io->out('Marking the migration ' . $fileName . ' as migrated...');
$this->dispatchShell($dispatchCommand);
}
/**
* After a file has been successfully created, we refresh the dump of the database
* to be able to generate a new diff afterward.
*
* @return void
*/
protected function refreshDump()
{
$dispatchCommand = 'migrations dump';
if (!empty($this->params['connection'])) {
$dispatchCommand .= ' -c ' . $this->params['connection'];
}
if (!empty($this->params['plugin'])) {
$dispatchCommand .= ' -p ' . $this->params['plugin'];
}
$this->_io->out('Creating a dump of the new database state...');
$this->dispatchShell($dispatchCommand);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Shell\Task;
/**
* This task class is needed in order to provide a correct autocompletion feature
* when using the CakePHP migrations shell plugin. It has no effect on the
* migrations process.
*/
class StatusTask extends CommandTask
{
/**
* {@inheritDoc}
*/
public function getOptionParser()
{
$parser = parent::getOptionParser();
$parser->addOption('format', [
'short' => 'f',
'help' => 'The output format: text or json. Defaults to text.',
'required' => false,
]);
return $parser;
}
}
+203
View File
@@ -0,0 +1,203 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations;
use Cake\Collection\Collection;
use Cake\ORM\TableRegistry;
use Phinx\Db\Table as BaseTable;
class Table extends BaseTable
{
/**
* Primary key for this table.
* Can either be a string or an array in case of composite
* primary key.
*
* @var string|array
*/
protected $primaryKey;
/**
* Add a primary key to a database table.
*
* @param string|array $columns Table Column(s)
* @return Table
*/
public function addPrimaryKey($columns)
{
$this->primaryKey = $columns;
return $this;
}
/**
* You can pass `autoIncrement` as an option and it will be converted
* to the correct option for phinx to create the column with an
* auto increment attribute
*
* {@inheritdoc}
*/
public function addColumn($columnName, $type = null, $options = [])
{
$options = $this->convertedAutoIncrement($options);
return parent::addColumn($columnName, $type, $options);
}
/**
* You can pass `autoIncrement` as an option and it will be converted
* to the correct option for phinx to create the column with an
* auto increment attribute
*
* {@inheritdoc}
*/
public function changeColumn($columnName, $type, array $options = [])
{
$options = $this->convertedAutoIncrement($options);
return parent::changeColumn($columnName, $type, $options);
}
/**
* Convert the `autoIncrement` option to the correct options for phinx.
*
* @param array $options Options
* @return array Converted options
*/
protected function convertedAutoIncrement($options)
{
if (isset($options['autoIncrement']) && $options['autoIncrement'] === true) {
$options['identity'] = true;
unset($options['autoIncrement']);
}
return $options;
}
/**
* {@inheritdoc}
*
* If using MySQL and no collation information has been given to the table options, a request to the information
* schema will be made to get the default database collation and apply it to the database. This is to prevent
* phinx default mechanism to put the collation to a default of "utf8_general_ci".
*/
public function create()
{
$options = $this->getTable()->getOptions();
if ((!isset($options['id']) || $options['id'] === false) && !empty($this->primaryKey)) {
$options['primary_key'] = $this->primaryKey;
$this->filterPrimaryKey();
}
if ($this->getAdapter()->getAdapterType() === 'mysql' && empty($options['collation'])) {
$encodingRequest = 'SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME
FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = "%s"';
$cakeConnection = $this->getAdapter()->getCakeConnection();
$connectionConfig = $cakeConnection->config();
$encodingRequest = sprintf($encodingRequest, $connectionConfig['database']);
$defaultEncoding = $cakeConnection->execute($encodingRequest)->fetch('assoc');
if (!empty($defaultEncoding['DEFAULT_COLLATION_NAME'])) {
$options['collation'] = $defaultEncoding['DEFAULT_COLLATION_NAME'];
}
}
$this->getTable()->setOptions($options);
parent::create();
}
/**
* {@inheritdoc}
*
* After a table update, the TableRegistry should be cleared in order to prevent issues with
* table schema stored in Table objects having columns that might have been renamed or removed during
* the update process.
*/
public function update()
{
parent::update();
TableRegistry::clear();
}
/**
* {@inheritDoc}
*
* We disable foreign key deletion for the SQLite adapter as SQLite does not support the feature natively and the
* process implemented by Phinx has serious side-effects (for instance it rename FK references in existing tables
* which breaks the database schema cohesion).
*/
public function dropForeignKey($columns, $constraint = null)
{
if ($this->getAdapter()->getAdapterType() == 'sqlite') {
return $this;
}
return parent::dropForeignKey($columns, $constraint);
}
/**
* This method is called in case a primary key was defined using the addPrimaryKey() method.
* It currently does something only if using SQLite.
* If a column is an auto-increment key in SQLite, it has to be a primary key and it has to defined
* when defining the column. Phinx takes care of that so we have to make sure columns defined as autoincrement were
* not added with the addPrimaryKey method, otherwise, SQL queries will be wrong.
*
* @return void
*/
protected function filterPrimaryKey()
{
$options = $this->getTable()->getOptions();
if ($this->getAdapter()->getAdapterType() !== 'sqlite' || empty($options['primary_key'])) {
return;
}
$primaryKey = $options['primary_key'];
if (!is_array($primaryKey)) {
$primaryKey = [$primaryKey];
}
$primaryKey = array_flip($primaryKey);
$columnsCollection = (new Collection($this->actions->getActions()))
->filter(function ($action) {
return $action instanceof \Phinx\Db\Action\AddColumn;
})
->map(function ($action) {
return $action->getColumn();
});
$primaryKeyColumns = $columnsCollection->filter(function ($columnDef, $key) use ($primaryKey) {
return isset($primaryKey[$columnDef->getName()]);
})->toArray();
if (empty($primaryKeyColumns)) {
return;
}
foreach ($primaryKeyColumns as $primaryKeyColumn) {
if ($primaryKeyColumn->isIdentity()) {
unset($primaryKey[$primaryKeyColumn->getName()]);
}
}
$primaryKey = array_flip($primaryKey);
if (!empty($primaryKey)) {
$options['primary_key'] = $primaryKey;
} else {
unset($options['primary_key']);
}
$this->getTable()->setOptions($options);
}
}
+189
View File
@@ -0,0 +1,189 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations;
use Cake\Core\App;
use Cake\Core\Plugin as CorePlugin;
use Cake\Database\Schema\Collection;
use Cake\Datasource\ConnectionManager;
use Cake\Filesystem\Folder;
use Cake\ORM\TableRegistry;
use ReflectionClass;
trait TableFinderTrait
{
/**
* Tables to skip
*
* @var array
*/
public $skipTables = ['phinxlog'];
/**
* Regex of Table name to skip
*
* @var string
*/
public $skipTablesRegex = '_phinxlog';
/**
* Gets a list of table to baked based on the Collection instance passed and the options passed to
* the shell call.
*
* @param \Cake\Database\Schema\Collection $collection Instance of the collection of a specific database
* connection.
* @param array $options Array of options passed to a shell call.
* @return array
*/
protected function getTablesToBake(Collection $collection, $options = [])
{
$options = array_merge(['require-table' => false, 'plugin' => null], $options);
$tables = $collection->listTables();
if (empty($tables)) {
return $tables;
}
if ($options['require-table'] === true || $options['plugin']) {
$tableNamesInModel = $this->getTableNames($options['plugin']);
if (empty($tableNamesInModel)) {
return [];
}
foreach ($tableNamesInModel as $num => $table) {
if (strpos($table, '.') !== false) {
$splitted = array_reverse(explode('.', $table, 2));
$config = ConnectionManager::getConfig($this->connection);
$key = isset($config['schema']) ? 'schema' : 'database';
if ($config[$key] === $splitted[1]) {
$table = $splitted[0];
}
}
if (!in_array($table, $tables)) {
unset($tableNamesInModel[$num]);
}
}
$tables = $tableNamesInModel;
} else {
foreach ($tables as $num => $table) {
if ((in_array($table, $this->skipTables)) || (strpos($table, $this->skipTablesRegex) !== false)) {
unset($tables[$num]);
continue;
}
}
}
return $tables;
}
/**
* Gets list Tables Names
*
* @param string|null $pluginName Plugin name if exists.
* @return array
*/
protected function getTableNames($pluginName = null)
{
if ($pluginName !== null && !CorePlugin::getCollection()->has($pluginName)) {
return [];
}
$list = [];
$tables = $this->findTables($pluginName);
if (empty($tables)) {
return [];
}
foreach ($tables as $num => $table) {
$list = array_merge($list, $this->fetchTableName($table, $pluginName));
}
return array_unique($list);
}
/**
* Find Table Class
*
* @param string $pluginName Plugin name if exists.
* @return array
*/
protected function findTables($pluginName = null)
{
$path = 'Model' . DS . 'Table' . DS;
if ($pluginName) {
$path = CorePlugin::path($pluginName) . 'src' . DS . $path;
} else {
$path = APP . $path;
}
if (!is_dir($path)) {
return [];
}
$tableDir = new Folder($path);
$tableDir = $tableDir->find('.*\.php');
return $tableDir;
}
/**
* fetch TableName From Table Object
*
* @param string $className Name of Table Class.
* @param string|null $pluginName Plugin name if exists.
* @return array
*/
protected function fetchTableName($className, $pluginName = null)
{
$tables = [];
$className = str_replace('Table.php', '', $className);
if ($pluginName !== null) {
$className = $pluginName . '.' . $className;
}
$namespacedClassName = App::className($className, 'Model/Table', 'Table');
if (!class_exists($namespacedClassName)) {
return $tables;
}
$reflection = new ReflectionClass($namespacedClassName);
if (!$reflection->isInstantiable()) {
return $tables;
}
$table = TableRegistry::get($className);
foreach ($table->associations()->keys() as $key) {
if ($table->associations()->get($key)->type() === 'belongsToMany') {
$tables[] = $table->associations()->get($key)->junction()->getTable();
}
}
$tableName = $table->getTable();
$splitted = array_reverse(explode('.', $tableName, 2));
if (isset($splitted[1])) {
$config = ConnectionManager::getConfig($this->connection);
if ($config) {
$key = isset($config['schema']) ? 'schema' : 'database';
if ($config[$key] === $splitted[1]) {
$tableName = $splitted[0];
}
}
}
$tables[] = $tableName;
return $tables;
}
}
@@ -0,0 +1,12 @@
<% foreach ($columns as $columnName => $columnAttributes):
$type = $columnAttributes['type'];
unset($columnAttributes['type']);
$columnAttributes = $this->Migration->getColumnOption($columnAttributes);
$columnAttributes = $this->Migration->stringifyList($columnAttributes, ['indent' => 4]);
if (!empty($columnAttributes)): %>
->addColumn('<%= $columnName %>', '<%= $type %>', [<%= $columnAttributes %>])
<% else: %>
->addColumn('<%= $columnName %>', '<%= $type %>')
<% endif; %>
<% endforeach; %>
@@ -0,0 +1,3 @@
<% foreach ($constraints as $table => $tableConstraints):
echo $this->element('Migrations.add-foreign-keys', ['constraints' => $tableConstraints, 'table' => $table]);
endforeach; %>
@@ -0,0 +1,44 @@
<%
$statement = $this->Migration->tableStatement($table, true);
$hasProcessedConstraint = false;
%>
<% foreach ($constraints as $constraint):
$constraintColumns = $constraint['columns'];
sort($constraintColumns);
if ($constraint['type'] !== 'unique'):
$hasProcessedConstraint = true;
$columnsList = '\'' . $constraint['columns'][0] . '\'';
if (count($constraint['columns']) > 1):
$columnsList = '[' . $this->Migration->stringifyList($constraint['columns'], ['indent' => 5]) . ']';
endif;
$this->Migration->returnedData['dropForeignKeys'][$table][] = $columnsList;
if (is_array($constraint['references'][1])):
$columnsReference = '[' . $this->Migration->stringifyList($constraint['references'][1], ['indent' => 5]) . ']';
else:
$columnsReference = '\'' . $constraint['references'][1] . '\'';
endif;
if (!isset($statement)):
$statement = $this->Migration->tableStatement($table);
endif;
if (!empty($statement)): %>
<%= $statement %>
<% unset($statement);
endif; %>
->addForeignKey(
<%= $columnsList %>,
'<%= $constraint['references'][0] %>',
<%= $columnsReference %>,
[
'update' => '<%= $this->Migration->formatConstraintAction($constraint['update']) %>',
'delete' => '<%= $this->Migration->formatConstraintAction($constraint['delete']) %>'
]
)
<% endif; %>
<% endforeach; %>
<% if (isset($this->Migration->tableStatements[$table]) && $hasProcessedConstraint): %>
->update();
<% endif; %>
@@ -0,0 +1,12 @@
<% foreach ($indexes as $indexName => $index): %>
->addIndex(
[<% echo $this->Migration->stringifyList($index['columns'], ['indent' => 5]); %>],
[<%
$params = ['name' => $indexName];
if ($index['type'] === 'unique'):
$params['unique'] = true;
endif;
echo $this->Migration->stringifyList($params, ['indent' => 5]);
%>]
)
<% endforeach; %>
@@ -0,0 +1,83 @@
<% foreach ($tables as $table => $schema):
$tableArgForMethods = $useSchema === true ? $schema : $table;
$tableArgForArray = $useSchema === true ? $table : $schema;
$foreignKeys = [];
$primaryKeysColumns = $this->Migration->primaryKeysColumnsList($tableArgForMethods);
$primaryKeys = $this->Migration->primaryKeys($tableArgForMethods);
$specialPk = $primaryKeys && (count($primaryKeys) > 1 || $primaryKeys[0]['name'] !== 'id' || $primaryKeys[0]['info']['columnType'] !== 'integer') && $autoId;
%>
<% if ($specialPk): %>
$this->table('<%= $tableArgForArray %>', ['id' => false, 'primary_key' => ['<%= implode("', '", \Cake\Utility\Hash::extract($primaryKeys, '{n}.name')) %>']])
<% elseif (!$primaryKeys && $autoId): %>
$this->table('<%= $tableArgForArray %>', ['id' => false])
<% else: %>
$this->table('<%= $tableArgForArray %>')
<% endif; %>
<% if ($specialPk || !$autoId):
foreach ($primaryKeys as $primaryKey) :
%>
->addColumn('<%= $primaryKey['name'] %>', '<%= $primaryKey['info']['columnType'] %>', [<%
$columnOptions = $this->Migration->getColumnOption($primaryKey['info']['options']);
echo $this->Migration->stringifyList($columnOptions, ['indent' => 4]);
%>])
<% endforeach; %>
<% if (!$autoId && $primaryKeys): %>
->addPrimaryKey(['<%= implode("', '", \Cake\Utility\Hash::extract($primaryKeys, '{n}.name')) %>'])
<% endif; %>
<% endif;
foreach ($this->Migration->columns($tableArgForMethods) as $column => $config):
%>
->addColumn('<%= $column %>', '<%= $config['columnType'] %>', [<%
$columnOptions = $this->Migration->getColumnOption($config['options']);
if ($config['columnType'] === 'boolean' && isset($columnOptions['default']) && $this->Migration->value($columnOptions['default']) !== 'null'):
$columnOptions['default'] = (bool)$columnOptions['default'];
endif;
echo $this->Migration->stringifyList($columnOptions, ['indent' => 4]);
%>])
<% endforeach;
$tableConstraints = $this->Migration->constraints($tableArgForMethods);
if (!empty($tableConstraints)):
sort($tableConstraints);
$constraints[$tableArgForArray] = $tableConstraints;
foreach ($constraints[$tableArgForArray] as $name => $constraint):
if ($constraint['type'] === 'foreign'):
$foreignKeys[] = $constraint['columns'];
endif;
if ($constraint['columns'] !== $primaryKeysColumns): %>
->addIndex(
[<% echo $this->Migration->stringifyList($constraint['columns'], ['indent' => 5]); %>]<% echo ($constraint['type'] === 'unique') ? ',' : ''; %>
<% if ($constraint['type'] === 'unique'): %>
['unique' => true]
<% endif; %>
)
<% endif;
endforeach;
endif;
foreach($this->Migration->indexes($tableArgForMethods) as $index):
sort($foreignKeys);
$indexColumns = $index['columns'];
sort($indexColumns);
if (!in_array($indexColumns, $foreignKeys)):
%>
->addIndex(
[<%
echo $this->Migration->stringifyList($index['columns'], ['indent' => 5]);
%>]<% echo ($index['type'] === 'fulltext') ? ',' : ''; %>
<%- if ($index['type'] === 'fulltext'): %>
['type' => 'fulltext']
<%- endif; %>
)
<% endif;
endforeach; %>
->create();
<% endforeach; %>
<% if (!empty($constraints)): %>
<% echo $this->element('Migrations.add-foreign-keys-from-create', ['constraints' => $constraints]); %>
<% endif; %>
@@ -0,0 +1,45 @@
<%
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since 0.1.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
%>
<?php
use Migrations\AbstractSeed;
/**
* <%= $name %> seed.
*/
class <%= $name %>Seed extends AbstractSeed
{
/**
* Run Method.
*
* Write your database seeder using this method.
*
* More information on writing seeds is available here:
* https://book.cakephp.org/phinx/0/en/seeding.html
*
* @return void
*/
public function run()
{
<% if ($records): %>
$data = <%= $records %>;
<% else: %>
$data = [];
<% endif; %>
$table = $this->table('<%= $table %>');
$table->insert($data)->save();
}
}
@@ -0,0 +1,205 @@
<%
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
$tables = $data['fullTables'];
unset($data['fullTables']);
$constraints = [];
$hasUnsignedPk = $this->Migration->hasUnsignedPrimaryKey($tables['add']);
$autoId = true;
if ($hasUnsignedPk) {
$autoId = false;
}
%>
<?php
use Migrations\AbstractMigration;
class <%= $name %> extends AbstractMigration
{
<%- if (!$autoId): %>
public $autoId = false;
<%- endif; %>
public function up()
{
<%- foreach ($data as $tableName => $tableDiff):
$hasRemoveFK = !empty($tableDiff['constraints']['remove']) || !empty($tableDiff['indexes']['remove']);
%>
<%- if ($hasRemoveFK): %>
$this->table('<%= $tableName %>')
<%- endif; %>
<%- if (!empty($tableDiff['constraints']['remove'])): %>
<%- foreach ($tableDiff['constraints']['remove'] as $constraintName => $constraintDefinition): %>
->dropForeignKey([], '<%= $constraintName %>')
<%- endforeach; %>
<%- endif; %>
<%- if (!empty($tableDiff['indexes']['remove'])): %>
<%- foreach ($tableDiff['indexes']['remove'] as $indexName => $indexDefinition): %>
->removeIndexByName('<%= $indexName %>')
<%- endforeach; %>
<%- endif; %>
<%- if ($hasRemoveFK): %>
->update();
<%- endif; %>
<%- if (!empty($tableDiff['columns']['remove']) || !empty($tableDiff['columns']['changed'])): %>
<%= $this->Migration->tableStatement($tableName, true) %>
<%- if (!empty($tableDiff['columns']['remove'])): %>
<%- foreach ($tableDiff['columns']['remove'] as $columnName => $columnDefinition): %>
->removeColumn('<%= $columnName %>')
<%- endforeach; %>
<%- endif; %>
<%- if (!empty($tableDiff['columns']['changed'])): %>
<%- foreach ($tableDiff['columns']['changed'] as $columnName => $columnAttributes):
$type = $columnAttributes['type'];
unset($columnAttributes['type']);
$columnAttributes = $this->Migration->getColumnOption($columnAttributes);
$columnAttributes = $this->Migration->stringifyList($columnAttributes, ['indent' => 4]);
if (!empty($columnAttributes)): %>
->changeColumn('<%= $columnName %>', '<%= $type %>', [<%= $columnAttributes %>])
<%- else: %>
->changeColumn('<%= $columnName %>', '<%= $type %>')
<%- endif; %>
<%- endforeach; %>
<%- endif; %>
<%- if (isset($this->Migration->tableStatements[$tableName])): %>
->update();
<%- endif; %>
<%- endif; %>
<%- endforeach; %>
<%- if (!empty($tables['add'])): %>
<%- echo $this->element('Migrations.create-tables', ['tables' => $tables['add'], 'autoId' => $autoId, 'useSchema' => true]) %>
<%- endif; %>
<%- foreach ($data as $tableName => $tableDiff): %>
<%- if (!empty($tableDiff['columns']['add']) || !empty($tableDiff['indexes']['add'])): %>
<%= $this->Migration->tableStatement($tableName, true) %>
<%- if (!empty($tableDiff['columns']['add'])): %>
<%- echo $this->element('Migrations.add-columns', ['columns' => $tableDiff['columns']['add']]) %>
<%- endif; %>
<%- if (!empty($tableDiff['indexes']['add'])): %>
<%- echo $this->element('Migrations.add-indexes', ['indexes' => $tableDiff['indexes']['add']]) %>
<%- endif;
if (isset($this->Migration->tableStatements[$tableName])): %>
->update();
<%- endif; %>
<%- endif; %>
<%- endforeach; %>
<%- foreach ($data as $tableName => $tableDiff): %>
<%- if (!empty($tableDiff['constraints']['add'])): %>
<%- echo $this->element(
'Migrations.add-foreign-keys',
['constraints' => $tableDiff['constraints']['add'], 'table' => $tableName]
); %>
<%- endif; %>
<%- endforeach; %>
<%- if (!empty($tables['remove'])): %>
<%- foreach ($tables['remove'] as $tableName => $table): %>
$this->table('<%= $tableName %>')->drop()->save();
<%- endforeach; %>
<%- endif; %>
}
public function down()
{
<%- $constraints = [];
$emptyLine = false;
if (!empty($this->Migration->returnedData['dropForeignKeys'])):
foreach ($this->Migration->returnedData['dropForeignKeys'] as $table => $columnsList):
$maxKey = count($columnsList) - 1;
if ($emptyLine === true): %>
<%- else:
$emptyLine = true;
endif; %>
$this->table('<%= $table %>')
<%- foreach ($columnsList as $key => $columns): %>
->dropForeignKey(
<%= $columns %>
)<%= ($key === $maxKey) ? '->save();' : '' %>
<%- endforeach; %>
<%- endforeach; %>
<%- endif; %>
<%- if (!empty($tables['remove'])): %>
<%- echo $this->element('Migrations.create-tables', ['tables' => $tables['remove'], 'autoId' => $autoId, 'useSchema' => true]) %>
<%- endif; %>
<%- foreach ($data as $tableName => $tableDiff):
unset($this->Migration->tableStatements[$tableName]);
if (!empty($tableDiff['indexes']['add'])): %>
$this->table('<%= $tableName %>')
<%- foreach ($tableDiff['indexes']['add'] as $indexName => $index): %>
->removeIndexByName('<%= $indexName %>')
<%- endforeach %>
->update();
<%- endif; %>
<%- if (!empty($tableDiff['columns']['remove']) ||
!empty($tableDiff['columns']['changed']) ||
!empty($tableDiff['columns']['add']) ||
!empty($tableDiff['indexes']['remove'])
): %>
<%= $this->Migration->tableStatement($tableName, true) %>
<%- endif; %>
<%- if (!empty($tableDiff['columns']['remove'])): %>
<%- echo $this->element('Migrations.add-columns', ['columns' => $tableDiff['columns']['remove']]) %>
<%- endif; %>
<%- if (!empty($tableDiff['columns']['changed'])):
$oldTableDef = $dumpSchema[$tableName];
foreach ($tableDiff['columns']['changed'] as $columnName => $columnAttributes):
$columnAttributes = $oldTableDef->getColumn($columnName);
$type = $columnAttributes['type'];
unset($columnAttributes['type']);
$columnAttributes = $this->Migration->getColumnOption($columnAttributes);
$columnAttributes = $this->Migration->stringifyList($columnAttributes, ['indent' => 4]);
if (!empty($columnAttributes)): %>
->changeColumn('<%= $columnName %>', '<%= $type %>', [<%= $columnAttributes %>])
<%- else: %>
->changeColumn('<%= $columnName %>', '<%= $type %>')
<%- endif; %>
<%- endforeach; %>
<%- endif; %>
<%- if (!empty($tableDiff['columns']['add'])): %>
<%- foreach ($tableDiff['columns']['add'] as $columnName => $columnAttributes): %>
->removeColumn('<%= $columnName %>')
<%- endforeach; %>
<%- endif; %>
<%- if (!empty($tableDiff['indexes']['remove'])): %>
<%- echo $this->element('Migrations.add-indexes', ['indexes' => $tableDiff['indexes']['remove']]) %>
<%- endif;
if (isset($this->Migration->tableStatements[$tableName])): %>
->update();
<%- endif; %>
<%- endforeach; %>
<%- foreach ($data as $tableName => $tableDiff): %>
<%- if (!empty($tableDiff['constraints']['remove'])): %>
<%- echo $this->element(
'Migrations.add-foreign-keys',
['constraints' => $tableDiff['constraints']['remove'], 'table' => $tableName]
); %>
<%- endif; %>
<%- endforeach; %>
<%- if (!empty($tables['add'])): %>
<%- foreach ($tables['add'] as $tableName => $table): %>
$this->table('<%= $tableName %>')->drop()->save();
<%- endforeach; %>
<%- endif; %>
}
}
@@ -0,0 +1,79 @@
<%
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
$wantedOptions = array_flip(['length', 'limit', 'default', 'unsigned', 'null', 'comment', 'autoIncrement', 'precision', 'scale']);
$tableMethod = $this->Migration->tableMethod($action);
$columnMethod = $this->Migration->columnMethod($action);
$indexMethod = $this->Migration->indexMethod($action);
%>
<?php
use Migrations\AbstractMigration;
class <%= $name %> extends AbstractMigration
{
<%- if ($tableMethod === 'create' && !empty($columns['primaryKey'])): %>
public $autoId = false;
<%- endif; %>
/**
* Change Method.
*
* More information on this method is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
* @return void
*/
public function change()
{
<% foreach ($tables as $table): %>
$table = $this->table('<%= $table%>');
<% if ($tableMethod !== 'drop') : %>
<% if ($columnMethod === 'removeColumn'): %>
<% foreach ($columns['fields'] as $column => $config): %>
<%= "\$table->$columnMethod('" . $column . "');"; %>
<% endforeach; %>
<% foreach ($columns['indexes'] as $column => $config): %>
<%= "\$table->$indexMethod([" . $this->Migration->stringifyList($config['columns']) . ");"; %>
<% endforeach; %>
<% else : %>
<% foreach ($columns['fields'] as $column => $config): %>
$table-><%= $columnMethod %>('<%= $column %>', '<%= $config['columnType'] %>', [<%
$columnOptions = $config['options'];
$columnOptions = array_intersect_key($columnOptions, $wantedOptions);
if (empty($columnOptions['comment'])) {
unset($columnOptions['comment']);
}
echo $this->Migration->stringifyList($columnOptions, ['indent' => 3]);
%>]);
<% endforeach; %>
<% foreach ($columns['indexes'] as $column => $config): %>
$table-><%= $indexMethod %>([<%=
$this->Migration->stringifyList($config['columns'], ['indent' => 3])
%>], [<%
$options = [];
echo $this->Migration->stringifyList($config['options'], ['indent' => 3]);
%>]);
<% endforeach; %>
<% if ($tableMethod === 'create' && !empty($columns['primaryKey'])): %>
$table->addPrimaryKey([<%=
$this->Migration->stringifyList($columns['primaryKey'], ['indent' => 3])
%>]);
<% endif; %>
<% endif; %>
<% endif; %>
$table-><%= $tableMethod %>();
<% endforeach; %>
}
}
@@ -0,0 +1,60 @@
<%
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use Cake\Database\Schema\Table;
$constraints = $foreignKeys = $dropForeignKeys = [];
$hasUnsignedPk = $this->Migration->hasUnsignedPrimaryKey($tables);
if ($autoId && $hasUnsignedPk) {
$autoId = false;
}
%>
<?php
use Migrations\AbstractMigration;
class <%= $name %> extends AbstractMigration
{
<%- if (!$autoId): %>
public $autoId = false;
<%- endif; %>
public function up()
{
<%- echo $this->element('Migrations.create-tables', ['tables' => $tables, 'autoId' => $autoId, 'useSchema' => false]) %>
}
public function down()
{
<%- if (!empty($this->Migration->returnedData['dropForeignKeys'])):
foreach ($this->Migration->returnedData['dropForeignKeys'] as $table => $columnsList):
$maxKey = count($columnsList) - 1;
%>
$this->table('<%= $table %>')
<%- foreach ($columnsList as $key => $columns): %>
->dropForeignKey(
<%= $columns %>
)<%= ($key === $maxKey) ? '->save();' : '' %>
<%- endforeach; %>
<%- endforeach;
endif;
%>
<%- foreach ($tables as $table): %>
$this->table('<%= $table%>')->drop()->save();
<%- endforeach; %>
}
}
@@ -0,0 +1,16 @@
<?php
use $useClassName;
class $className extends $baseClassName
{
/**
* Change Method.
*
* More information on this method is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
* @return void
*/
public function change()
{
}
}
+282
View File
@@ -0,0 +1,282 @@
<?php
namespace Migrations\Util;
use Cake\Collection\Collection;
use Cake\Utility\Hash;
use ReflectionClass;
/**
* Utility class used to parse arguments passed to a ``bake migration`` class
*/
class ColumnParser
{
/**
* Regex used to parse the column definition passed through the shell
*
* @var string
*/
protected $regexpParseColumn = '/
^
(\w+)
(?::(\w+\??
(?:\[
(?:[0-9]|[1-9][0-9]+)
(?:,(?:[0-9]|[1-9][0-9]+))?
\])?
))?
(?::(\w+))?
(?::(\w+))?
$
/x';
/**
* Regex used to parse the field type and length
*
* @var string
*/
protected $regexpParseField = '/(\w+\??)\[([0-9,]+)\]/';
/**
* Parses a list of arguments into an array of fields
*
* @param array $arguments A list of arguments being parsed
* @return array
*/
public function parseFields($arguments)
{
$fields = [];
$arguments = $this->validArguments($arguments);
foreach ($arguments as $field) {
preg_match($this->regexpParseColumn, $field, $matches);
$field = $matches[1];
$type = Hash::get($matches, 2);
$indexType = Hash::get($matches, 3);
$typeIsPk = in_array($type, ['primary', 'primary_key']);
$isPrimaryKey = false;
if ($typeIsPk || in_array($indexType, ['primary', 'primary_key'])) {
$isPrimaryKey = true;
if ($typeIsPk) {
$type = 'primary';
}
}
$nullable = (bool)strpos($type, '?');
$type = $nullable ? str_replace('?', '', $type) : $type;
list($type, $length) = $this->getTypeAndLength($field, $type);
$fields[$field] = [
'columnType' => $type,
'options' => [
'null' => $nullable,
'default' => null,
],
];
if ($length !== null) {
if (is_array($length)) {
list($fields[$field]['options']['precision'], $fields[$field]['options']['scale']) = $length;
} else {
$fields[$field]['options']['limit'] = $length;
}
}
if ($isPrimaryKey === true && $type === 'integer') {
$fields[$field]['options']['autoIncrement'] = true;
}
}
return $fields;
}
/**
* Parses a list of arguments into an array of indexes
*
* @param array $arguments A list of arguments being parsed
* @return array
*/
public function parseIndexes($arguments)
{
$indexes = [];
$arguments = $this->validArguments($arguments);
foreach ($arguments as $field) {
preg_match($this->regexpParseColumn, $field, $matches);
$field = $matches[1];
$type = Hash::get($matches, 2);
$indexType = Hash::get($matches, 3);
$indexName = Hash::get($matches, 4);
if (
in_array($type, ['primary', 'primary_key']) ||
in_array($indexType, ['primary', 'primary_key']) ||
$indexType === null
) {
continue;
}
$indexUnique = false;
if ($indexType === 'unique') {
$indexUnique = true;
}
$indexName = $this->getIndexName($field, $indexType, $indexName, $indexUnique);
if (empty($indexes[$indexName])) {
$indexes[$indexName] = [
'columns' => [],
'options' => [
'unique' => $indexUnique,
'name' => $indexName,
],
];
}
$indexes[$indexName]['columns'][] = $field;
}
return $indexes;
}
/**
* Parses a list of arguments into an array of fields composing the primary key
* of the table
*
* @param array $arguments A list of arguments being parsed
* @return array
*/
public function parsePrimaryKey($arguments)
{
$primaryKey = [];
$arguments = $this->validArguments($arguments);
foreach ($arguments as $field) {
preg_match($this->regexpParseColumn, $field, $matches);
$field = $matches[1];
$type = Hash::get($matches, 2);
$indexType = Hash::get($matches, 3);
if (in_array($type, ['primary', 'primary_key']) || in_array($indexType, ['primary', 'primary_key'])) {
$primaryKey[] = $field;
}
}
return $primaryKey;
}
/**
* Returns a list of only valid arguments
*
* @param array $arguments A list of arguments
* @return array
*/
public function validArguments($arguments)
{
$collection = new Collection($arguments);
return $collection->filter(function ($value, $field) {
return preg_match($this->regexpParseColumn, $field);
})->toArray();
}
/**
* Get the type and length of a field based on the field and the type passed
*
* @param string $field Name of field
* @param string $type User-specified type
* @return array First value is the field type, second value is the field length. If no length
* can be extracted, null is returned for the second value
*/
public function getTypeAndLength($field, $type)
{
if (preg_match($this->regexpParseField, $type, $matches)) {
if (strpos($matches[2], ',') !== false) {
$matches[2] = explode(',', $matches[2]);
}
return [$matches[1], $matches[2]];
}
$fieldType = $this->getType($field, $type);
$length = $this->getLength($fieldType);
return [$fieldType, $length];
}
/**
* Retrieves a type that should be used for a specific field
*
* @param string $field Name of field
* @param string $type User-specified type
* @return string
*/
public function getType($field, $type)
{
$reflector = new ReflectionClass('Phinx\Db\Adapter\AdapterInterface');
$collection = new Collection($reflector->getConstants());
$validTypes = $collection->filter(function ($value, $constant) {
return substr($constant, 0, strlen('PHINX_TYPE_')) === 'PHINX_TYPE_';
})->toArray();
$fieldType = $type;
if ($type === null || !in_array($type, $validTypes)) {
if ($type === 'primary') {
$fieldType = 'integer';
} elseif ($field === 'id') {
$fieldType = 'integer';
} elseif (in_array($field, ['created', 'modified', 'updated']) || substr($field, -3) === '_at') {
$fieldType = 'datetime';
} elseif (in_array($field, ['latitude', 'longitude'])) {
$fieldType = 'decimal';
} else {
$fieldType = 'string';
}
}
return $fieldType;
}
/**
* Returns the default length to be used for a given fie
*
* @param string $type User-specified type
* @return int
*/
public function getLength($type)
{
$length = null;
if ($type === 'string') {
$length = 255;
} elseif ($type === 'integer') {
$length = 11;
} elseif ($type === 'biginteger') {
$length = 20;
} elseif ($type === 'decimal') {
$length = [10, 6];
}
return $length;
}
/**
* Returns the default length to be used for a given fie
*
* @param string $field Name of field
* @param string $indexType Type of index
* @param string $indexName Name of index
* @param bool $indexUnique Whether this is a unique index or not
* @return string
*/
public function getIndexName($field, $indexType, $indexName, $indexUnique)
{
if (empty($indexName)) {
$indexName = strtoupper('BY_' . $field);
if ($indexType === 'primary') {
$indexName = 'PRIMARY';
} elseif ($indexUnique) {
$indexName = strtoupper('UNIQUE_' . $field);
}
}
return $indexName;
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Util;
use Cake\Datasource\ConnectionManager;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Trait gathering schema collection used for caching
*/
trait SchemaTrait
{
/**
* Helper method to get the schema collection.
*
* @param \Symfony\Component\Console\Input\InputInterface $input Input object.
* @param \Symfony\Component\Console\Input\OutputInterface $output Output object.
* @return null|\Cake\Database\Schema\Collection
*/
protected function _getSchema(InputInterface $input, OutputInterface $output)
{
$connectionName = $input->getOption('connection');
$connection = ConnectionManager::get($connectionName);
if (!method_exists($connection, 'getSchemaCollection')) {
$msg = sprintf(
'The "%s" connection is not compatible with orm caching, ' .
'as it does not implement a "getSchemaCollection()" method.',
$connectionName
);
$output->writeln('<error>' . $msg . '</error>');
return null;
}
$config = $connection->config();
if (empty($config['cacheMetadata'])) {
$output->writeln('Metadata cache was disabled in config. Enable to cache or clear.');
return null;
}
$connection->cacheMetadata(true);
return $connection->getSchemaCollection();
}
}
+82
View File
@@ -0,0 +1,82 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Util;
use Cake\Core\Plugin as CorePlugin;
use Cake\Utility\Inflector;
use Symfony\Component\Console\Input\InputInterface;
/**
* Trait gathering useful methods needed in various places of the plugin
*/
trait UtilTrait
{
/**
* Get the plugin name based on the current InputInterface
*
* @param \Symfony\Component\Console\Input\InputInterface $input Input of the current command.
* @return string|null
*/
protected function getPlugin(InputInterface $input)
{
$plugin = $input->getOption('plugin') ?: null;
return $plugin;
}
/**
* Get the phinx table name used to store migrations data
*
* @param string $plugin Plugin name
* @return string
*/
protected function getPhinxTable($plugin = null)
{
$table = 'phinxlog';
if (empty($plugin)) {
return $table;
}
$plugin = Inflector::underscore($plugin) . '_';
$plugin = str_replace(['\\', '/', '.'], '_', $plugin);
return $plugin . $table;
}
/**
* Get the migrations or seeds files path based on the current InputInterface
*
* @param \Symfony\Component\Console\Input\InputInterface $input Input of the current command.
* @param string $default Default folder to set if no source option is found in the $input param
* @return string
*/
protected function getOperationsPath(InputInterface $input, $default = 'Migrations')
{
$folder = $input->getOption('source') ?: $default;
$dir = ROOT . DS . 'config' . DS . $folder;
if (defined('CONFIG')) {
$dir = CONFIG . $folder;
}
$plugin = $this->getPlugin($input);
if ($plugin !== null) {
$dir = CorePlugin::path($plugin) . 'config' . DS . $folder;
}
return $dir;
}
}
@@ -0,0 +1,510 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\View\Helper;
use Cake\Database\Schema\Table;
use Cake\Utility\Hash;
use Cake\Utility\Inflector;
use Cake\View\Helper;
use Cake\View\View;
/**
* Migration Helper class for output of field data in migration files.
*
* MigrationHelper encloses all methods needed while working with HTML pages.
*/
class MigrationHelper extends Helper
{
/**
* Schemas list for tables analyzed during migration baking
*
* @var array
*/
protected $schemas = [];
/**
* Stores the ``$this->table()`` statements issued while baking.
* It helps prevent duplicate calls in case of complex conditions
*
* @var array
*/
public $tableStatements = [];
public $returnedData = [];
/**
* Constructor
*
* ### Settings
*
* - `collection` \Cake\Database\Schema\Collection
*
* @param \Cake\View\View $View The View this helper is being attached to.
* @param array $config Configuration settings for the helper.
*/
public function __construct(View $View, array $config = [])
{
parent::__construct($View, $config);
}
/**
* Returns the method to be used for the Table::save()
*
* @param string $action Name of action to take against the table
* @return string
*/
public function tableMethod($action)
{
if ($action === 'drop_table') {
return 'drop';
}
if ($action === 'create_table') {
return 'create';
}
return 'update';
}
/**
* Returns the method to be used for the index manipulation
*
* @param string $action Name of action to take against the table
* @return string
*/
public function indexMethod($action)
{
if ($action === 'drop_field') {
return 'removeIndex';
}
return 'addIndex';
}
/**
* Returns the method to be used for the column manipulation
*
* @param string $action Name of action to take against the table
* @return string
*/
public function columnMethod($action)
{
if ($action === 'drop_field') {
return 'removeColumn';
}
if ($action === 'alter_field') {
return 'changeColumn';
}
return 'addColumn';
}
/**
* Returns the Cake\Database\Schema\Table for $table
*
* @param string $table Name of the table to get the Schema for
* @return \Cake\Database\Schema\Table
*/
protected function schema($table)
{
if (isset($this->schemas[$table])) {
return $this->schemas[$table];
}
if ($table instanceof Table) {
return $this->schemas[$table->name()] = $table;
}
$collection = $this->getConfig('collection');
$schema = $collection->describe($table);
$this->schemas[$table] = $schema;
return $schema;
}
/**
* Returns an array of column data for a given table
*
* @param string $table Name of the table to retrieve columns for
* @return array
*/
public function columns($table)
{
$tableSchema = $table;
if (!($table instanceof Table)) {
$tableSchema = $this->schema($table);
}
$columns = [];
$tablePrimaryKeys = $tableSchema->primaryKey();
foreach ($tableSchema->columns() as $column) {
if (in_array($column, $tablePrimaryKeys)) {
continue;
}
$columns[$column] = $this->column($tableSchema, $column);
}
return $columns;
}
/**
* Returns an array of indexes for a given table
*
* @param string $table Name of the table to retrieve indexes for
* @return array
*/
public function indexes($table)
{
$tableSchema = $table;
if (!($table instanceof Table)) {
$tableSchema = $this->schema($table);
}
$tableIndexes = $tableSchema->indexes();
$indexes = [];
if (!empty($tableIndexes)) {
foreach ($tableIndexes as $name) {
$indexes[$name] = $tableSchema->getIndex($name);
}
}
return $indexes;
}
/**
* Returns an array of constraints for a given table
*
* @param string $table Name of the table to retrieve constraints for
* @return array
*/
public function constraints($table)
{
$tableSchema = $table;
if (!($table instanceof Table)) {
$tableSchema = $this->schema($table);
}
$constraints = [];
$tableConstraints = $tableSchema->constraints();
if (empty($tableConstraints)) {
return $constraints;
}
if ($tableConstraints[0] === 'primary') {
unset($tableConstraints[0]);
}
if (!empty($tableConstraints)) {
foreach ($tableConstraints as $name) {
$constraint = $tableSchema->getConstraint($name);
if (isset($constraint['update'])) {
$constraint['update'] = $this->formatConstraintAction($constraint['update']);
$constraint['delete'] = $this->formatConstraintAction($constraint['delete']);
}
$constraints[$name] = $constraint;
}
}
return $constraints;
}
/**
* Format a constraint action if it is not already in the format expected by Phinx
*
* @param string $constraint Constraint action name
* @return string Constraint action name altered if needed.
*/
public function formatConstraintAction($constraint)
{
if (defined('\Phinx\Db\Table\ForeignKey::' . $constraint)) {
return $constraint;
}
return strtoupper(Inflector::underscore($constraint));
}
/**
* Returns the primary key data for a given table
*
* @param string $table Name of the table ot retrieve primary key for
* @return array
*/
public function primaryKeys($table)
{
$tableSchema = $table;
if (!($table instanceof Table)) {
$tableSchema = $this->schema($table);
}
$primaryKeys = [];
$tablePrimaryKeys = $tableSchema->primaryKey();
foreach ($tableSchema->columns() as $column) {
if (in_array($column, $tablePrimaryKeys)) {
$primaryKeys[] = ['name' => $column, 'info' => $this->column($tableSchema, $column)];
}
}
return $primaryKeys;
}
/**
* Returns whether the $tables list given as arguments contains primary keys
* unsigned.
*
* @param array $tables List of tables to check
* @return bool
*/
public function hasUnsignedPrimaryKey($tables)
{
foreach ($tables as $table) {
$tableSchema = $table;
if (!($table instanceof Table)) {
$tableSchema = $this->schema($table);
}
$tablePrimaryKeys = $tableSchema->primaryKey();
foreach ($tablePrimaryKeys as $primaryKey) {
$column = $tableSchema->getColumn($primaryKey);
if (isset($column['unsigned']) && $column['unsigned'] === true) {
return true;
}
}
}
return false;
}
/**
* Returns the primary key columns name for a given table
*
* @param string $table Name of the table ot retrieve primary key for
* @return array
*/
public function primaryKeysColumnsList($table)
{
$primaryKeys = $this->primaryKeys($table);
$primaryKeysColumns = Hash::extract($primaryKeys, '{n}.name');
sort($primaryKeysColumns);
return $primaryKeysColumns;
}
/**
* Returns an array of column data for a single column
*
* @param \Cake\Database\Schema\Table $tableSchema Name of the table to retrieve columns for
* @param string $column A column to retrieve data for
* @return array
*/
public function column($tableSchema, $column)
{
return [
'columnType' => $tableSchema->getColumnType($column),
'options' => $this->attributes($tableSchema, $column),
];
}
/**
* Compute the final array of options to display in a `addColumn` or `changeColumn` instruction.
* The method also takes care of translating properties names between CakePHP database layer and phinx database
* layer.
*
* @param array $options Array of options to compute the final list from.
* @return array
*/
public function getColumnOption($options)
{
$wantedOptions = array_flip([
'length',
'limit',
'default',
'signed',
'null',
'comment',
'autoIncrement',
'precision',
'after',
]);
$columnOptions = array_intersect_key($options, $wantedOptions);
if (empty($columnOptions['comment'])) {
unset($columnOptions['comment']);
}
if (empty($columnOptions['autoIncrement'])) {
unset($columnOptions['autoIncrement']);
}
if (isset($columnOptions['signed']) && $columnOptions['signed'] === true) {
unset($columnOptions['signed']);
}
if (empty($columnOptions['precision'])) {
unset($columnOptions['precision']);
} else {
// due to Phinx using different naming for the precision and scale to CakePHP
$columnOptions['scale'] = $columnOptions['precision'];
if (isset($columnOptions['limit'])) {
$columnOptions['precision'] = $columnOptions['limit'];
unset($columnOptions['limit']);
}
if (isset($columnOptions['length'])) {
$columnOptions['precision'] = $columnOptions['length'];
unset($columnOptions['length']);
}
}
return $columnOptions;
}
/**
* Returns a string-like representation of a value
*
* @param string $value A value to represent as a string
* @param bool $numbersAsString Set tu true to return as string.
* @return mixed
*/
public function value($value, $numbersAsString = false)
{
if ($value === null || $value === 'null' || $value === 'NULL') {
return 'null';
}
if ($value === 'true' || $value === 'false') {
return $value;
}
if (is_bool($value)) {
return $value ? 'true' : 'false';
}
if (!$numbersAsString && (is_numeric($value) || ctype_digit($value))) {
return (float)$value;
}
return sprintf("'%s'", addslashes($value));
}
/**
* Returns an array of attributes for a given table column
*
* @param string $table Name of the table to retrieve columns for
* @param string $column A column to retrieve attributes for
* @return array
*/
public function attributes($table, $column)
{
$tableSchema = $table;
if (!($table instanceof Table)) {
$tableSchema = $this->schema($table);
}
$validOptions = [
'length', 'limit',
'default', 'null',
'precision', 'scale',
'after', 'update',
'comment', 'unsigned',
'signed', 'properties',
'autoIncrement', 'unique',
];
$attributes = [];
$options = $tableSchema->getColumn($column);
foreach ($options as $_option => $value) {
$option = $_option;
switch ($_option) {
case 'length':
$option = 'limit';
break;
case 'unsigned':
$option = 'signed';
$value = (bool)!$value;
break;
case 'unique':
$value = (bool)$value;
break;
}
if (!in_array($option, $validOptions)) {
continue;
}
$attributes[$option] = $value;
}
ksort($attributes);
return $attributes;
}
/**
* Returns an array converted into a formatted multiline string
*
* @param array $list array of items to be stringified
* @param array $options options to use
* @return string
*/
public function stringifyList(array $list, array $options = [])
{
$options += [
'indent' => 2,
];
if (!$list) {
return '';
}
ksort($list);
foreach ($list as $k => &$v) {
if (is_array($v)) {
$v = $this->stringifyList($v, [
'indent' => $options['indent'] + 1,
]);
$v = sprintf('[%s]', $v);
} else {
$v = $this->value($v, $k === 'default');
}
if (!is_numeric($k)) {
$v = "'$k' => $v";
}
}
$start = $end = '';
$join = ', ';
if ($options['indent']) {
$join = ',';
$start = "\n" . str_repeat(" ", $options['indent']);
$join .= $start;
$end = "\n" . str_repeat(" ", $options['indent'] - 1);
}
return $start . implode($join, $list) . ',' . $end;
}
/**
* Returns a $this->table() statement only if it was not issued already
*
* @param string $table Table for which the statement is needed
* @param bool $reset Reset previously set statement.
* @return string
*/
public function tableStatement($table, $reset = false)
{
if ($reset === true) {
unset($this->tableStatements[$table]);
}
if (!isset($this->tableStatements[$table])) {
$this->tableStatements[$table] = true;
return '$this->table(\'' . $table . '\')';
}
return '';
}
}