├── .phive └── phars.xml ├── Dockerfile ├── LICENSE.txt ├── README.md ├── composer.json ├── config └── app.example.php ├── docs.Dockerfile ├── docs ├── config │ ├── __init__.py │ └── all.py ├── en │ ├── conf.py │ ├── contents.rst │ ├── index.rst │ ├── seeding.rst │ ├── upgrading-to-builtin-backend.rst │ └── writing-migrations.rst ├── fr │ ├── conf.py │ ├── contents.rst │ └── index.rst ├── ja │ ├── conf.py │ ├── contents.rst │ └── index.rst ├── pt │ ├── conf.py │ ├── contents.rst │ └── index.rst └── ru │ ├── conf.py │ ├── contents.rst │ └── index.rst ├── phpcs.xml ├── phpstan-baseline.neon ├── psalm-baseline.xml ├── psalm.xml ├── src ├── AbstractMigration.php ├── AbstractSeed.php ├── BaseMigration.php ├── BaseSeed.php ├── CakeAdapter.php ├── CakeManager.php ├── Command │ ├── BakeMigrationCommand.php │ ├── BakeMigrationDiffCommand.php │ ├── BakeMigrationSnapshotCommand.php │ ├── BakeSeedCommand.php │ ├── BakeSimpleMigrationCommand.php │ ├── DumpCommand.php │ ├── EntryCommand.php │ ├── MarkMigratedCommand.php │ ├── MigrateCommand.php │ ├── MigrationsCacheBuildCommand.php │ ├── MigrationsCacheClearCommand.php │ ├── MigrationsCommand.php │ ├── MigrationsCreateCommand.php │ ├── MigrationsDumpCommand.php │ ├── MigrationsMarkMigratedCommand.php │ ├── MigrationsMigrateCommand.php │ ├── MigrationsRollbackCommand.php │ ├── MigrationsSeedCommand.php │ ├── MigrationsStatusCommand.php │ ├── Phinx │ │ ├── BaseCommand.php │ │ ├── CacheBuild.php │ │ ├── CacheClear.php │ │ ├── CommandTrait.php │ │ ├── Create.php │ │ ├── Dump.php │ │ ├── MarkMigrated.php │ │ ├── Migrate.php │ │ ├── Rollback.php │ │ ├── Seed.php │ │ └── Status.php │ ├── RollbackCommand.php │ ├── SeedCommand.php │ ├── SnapshotTrait.php │ └── StatusCommand.php ├── Config │ ├── Config.php │ └── ConfigInterface.php ├── ConfigurationTrait.php ├── Db │ ├── Action │ │ ├── Action.php │ │ ├── AddColumn.php │ │ ├── AddForeignKey.php │ │ ├── AddIndex.php │ │ ├── ChangeColumn.php │ │ ├── ChangeComment.php │ │ ├── ChangePrimaryKey.php │ │ ├── CreateTable.php │ │ ├── DropForeignKey.php │ │ ├── DropIndex.php │ │ ├── DropTable.php │ │ ├── RemoveColumn.php │ │ ├── RenameColumn.php │ │ └── RenameTable.php │ ├── Adapter │ │ ├── AbstractAdapter.php │ │ ├── AdapterFactory.php │ │ ├── AdapterInterface.php │ │ ├── AdapterWrapper.php │ │ ├── DirectActionInterface.php │ │ ├── MysqlAdapter.php │ │ ├── PhinxAdapter.php │ │ ├── PostgresAdapter.php │ │ ├── RecordingAdapter.php │ │ ├── SqliteAdapter.php │ │ ├── SqlserverAdapter.php │ │ ├── TimedOutputAdapter.php │ │ ├── UnsupportedColumnTypeException.php │ │ └── WrapperInterface.php │ ├── AlterInstructions.php │ ├── Expression.php │ ├── Literal.php │ ├── Plan │ │ ├── AlterTable.php │ │ ├── Intent.php │ │ ├── NewTable.php │ │ ├── Plan.php │ │ └── Solver │ │ │ └── ActionSplitter.php │ ├── Table.php │ └── Table │ │ ├── Column.php │ │ ├── ForeignKey.php │ │ ├── Index.php │ │ └── Table.php ├── Middleware │ └── PendingMigrationsMiddleware.php ├── Migration │ ├── BackendInterface.php │ ├── BuiltinBackend.php │ ├── Environment.php │ ├── IrreversibleMigrationException.php │ ├── Manager.php │ ├── ManagerFactory.php │ └── PhinxBackend.php ├── MigrationInterface.php ├── Migrations.php ├── MigrationsDispatcher.php ├── MigrationsPlugin.php ├── SeedInterface.php ├── Shim │ ├── MigrationAdapter.php │ ├── OutputAdapter.php │ └── SeedAdapter.php ├── Table.php ├── TestSuite │ └── Migrator.php ├── Util │ ├── ColumnParser.php │ ├── SchemaTrait.php │ ├── TableFinder.php │ ├── Util.php │ └── UtilTrait.php └── View │ └── Helper │ └── MigrationHelper.php └── templates ├── Phinx └── create.php.template └── bake ├── Seed └── seed.twig ├── config ├── diff.twig ├── skeleton.twig └── snapshot.twig └── element ├── add-columns.twig ├── add-foreign-keys.twig ├── add-indexes.twig └── create-tables.twig /.phive/phars.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Basic docker based environment 2 | # Necessary to trick dokku into building the documentation 3 | # using dockerfile instead of herokuish 4 | FROM ubuntu:22.04 5 | 6 | # Add basic tools 7 | RUN apt-get update && \ 8 | apt-get install -y build-essential \ 9 | software-properties-common \ 10 | curl \ 11 | git \ 12 | libxml2 \ 13 | libffi-dev \ 14 | libssl-dev && \ 15 | LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php && \ 16 | apt-get update && \ 17 | apt-get install -y php8.1-cli php8.1-mbstring php8.1-xml php8.1-zip php8.1-intl php8.1-opcache php8.1-sqlite &&\ 18 | apt-get clean &&\ 19 | rm -rf /var/lib/apt/lists/* 20 | 21 | WORKDIR /code 22 | 23 | VOLUME ["/code"] 24 | 25 | CMD [ '/bin/bash' ] 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org) 4 | Copyright (c) 2005-present, Cake Software Foundation, Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a 7 | copy of this software and associated documentation files (the "Software"), 8 | to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | and/or sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 22 | DEALINGS IN THE SOFTWARE. 23 | 24 | Cake Software Foundation, Inc. 25 | 1785 E. Sahara Avenue, 26 | Suite 490-204 27 | Las Vegas, Nevada 89104, 28 | United States of America. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Migrations plugin for CakePHP 2 | 3 | [![CI](https://github.com/cakephp/migrations/actions/workflows/ci.yml/badge.svg)](https://github.com/cakephp/migrations/actions/workflows/ci.yml) 4 | [![Coverage Status](https://img.shields.io/codecov/c/github/cakephp/migrations/3.x.svg?style=flat-square)](https://app.codecov.io/github/cakephp/migrations/tree/3.x) 5 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.txt) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/cakephp/migrations.svg?style=flat-square)](https://packagist.org/packages/cakephp/migrations) 7 | 8 | This is a Database Migrations system for CakePHP. 9 | 10 | The plugin consists of a CakePHP CLI wrapper for the [Phinx](https://book.cakephp.org/phinx/0/en/index.html) migrations library. 11 | 12 | This branch is for use with CakePHP **5.x**. See [version map](https://github.com/cakephp/migrations/wiki#version-map) for details. 13 | 14 | ## Installation 15 | 16 | You can install this plugin into your CakePHP application using [Composer](https://getcomposer.org). 17 | 18 | Run the following command 19 | ```sh 20 | composer require cakephp/migrations 21 | ``` 22 | 23 | ## Configuration 24 | 25 | You can load the plugin using the shell command: 26 | 27 | ``` 28 | bin/cake plugin load Migrations --only-cli 29 | ``` 30 | 31 | If you are using the PendingMigrations middleware, use: 32 | ``` 33 | bin/cake plugin load Migrations 34 | ``` 35 | 36 | ### Enabling the builtin backend 37 | 38 | In a future release, migrations will be switching to a new backend based on the CakePHP ORM. We're aiming 39 | to be compatible with as many existing migrations as possible, and could use your feedback. Enable the 40 | new backend with: 41 | 42 | ```php 43 | // in app/config/app_local.php 44 | $config = [ 45 | // Other configuration 46 | 'Migrations' => ['backend' => 'builtin'], 47 | ]; 48 | 49 | ``` 50 | 51 | ## Documentation 52 | 53 | Full documentation of the plugin can be found on the [CakePHP Cookbook](https://book.cakephp.org/migrations/4/). 54 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cakephp/migrations", 3 | "description": "Database Migration plugin for CakePHP based on Phinx", 4 | "license": "MIT", 5 | "type": "cakephp-plugin", 6 | "keywords": [ 7 | "cakephp", 8 | "migrations", 9 | "cli" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "CakePHP Community", 14 | "homepage": "https://github.com/cakephp/migrations/graphs/contributors" 15 | } 16 | ], 17 | "homepage": "https://github.com/cakephp/migrations", 18 | "support": { 19 | "issues": "https://github.com/cakephp/migrations/issues", 20 | "forum": "https://stackoverflow.com/tags/cakephp", 21 | "irc": "irc://irc.freenode.org/cakephp", 22 | "source": "https://github.com/cakephp/migrations" 23 | }, 24 | "require": { 25 | "php": ">=8.1", 26 | "cakephp/cache": "^5.0", 27 | "cakephp/orm": "^5.0", 28 | "robmorgan/phinx": "^0.16.0" 29 | }, 30 | "require-dev": { 31 | "cakephp/bake": "^3.0", 32 | "cakephp/cakephp": "^5.0.11", 33 | "cakephp/cakephp-codesniffer": "^5.0", 34 | "phpunit/phpunit": "^10.5.5 || ^11.1.3" 35 | }, 36 | "suggest": { 37 | "cakephp/bake": "If you want to generate migrations.", 38 | "dereuromark/cakephp-ide-helper": "If you want to have IDE suggest/autocomplete when creating migrations." 39 | }, 40 | "minimum-stability": "dev", 41 | "prefer-stable": true, 42 | "autoload": { 43 | "psr-4": { 44 | "Migrations\\": "src/" 45 | } 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "Cake\\Test\\": "./vendor/cakephp/cakephp/tests/", 50 | "Migrations\\Test\\": "tests/", 51 | "SimpleSnapshot\\": "tests/test_app/Plugin/SimpleSnapshot/src/", 52 | "TestApp\\": "tests/test_app/App/", 53 | "TestBlog\\": "tests/test_app/Plugin/TestBlog/src/" 54 | } 55 | }, 56 | "config": { 57 | "allow-plugins": { 58 | "dealerdirect/phpcodesniffer-composer-installer": true 59 | } 60 | }, 61 | "scripts": { 62 | "check": [ 63 | "@cs-check", 64 | "@stan", 65 | "@test" 66 | ], 67 | "cs-check": "phpcs -p", 68 | "cs-fix": "phpcbf -p", 69 | "phpstan": "tools/phpstan analyse", 70 | "psalm": "tools/psalm --show-info=false", 71 | "psalm-baseline": "tools/psalm --set-baseline=psalm-baseline.xml", 72 | "stan": [ 73 | "@phpstan", 74 | "@psalm" 75 | ], 76 | "stan-baseline": "tools/phpstan --generate-baseline", 77 | "stan-setup": "phive install", 78 | "lowest": "validate-prefer-lowest", 79 | "lowest-setup": "composer update --prefer-lowest --prefer-stable --prefer-dist --no-interaction && cp composer.json composer.backup && composer require --dev dereuromark/composer-prefer-lowest && mv composer.backup composer.json", 80 | "test": "phpunit", 81 | "test-coverage": "phpunit --coverage-clover=clover.xml" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /config/app.example.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'backend' => 'builtin', 10 | 'unsigned_primary_keys' => null, 11 | 'column_null_default' => null, 12 | ], 13 | ]; 14 | -------------------------------------------------------------------------------- /docs.Dockerfile: -------------------------------------------------------------------------------- 1 | # Generate the HTML output. 2 | FROM ghcr.io/cakephp/docs-builder as builder 3 | 4 | COPY docs /data/docs 5 | 6 | RUN cd /data/docs-builder && \ 7 | # In the future repeat website for each version 8 | make website LANGS="en fr ja pt ru" SOURCE=/data/docs DEST=/data/website/ 9 | 10 | # Build a small nginx container with just the static site in it. 11 | FROM ghcr.io/cakephp/docs-builder:runtime as runtime 12 | 13 | ENV LANGS="en fr ja pt ru" 14 | ENV SEARCH_SOURCE="/usr/share/nginx/html" 15 | ENV SEARCH_URL_PREFIX="/migrations/4" 16 | 17 | COPY --from=builder /data/docs /data/docs 18 | COPY --from=builder /data/website /data/website 19 | COPY --from=builder /data/docs-builder/nginx.conf /etc/nginx/conf.d/default.conf 20 | 21 | 22 | # Move each version into place 23 | RUN cp -R /data/website/html/* /usr/share/nginx/html \ 24 | && rm -rf /data/website/ 25 | 26 | RUN ln -s /usr/share/nginx/html /usr/share/nginx/html/2.x 27 | -------------------------------------------------------------------------------- /docs/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cakephp/migrations/a735a6f39e8452727b6281834529d40bc6efe741/docs/config/__init__.py -------------------------------------------------------------------------------- /docs/config/all.py: -------------------------------------------------------------------------------- 1 | # Global configuration information used across all the 2 | # translations of documentation. 3 | # 4 | # Import the base theme configuration 5 | from cakephpsphinx.config.all import * 6 | 7 | # The version info for the project you're documenting, acts as replacement for 8 | # |version| and |release|, also used in various other places throughout the 9 | # built documents. 10 | # 11 | 12 | # The full version, including alpha/beta/rc tags. 13 | release = '4.x' 14 | 15 | # The search index version. 16 | search_version = 'migrations-4' 17 | 18 | # The marketing display name for the book. 19 | version_name = '' 20 | 21 | # Project name shown in the black header bar 22 | project = 'CakePHP Migrations' 23 | 24 | # Other versions that display in the version picker menu. 25 | version_list = [ 26 | {'name': '2.x', 'number': 'migrations/2', 'title': '2.x'}, 27 | {'name': '3.x', 'number': 'migrations/3', 'title': '3.x'}, 28 | {'name': '4.x', 'number': 'migrations/4', 'title': '4.x', 'current': True}, 29 | ] 30 | 31 | # Languages available. 32 | languages = ['en', 'fr', 'ja', 'pt', 'ru'] 33 | 34 | # The GitHub branch name for this version of the docs 35 | # for edit links to point at. 36 | branch = '4.x' 37 | 38 | # Current version being built 39 | version = '4.x' 40 | 41 | # Language in use for this directory. 42 | language = 'en' 43 | 44 | show_root_link = True 45 | 46 | repository = 'cakephp/migrations' 47 | 48 | source_path = 'docs/' 49 | 50 | hide_page_contents = ('search', '404', 'contents') 51 | 52 | is_prerelease = False 53 | -------------------------------------------------------------------------------- /docs/en/conf.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | # Append the top level directory of the docs, so we can import from the config dir. 4 | sys.path.insert(0, os.path.abspath('..')) 5 | 6 | # Pull in all the configuration options defined in the global config file.. 7 | from config.all import * 8 | 9 | language = 'en' 10 | -------------------------------------------------------------------------------- /docs/en/contents.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 2 3 | :caption: CakePHP Migrations 4 | 5 | /index 6 | /writing-migrations 7 | /seeding 8 | /upgrading-to-builtin-backend 9 | -------------------------------------------------------------------------------- /docs/en/upgrading-to-builtin-backend.rst: -------------------------------------------------------------------------------- 1 | Upgrading to the builtin backend 2 | ################################ 3 | 4 | As of migrations 4.3 there is a new migrations backend that uses CakePHP's 5 | database abstractions and ORM. In 4.4, the ``builtin`` backend became the 6 | default backend. Longer term this will allow for phinx to be 7 | removed as a dependency. This greatly reduces the dependency footprint of 8 | migrations. 9 | 10 | What is the same? 11 | ================= 12 | 13 | Your migrations shouldn't have to change much to adapt to the new backend. 14 | The migrations backend implements all of the phinx interfaces and can run 15 | migrations based on phinx classes. If your migrations don't work in a way that 16 | could be addressed by the changes outlined below, please open an issue, as we'd 17 | like to maintain as much compatibility as we can. 18 | 19 | What is different? 20 | ================== 21 | 22 | If your migrations are using the ``AdapterInterface`` to fetch rows or update 23 | rows you will need to update your code. If you use ``Adapter::query()`` to 24 | execute queries, the return of this method is now 25 | ``Cake\Database\StatementInterface`` instead. This impacts ``fetchAll()``, 26 | and ``fetch()``:: 27 | 28 | // This 29 | $stmt = $this->getAdapter()->query('SELECT * FROM articles'); 30 | $rows = $stmt->fetchAll(); 31 | 32 | // Now needs to be 33 | $stmt = $this->getAdapter()->query('SELECT * FROM articles'); 34 | $rows = $stmt->fetchAll('assoc'); 35 | 36 | Similar changes are for fetching a single row:: 37 | 38 | // This 39 | $stmt = $this->getAdapter()->query('SELECT * FROM articles'); 40 | $rows = $stmt->fetch(); 41 | 42 | // Now needs to be 43 | $stmt = $this->getAdapter()->query('SELECT * FROM articles'); 44 | $rows = $stmt->fetch('assoc'); 45 | 46 | Problems with the new backend? 47 | ============================== 48 | 49 | The new backend is enabled by default. If your migrations contain errors when 50 | run with the builtin backend, please open `an issue 51 | `_. You can also switch back 52 | to the ``phinx`` backend through application configuration. Add the 53 | following to your ``config/app.php``:: 54 | 55 | return [ 56 | // Other configuration. 57 | 'Migrations' => ['backend' => 'phinx'], 58 | ]; 59 | -------------------------------------------------------------------------------- /docs/fr/conf.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | # Append the top level directory of the docs, so we can import from the config dir. 4 | sys.path.insert(0, os.path.abspath('..')) 5 | 6 | # Pull in all the configuration options defined in the global config file.. 7 | from config.all import * 8 | 9 | language = 'fr' 10 | -------------------------------------------------------------------------------- /docs/fr/contents.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 2 3 | :caption: CakePHP Migrations 4 | 5 | /index 6 | -------------------------------------------------------------------------------- /docs/ja/conf.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | # Append the top level directory of the docs, so we can import from the config dir. 4 | sys.path.insert(0, os.path.abspath('..')) 5 | 6 | # Pull in all the configuration options defined in the global config file.. 7 | from config.all import * 8 | 9 | language = 'ja' 10 | -------------------------------------------------------------------------------- /docs/ja/contents.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 2 3 | :caption: CakePHP Migrations 4 | 5 | /index 6 | -------------------------------------------------------------------------------- /docs/pt/conf.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | # Append the top level directory of the docs, so we can import from the config dir. 4 | sys.path.insert(0, os.path.abspath('..')) 5 | 6 | # Pull in all the configuration options defined in the global config file.. 7 | from config.all import * 8 | 9 | language = 'pt' 10 | -------------------------------------------------------------------------------- /docs/pt/contents.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 2 3 | :caption: CakePHP Migrations 4 | 5 | /index 6 | -------------------------------------------------------------------------------- /docs/ru/conf.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | # Append the top level directory of the docs, so we can import from the config dir. 4 | sys.path.insert(0, os.path.abspath('..')) 5 | 6 | # Pull in all the configuration options defined in the global config file.. 7 | from config.all import * 8 | 9 | language = 'ru' 10 | -------------------------------------------------------------------------------- /docs/ru/contents.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 2 3 | :caption: CakePHP Migrations 4 | 5 | /index 6 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src/ 6 | tests/ 7 | 8 | */tests/comparisons/* 9 | */tests/TestCase/Util/_files/* 10 | */test_app/config/* 11 | */TestBlog/config/* 12 | */BarPlugin/config/* 13 | */FooPlugin/config/* 14 | */Migrator/config/* 15 | 16 | 17 | 18 | 19 | 20 | 0 21 | 22 | 23 | tests/comparisons/* 24 | test_app/config/* 25 | test_app/**/config/* 26 | 27 | 28 | tests/comparisons/* 29 | 30 | 31 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/AbstractMigration.php: -------------------------------------------------------------------------------- 1 | getAdapter()->hasTransactions(); 46 | } 47 | 48 | /** 49 | * Returns an instance of the Table class. 50 | * 51 | * You can use this class to create and manipulate tables. 52 | * 53 | * @param string $tableName Table Name 54 | * @param array $options Options 55 | * @return \Migrations\Table 56 | */ 57 | public function table(string $tableName, array $options = []): Table 58 | { 59 | if ($this->autoId === false) { 60 | $options['id'] = false; 61 | } 62 | 63 | $table = new Table($tableName, $options, $this->getAdapter()); 64 | $this->tables[] = $table; 65 | 66 | return $table; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/AbstractSeed.php: -------------------------------------------------------------------------------- 1 | getOutput()->writeln(''); 49 | $this->getOutput()->writeln( 50 | ' ====' . 51 | ' ' . $seeder . ':' . 52 | ' seeding', 53 | ); 54 | 55 | $start = microtime(true); 56 | $this->runCall($seeder); 57 | $end = microtime(true); 58 | 59 | $this->getOutput()->writeln( 60 | ' ====' . 61 | ' ' . $seeder . ':' . 62 | ' seeded' . 63 | ' ' . sprintf('%.4fs', $end - $start) . '', 64 | ); 65 | $this->getOutput()->writeln(''); 66 | } 67 | 68 | /** 69 | * Calls another seeder from this seeder. 70 | * It will load the Seed class you are calling and run it. 71 | * 72 | * @param string $seeder Name of the seeder to call from the current seed 73 | * @return void 74 | */ 75 | protected function runCall(string $seeder): void 76 | { 77 | [$pluginName, $seeder] = pluginSplit($seeder); 78 | 79 | $argv = [ 80 | 'seed', 81 | '--seed', 82 | $seeder, 83 | ]; 84 | 85 | $plugin = $pluginName ?: $this->input->getOption('plugin'); 86 | if ($plugin !== null) { 87 | $argv[] = '--plugin'; 88 | $argv[] = $plugin; 89 | } 90 | 91 | $connection = $this->input->getOption('connection'); 92 | if ($connection !== null) { 93 | $argv[] = '--connection'; 94 | $argv[] = $connection; 95 | } 96 | 97 | $source = $this->input->getOption('source'); 98 | if ($source !== null) { 99 | $argv[] = '--source'; 100 | $argv[] = $source; 101 | } 102 | 103 | $seedCommand = new Seed(); 104 | $input = new ArgvInput($argv, $seedCommand->getDefinition()); 105 | $seedCommand->setInput($input); 106 | $config = $seedCommand->getConfig(); 107 | 108 | $seedPaths = $config->getSeedPaths(); 109 | require_once array_pop($seedPaths) . DS . $seeder . '.php'; 110 | /** @var \Phinx\Seed\SeedInterface $seeder */ 111 | $seeder = new $seeder(); 112 | $seeder->setOutput($this->getOutput()); 113 | $seeder->setAdapter($this->getAdapter()); 114 | $seeder->setInput($this->input); 115 | $seeder->run(); 116 | } 117 | 118 | /** 119 | * Sets the InputInterface this Seed class is being used with. 120 | * 121 | * @param \Symfony\Component\Console\Input\InputInterface $input Input object. 122 | * @return $this 123 | */ 124 | public function setInput(InputInterface $input) 125 | { 126 | $this->input = $input; 127 | 128 | return $this; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/CakeAdapter.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 53 | $pdo = $adapter->getConnection(); 54 | 55 | if ($pdo->getAttribute(PDO::ATTR_ERRMODE) !== PDO::ERRMODE_EXCEPTION) { 56 | $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 57 | } 58 | $connection->cacheMetadata(false); 59 | 60 | if ($connection->getDriver() instanceof Postgres) { 61 | $config = $connection->config(); 62 | $schema = empty($config['schema']) ? 'public' : $config['schema']; 63 | $pdo->exec('SET search_path TO ' . $pdo->quote($schema)); 64 | } 65 | 66 | $driver = $connection->getDriver(); 67 | $prop = new ReflectionProperty($driver, 'pdo'); 68 | $prop->setValue($driver, $pdo); 69 | } 70 | 71 | /** 72 | * Gets the CakePHP Connection object. 73 | * 74 | * @return \Cake\Database\Connection 75 | */ 76 | public function getCakeConnection(): Connection 77 | { 78 | return $this->connection; 79 | } 80 | 81 | /** 82 | * Returns a new Query object 83 | * 84 | * @param string $type The type of query to generate 85 | * (one of the `\Cake\Database\Query::TYPE_*` constants). 86 | * @return \Cake\Database\Query 87 | */ 88 | public function getQueryBuilder(string $type): Query 89 | { 90 | return match ($type) { 91 | Query::TYPE_SELECT => $this->getCakeConnection()->selectQuery(), 92 | Query::TYPE_INSERT => $this->getCakeConnection()->insertQuery(), 93 | Query::TYPE_UPDATE => $this->getCakeConnection()->updateQuery(), 94 | Query::TYPE_DELETE => $this->getCakeConnection()->deleteQuery(), 95 | default => throw new InvalidArgumentException( 96 | 'Query type must be one of: `select`, `insert`, `update`, `delete`.', 97 | ) 98 | }; 99 | } 100 | 101 | /** 102 | * Returns the adapter type name, for example mysql 103 | * 104 | * @return string 105 | */ 106 | public function getAdapterType(): string 107 | { 108 | return $this->getAdapter()->getAdapterType(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Command/DumpCommand.php: -------------------------------------------------------------------------------- 1 | 50 | */ 51 | public static function extractArgs(Arguments $args): array 52 | { 53 | /** @var array $newArgs */ 54 | $newArgs = []; 55 | if ($args->getOption('connection')) { 56 | $newArgs[] = '-c'; 57 | $newArgs[] = $args->getOption('connection'); 58 | } 59 | if ($args->getOption('plugin')) { 60 | $newArgs[] = '-p'; 61 | $newArgs[] = $args->getOption('plugin'); 62 | } 63 | if ($args->getOption('source')) { 64 | $newArgs[] = '-s'; 65 | $newArgs[] = $args->getOption('source'); 66 | } 67 | 68 | return $newArgs; 69 | } 70 | 71 | /** 72 | * Configure the option parser 73 | * 74 | * @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure 75 | * @return \Cake\Console\ConsoleOptionParser 76 | */ 77 | public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser 78 | { 79 | $parser->setDescription([ 80 | 'Dumps the current schema of the database to be used while baking a diff', 81 | '', 82 | 'migrations dump -c secondary', 83 | ])->addOption('plugin', [ 84 | 'short' => 'p', 85 | 'help' => 'The plugin to dump migrations for', 86 | ])->addOption('connection', [ 87 | 'short' => 'c', 88 | 'help' => 'The datasource connection to use', 89 | 'default' => 'default', 90 | ])->addOption('source', [ 91 | 'short' => 's', 92 | 'help' => 'The folder under src/Config that migrations are in', 93 | 'default' => ConfigInterface::DEFAULT_MIGRATION_FOLDER, 94 | ]); 95 | 96 | return $parser; 97 | } 98 | 99 | /** 100 | * Execute the command. 101 | * 102 | * @param \Cake\Console\Arguments $args The command arguments. 103 | * @param \Cake\Console\ConsoleIo $io The console io 104 | * @return int|null The exit code or null for success 105 | */ 106 | public function execute(Arguments $args, ConsoleIo $io): ?int 107 | { 108 | $factory = new ManagerFactory([ 109 | 'plugin' => $args->getOption('plugin'), 110 | 'source' => $args->getOption('source'), 111 | 'connection' => $args->getOption('connection'), 112 | ]); 113 | $config = $factory->createConfig(); 114 | $path = $config->getMigrationPath(); 115 | $connectionName = (string)$config->getConnection(); 116 | $connection = ConnectionManager::get($connectionName); 117 | assert($connection instanceof Connection); 118 | 119 | $collection = $connection->getSchemaCollection(); 120 | $options = [ 121 | 'require-table' => false, 122 | 'plugin' => $args->getOption('plugin'), 123 | ]; 124 | // The connection property is used by the trait methods. 125 | $this->connection = $connectionName; 126 | $finder = new TableFinder($connectionName); 127 | $tables = $finder->getTablesToBake($collection, $options); 128 | 129 | $dump = []; 130 | if ($tables) { 131 | foreach ($tables as $table) { 132 | $schema = $collection->describe($table); 133 | $dump[$table] = $schema; 134 | } 135 | } 136 | 137 | $filePath = $path . DS . 'schema-dump-' . $connectionName . '.lock'; 138 | $io->out("Writing dump file `{$filePath}`..."); 139 | if (file_put_contents($filePath, serialize($dump))) { 140 | $io->out("Dump file `{$filePath}` was successfully written"); 141 | 142 | return self::CODE_SUCCESS; 143 | } 144 | $io->out("An error occurred while writing dump file `{$filePath}`"); 145 | 146 | return self::CODE_ERROR; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Command/EntryCommand.php: -------------------------------------------------------------------------------- 1 | commands = $commands; 54 | } 55 | 56 | /** 57 | * Run the command. 58 | * 59 | * Override the run() method for special handling of the `--help` option. 60 | * 61 | * @param array $argv Arguments from the CLI environment. 62 | * @param \Cake\Console\ConsoleIo $io The console io 63 | * @return int|null Exit code or null for success. 64 | */ 65 | public function run(array $argv, ConsoleIo $io): ?int 66 | { 67 | $this->initialize(); 68 | 69 | $parser = $this->getOptionParser(); 70 | try { 71 | [$options, $arguments] = $parser->parse($argv); 72 | $args = new Arguments( 73 | $arguments, 74 | $options, 75 | $parser->argumentNames(), 76 | ); 77 | } catch (ConsoleException $e) { 78 | $io->err('Error: ' . $e->getMessage()); 79 | 80 | return static::CODE_ERROR; 81 | } 82 | $this->setOutputLevel($args, $io); 83 | 84 | // This is the variance from Command::run() 85 | if (!$args->getArgumentAt(0) && $args->getOption('help')) { 86 | $backend = Configure::read('Migrations.backend', 'builtin'); 87 | $io->out([ 88 | 'Migrations', 89 | '', 90 | "Migrations provides commands for managing your application's database schema and initial data.", 91 | '', 92 | "Using {$backend} backend.", 93 | '', 94 | ]); 95 | $help = $this->getHelp(); 96 | $this->executeCommand($help, [], $io); 97 | 98 | return static::CODE_SUCCESS; 99 | } 100 | 101 | return $this->execute($args, $io); 102 | } 103 | 104 | /** 105 | * Execute the command. 106 | * 107 | * @param \Cake\Console\Arguments $args The command arguments. 108 | * @param \Cake\Console\ConsoleIo $io The console io 109 | * @return int|null The exit code or null for success 110 | */ 111 | public function execute(Arguments $args, ConsoleIo $io): ?int 112 | { 113 | if ($args->hasArgumentAt(0)) { 114 | $name = $args->getArgumentAt(0); 115 | $io->err( 116 | "Could not find migrations command named `$name`." 117 | . ' Run `migrations --help` to get a list of commands.', 118 | ); 119 | 120 | return static::CODE_ERROR; 121 | } 122 | $io->err('No command provided. Run `migrations --help` to get a list of commands.'); 123 | 124 | return static::CODE_ERROR; 125 | } 126 | 127 | /** 128 | * Gets the generated help command 129 | * 130 | * @return \Cake\Console\Command\HelpCommand 131 | */ 132 | public function getHelp(): HelpCommand 133 | { 134 | $help = new HelpCommand(); 135 | $commands = []; 136 | foreach ($this->commands as $command => $class) { 137 | if (str_starts_with($command, 'migrations')) { 138 | $parts = explode(' ', $command); 139 | 140 | // Remove `migrations` 141 | array_shift($parts); 142 | if (count($parts) === 0) { 143 | continue; 144 | } 145 | $commands[$command] = $class; 146 | } 147 | } 148 | 149 | $CommandCollection = new CommandCollection($commands); 150 | $help->setCommandCollection($CommandCollection); 151 | 152 | return $help; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Command/MarkMigratedCommand.php: -------------------------------------------------------------------------------- 1 | setDescription([ 48 | 'Mark a migration as applied', 49 | '', 50 | 'Can mark one or more migrations as applied without applying the changes in the migration.', 51 | '', 52 | 'migrations mark_migrated --connection secondary', 53 | 'Mark all migrations as applied', 54 | '', 55 | 'migrations mark_migrated --connection secondary --target 003', 56 | 'mark migrations as applied up to the 003', 57 | '', 58 | 'migrations mark_migrated --target 003 --only', 59 | 'mark only 003 as applied.', 60 | '', 61 | 'migrations mark_migrated --target 003 --exclude', 62 | 'mark up to 003, but not 003 as applied.', 63 | ])->addOption('plugin', [ 64 | 'short' => 'p', 65 | 'help' => 'The plugin to mark migrations for', 66 | ])->addOption('connection', [ 67 | 'short' => 'c', 68 | 'help' => 'The datasource connection to use', 69 | 'default' => 'default', 70 | ])->addOption('source', [ 71 | 'short' => 's', 72 | 'default' => ConfigInterface::DEFAULT_MIGRATION_FOLDER, 73 | 'help' => 'The folder where your migrations are', 74 | ])->addOption('target', [ 75 | 'short' => 't', 76 | 'help' => 'Migrations from the beginning to the provided version will be marked as applied.', 77 | ])->addOption('only', [ 78 | 'short' => 'o', 79 | 'help' => 'If present, only the target migration will be marked as applied.', 80 | 'boolean' => true, 81 | ])->addOption('exclude', [ 82 | 'short' => 'x', 83 | 'help' => 'If present, migrations from the beginning until the target version but not including the target will be marked as applied.', 84 | 'boolean' => true, 85 | ]); 86 | 87 | return $parser; 88 | } 89 | 90 | /** 91 | * Checks for an invalid use of `--exclude` or `--only` 92 | * 93 | * @param \Cake\Console\Arguments $args The console arguments 94 | * @return bool Returns true when it is an invalid use of `--exclude` or `--only` otherwise false 95 | */ 96 | protected function invalidOnlyOrExclude(Arguments $args): bool 97 | { 98 | return ($args->getOption('exclude') && $args->getOption('only')) || 99 | ($args->getOption('exclude') || $args->getOption('only')) && 100 | $args->getOption('target') === null; 101 | } 102 | 103 | /** 104 | * Execute the command. 105 | * 106 | * @param \Cake\Console\Arguments $args The command arguments. 107 | * @param \Cake\Console\ConsoleIo $io The console io 108 | * @return int|null The exit code or null for success 109 | */ 110 | public function execute(Arguments $args, ConsoleIo $io): ?int 111 | { 112 | $factory = new ManagerFactory([ 113 | 'plugin' => $args->getOption('plugin'), 114 | 'source' => $args->getOption('source'), 115 | 'connection' => $args->getOption('connection'), 116 | ]); 117 | $manager = $factory->createManager($io); 118 | $config = $manager->getConfig(); 119 | $path = $config->getMigrationPath(); 120 | 121 | if ($this->invalidOnlyOrExclude($args)) { 122 | $io->err( 123 | 'You should use `--exclude` OR `--only` (not both) along with a `--target` !', 124 | ); 125 | 126 | return self::CODE_ERROR; 127 | } 128 | 129 | try { 130 | $versions = $manager->getVersionsToMark($args); 131 | } catch (InvalidArgumentException $e) { 132 | $io->err(sprintf('%s', $e->getMessage())); 133 | 134 | return self::CODE_ERROR; 135 | } 136 | 137 | $output = $manager->markVersionsAsMigrated($path, $versions); 138 | array_map(fn($line) => $io->out($line), $output); 139 | 140 | return self::CODE_SUCCESS; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Command/MigrationsCacheBuildCommand.php: -------------------------------------------------------------------------------- 1 | setName('orm-cache-build') 25 | ->setDescription( 26 | 'Build all metadata caches for the connection. ' . 27 | 'If a table name is provided, only that table will be cached.', 28 | ) 29 | ->addOption( 30 | 'connection', 31 | null, 32 | InputOption::VALUE_OPTIONAL, 33 | 'The connection to build/clear metadata cache data for.', 34 | 'default', 35 | ) 36 | ->addArgument( 37 | 'name', 38 | InputArgument::OPTIONAL, 39 | 'A specific table you want to clear/refresh cached data for.', 40 | ); 41 | } 42 | 43 | /** 44 | * @inheritDoc 45 | */ 46 | protected function execute(InputInterface $input, OutputInterface $output): int 47 | { 48 | /** @var string $name */ 49 | $name = $input->getArgument('name'); 50 | $schema = $this->_getSchema($input, $output); 51 | if (!$schema) { 52 | return static::CODE_ERROR; 53 | } 54 | $tables = [$name]; 55 | if (!$name) { 56 | $tables = $schema->listTables(); 57 | } 58 | foreach ($tables as $table) { 59 | $output->writeln('Building metadata cache for ' . $table); 60 | $schema->describe($table, ['forceRefresh' => true]); 61 | } 62 | $output->writeln('Cache build complete'); 63 | 64 | return static::CODE_SUCCESS; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Command/Phinx/CacheClear.php: -------------------------------------------------------------------------------- 1 | setName('orm-cache-clear') 25 | ->setDescription( 26 | 'Clear all metadata caches for the connection. ' . 27 | 'If a table name is provided, only that table will be removed.', 28 | ) 29 | ->addOption( 30 | 'connection', 31 | null, 32 | InputOption::VALUE_OPTIONAL, 33 | 'The connection to build/clear metadata cache data for.', 34 | 'default', 35 | ) 36 | ->addArgument( 37 | 'name', 38 | InputArgument::OPTIONAL, 39 | 'A specific table you want to clear/refresh cached data for.', 40 | ); 41 | } 42 | 43 | /** 44 | * @inheritDoc 45 | */ 46 | protected function execute(InputInterface $input, OutputInterface $output): int 47 | { 48 | $schema = $this->_getSchema($input, $output); 49 | /** @var string $name */ 50 | $name = $input->getArgument('name'); 51 | if (!$schema) { 52 | return static::CODE_ERROR; 53 | } 54 | $tables = [$name]; 55 | if (!$name) { 56 | $tables = $schema->listTables(); 57 | } 58 | $cacher = $schema->getCacher(); 59 | foreach ($tables as $table) { 60 | $output->writeln(sprintf( 61 | 'Clearing metadata cache for %s', 62 | $table, 63 | )); 64 | $cacher->delete($table); 65 | } 66 | $output->writeln('Cache clear complete'); 67 | 68 | return static::CODE_SUCCESS; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Command/Phinx/CommandTrait.php: -------------------------------------------------------------------------------- 1 | beforeExecute($input, $output); 36 | 37 | return parent::execute($input, $output); 38 | } 39 | 40 | /** 41 | * Overrides the action execute method in order to vanish the idea of environments 42 | * from phinx. CakePHP does not believe in the idea of having in-app environments 43 | * 44 | * @param \Symfony\Component\Console\Input\InputInterface $input the input object 45 | * @param \Symfony\Component\Console\Output\OutputInterface $output the output object 46 | * @return void 47 | */ 48 | protected function beforeExecute(InputInterface $input, OutputInterface $output): void 49 | { 50 | $this->setInput($input); 51 | $this->addOption('--environment', '-e', InputArgument::OPTIONAL); 52 | $input->setOption('environment', 'default'); 53 | } 54 | 55 | /** 56 | * A callback method that is used to inject the PDO object created from phinx into 57 | * the CakePHP connection. This is needed in case the user decides to use tables 58 | * from the ORM and executes queries. 59 | * 60 | * @param \Symfony\Component\Console\Input\InputInterface $input the input object 61 | * @param \Symfony\Component\Console\Output\OutputInterface $output the output object 62 | * @return void 63 | */ 64 | public function bootstrap(InputInterface $input, OutputInterface $output): void 65 | { 66 | parent::bootstrap($input, $output); 67 | $name = $this->getConnectionName($input); 68 | $this->connection = $name; 69 | ConnectionManager::alias($name, 'default'); 70 | /** @var \Cake\Database\Connection $connection */ 71 | $connection = ConnectionManager::get($name); 72 | 73 | $manager = $this->getManager(); 74 | 75 | if (!$manager instanceof CakeManager) { 76 | $this->setManager(new CakeManager($this->getConfig(), $input, $output)); 77 | } 78 | /** @var \Phinx\Migration\Manager\Environment $env */ 79 | /** @psalm-suppress PossiblyNullReference */ 80 | $env = $this->getManager()->getEnvironment('default'); 81 | $adapter = $env->getAdapter(); 82 | if (!$adapter instanceof CakeAdapter) { 83 | $env->setAdapter(new CakeAdapter($adapter, $connection)); 84 | } 85 | } 86 | 87 | /** 88 | * Sets the input object that should be used for the command class. This object 89 | * is used to inspect the extra options that are needed for CakePHP apps. 90 | * 91 | * @param \Symfony\Component\Console\Input\InputInterface $input the input object 92 | * @return void 93 | */ 94 | public function setInput(InputInterface $input): void 95 | { 96 | $this->input = $input; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Command/Phinx/Create.php: -------------------------------------------------------------------------------- 1 | setName('create') 44 | ->setDescription('Create a new migration') 45 | ->addArgument('name', InputArgument::REQUIRED, 'What is the name of the migration?') 46 | ->setHelp(sprintf( 47 | '%sCreates a new database migration file%s', 48 | PHP_EOL, 49 | PHP_EOL, 50 | )) 51 | ->addOption('plugin', 'p', InputOption::VALUE_REQUIRED, 'The plugin the file should be created for') 52 | ->addOption('connection', 'c', InputOption::VALUE_REQUIRED, 'The datasource connection to use') 53 | ->addOption('source', 's', InputOption::VALUE_REQUIRED, 'The folder where migrations are in') 54 | ->addOption('template', 't', InputOption::VALUE_REQUIRED, 'Use an alternative template') 55 | ->addOption( 56 | 'class', 57 | 'l', 58 | InputOption::VALUE_REQUIRED, 59 | 'Use a class implementing "' . parent::CREATION_INTERFACE . '" to generate the template', 60 | ) 61 | ->addOption( 62 | 'path', 63 | null, 64 | InputOption::VALUE_REQUIRED, 65 | 'Specify the path in which to create this migration', 66 | ); 67 | } 68 | 69 | /** 70 | * Configures Phinx Create command CLI options that are unused by this extended 71 | * command. 72 | * 73 | * @param \Symfony\Component\Console\Input\InputInterface $input the input object 74 | * @param \Symfony\Component\Console\Output\OutputInterface $output the output object 75 | * @return void 76 | */ 77 | protected function beforeExecute(InputInterface $input, OutputInterface $output): void 78 | { 79 | // Set up as a dummy, its value is not going to be used, as a custom 80 | // template will always be set. 81 | $this->addOption('style', null, InputOption::VALUE_OPTIONAL); 82 | 83 | $this->parentBeforeExecute($input, $output); 84 | } 85 | 86 | /** 87 | * Overrides the action execute method in order to vanish the idea of environments 88 | * from phinx. CakePHP does not believe in the idea of having in-app environments 89 | * 90 | * @param \Symfony\Component\Console\Input\InputInterface $input the input object 91 | * @param \Symfony\Component\Console\Output\OutputInterface $output the output object 92 | * @return int 93 | */ 94 | protected function execute(InputInterface $input, OutputInterface $output): int 95 | { 96 | $result = $this->parentExecute($input, $output); 97 | 98 | $output->writeln('renaming file in CamelCase to follow CakePHP convention...'); 99 | 100 | $migrationPaths = $this->getConfig()->getMigrationPaths(); 101 | $migrationPath = array_pop($migrationPaths) . DS; 102 | /** @var string $name */ 103 | $name = $input->getArgument('name'); 104 | // phpcs:ignore SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable 105 | [$phinxTimestamp, $phinxName] = explode('_', Util::mapClassNameToFileName($name), 2); 106 | $migrationFilename = glob($migrationPath . '*' . $phinxName); 107 | 108 | if (!$migrationFilename) { 109 | $output->writeln('An error occurred while renaming file'); 110 | } else { 111 | $migrationFilename = $migrationFilename[0]; 112 | $path = dirname($migrationFilename) . DS; 113 | $name = Inflector::camelize($name); 114 | $newPath = $path . Util::getCurrentTimestamp() . '_' . $name . '.php'; 115 | 116 | $output->writeln('renaming file in CamelCase to follow CakePHP convention...'); 117 | if (rename($migrationFilename, $newPath)) { 118 | $output->writeln(sprintf('File successfully renamed to %s', $newPath)); 119 | } else { 120 | $output->writeln(sprintf('An error occurred while renaming file to %s', $newPath)); 121 | } 122 | } 123 | 124 | return $result; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Command/Phinx/Dump.php: -------------------------------------------------------------------------------- 1 | setName('dump') 52 | ->setDescription('Dumps the current schema of the database to be used while baking a diff') 53 | ->setHelp(sprintf( 54 | '%sDumps the current schema of the database to be used while baking a diff%s', 55 | PHP_EOL, 56 | PHP_EOL, 57 | )) 58 | ->addOption('plugin', 'p', InputOption::VALUE_REQUIRED, 'The plugin the file should be created for') 59 | ->addOption('connection', 'c', InputOption::VALUE_REQUIRED, 'The datasource connection to use') 60 | ->addOption('source', 's', InputOption::VALUE_REQUIRED, 'The folder where migrations are in'); 61 | } 62 | 63 | /** 64 | * @param \Symfony\Component\Console\Output\OutputInterface $output The output object. 65 | * @return \Symfony\Component\Console\Output\OutputInterface|null 66 | */ 67 | public function output(?OutputInterface $output = null): ?OutputInterface 68 | { 69 | if ($output !== null) { 70 | $this->output = $output; 71 | } 72 | 73 | return $this->output; 74 | } 75 | 76 | /** 77 | * Dumps the current schema to be used when baking a diff 78 | * 79 | * @param \Symfony\Component\Console\Input\InputInterface $input the input object 80 | * @param \Symfony\Component\Console\Output\OutputInterface $output the output object 81 | * @return int 82 | */ 83 | protected function execute(InputInterface $input, OutputInterface $output): int 84 | { 85 | $this->setInput($input); 86 | $this->bootstrap($input, $output); 87 | $this->output($output); 88 | 89 | $path = $this->getOperationsPath($input); 90 | $connectionName = $input->getOption('connection') ?: 'default'; 91 | assert(is_string($connectionName), 'Connection name must be a string'); 92 | $connection = ConnectionManager::get($connectionName); 93 | assert($connection instanceof Connection); 94 | $collection = $connection->getSchemaCollection(); 95 | 96 | $options = [ 97 | 'require-table' => false, 98 | 'plugin' => $this->getPlugin($input), 99 | ]; 100 | $finder = new TableFinder($connectionName); 101 | $tables = $finder->getTablesToBake($collection, $options); 102 | 103 | $dump = []; 104 | if ($tables) { 105 | foreach ($tables as $table) { 106 | $schema = $collection->describe($table); 107 | $dump[$table] = $schema; 108 | } 109 | } 110 | 111 | $filePath = $path . DS . 'schema-dump-' . $connectionName . '.lock'; 112 | $output->writeln(sprintf('Writing dump file `%s`...', $filePath)); 113 | if (file_put_contents($filePath, serialize($dump))) { 114 | $output->writeln(sprintf('Dump file `%s` was successfully written', $filePath)); 115 | 116 | return BaseCommand::CODE_SUCCESS; 117 | } 118 | 119 | $output->writeln(sprintf( 120 | 'An error occurred while writing dump file `%s`', 121 | $filePath, 122 | )); 123 | 124 | return BaseCommand::CODE_ERROR; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Command/Phinx/Migrate.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | use EventDispatcherTrait; 36 | 37 | /** 38 | * Configures the current command. 39 | * 40 | * @return void 41 | */ 42 | protected function configure(): void 43 | { 44 | $this->setName('migrate') 45 | ->setDescription('Migrate the database') 46 | ->setHelp('runs all available migrations, optionally up to a specific version') 47 | ->addOption('--target', '-t', InputOption::VALUE_REQUIRED, 'The version number to migrate to') 48 | ->addOption('--date', '-d', InputOption::VALUE_REQUIRED, 'The date to migrate to') 49 | ->addOption( 50 | '--dry-run', 51 | '-x', 52 | InputOption::VALUE_NONE, 53 | 'Dump queries to standard output instead of executing it', 54 | ) 55 | ->addOption( 56 | '--plugin', 57 | '-p', 58 | InputOption::VALUE_REQUIRED, 59 | 'The plugin containing the migrations', 60 | ) 61 | ->addOption('--connection', '-c', InputOption::VALUE_REQUIRED, 'The datasource connection to use') 62 | ->addOption('--source', '-s', InputOption::VALUE_REQUIRED, 'The folder where migrations are in') 63 | ->addOption( 64 | '--fake', 65 | null, 66 | InputOption::VALUE_NONE, 67 | "Mark any migrations selected as run, but don't actually execute them", 68 | ) 69 | ->addOption( 70 | '--no-lock', 71 | null, 72 | InputOption::VALUE_NONE, 73 | 'If present, no lock file will be generated after migrating', 74 | ); 75 | } 76 | 77 | /** 78 | * Overrides the action execute method in order to vanish the idea of environments 79 | * from phinx. CakePHP does not believe in the idea of having in-app environments 80 | * 81 | * @param \Symfony\Component\Console\Input\InputInterface $input the input object 82 | * @param \Symfony\Component\Console\Output\OutputInterface $output the output object 83 | * @return int 84 | */ 85 | protected function execute(InputInterface $input, OutputInterface $output): int 86 | { 87 | $event = $this->dispatchEvent('Migration.beforeMigrate'); 88 | if ($event->isStopped()) { 89 | return $event->getResult() ? BaseCommand::CODE_SUCCESS : BaseCommand::CODE_ERROR; 90 | } 91 | $result = $this->parentExecute($input, $output); 92 | $this->dispatchEvent('Migration.afterMigrate'); 93 | 94 | return $result; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Command/Phinx/Rollback.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | use EventDispatcherTrait; 36 | 37 | /** 38 | * Configures the current command. 39 | * 40 | * @return void 41 | */ 42 | protected function configure(): void 43 | { 44 | $this->setName('rollback') 45 | ->setDescription('Rollback the last or to a specific migration') 46 | ->setHelp('reverts the last migration, or optionally up to a specific version') 47 | ->addOption('--target', '-t', InputOption::VALUE_REQUIRED, 'The version number to rollback to') 48 | ->addOption('--date', '-d', InputOption::VALUE_REQUIRED, 'The date to migrate to') 49 | ->addOption('--plugin', '-p', InputOption::VALUE_REQUIRED, 'The plugin containing the migrations') 50 | ->addOption('--connection', '-c', InputOption::VALUE_REQUIRED, 'The datasource connection to use') 51 | ->addOption('--source', '-s', InputOption::VALUE_REQUIRED, 'The folder where migrations are in') 52 | ->addOption('--force', '-f', InputOption::VALUE_NONE, 'Force rollback to ignore breakpoints') 53 | ->addOption( 54 | '--dry-run', 55 | '-x', 56 | InputOption::VALUE_NONE, 57 | 'Dump queries to standard output instead of executing it', 58 | ) 59 | ->addOption( 60 | '--fake', 61 | null, 62 | InputOption::VALUE_NONE, 63 | "Mark any rollbacks selected as run, but don't actually execute them", 64 | ) 65 | ->addOption( 66 | '--no-lock', 67 | null, 68 | InputOption::VALUE_NONE, 69 | 'Whether a lock file should be generated after rolling back', 70 | ); 71 | } 72 | 73 | /** 74 | * Overrides the action execute method in order to vanish the idea of environments 75 | * from phinx. CakePHP does not believe in the idea of having in-app environments 76 | * 77 | * @param \Symfony\Component\Console\Input\InputInterface $input the input object 78 | * @param \Symfony\Component\Console\Output\OutputInterface $output the output object 79 | * @return int 80 | */ 81 | protected function execute(InputInterface $input, OutputInterface $output): int 82 | { 83 | $event = $this->dispatchEvent('Migration.beforeRollback'); 84 | if ($event->isStopped()) { 85 | return $event->getResult() ? BaseCommand::CODE_SUCCESS : BaseCommand::CODE_ERROR; 86 | } 87 | $result = $this->parentExecute($input, $output); 88 | $this->dispatchEvent('Migration.afterRollback'); 89 | 90 | return $result; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Command/Phinx/Seed.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | use EventDispatcherTrait; 36 | 37 | /** 38 | * Configures the current command. 39 | * 40 | * @return void 41 | */ 42 | protected function configure(): void 43 | { 44 | $this->setName('seed') 45 | ->setDescription('Seed the database with data') 46 | ->setHelp('runs all available migrations, optionally up to a specific version') 47 | ->addOption( 48 | '--seed', 49 | null, 50 | InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 51 | 'What is the name of the seeder?', 52 | ) 53 | ->addOption('--plugin', '-p', InputOption::VALUE_REQUIRED, 'The plugin containing the migrations') 54 | ->addOption('--connection', '-c', InputOption::VALUE_REQUIRED, 'The datasource connection to use') 55 | ->addOption('--source', '-s', InputOption::VALUE_REQUIRED, 'The folder where migrations are in'); 56 | } 57 | 58 | /** 59 | * Overrides the action execute method in order to vanish the idea of environments 60 | * from phinx. CakePHP does not believe in the idea of having in-app environments 61 | * 62 | * @param \Symfony\Component\Console\Input\InputInterface $input the input object 63 | * @param \Symfony\Component\Console\Output\OutputInterface $output the output object 64 | * @return int 65 | */ 66 | protected function execute(InputInterface $input, OutputInterface $output): int 67 | { 68 | $event = $this->dispatchEvent('Migration.beforeSeed'); 69 | if ($event->isStopped()) { 70 | return $event->getResult() ? BaseCommand::CODE_SUCCESS : BaseCommand::CODE_ERROR; 71 | } 72 | 73 | $seed = $input->getOption('seed'); 74 | if ($seed && !is_array($seed)) { 75 | $input->setOption('seed', [$seed]); 76 | } 77 | 78 | $this->setInput($input); 79 | $this->bootstrap($input, $output); 80 | $this->getManager()->setInput($input); 81 | $result = $this->parentExecute($input, $output); 82 | $this->dispatchEvent('Migration.afterSeed'); 83 | 84 | return $result; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Command/Phinx/Status.php: -------------------------------------------------------------------------------- 1 | setName('status') 39 | ->setDescription('Show migration status') 40 | ->addOption( 41 | '--format', 42 | '-f', 43 | InputOption::VALUE_REQUIRED, 44 | 'The output format: text or json. Defaults to text.', 45 | ) 46 | ->setHelp('prints a list of all migrations, along with their current status') 47 | ->addOption('--plugin', '-p', InputOption::VALUE_REQUIRED, 'The plugin containing the migrations') 48 | ->addOption('--connection', '-c', InputOption::VALUE_REQUIRED, 'The datasource connection to use') 49 | ->addOption('--source', '-s', InputOption::VALUE_REQUIRED, 'The folder where migrations are in'); 50 | } 51 | 52 | /** 53 | * Show the migration status. 54 | * 55 | * @param \Symfony\Component\Console\Input\InputInterface $input the input object 56 | * @param \Symfony\Component\Console\Output\OutputInterface $output the output object 57 | * @return int 58 | */ 59 | protected function execute(InputInterface $input, OutputInterface $output): int 60 | { 61 | $this->beforeExecute($input, $output); 62 | $this->bootstrap($input, $output); 63 | 64 | /** @var string|null $environment */ 65 | $environment = $input->getOption('environment'); 66 | /** @var string|null $format */ 67 | $format = $input->getOption('format'); 68 | 69 | if ($environment === null) { 70 | $environment = $this->getManager()->getConfig()->getDefaultEnvironment(); 71 | $output->writeln('warning no environment specified, defaulting to: ' . $environment); 72 | } else { 73 | $output->writeln('using environment ' . $environment); 74 | } 75 | if ($format !== null) { 76 | $output->writeln('using format ' . $format); 77 | } 78 | 79 | $migrations = $this->getManager()->printStatus($environment, $format); 80 | 81 | switch ($format) { 82 | case 'json': 83 | $flags = 0; 84 | if ($input->getOption('verbose')) { 85 | $flags = JSON_PRETTY_PRINT; 86 | } 87 | $migrationString = (string)json_encode($migrations, $flags); 88 | $this->getManager()->getOutput()->writeln($migrationString); 89 | break; 90 | default: 91 | $this->display($migrations); 92 | break; 93 | } 94 | 95 | return BaseCommand::CODE_SUCCESS; 96 | } 97 | 98 | /** 99 | * Will output the status of the migrations 100 | * 101 | * @param array> $migrations Migrations array. 102 | * @return void 103 | */ 104 | protected function display(array $migrations): void 105 | { 106 | $output = $this->getManager()->getOutput(); 107 | 108 | if ($migrations) { 109 | $output->writeln(''); 110 | $output->writeln(' Status Migration ID Migration Name '); 111 | $output->writeln('-----------------------------------------'); 112 | 113 | foreach ($migrations as $migration) { 114 | $status = $migration['status'] === 'up' ? ' up ' : ' down '; 115 | $maxNameLength = $this->getManager()->maxNameLength; 116 | $name = $migration['name'] ? 117 | ' ' . str_pad($migration['name'], $maxNameLength, ' ') . ' ' : 118 | ' ** MISSING **'; 119 | 120 | $missingComment = ''; 121 | if (!empty($migration['missing'])) { 122 | $missingComment = ' ** MISSING **'; 123 | } 124 | 125 | $output->writeln( 126 | $status . 127 | sprintf(' %14.0f ', $migration['id']) . 128 | $name . 129 | $missingComment, 130 | ); 131 | } 132 | 133 | $output->writeln(''); 134 | } else { 135 | $msg = 'There are no available migrations. Try creating one using the create command.'; 136 | $output->writeln(''); 137 | $output->writeln($msg); 138 | $output->writeln(''); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Command/SeedCommand.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | use EventDispatcherTrait; 34 | 35 | /** 36 | * The default name added to the application command list 37 | * 38 | * @return string 39 | */ 40 | public static function defaultName(): string 41 | { 42 | return 'migrations seed'; 43 | } 44 | 45 | /** 46 | * Configure the option parser 47 | * 48 | * @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure 49 | * @return \Cake\Console\ConsoleOptionParser 50 | */ 51 | public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser 52 | { 53 | $parser->setDescription([ 54 | 'Seed the database with data', 55 | '', 56 | 'Runs a seeder script that can populate the database with data, or run mutations', 57 | '', 58 | 'migrations seed --connection secondary --seed UserSeeder', 59 | '', 60 | 'The `--seed` option can be supplied multiple times to run more than one seeder', 61 | ])->addOption('plugin', [ 62 | 'short' => 'p', 63 | 'help' => 'The plugin to run seeders in', 64 | ])->addOption('connection', [ 65 | 'short' => 'c', 66 | 'help' => 'The datasource connection to use', 67 | 'default' => 'default', 68 | ])->addOption('source', [ 69 | 'short' => 's', 70 | 'default' => ConfigInterface::DEFAULT_SEED_FOLDER, 71 | 'help' => 'The folder where your seeders are.', 72 | ])->addOption('seed', [ 73 | 'help' => 'The name of the seeder that you want to run.', 74 | 'multiple' => true, 75 | ]); 76 | 77 | return $parser; 78 | } 79 | 80 | /** 81 | * Execute the command. 82 | * 83 | * @param \Cake\Console\Arguments $args The command arguments. 84 | * @param \Cake\Console\ConsoleIo $io The console io 85 | * @return int|null The exit code or null for success 86 | */ 87 | public function execute(Arguments $args, ConsoleIo $io): ?int 88 | { 89 | $event = $this->dispatchEvent('Migration.beforeSeed'); 90 | if ($event->isStopped()) { 91 | return $event->getResult() ? self::CODE_SUCCESS : self::CODE_ERROR; 92 | } 93 | $result = $this->executeSeeds($args, $io); 94 | $this->dispatchEvent('Migration.afterSeed'); 95 | 96 | return $result; 97 | } 98 | 99 | /** 100 | * Execute seeders based on console inputs. 101 | * 102 | * @param \Cake\Console\Arguments $args The command arguments. 103 | * @param \Cake\Console\ConsoleIo $io The console io 104 | * @return int|null The exit code or null for success 105 | */ 106 | protected function executeSeeds(Arguments $args, ConsoleIo $io): ?int 107 | { 108 | $factory = new ManagerFactory([ 109 | 'plugin' => $args->getOption('plugin'), 110 | 'source' => $args->getOption('source'), 111 | 'connection' => $args->getOption('connection'), 112 | ]); 113 | $manager = $factory->createManager($io); 114 | $config = $manager->getConfig(); 115 | if (version_compare(Configure::version(), '5.2.0', '>=')) { 116 | $seeds = (array)$args->getArrayOption('seed'); 117 | } else { 118 | $seeds = (array)$args->getMultipleOption('seed'); 119 | } 120 | 121 | $versionOrder = $config->getVersionOrder(); 122 | $io->out('using connection ' . (string)$args->getOption('connection')); 123 | $io->out('using paths ' . $config->getMigrationPath()); 124 | $io->out('ordering by ' . $versionOrder . ' time'); 125 | 126 | $start = microtime(true); 127 | if (!$seeds) { 128 | // run all the seed(ers) 129 | $manager->seed(); 130 | } else { 131 | // run seed(ers) specified in a comma-separated list of classes 132 | foreach ($seeds as $seed) { 133 | $manager->seed(trim($seed)); 134 | } 135 | } 136 | $end = microtime(true); 137 | 138 | $io->out('All Done. Took ' . sprintf('%.4fs', $end - $start) . ''); 139 | 140 | return self::CODE_SUCCESS; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Command/SnapshotTrait.php: -------------------------------------------------------------------------------- 1 | markSnapshotApplied($path, $args, $io); 35 | 36 | if (!$args->getOption('no-lock')) { 37 | $this->refreshDump($args, $io); 38 | } 39 | } 40 | 41 | return $createFile; 42 | } 43 | 44 | /** 45 | * @internal 46 | * @return bool Whether the builtin backend is active. 47 | */ 48 | protected function useBuiltinBackend(): bool 49 | { 50 | return Configure::read('Migrations.backend', 'builtin') === 'builtin'; 51 | } 52 | 53 | /** 54 | * Will mark a snapshot created, the snapshot being identified by its 55 | * full file path. 56 | * 57 | * @param string $path Path to the newly created snapshot 58 | * @param \Cake\Console\Arguments $args The command arguments. 59 | * @param \Cake\Console\ConsoleIo $io The console io 60 | * @return void 61 | */ 62 | protected function markSnapshotApplied(string $path, Arguments $args, ConsoleIo $io): void 63 | { 64 | $fileName = pathinfo($path, PATHINFO_FILENAME); 65 | [$version, ] = explode('_', $fileName, 2); 66 | 67 | $newArgs = []; 68 | $newArgs[] = '-t'; 69 | $newArgs[] = $version; 70 | $newArgs[] = '-o'; 71 | 72 | $newArgs = array_merge($newArgs, $this->parseOptions($args)); 73 | 74 | $io->out('Marking the migration ' . $fileName . ' as migrated...'); 75 | if ($this->useBuiltinBackend()) { 76 | $this->executeCommand(MarkMigratedCommand::class, $newArgs, $io); 77 | } else { 78 | $this->executeCommand(MigrationsMarkMigratedCommand::class, $newArgs, $io); 79 | } 80 | } 81 | 82 | /** 83 | * After a file has been successfully created, we refresh the dump of the database 84 | * to be able to generate a new diff afterward. 85 | * 86 | * @param \Cake\Console\Arguments $args The command arguments. 87 | * @param \Cake\Console\ConsoleIo $io The console io 88 | * @return void 89 | */ 90 | protected function refreshDump(Arguments $args, ConsoleIo $io): void 91 | { 92 | $newArgs = $this->parseOptions($args); 93 | 94 | $io->out('Creating a dump of the new database state...'); 95 | if ($this->useBuiltinBackend()) { 96 | $this->executeCommand(DumpCommand::class, $newArgs, $io); 97 | } else { 98 | $this->executeCommand(MigrationsDumpCommand::class, $newArgs, $io); 99 | } 100 | } 101 | 102 | /** 103 | * Will parse 'connection', 'plugin' and 'source' options into a new Array 104 | * 105 | * @param \Cake\Console\Arguments $args The command arguments. 106 | * @return array Array containing the short for the option followed by its value 107 | */ 108 | protected function parseOptions(Arguments $args): array 109 | { 110 | $newArgs = []; 111 | if ($args->getOption('connection')) { 112 | $newArgs[] = '-c'; 113 | $newArgs[] = $args->getOption('connection'); 114 | } 115 | 116 | if ($args->getOption('plugin')) { 117 | $newArgs[] = '-p'; 118 | $newArgs[] = $args->getOption('plugin'); 119 | } 120 | 121 | if ($args->getOption('source')) { 122 | $newArgs[] = '-s'; 123 | $newArgs[] = $args->getOption('source'); 124 | } 125 | 126 | return $newArgs; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Command/StatusCommand.php: -------------------------------------------------------------------------------- 1 | setDescription([ 62 | 'The status command prints a list of all migrations, along with their current status', 63 | '', 64 | 'migrations status -c secondary', 65 | 'migrations status -c secondary -f json', 66 | ])->addOption('plugin', [ 67 | 'short' => 'p', 68 | 'help' => 'The plugin to run migrations for', 69 | ])->addOption('connection', [ 70 | 'short' => 'c', 71 | 'help' => 'The datasource connection to use', 72 | 'default' => 'default', 73 | ])->addOption('source', [ 74 | 'short' => 's', 75 | 'help' => 'The folder under src/Config that migrations are in', 76 | 'default' => ConfigInterface::DEFAULT_MIGRATION_FOLDER, 77 | ])->addOption('format', [ 78 | 'short' => 'f', 79 | 'help' => 'The output format: text or json. Defaults to text.', 80 | 'choices' => ['text', 'json'], 81 | 'default' => 'text', 82 | ]); 83 | 84 | return $parser; 85 | } 86 | 87 | /** 88 | * Execute the command. 89 | * 90 | * @param \Cake\Console\Arguments $args The command arguments. 91 | * @param \Cake\Console\ConsoleIo $io The console io 92 | * @return int|null The exit code or null for success 93 | */ 94 | public function execute(Arguments $args, ConsoleIo $io): ?int 95 | { 96 | /** @var string|null $format */ 97 | $format = $args->getOption('format'); 98 | 99 | $factory = new ManagerFactory([ 100 | 'plugin' => $args->getOption('plugin'), 101 | 'source' => $args->getOption('source'), 102 | 'connection' => $args->getOption('connection'), 103 | 'dry-run' => $args->getOption('dry-run'), 104 | ]); 105 | $manager = $factory->createManager($io); 106 | $migrations = $manager->printStatus($format); 107 | 108 | switch ($format) { 109 | case 'json': 110 | $flags = 0; 111 | if ($args->getOption('verbose')) { 112 | $flags = JSON_PRETTY_PRINT; 113 | } 114 | $migrationString = (string)json_encode($migrations, $flags); 115 | $io->out($migrationString); 116 | break; 117 | default: 118 | $this->display($migrations, $io); 119 | break; 120 | } 121 | 122 | return Command::CODE_SUCCESS; 123 | } 124 | 125 | /** 126 | * Print migration status to stdout. 127 | * 128 | * @param array $migrations 129 | * @param \Cake\Console\ConsoleIo $io The console io 130 | * @return void 131 | */ 132 | protected function display(array $migrations, ConsoleIo $io): void 133 | { 134 | if ($migrations) { 135 | $rows = []; 136 | $rows[] = ['Status', 'Migration ID', 'Migration Name']; 137 | 138 | foreach ($migrations as $migration) { 139 | $status = $migration['status'] === 'up' ? 'up' : 'down'; 140 | $name = $migration['name'] ? 141 | '' . $migration['name'] . '' : 142 | '** MISSING **'; 143 | 144 | $missingComment = ''; 145 | if (!empty($migration['missing'])) { 146 | $missingComment = '** MISSING **'; 147 | } 148 | $rows[] = [$status, sprintf('%14.0f ', $migration['id']), $name . $missingComment]; 149 | } 150 | $io->helper('table')->output($rows); 151 | } else { 152 | $msg = 'There are no available migrations. Try creating one using the create command.'; 153 | $io->err(''); 154 | $io->err($msg); 155 | $io->err(''); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Config/ConfigInterface.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | interface ConfigInterface extends ArrayAccess 19 | { 20 | public const DEFAULT_MIGRATION_FOLDER = 'Migrations'; 21 | public const DEFAULT_SEED_FOLDER = 'Seeds'; 22 | 23 | /** 24 | * Returns the configuration for the current environment. 25 | * 26 | * This method returns null if the specified environment 27 | * doesn't exist. 28 | * 29 | * @return array|null 30 | */ 31 | public function getEnvironment(): ?array; 32 | 33 | /** 34 | * Gets the path to search for migration files. 35 | * 36 | * @return string 37 | */ 38 | public function getMigrationPath(): string; 39 | 40 | /** 41 | * Gets the path to search for seed files. 42 | * 43 | * @return string 44 | */ 45 | public function getSeedPath(): string; 46 | 47 | /** 48 | * Get the connection name 49 | * 50 | * @return string|false 51 | */ 52 | public function getConnection(): string|false; 53 | 54 | /** 55 | * Get the template file name. 56 | * 57 | * @return string|false 58 | */ 59 | public function getTemplateFile(): string|false; 60 | 61 | /** 62 | * Get the template class name. 63 | * 64 | * @return string|false 65 | */ 66 | public function getTemplateClass(): string|false; 67 | 68 | /** 69 | * Get the template style to use, either change or up_down. 70 | * 71 | * @return string 72 | */ 73 | public function getTemplateStyle(): string; 74 | 75 | /** 76 | * Get the version order. 77 | * 78 | * @return string 79 | */ 80 | public function getVersionOrder(): string; 81 | 82 | /** 83 | * Is version order creation time? 84 | * 85 | * @return bool 86 | */ 87 | public function isVersionOrderCreationTime(): bool; 88 | 89 | /** 90 | * Gets the base class name for migrations. 91 | * 92 | * @param bool $dropNamespace Return the base migration class name without the namespace. 93 | * @return string 94 | */ 95 | public function getMigrationBaseClassName(bool $dropNamespace = true): string; 96 | 97 | /** 98 | * Gets the base class name for seeders. 99 | * 100 | * @param bool $dropNamespace Return the base seeder class name without the namespace. 101 | * @return string 102 | */ 103 | public function getSeedBaseClassName(bool $dropNamespace = true): string; 104 | 105 | /** 106 | * Get the seeder template file name or null if not set. 107 | * 108 | * @return string|null 109 | */ 110 | public function getSeedTemplateFile(): ?string; 111 | } 112 | -------------------------------------------------------------------------------- /src/Db/Action/Action.php: -------------------------------------------------------------------------------- 1 | table = $table; 28 | } 29 | 30 | /** 31 | * The table this action will be applied to 32 | * 33 | * @return \Migrations\Db\Table\Table 34 | */ 35 | public function getTable(): Table 36 | { 37 | return $this->table; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Db/Action/AddColumn.php: -------------------------------------------------------------------------------- 1 | column = $column; 34 | } 35 | 36 | /** 37 | * Returns a new AddColumn object after assembling the given commands 38 | * 39 | * @param \Migrations\Db\Table\Table $table The table to add the column to 40 | * @param string $columnName The column name 41 | * @param string|\Migrations\Db\Literal $type The column type 42 | * @param array $options The column options 43 | * @return self 44 | */ 45 | public static function build(Table $table, string $columnName, string|Literal $type, array $options = []): self 46 | { 47 | $column = new Column(); 48 | $column->setName($columnName); 49 | $column->setType($type); 50 | $column->setOptions($options); // map options to column methods 51 | 52 | return new AddColumn($table, $column); 53 | } 54 | 55 | /** 56 | * Returns the column to be added 57 | * 58 | * @return \Migrations\Db\Table\Column 59 | */ 60 | public function getColumn(): Column 61 | { 62 | return $this->column; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Db/Action/AddForeignKey.php: -------------------------------------------------------------------------------- 1 | foreignKey = $fk; 33 | } 34 | 35 | /** 36 | * Creates a new AddForeignKey object after building the foreign key with 37 | * the passed attributes 38 | * 39 | * @param \Migrations\Db\Table\Table $table The table object to add the foreign key to 40 | * @param string|string[] $columns The columns for the foreign key 41 | * @param \Migrations\Db\Table\Table|string $referencedTable The table the foreign key references 42 | * @param string|string[] $referencedColumns The columns in the referenced table 43 | * @param array $options Extra options for the foreign key 44 | * @param string|null $name The name of the foreign key 45 | * @return self 46 | */ 47 | public static function build(Table $table, string|array $columns, Table|string $referencedTable, string|array $referencedColumns = ['id'], array $options = [], ?string $name = null): self 48 | { 49 | if (is_string($referencedColumns)) { 50 | $referencedColumns = [$referencedColumns]; // str to array 51 | } 52 | 53 | if (is_string($referencedTable)) { 54 | $referencedTable = new Table($referencedTable); 55 | } 56 | 57 | $fk = new ForeignKey(); 58 | $fk->setReferencedTable($referencedTable) 59 | ->setColumns($columns) 60 | ->setReferencedColumns($referencedColumns) 61 | ->setOptions($options); 62 | 63 | if ($name !== null) { 64 | $fk->setName($name); 65 | } 66 | 67 | return new AddForeignKey($table, $fk); 68 | } 69 | 70 | /** 71 | * Returns the foreign key to be added 72 | * 73 | * @return \Migrations\Db\Table\ForeignKey 74 | */ 75 | public function getForeignKey(): ForeignKey 76 | { 77 | return $this->foreignKey; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Db/Action/AddIndex.php: -------------------------------------------------------------------------------- 1 | index = $index; 33 | } 34 | 35 | /** 36 | * Creates a new AddIndex object after building the index object with the 37 | * provided arguments 38 | * 39 | * @param \Migrations\Db\Table\Table $table The table to add the index to 40 | * @param string|string[]|\Migrations\Db\Table\Index $columns The columns to index 41 | * @param array $options Additional options for the index creation 42 | * @return self 43 | */ 44 | public static function build(Table $table, string|array|Index $columns, array $options = []): self 45 | { 46 | // create a new index object if strings or an array of strings were supplied 47 | if (!($columns instanceof Index)) { 48 | $index = new Index(); 49 | 50 | $index->setColumns($columns); 51 | $index->setOptions($options); 52 | } else { 53 | $index = $columns; 54 | } 55 | 56 | return new AddIndex($table, $index); 57 | } 58 | 59 | /** 60 | * Returns the index to be added 61 | * 62 | * @return \Migrations\Db\Table\Index 63 | */ 64 | public function getIndex(): Index 65 | { 66 | return $this->index; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Db/Action/ChangeColumn.php: -------------------------------------------------------------------------------- 1 | columnName = $columnName; 42 | $this->column = $column; 43 | 44 | // if the name was omitted use the existing column name 45 | if ($column->getName() === null || strlen((string)$column->getName()) === 0) { 46 | $column->setName($columnName); 47 | } 48 | } 49 | 50 | /** 51 | * Creates a new ChangeColumn object after building the column definition 52 | * out of the provided arguments 53 | * 54 | * @param \Migrations\Db\Table\Table $table The table to alter 55 | * @param string $columnName The name of the column to change 56 | * @param string|\Migrations\Db\Literal $type The type of the column 57 | * @param array $options Additional options for the column 58 | * @return self 59 | */ 60 | public static function build(Table $table, string $columnName, string|Literal $type, array $options = []): self 61 | { 62 | $column = new Column(); 63 | $column->setName($columnName); 64 | $column->setType($type); 65 | $column->setOptions($options); // map options to column methods 66 | 67 | return new ChangeColumn($table, $columnName, $column); 68 | } 69 | 70 | /** 71 | * Returns the name of the column to change 72 | * 73 | * @return string 74 | */ 75 | public function getColumnName(): string 76 | { 77 | return $this->columnName; 78 | } 79 | 80 | /** 81 | * Returns the column definition 82 | * 83 | * @return \Migrations\Db\Table\Column 84 | */ 85 | public function getColumn(): Column 86 | { 87 | return $this->column; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Db/Action/ChangeComment.php: -------------------------------------------------------------------------------- 1 | newComment = $newComment; 32 | } 33 | 34 | /** 35 | * Return the new comment for the table 36 | * 37 | * @return string|null 38 | */ 39 | public function getNewComment(): ?string 40 | { 41 | return $this->newComment; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Db/Action/ChangePrimaryKey.php: -------------------------------------------------------------------------------- 1 | newColumns = $newColumns; 32 | } 33 | 34 | /** 35 | * Return the new columns for the primary key 36 | * 37 | * @return string|string[]|null 38 | */ 39 | public function getNewColumns(): string|array|null 40 | { 41 | return $this->newColumns; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Db/Action/CreateTable.php: -------------------------------------------------------------------------------- 1 | foreignKey = $foreignKey; 33 | } 34 | 35 | /** 36 | * Creates a new DropForeignKey object after building the ForeignKey 37 | * definition out of the passed arguments. 38 | * 39 | * @param \Migrations\Db\Table\Table $table The table to delete the foreign key from 40 | * @param string|string[] $columns The columns participating in the foreign key 41 | * @param string|null $constraint The constraint name 42 | * @return self 43 | */ 44 | public static function build(Table $table, string|array $columns, ?string $constraint = null): self 45 | { 46 | if (is_string($columns)) { 47 | $columns = [$columns]; 48 | } 49 | 50 | $foreignKey = new ForeignKey(); 51 | $foreignKey->setColumns($columns); 52 | 53 | if ($constraint) { 54 | $foreignKey->setName($constraint); 55 | } 56 | 57 | return new DropForeignKey($table, $foreignKey); 58 | } 59 | 60 | /** 61 | * Returns the foreign key to remove 62 | * 63 | * @return \Migrations\Db\Table\ForeignKey 64 | */ 65 | public function getForeignKey(): ForeignKey 66 | { 67 | return $this->foreignKey; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Db/Action/DropIndex.php: -------------------------------------------------------------------------------- 1 | index = $index; 33 | } 34 | 35 | /** 36 | * Creates a new DropIndex object after assembling the passed 37 | * arguments. 38 | * 39 | * @param \Migrations\Db\Table\Table $table The table where the index is 40 | * @param string[] $columns the indexed columns 41 | * @return self 42 | */ 43 | public static function build(Table $table, array $columns = []): self 44 | { 45 | $index = new Index(); 46 | $index->setColumns($columns); 47 | 48 | return new DropIndex($table, $index); 49 | } 50 | 51 | /** 52 | * Creates a new DropIndex when the name of the index to drop 53 | * is known. 54 | * 55 | * @param \Migrations\Db\Table\Table $table The table where the index is 56 | * @param string $name The name of the index 57 | * @return self 58 | */ 59 | public static function buildFromName(Table $table, string $name): self 60 | { 61 | $index = new Index(); 62 | $index->setName($name); 63 | 64 | return new DropIndex($table, $index); 65 | } 66 | 67 | /** 68 | * Returns the index to be dropped 69 | * 70 | * @return \Migrations\Db\Table\Index 71 | */ 72 | public function getIndex(): Index 73 | { 74 | return $this->index; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Db/Action/DropTable.php: -------------------------------------------------------------------------------- 1 | column = $column; 33 | } 34 | 35 | /** 36 | * Creates a new RemoveColumn object after assembling the 37 | * passed arguments. 38 | * 39 | * @param \Migrations\Db\Table\Table $table The table where the column is 40 | * @param string $columnName The name of the column to drop 41 | * @return self 42 | */ 43 | public static function build(Table $table, string $columnName): self 44 | { 45 | $column = new Column(); 46 | $column->setName($columnName); 47 | 48 | return new RemoveColumn($table, $column); 49 | } 50 | 51 | /** 52 | * Returns the column to be dropped 53 | * 54 | * @return \Migrations\Db\Table\Column 55 | */ 56 | public function getColumn(): Column 57 | { 58 | return $this->column; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Db/Action/RenameColumn.php: -------------------------------------------------------------------------------- 1 | newName = $newName; 41 | $this->column = $column; 42 | } 43 | 44 | /** 45 | * Creates a new RenameColumn object after building the passed 46 | * arguments 47 | * 48 | * @param \Migrations\Db\Table\Table $table The table where the column is 49 | * @param string $columnName The name of the column to be changed 50 | * @param string $newName The new name for the column 51 | * @return self 52 | */ 53 | public static function build(Table $table, string $columnName, string $newName): self 54 | { 55 | $column = new Column(); 56 | $column->setName($columnName); 57 | 58 | return new RenameColumn($table, $column, $newName); 59 | } 60 | 61 | /** 62 | * Returns the column to be changed 63 | * 64 | * @return \Migrations\Db\Table\Column 65 | */ 66 | public function getColumn(): Column 67 | { 68 | return $this->column; 69 | } 70 | 71 | /** 72 | * Returns the new name for the column 73 | * 74 | * @return string 75 | */ 76 | public function getNewName(): string 77 | { 78 | return $this->newName; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Db/Action/RenameTable.php: -------------------------------------------------------------------------------- 1 | newName = $newName; 32 | } 33 | 34 | /** 35 | * Return the new name for the table 36 | * 37 | * @return string 38 | */ 39 | public function getNewName(): string 40 | { 41 | return $this->newName; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Db/Adapter/AdapterFactory.php: -------------------------------------------------------------------------------- 1 | 44 | * @phpstan-var array|\Closure> 45 | * @psalm-var array|\Closure> 46 | */ 47 | protected array $adapters = [ 48 | 'mysql' => MysqlAdapter::class, 49 | 'postgres' => PostgresAdapter::class, 50 | 'sqlite' => SqliteAdapter::class, 51 | 'sqlserver' => SqlserverAdapter::class, 52 | ]; 53 | 54 | /** 55 | * Class map of adapters wrappers, indexed by name. 56 | * 57 | * @var array 58 | * @psalm-var array> 59 | */ 60 | protected array $wrappers = [ 61 | 'record' => RecordingAdapter::class, 62 | 'timed' => TimedOutputAdapter::class, 63 | ]; 64 | 65 | /** 66 | * Register an adapter class with a given name. 67 | * 68 | * @param string $name Name 69 | * @param \Closure|string $class Class or factory method for the adapter. 70 | * @throws \RuntimeException 71 | * @return $this 72 | */ 73 | public function registerAdapter(string $name, Closure|string $class) 74 | { 75 | if ( 76 | !($class instanceof Closure || is_subclass_of($class, AdapterInterface::class)) 77 | ) { 78 | throw new RuntimeException(sprintf( 79 | 'Adapter class "%s" must implement Migrations\\Db\\Adapter\\AdapterInterface', 80 | $class, 81 | )); 82 | } 83 | $this->adapters[$name] = $class; 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * Get an adapter instance by name. 90 | * 91 | * @param string $name Name 92 | * @param array $options Options 93 | * @return \Migrations\Db\Adapter\AdapterInterface 94 | */ 95 | public function getAdapter(string $name, array $options): AdapterInterface 96 | { 97 | if (empty($this->adapters[$name])) { 98 | throw new RuntimeException(sprintf( 99 | 'Adapter "%s" has not been registered', 100 | $name, 101 | )); 102 | } 103 | $classOrFactory = $this->adapters[$name]; 104 | if ($classOrFactory instanceof Closure) { 105 | return $classOrFactory($options); 106 | } 107 | 108 | return new $classOrFactory($options); 109 | } 110 | 111 | /** 112 | * Add or replace a wrapper with a fully qualified class name. 113 | * 114 | * @param string $name Name 115 | * @param string $class Class 116 | * @throws \RuntimeException 117 | * @return $this 118 | */ 119 | public function registerWrapper(string $name, string $class) 120 | { 121 | if (!is_subclass_of($class, WrapperInterface::class)) { 122 | throw new RuntimeException(sprintf( 123 | 'Wrapper class "%s" must implement Migrations\\Db\\Adapter\\WrapperInterface', 124 | $class, 125 | )); 126 | } 127 | $this->wrappers[$name] = $class; 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * Get a wrapper class by name. 134 | * 135 | * @param string $name Name 136 | * @throws \RuntimeException 137 | * @return class-string<\Migrations\Db\Adapter\WrapperInterface> 138 | */ 139 | protected function getWrapperClass(string $name): string 140 | { 141 | if (empty($this->wrappers[$name])) { 142 | throw new RuntimeException(sprintf( 143 | 'Wrapper "%s" has not been registered', 144 | $name, 145 | )); 146 | } 147 | 148 | return $this->wrappers[$name]; 149 | } 150 | 151 | /** 152 | * Get a wrapper instance by name. 153 | * 154 | * @param string $name Name 155 | * @param \Migrations\Db\Adapter\AdapterInterface $adapter Adapter 156 | * @return \Migrations\Db\Adapter\WrapperInterface 157 | */ 158 | public function getWrapper(string $name, AdapterInterface $adapter): WrapperInterface 159 | { 160 | $class = $this->getWrapperClass($name); 161 | 162 | return new $class($adapter); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Db/Adapter/DirectActionInterface.php: -------------------------------------------------------------------------------- 1 | commands[] = new CreateTable($table); 52 | } 53 | 54 | /** 55 | * @inheritDoc 56 | */ 57 | public function executeActions(Table $table, array $actions): void 58 | { 59 | $this->commands = array_merge($this->commands, $actions); 60 | } 61 | 62 | /** 63 | * Gets an array of the recorded commands in reverse. 64 | * 65 | * @throws \Migrations\Migration\IrreversibleMigrationException if a command cannot be reversed. 66 | * @return \Migrations\Db\Plan\Intent 67 | */ 68 | public function getInvertedCommands(): Intent 69 | { 70 | $inverted = new Intent(); 71 | 72 | foreach (array_reverse($this->commands) as $command) { 73 | switch (true) { 74 | case $command instanceof CreateTable: 75 | /** @var \Migrations\Db\Action\CreateTable $command */ 76 | $inverted->addAction(new DropTable($command->getTable())); 77 | break; 78 | 79 | case $command instanceof RenameTable: 80 | /** @var \Migrations\Db\Action\RenameTable $command */ 81 | $inverted->addAction(new RenameTable(new Table($command->getNewName()), $command->getTable()->getName())); 82 | break; 83 | 84 | case $command instanceof AddColumn: 85 | /** @var \Migrations\Db\Action\AddColumn $command */ 86 | $inverted->addAction(new RemoveColumn($command->getTable(), $command->getColumn())); 87 | break; 88 | 89 | case $command instanceof RenameColumn: 90 | /** @var \Migrations\Db\Action\RenameColumn $command */ 91 | $column = clone $command->getColumn(); 92 | $name = (string)$column->getName(); 93 | $column->setName($command->getNewName()); 94 | $inverted->addAction(new RenameColumn($command->getTable(), $column, $name)); 95 | break; 96 | 97 | case $command instanceof AddIndex: 98 | /** @var \Migrations\Db\Action\AddIndex $command */ 99 | $inverted->addAction(new DropIndex($command->getTable(), $command->getIndex())); 100 | break; 101 | 102 | case $command instanceof AddForeignKey: 103 | /** @var \Migrations\Db\Action\AddForeignKey $command */ 104 | $inverted->addAction(new DropForeignKey($command->getTable(), $command->getForeignKey())); 105 | break; 106 | 107 | default: 108 | throw new IrreversibleMigrationException(sprintf( 109 | 'Cannot reverse a "%s" command', 110 | get_class($command), 111 | )); 112 | } 113 | } 114 | 115 | return $inverted; 116 | } 117 | 118 | /** 119 | * Execute the recorded commands in reverse. 120 | * 121 | * @return void 122 | */ 123 | public function executeInvertedCommands(): void 124 | { 125 | $plan = new Plan($this->getInvertedCommands()); 126 | $plan->executeInverse($this->getAdapter()); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Db/Adapter/UnsupportedColumnTypeException.php: -------------------------------------------------------------------------------- 1 | alterParts = $alterParts; 36 | $this->postSteps = $postSteps; 37 | } 38 | 39 | /** 40 | * Adds another part to the single ALTER instruction 41 | * 42 | * @param string $part The SQL snipped to add as part of the ALTER instruction 43 | * @return void 44 | */ 45 | public function addAlter(string $part): void 46 | { 47 | $this->alterParts[] = $part; 48 | } 49 | 50 | /** 51 | * Adds a SQL command to be executed after the ALTER instruction. 52 | * This method allows a callable, with will get an empty array as state 53 | * for the first time and will pass the return value of the callable to 54 | * the next callable, if present. 55 | * 56 | * This allows to keep a single state across callbacks. 57 | * 58 | * @param string|callable $sql The SQL to run after, or a callable to execute 59 | * @return void 60 | */ 61 | public function addPostStep(string|callable $sql): void 62 | { 63 | $this->postSteps[] = $sql; 64 | } 65 | 66 | /** 67 | * Returns the alter SQL snippets 68 | * 69 | * @return string[] 70 | */ 71 | public function getAlterParts(): array 72 | { 73 | return $this->alterParts; 74 | } 75 | 76 | /** 77 | * Returns the SQL commands to run after the ALTER instruction 78 | * 79 | * @return (string|callable)[] 80 | */ 81 | public function getPostSteps(): array 82 | { 83 | return $this->postSteps; 84 | } 85 | 86 | /** 87 | * Merges another AlterInstructions object to this one 88 | * 89 | * @param \Migrations\Db\AlterInstructions $other The other collection of instructions to merge in 90 | * @return void 91 | */ 92 | public function merge(AlterInstructions $other): void 93 | { 94 | $this->alterParts = array_merge($this->alterParts, $other->getAlterParts()); 95 | $this->postSteps = array_merge($this->postSteps, $other->getPostSteps()); 96 | } 97 | 98 | /** 99 | * Executes the ALTER instruction and all of the post steps. 100 | * 101 | * @param string $alterTemplate The template for the alter instruction 102 | * @param callable $executor The function to be used to execute all instructions 103 | * @return void 104 | */ 105 | public function execute(string $alterTemplate, callable $executor): void 106 | { 107 | if ($this->alterParts) { 108 | $alter = sprintf($alterTemplate, implode(', ', $this->alterParts)); 109 | $executor($alter); 110 | } 111 | 112 | $state = []; 113 | 114 | foreach ($this->postSteps as $instruction) { 115 | if (is_callable($instruction)) { 116 | $state = $instruction($state); 117 | continue; 118 | } 119 | 120 | $executor($instruction); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Db/Expression.php: -------------------------------------------------------------------------------- 1 | value = $value; 24 | } 25 | 26 | /** 27 | * @return string Returns the expression 28 | */ 29 | public function __toString(): string 30 | { 31 | return $this->value; 32 | } 33 | 34 | /** 35 | * @param string $value The expression 36 | * @return self 37 | */ 38 | public static function from(string $value): Expression 39 | { 40 | return new self($value); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Db/Literal.php: -------------------------------------------------------------------------------- 1 | value = $value; 28 | } 29 | 30 | /** 31 | * @return string Returns the literal's value 32 | */ 33 | public function __toString(): string 34 | { 35 | return $this->value; 36 | } 37 | 38 | /** 39 | * @param string $value The literal's value 40 | * @return self 41 | */ 42 | public static function from(string $value): Literal 43 | { 44 | return new self($value); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Db/Plan/AlterTable.php: -------------------------------------------------------------------------------- 1 | table = $table; 41 | } 42 | 43 | /** 44 | * Adds another action to the collection 45 | * 46 | * @param \Migrations\Db\Action\Action $action The action to add 47 | * @return void 48 | */ 49 | public function addAction(Action $action): void 50 | { 51 | $this->actions[] = $action; 52 | } 53 | 54 | /** 55 | * Returns the table associated to this collection 56 | * 57 | * @return \Migrations\Db\Table\Table 58 | */ 59 | public function getTable(): Table 60 | { 61 | return $this->table; 62 | } 63 | 64 | /** 65 | * Returns an array with all collected actions 66 | * 67 | * @return \Migrations\Db\Action\Action[] 68 | */ 69 | public function getActions(): array 70 | { 71 | return $this->actions; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Db/Plan/Intent.php: -------------------------------------------------------------------------------- 1 | actions[] = $action; 34 | } 35 | 36 | /** 37 | * Returns the full list of actions 38 | * 39 | * @return \Migrations\Db\Action\Action[] 40 | */ 41 | public function getActions(): array 42 | { 43 | return $this->actions; 44 | } 45 | 46 | /** 47 | * Merges another Intent object with this one 48 | * 49 | * @param \Migrations\Db\Plan\Intent $another The other intent to merge in 50 | * @return void 51 | */ 52 | public function merge(Intent $another): void 53 | { 54 | $this->actions = array_merge($this->actions, $another->getActions()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Db/Plan/NewTable.php: -------------------------------------------------------------------------------- 1 | table = $table; 49 | } 50 | 51 | /** 52 | * Adds a column to the collection 53 | * 54 | * @param \Migrations\Db\Table\Column $column The column description 55 | * @return void 56 | */ 57 | public function addColumn(Column $column): void 58 | { 59 | $this->columns[] = $column; 60 | } 61 | 62 | /** 63 | * Adds an index to the collection 64 | * 65 | * @param \Migrations\Db\Table\Index $index The index description 66 | * @return void 67 | */ 68 | public function addIndex(Index $index): void 69 | { 70 | $this->indexes[] = $index; 71 | } 72 | 73 | /** 74 | * Returns the table object associated to this collection 75 | * 76 | * @return \Migrations\Db\Table\Table 77 | */ 78 | public function getTable(): Table 79 | { 80 | return $this->table; 81 | } 82 | 83 | /** 84 | * Returns the columns collection 85 | * 86 | * @return \Migrations\Db\Table\Column[] 87 | */ 88 | public function getColumns(): array 89 | { 90 | return $this->columns; 91 | } 92 | 93 | /** 94 | * Returns the indexes collection 95 | * 96 | * @return \Migrations\Db\Table\Index[] 97 | */ 98 | public function getIndexes(): array 99 | { 100 | return $this->indexes; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Db/Plan/Solver/ActionSplitter.php: -------------------------------------------------------------------------------- 1 | conflictClass = $conflictClass; 59 | $this->conflictClassDual = $conflictClassDual; 60 | $this->conflictFilter = $conflictFilter; 61 | } 62 | 63 | /** 64 | * Returns a sequence of AlterTable instructions that are non conflicting 65 | * based on the constructor parameters. 66 | * 67 | * @param \Migrations\Db\Plan\AlterTable $alter The collection of actions to inspect 68 | * @return \Migrations\Db\Plan\AlterTable[] A list of AlterTable that can be executed without 69 | * this type of conflict 70 | */ 71 | public function __invoke(AlterTable $alter): array 72 | { 73 | $conflictActions = array_filter($alter->getActions(), function ($action) { 74 | return $action instanceof $this->conflictClass; 75 | }); 76 | 77 | $originalAlter = new AlterTable($alter->getTable()); 78 | $newAlter = new AlterTable($alter->getTable()); 79 | 80 | foreach ($alter->getActions() as $action) { 81 | if (!$action instanceof $this->conflictClassDual) { 82 | $originalAlter->addAction($action); 83 | continue; 84 | } 85 | 86 | $found = false; 87 | $matches = $this->conflictFilter; 88 | foreach ($conflictActions as $ca) { 89 | if ($matches($ca, $action)) { 90 | $found = true; 91 | break; 92 | } 93 | } 94 | 95 | if ($found) { 96 | $newAlter->addAction($action); 97 | } else { 98 | $originalAlter->addAction($action); 99 | } 100 | } 101 | 102 | return [$originalAlter, $newAlter]; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Db/Table/Table.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | protected array $options; 28 | 29 | /** 30 | * @param string $name The table name 31 | * @param array $options The creation options for this table 32 | * @throws \InvalidArgumentException 33 | */ 34 | public function __construct(string $name, array $options = []) 35 | { 36 | if (!$name) { 37 | throw new InvalidArgumentException('Cannot use an empty table name'); 38 | } 39 | 40 | $this->name = $name; 41 | $this->options = $options; 42 | } 43 | 44 | /** 45 | * Sets the table name. 46 | * 47 | * @param string $name The name of the table 48 | * @return $this 49 | */ 50 | public function setName(string $name) 51 | { 52 | $this->name = $name; 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * Gets the table name. 59 | * 60 | * @return string 61 | */ 62 | public function getName(): string 63 | { 64 | return $this->name; 65 | } 66 | 67 | /** 68 | * Gets the table options 69 | * 70 | * @return array 71 | */ 72 | public function getOptions(): array 73 | { 74 | return $this->options; 75 | } 76 | 77 | /** 78 | * Sets the table options 79 | * 80 | * @param array $options The options for the table creation 81 | * @return $this 82 | */ 83 | public function setOptions(array $options) 84 | { 85 | $this->options = $options; 86 | 87 | return $this; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Migration/BackendInterface.php: -------------------------------------------------------------------------------- 1 | $options Options to pass to the command 12 | * Available options are : 13 | * 14 | * - `format` Format to output the response. Can be 'json' 15 | * - `connection` The datasource connection to use 16 | * - `source` The folder where migrations are in 17 | * - `plugin` The plugin containing the migrations 18 | * @return array The migrations list and their statuses 19 | */ 20 | public function status(array $options = []): array; 21 | 22 | /** 23 | * Migrates available migrations 24 | * 25 | * @param array $options Options to pass to the command 26 | * Available options are : 27 | * 28 | * - `target` The version number to migrate to. If not provided, will migrate 29 | * everything it can 30 | * - `connection` The datasource connection to use 31 | * - `source` The folder where migrations are in 32 | * - `plugin` The plugin containing the migrations 33 | * - `date` The date to migrate to 34 | * @return bool Success 35 | */ 36 | public function migrate(array $options = []): bool; 37 | 38 | /** 39 | * Rollbacks migrations 40 | * 41 | * @param array $options Options to pass to the command 42 | * Available options are : 43 | * 44 | * - `target` The version number to migrate to. If not provided, will only migrate 45 | * the last migrations registered in the phinx log 46 | * - `connection` The datasource connection to use 47 | * - `source` The folder where migrations are in 48 | * - `plugin` The plugin containing the migrations 49 | * - `date` The date to rollback to 50 | * @return bool Success 51 | */ 52 | public function rollback(array $options = []): bool; 53 | 54 | /** 55 | * Marks a migration as migrated 56 | * 57 | * @param int|string|null $version The version number of the migration to mark as migrated 58 | * @param array $options Options to pass to the command 59 | * Available options are : 60 | * 61 | * - `connection` The datasource connection to use 62 | * - `source` The folder where migrations are in 63 | * - `plugin` The plugin containing the migrations 64 | * @return bool Success 65 | */ 66 | public function markMigrated(int|string|null $version = null, array $options = []): bool; 67 | 68 | /** 69 | * Seed the database using a seed file 70 | * 71 | * @param array $options Options to pass to the command 72 | * Available options are : 73 | * 74 | * - `connection` The datasource connection to use 75 | * - `source` The folder where migrations are in 76 | * - `plugin` The plugin containing the migrations 77 | * - `seed` The seed file to use 78 | * @return bool Success 79 | */ 80 | public function seed(array $options = []): bool; 81 | } 82 | -------------------------------------------------------------------------------- /src/Migration/BuiltinBackend.php: -------------------------------------------------------------------------------- 1 | 43 | */ 44 | protected array $default = []; 45 | 46 | /** 47 | * Current command being run. 48 | * Useful if some logic needs to be applied in the ConfigurationTrait depending 49 | * on the command 50 | * 51 | * @var string 52 | */ 53 | protected string $command; 54 | 55 | /** 56 | * Constructor 57 | * 58 | * @param array $default Default option to be used when calling a method. 59 | * Available options are : 60 | * - `connection` The datasource connection to use 61 | * - `source` The folder where migrations are in 62 | * - `plugin` The plugin containing the migrations 63 | */ 64 | public function __construct(array $default = []) 65 | { 66 | if ($default) { 67 | $this->default = $default; 68 | } 69 | } 70 | 71 | /** 72 | * {@inheritDoc} 73 | */ 74 | public function status(array $options = []): array 75 | { 76 | $manager = $this->getManager($options); 77 | 78 | return $manager->printStatus($options['format'] ?? null); 79 | } 80 | 81 | /** 82 | * {@inheritDoc} 83 | */ 84 | public function migrate(array $options = []): bool 85 | { 86 | $manager = $this->getManager($options); 87 | 88 | if (!empty($options['date'])) { 89 | $date = new DateTime($options['date']); 90 | 91 | $manager->migrateToDateTime($date); 92 | 93 | return true; 94 | } 95 | 96 | $manager->migrate($options['target'] ?? null); 97 | 98 | return true; 99 | } 100 | 101 | /** 102 | * {@inheritDoc} 103 | */ 104 | public function rollback(array $options = []): bool 105 | { 106 | $manager = $this->getManager($options); 107 | 108 | if (!empty($options['date'])) { 109 | $date = new DateTime($options['date']); 110 | 111 | $manager->rollbackToDateTime($date); 112 | 113 | return true; 114 | } 115 | 116 | $manager->rollback($options['target'] ?? null); 117 | 118 | return true; 119 | } 120 | 121 | /** 122 | * {@inheritDoc} 123 | */ 124 | public function markMigrated(int|string|null $version = null, array $options = []): bool 125 | { 126 | if ( 127 | isset($options['target']) && 128 | isset($options['exclude']) && 129 | isset($options['only']) 130 | ) { 131 | $exceptionMessage = 'You should use `exclude` OR `only` (not both) along with a `target` argument'; 132 | throw new InvalidArgumentException($exceptionMessage); 133 | } 134 | $args = new Arguments([(string)$version], $options, ['version']); 135 | 136 | $manager = $this->getManager($options); 137 | $config = $manager->getConfig(); 138 | $path = $config->getMigrationPath(); 139 | 140 | $versions = $manager->getVersionsToMark($args); 141 | $manager->markVersionsAsMigrated($path, $versions); 142 | 143 | return true; 144 | } 145 | 146 | /** 147 | * {@inheritDoc} 148 | */ 149 | public function seed(array $options = []): bool 150 | { 151 | $options['source'] ??= ConfigInterface::DEFAULT_SEED_FOLDER; 152 | $seed = $options['seed'] ?? null; 153 | 154 | $manager = $this->getManager($options); 155 | $manager->seed($seed); 156 | 157 | return true; 158 | } 159 | 160 | /** 161 | * Returns an instance of Manager 162 | * 163 | * @param array $options The options for manager creation 164 | * @return \Migrations\Migration\Manager Instance of Manager 165 | */ 166 | public function getManager(array $options): Manager 167 | { 168 | $options += $this->default; 169 | 170 | $factory = new ManagerFactory([ 171 | 'plugin' => $options['plugin'] ?? null, 172 | 'source' => $options['source'] ?? ConfigInterface::DEFAULT_MIGRATION_FOLDER, 173 | 'connection' => $options['connection'] ?? 'default', 174 | ]); 175 | $io = new ConsoleIo( 176 | new StubConsoleOutput(), 177 | new StubConsoleOutput(), 178 | new StubConsoleInput([]), 179 | ); 180 | 181 | return $factory->createManager($io); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Migration/IrreversibleMigrationException.php: -------------------------------------------------------------------------------- 1 | options[$name])) { 59 | return null; 60 | } 61 | 62 | return $this->options[$name]; 63 | } 64 | 65 | /** 66 | * Create a ConfigInterface instance based on the factory options. 67 | * 68 | * @return \Migrations\Config\ConfigInterface 69 | */ 70 | public function createConfig(): ConfigInterface 71 | { 72 | $folder = (string)$this->getOption('source'); 73 | 74 | // Get the filepath for migrations and seeds. 75 | // We rely on factory parameters to define which directory to use. 76 | $dir = ROOT . DS . 'config' . DS . $folder; 77 | if (defined('CONFIG')) { 78 | $dir = CONFIG . $folder; 79 | } 80 | $plugin = (string)$this->getOption('plugin') ?: null; 81 | if ($plugin) { 82 | $dir = Plugin::path($plugin) . 'config' . DS . $folder; 83 | } 84 | 85 | // Get the phinxlog table name. Plugins have separate migration history. 86 | // The names and separate table history is something we could change in the future. 87 | $table = Util::tableName($plugin); 88 | $templatePath = dirname(__DIR__) . DS . 'templates' . DS; 89 | $connectionName = (string)$this->getOption('connection'); 90 | 91 | if (str_contains($connectionName, '://')) { 92 | /** @var array $connectionConfig */ 93 | $connectionConfig = ConnectionManager::parseDsn($connectionName); 94 | $connectionName = 'tmp'; 95 | if (!ConnectionManager::getConfig($connectionName)) { 96 | ConnectionManager::setConfig($connectionName, $connectionConfig); 97 | } 98 | } else { 99 | $connectionConfig = ConnectionManager::getConfig($connectionName); 100 | } 101 | if (!$connectionConfig) { 102 | throw new RuntimeException("Could not find connection `{$connectionName}`"); 103 | } 104 | if (!isset($connectionConfig['database'])) { 105 | throw new RuntimeException("The `{$connectionName}` connection has no `database` key defined."); 106 | } 107 | 108 | /** @var array $connectionConfig */ 109 | $adapter = $connectionConfig['scheme'] ?? null; 110 | $adapterConfig = [ 111 | 'adapter' => $adapter, 112 | 'connection' => $connectionName, 113 | 'database' => $connectionConfig['database'], 114 | 'migration_table' => $table, 115 | 'dryrun' => $this->getOption('dry-run'), 116 | ]; 117 | 118 | $configData = [ 119 | 'paths' => [ 120 | 'migrations' => $dir, 121 | 'seeds' => $dir, 122 | ], 123 | 'templates' => [ 124 | 'file' => $templatePath . 'Phinx/create.php.template', 125 | ], 126 | 'migration_base_class' => 'Migrations\AbstractMigration', 127 | 'environment' => $adapterConfig, 128 | 'plugin' => $plugin, 129 | 'source' => (string)$this->getOption('source'), 130 | 'feature_flags' => [ 131 | 'unsigned_primary_keys' => Configure::read('Migrations.unsigned_primary_keys'), 132 | 'column_null_default' => Configure::read('Migrations.column_null_default'), 133 | ], 134 | ]; 135 | 136 | return new Config($configData); 137 | } 138 | 139 | /** 140 | * Get the migration manager for the current CLI options and application configuration. 141 | * 142 | * @param \Cake\Console\ConsoleIo $io The command io. 143 | * @param \Migrations\Config\ConfigInterface $config A config instance. Providing null will create a new Config 144 | * based on the factory constructor options. 145 | * @return \Migrations\Migration\Manager 146 | */ 147 | public function createManager(ConsoleIo $io, ?ConfigInterface $config = null): Manager 148 | { 149 | $config ??= $this->createConfig(); 150 | 151 | return new Manager($config, $io); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/MigrationsDispatcher.php: -------------------------------------------------------------------------------- 1 | 31 | * @psalm-return array|class-string<\Migrations\Command\Phinx\BaseCommand>> 32 | */ 33 | public static function getCommands(): array 34 | { 35 | return [ 36 | 'Create' => Phinx\Create::class, 37 | 'Dump' => Phinx\Dump::class, 38 | 'MarkMigrated' => Phinx\MarkMigrated::class, 39 | 'Migrate' => Phinx\Migrate::class, 40 | 'Rollback' => Phinx\Rollback::class, 41 | 'Seed' => Phinx\Seed::class, 42 | 'Status' => Phinx\Status::class, 43 | 'CacheBuild' => Phinx\CacheBuild::class, 44 | 'CacheClear' => Phinx\CacheClear::class, 45 | ]; 46 | } 47 | 48 | /** 49 | * Initialize the Phinx console application. 50 | * 51 | * @param string $version The Application Version 52 | */ 53 | public function __construct(string $version) 54 | { 55 | parent::__construct('Migrations plugin, based on Phinx by Rob Morgan.', $version); 56 | // Update this to use the methods 57 | foreach ($this->getCommands() as $value) { 58 | $this->add(new $value()); 59 | } 60 | $this->setCatchExceptions(false); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/MigrationsPlugin.php: -------------------------------------------------------------------------------- 1 | 60 | * @psalm-var array> 61 | */ 62 | protected array $migrationCommandsList = [ 63 | MigrationsCommand::class, 64 | MigrationsCreateCommand::class, 65 | MigrationsDumpCommand::class, 66 | MigrationsMarkMigratedCommand::class, 67 | MigrationsMigrateCommand::class, 68 | MigrationsCacheBuildCommand::class, 69 | MigrationsCacheClearCommand::class, 70 | MigrationsRollbackCommand::class, 71 | MigrationsSeedCommand::class, 72 | MigrationsStatusCommand::class, 73 | ]; 74 | 75 | /** 76 | * Initialize configuration with defaults. 77 | * 78 | * @param \Cake\Core\PluginApplicationInterface $app The application. 79 | * @return void 80 | */ 81 | public function bootstrap(PluginApplicationInterface $app): void 82 | { 83 | parent::bootstrap($app); 84 | 85 | if (!Configure::check('Migrations.backend')) { 86 | Configure::write('Migrations.backend', 'builtin'); 87 | } 88 | } 89 | 90 | /** 91 | * Add migrations commands. 92 | * 93 | * @param \Cake\Console\CommandCollection $commands The command collection to update 94 | * @return \Cake\Console\CommandCollection 95 | */ 96 | public function console(CommandCollection $commands): CommandCollection 97 | { 98 | if (Configure::read('Migrations.backend') == 'builtin') { 99 | $classes = [ 100 | DumpCommand::class, 101 | EntryCommand::class, 102 | MarkMigratedCommand::class, 103 | MigrateCommand::class, 104 | RollbackCommand::class, 105 | SeedCommand::class, 106 | StatusCommand::class, 107 | ]; 108 | $hasBake = class_exists(SimpleBakeCommand::class); 109 | if ($hasBake) { 110 | $classes[] = BakeMigrationCommand::class; 111 | $classes[] = BakeMigrationDiffCommand::class; 112 | $classes[] = BakeMigrationSnapshotCommand::class; 113 | $classes[] = BakeSeedCommand::class; 114 | } 115 | $found = []; 116 | foreach ($classes as $class) { 117 | $name = $class::defaultName(); 118 | // If the short name has been used, use the full name. 119 | // This allows app commands to have name preference. 120 | // and app commands to overwrite migration commands. 121 | if (!$commands->has($name)) { 122 | $found[$name] = $class; 123 | } 124 | $found['migrations.' . $name] = $class; 125 | } 126 | if ($hasBake) { 127 | $found['migrations create'] = BakeMigrationCommand::class; 128 | } 129 | 130 | $commands->addMany($found); 131 | 132 | return $commands; 133 | } 134 | 135 | if (class_exists(SimpleBakeCommand::class)) { 136 | $found = $commands->discoverPlugin($this->getName()); 137 | 138 | return $commands->addMany($found); 139 | } 140 | 141 | $found = []; 142 | // Convert to a method and use config to toggle command names. 143 | foreach ($this->migrationCommandsList as $class) { 144 | $name = $class::defaultName(); 145 | // If the short name has been used, use the full name. 146 | // This allows app commands to have name preference. 147 | // and app commands to overwrite migration commands. 148 | if (!$commands->has($name)) { 149 | $found[$name] = $class; 150 | } 151 | // full name 152 | $found['migrations.' . $name] = $class; 153 | } 154 | 155 | return $commands->addMany($found); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/SeedInterface.php: -------------------------------------------------------------------------------- 1 | \Table class. 157 | * 158 | * You can use this class to create and manipulate tables. 159 | * 160 | * @param string $tableName Table name 161 | * @param array $options Options 162 | * @return \Migrations\Db\Table 163 | */ 164 | public function table(string $tableName, array $options = []): Table; 165 | 166 | /** 167 | * Checks to see if the seed should be executed. 168 | * 169 | * Returns true by default. 170 | * 171 | * You can use this to prevent a seed from executing. 172 | * 173 | * @return bool 174 | */ 175 | public function shouldExecute(): bool; 176 | 177 | /** 178 | * Gives the ability to a seeder to call another seeder. 179 | * This is particularly useful if you need to run the seeders of your applications in a specific sequences, 180 | * for instance to respect foreign key constraints. 181 | * 182 | * @param string $seeder Name of the seeder to call from the current seed 183 | * @param array $options The CLI options for the seeder. 184 | * @return void 185 | */ 186 | public function call(string $seeder, array $options = []): void; 187 | } 188 | -------------------------------------------------------------------------------- /src/Shim/OutputAdapter.php: -------------------------------------------------------------------------------- 1 | io->out($messages, $newline ? 1 : 0); 43 | } 44 | 45 | /** 46 | * @inheritDoc 47 | */ 48 | public function writeln(string|iterable $messages, $options = 0): void 49 | { 50 | if ($messages instanceof Traversable) { 51 | $messages = iterator_to_array($messages); 52 | } 53 | $this->io->out($messages, 1); 54 | } 55 | 56 | /** 57 | * Sets the verbosity of the output. 58 | * 59 | * @param self::VERBOSITY_* $level 60 | * @return void 61 | */ 62 | public function setVerbosity(int $level): void 63 | { 64 | // TODO map values 65 | $this->io->level($level); 66 | } 67 | 68 | /** 69 | * Gets the current verbosity of the output. 70 | * 71 | * @return self::VERBOSITY_* 72 | */ 73 | public function getVerbosity(): int 74 | { 75 | // TODO map values 76 | return $this->io->level(); 77 | } 78 | 79 | /** 80 | * Returns whether verbosity is quiet (-q). 81 | */ 82 | public function isQuiet(): bool 83 | { 84 | return $this->io->level() === ConsoleIo::QUIET; 85 | } 86 | 87 | /** 88 | * Returns whether verbosity is verbose (-v). 89 | */ 90 | public function isVerbose(): bool 91 | { 92 | return $this->io->level() === ConsoleIo::VERBOSE; 93 | } 94 | 95 | /** 96 | * Returns whether verbosity is very verbose (-vv). 97 | */ 98 | public function isVeryVerbose(): bool 99 | { 100 | return false; 101 | } 102 | 103 | /** 104 | * Returns whether verbosity is debug (-vvv). 105 | */ 106 | public function isDebug(): bool 107 | { 108 | return false; 109 | } 110 | 111 | /** 112 | * Sets the decorated flag. 113 | * 114 | * @return void 115 | */ 116 | public function setDecorated(bool $decorated): void 117 | { 118 | throw new RuntimeException('setDecorated is not implemented'); 119 | } 120 | 121 | /** 122 | * Gets the decorated flag. 123 | */ 124 | public function isDecorated(): bool 125 | { 126 | throw new RuntimeException('isDecorated is not implemented'); 127 | } 128 | 129 | /** 130 | * @return void 131 | */ 132 | public function setFormatter(OutputFormatterInterface $formatter): void 133 | { 134 | throw new RuntimeException('setFormatter is not implemented'); 135 | } 136 | 137 | /** 138 | * Returns current output formatter instance. 139 | */ 140 | public function getFormatter(): OutputFormatterInterface 141 | { 142 | throw new RuntimeException('getFormatter is not implemented'); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Util/SchemaTrait.php: -------------------------------------------------------------------------------- 1 | getOption('connection'); 37 | /** @var \Cake\Database\Connection|\Cake\Datasource\ConnectionInterface $connection */ 38 | $connection = ConnectionManager::get($connectionName); 39 | 40 | if (!method_exists($connection, 'getSchemaCollection')) { 41 | $msg = sprintf( 42 | 'The "%s" connection is not compatible with orm caching, ' . 43 | 'as it does not implement a "getSchemaCollection()" method.', 44 | $connectionName, 45 | ); 46 | $output->writeln('' . $msg . ''); 47 | 48 | return null; 49 | } 50 | 51 | $config = $connection->config(); 52 | 53 | if (empty($config['cacheMetadata'])) { 54 | $output->writeln('Metadata cache was disabled in config. Enable to cache or clear.'); 55 | 56 | return null; 57 | } 58 | 59 | $connection->cacheMetadata(true); 60 | 61 | /** 62 | * @var \Cake\Database\Schema\CachedCollection 63 | */ 64 | return $connection->getSchemaCollection(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Util/UtilTrait.php: -------------------------------------------------------------------------------- 1 | getOption('plugin') ?: null; 34 | 35 | return $plugin; 36 | } 37 | 38 | /** 39 | * Get the phinx table name used to store migrations data 40 | * 41 | * @param string|null $plugin Plugin name 42 | * @return string 43 | */ 44 | protected function getPhinxTable(?string $plugin = null): string 45 | { 46 | $table = 'phinxlog'; 47 | 48 | if (!$plugin) { 49 | return $table; 50 | } 51 | 52 | $plugin = Inflector::underscore($plugin) . '_'; 53 | $plugin = str_replace(['\\', '/', '.'], '_', $plugin); 54 | 55 | return $plugin . $table; 56 | } 57 | 58 | /** 59 | * Get the migrations or seeds files path based on the current InputInterface 60 | * 61 | * @param \Symfony\Component\Console\Input\InputInterface $input Input of the current command. 62 | * @param string $default Default folder to set if no source option is found in the $input param 63 | * @return string 64 | */ 65 | protected function getOperationsPath(InputInterface $input, string $default = 'Migrations'): string 66 | { 67 | $folder = $input->getOption('source') ?: $default; 68 | 69 | $dir = ROOT . DS . 'config' . DS . $folder; 70 | 71 | if (defined('CONFIG')) { 72 | $dir = CONFIG . $folder; 73 | } 74 | 75 | $plugin = $this->getPlugin($input); 76 | 77 | if ($plugin !== null) { 78 | $dir = CorePlugin::path($plugin) . 'config' . DS . $folder; 79 | } 80 | 81 | return $dir; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /templates/Phinx/create.php.template: -------------------------------------------------------------------------------- 1 | table('{{ table }}'); 58 | $table->insert($data)->save(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /templates/bake/config/skeleton.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * CakePHP(tm) : Rapid Development Framework (https://cakephp.org) 4 | * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) 5 | * 6 | * Licensed under The MIT License 7 | * For full copyright and license information, please see the LICENSE.txt 8 | * Redistributions of files must retain the above copyright notice. 9 | * 10 | * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) 11 | * @link https://cakephp.org CakePHP(tm) Project 12 | * @since 3.0.0 13 | * @license https://www.opensource.org/licenses/mit-license.php MIT License 14 | */ 15 | #} 16 | {% set wantedOptions = {'length': '', 'limit': '', 'default': '', 'unsigned': '', 'null': '', 'comment': '', 'autoIncrement': '', 'precision': '', 'scale': ''} %} 17 | {% set tableMethod = Migration.tableMethod(action) %} 18 | {% set columnMethod = Migration.columnMethod(action) %} 19 | {% set indexMethod = Migration.indexMethod(action) %} 20 | table('{{ table }}'); 52 | {% if tableMethod != 'drop' %} 53 | {% if columnMethod == 'removeColumn' %} 54 | {% for column, config in columns['fields'] %} 55 | $table->{{ columnMethod }}('{{ column }}'); 56 | {% endfor %} 57 | {% for column, config in columns['indexes'] %} 58 | $table->{{ indexMethod }}([{{ 59 | Migration.stringifyList(config['columns']) | raw 60 | }}]); 61 | {% endfor %} 62 | {% else %} 63 | {% for column, config in columns['fields'] %} 64 | $table->{{ columnMethod }}('{{ column }}', '{{ config['columnType'] }}', [{{ 65 | Migration.stringifyList(config['options'], {'indent': 3}, wantedOptions) | raw 66 | }}]); 67 | {% endfor %} 68 | {% for column, config in columns['indexes'] %} 69 | $table->{{ indexMethod }}([{{ 70 | Migration.stringifyList(config['columns'], {'indent': 3}) | raw }} 71 | ], [{{ 72 | Migration.stringifyList(config['options'], {'indent': 3}) | raw 73 | }}]); 74 | {% endfor %} 75 | {% if tableMethod == 'create' and columns['primaryKey'] is not empty %} 76 | $table->addPrimaryKey([{{ 77 | Migration.stringifyList(columns['primaryKey'], {'indent': 3}) | raw 78 | }}]); 79 | {% endif %} 80 | {% endif %} 81 | {% endif %} 82 | $table->{{ tableMethod }}(){% if tableMethod == 'drop' %}->save(){% endif %}; 83 | {% endfor %} 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /templates/bake/config/snapshot.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * CakePHP(tm) : Rapid Development Framework (https://cakephp.org) 4 | * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) 5 | * 6 | * Licensed under The MIT License 7 | * For full copyright and license information, please see the LICENSE.txt 8 | * Redistributions of files must retain the above copyright notice. 9 | * 10 | * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) 11 | * @link https://cakephp.org CakePHP(tm) Project 12 | * @since 3.0.0 13 | * @license https://www.opensource.org/licenses/mit-license.php MIT License 14 | */ 15 | #} 16 | {% set constraints = [] %} 17 | {% set foreignKeys = [] %} 18 | {% set dropForeignKeys = [] %} 19 | {% if autoId and Migration.hasAutoIdIncompatiblePrimaryKey(tables) %} 20 | {% set autoId = false %} 21 | {% endif %} 22 | table('{{ table }}') 70 | {% for key, columns in columnsList %} 71 | ->dropForeignKey( 72 | {{ columns|raw }} 73 | ){{ (key == maxKey) ? '->save();' : '' }} 74 | {% endfor %} 75 | 76 | {% endfor %} 77 | {% endif %} 78 | {% for table in tables %} 79 | $this->table('{{ table }}')->drop()->save(); 80 | {% endfor %} 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /templates/bake/element/add-columns.twig: -------------------------------------------------------------------------------- 1 | {% for columnName, columnAttributes in columns %} 2 | {% set type = columnAttributes['type'] %} 3 | {% set columnAttributes = Migration.getColumnOption(columnAttributes) %} 4 | {% set columnAttributes = Migration.stringifyList(columnAttributes, {'indent': 4, 'remove': ['type']}) %} 5 | {% if columnAttributes is not empty %} 6 | ->addColumn('{{ columnName }}', '{{ type }}', [{{ columnAttributes | raw }}]) 7 | {% else %} 8 | ->addColumn('{{ columnName }}', '{{ type }}') 9 | {% endif -%} 10 | {% endfor -%} 11 | -------------------------------------------------------------------------------- /templates/bake/element/add-foreign-keys.twig: -------------------------------------------------------------------------------- 1 | {% set statement = Migration.tableStatement(table, true) %} 2 | {% set hasProcessedConstraint = false %} 3 | {% for constraintName, constraint in constraints %} 4 | {% set constraintColumns = constraint['columns']|sort %} 5 | {% if constraint['type'] == 'foreign' %} 6 | {% set hasProcessedConstraint = true %} 7 | {% set columnsList = '\'' ~ constraint['columns'][0] ~ '\'' %} 8 | {% set storedColumnList = columnsList %} 9 | {% set indent = backend == 'builtin' ? 6 : 5 %} 10 | {% if constraint['columns']|length > 1 %} 11 | {% set storedColumnList = '[' ~ Migration.stringifyList(constraint['columns'], {'indent': 5}) ~ ']' %} 12 | {% set columnsList = '[' ~ Migration.stringifyList(constraint['columns'], {'indent': indent}) ~ ']' %} 13 | {% endif %} 14 | {% set record = Migration.storeReturnedData(table, storedColumnList) %} 15 | {% if constraint['references'][1] is iterable %} 16 | {% set columnsReference = '[' ~ Migration.stringifyList(constraint['references'][1], {'indent': indent}) ~ ']' %} 17 | {% else %} 18 | {% set columnsReference = '\'' ~ constraint['references'][1] ~ '\'' %} 19 | {% endif %} 20 | {% if statement is not defined %} 21 | {% set statement = Migration.tableStatement(table) %} 22 | {% endif %} 23 | {% if statement is not empty %} 24 | 25 | {{ statement | raw }} 26 | {% set statement = null %} 27 | {% endif %} 28 | {% if backend == 'builtin' %} 29 | ->addForeignKey( 30 | $this->foreignKey({{ columnsList | raw }}) 31 | ->setReferencedTable('{{ constraint['references'][0] }}') 32 | ->setReferencedColumns({{ columnsReference | raw }}) 33 | ->setOnDelete('{{ Migration.formatConstraintAction(constraint['delete']) | raw }}') 34 | ->setOnUpdate('{{ Migration.formatConstraintAction(constraint['update']) | raw }}') 35 | ->setName('{{ constraintName }}') 36 | ) 37 | {% else %} 38 | ->addForeignKey( 39 | {{ columnsList | raw }}, 40 | '{{ constraint['references'][0] }}', 41 | {{ columnsReference | raw }}, 42 | [ 43 | 'update' => '{{ Migration.formatConstraintAction(constraint['update']) | raw }}', 44 | 'delete' => '{{ Migration.formatConstraintAction(constraint['delete']) | raw }}', 45 | 'constraint' => '{{ constraintName }}' 46 | ] 47 | ) 48 | {% endif %} 49 | {% endif %} 50 | {% endfor %} 51 | {% if Migration.wasTableStatementGeneratedFor(table) and hasProcessedConstraint %} 52 | ->update(); 53 | {% endif -%} 54 | -------------------------------------------------------------------------------- /templates/bake/element/add-indexes.twig: -------------------------------------------------------------------------------- 1 | {% for indexName, index in indexes %} 2 | {% set columnsList = '\'' ~ index['columns'][0] ~ '\'' %} 3 | {% if index['columns']|length > 1 %} 4 | {% set columnsList = '[' ~ Migration.stringifyList(index['columns'], {'indent': 6}) ~ ']' %} 5 | {% endif %} 6 | ->addIndex( 7 | {% if backend == 'builtin' %} 8 | $this->index({{ columnsList | raw }}) 9 | ->setName('{{ indexName }}') 10 | {% if index['type'] == 'unique' %} 11 | ->setType('unique') 12 | {% elseif index['type'] == 'fulltext' %} 13 | ->setType('fulltext') 14 | {% endif %} 15 | {% if index['options'] %} 16 | ->setOptions([{{ Migration.stringifyList(index['options'], {'indent': 6}) | raw }}]) 17 | {% endif %} 18 | ) 19 | {% else %} 20 | [{{ Migration.stringifyList(index['columns'], {'indent': 5}) | raw }}], 21 | {% set params = {'name': indexName} %} 22 | {% if index['type'] == 'unique' %} 23 | {% set params = params|merge({'unique': true}) %} 24 | {% endif %} 25 | [{{ Migration.stringifyList(params, {'indent': 5}) | raw }}] 26 | ) 27 | {% endif %} 28 | {% endfor %} 29 | -------------------------------------------------------------------------------- /templates/bake/element/create-tables.twig: -------------------------------------------------------------------------------- 1 | {% set createData = Migration.getCreateTablesElementData(tables) %} 2 | {% for table, schema in tables %} 3 | {% set tableArgForMethods = useSchema ? schema : table %} 4 | {% set tableArgForArray = useSchema ? table : schema %} 5 | {% set foreignKeys = [] %} 6 | {% set primaryKeysColumns = Migration.primaryKeysColumnsList(tableArgForMethods) %} 7 | {% set primaryKeys = Migration.primaryKeys(tableArgForMethods) %} 8 | {% set specialPk = primaryKeys and (primaryKeys|length > 1 or primaryKeys[0]['name'] != 'id' or primaryKeys[0]['info']['columnType'] != 'integer') and autoId %} 9 | {% if loop.index > 1 %} 10 | 11 | {% endif -%} 12 | {% if specialPk %} 13 | $this->table('{{ tableArgForArray }}', ['id' => false, 'primary_key' => ['{{ Migration.extract(primaryKeys)|join("', '")|raw }}']]) 14 | {% elseif not primaryKeys and autoId %} 15 | $this->table('{{ tableArgForArray }}', ['id' => false]) 16 | {% else %} 17 | $this->table('{{ tableArgForArray }}') 18 | {% endif %} 19 | {% if specialPk or not autoId %} 20 | {% for primaryKey in primaryKeys %} 21 | {% set columnOptions = Migration.getColumnOption(primaryKey['info']['options']) %} 22 | ->addColumn('{{ primaryKey['name'] }}', '{{ primaryKey['info']['columnType'] }}', [{{ Migration.stringifyList(columnOptions, {'indent': 4}) | raw }}]) 23 | {% endfor %} 24 | {% if not autoId and primaryKeys %} 25 | ->addPrimaryKey(['{{ Migration.extract(primaryKeys) 26 | | join("', '") | raw }}']) 27 | {% endif %} 28 | {% endif %} 29 | {% for column, config in Migration.columns(tableArgForMethods) %} 30 | {% set columnOptions = Migration.getColumnOption(config['options']) %} 31 | {% if config['columnType'] == 'boolean' and columnOptions['default'] is defined and (Migration.value(columnOptions['default'])) is not same as('null') %} 32 | {% set default = columnOptions['default'] ? true : false %} 33 | {% set columnOptions = columnOptions|merge({'default': default}) %} 34 | {% endif %} 35 | ->addColumn('{{ column }}', '{{ config['columnType'] }}', [{{ 36 | Migration.stringifyList(columnOptions, {'indent': 4}) | raw 37 | }}]) 38 | {% endfor %} 39 | {% if createData.tables[table].constraints is not empty %} 40 | {% for name, constraint in createData.tables[table].constraints %} 41 | {% if constraint['type'] == 'unique' %} 42 | {{ element('Migrations.add-indexes', { 43 | indexes: {(name): constraint}, 44 | backend: backend, 45 | }) -}} 46 | {% endif %} 47 | {% endfor %} 48 | {% endif %} 49 | {{- element('Migrations.add-indexes', { 50 | indexes: createData.tables[table].indexes, 51 | backend: backend, 52 | }) }} ->create(); 53 | {% endfor -%} 54 | {% if createData.constraints %} 55 | {% for table, tableConstraints in createData.constraints %} 56 | {{- element('Migrations.add-foreign-keys', { 57 | constraints: tableConstraints, 58 | table: table, 59 | backend: backend, 60 | }) 61 | -}} 62 | {% endfor -%} 63 | {% endif -%} 64 | --------------------------------------------------------------------------------