├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── ---bug-report.md
│ └── ---feature-request.md
└── workflows
│ ├── code-style.yml
│ ├── stale.yml
│ ├── static-analysis.yml
│ └── test.yml
├── .gitignore
├── .php-cs-fixer.dist.php
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── composer.json
├── config
└── elastic.migrations.php
├── database
└── migrations
│ └── 2019_15_12_112000_create_elastic_migrations_table.php
├── ide.json
├── phpstan-baseline.neon
├── phpstan.neon.dist
├── phpunit.xml.dist
├── src
├── Adapters
│ └── IndexManagerAdapter.php
├── Console
│ ├── FreshCommand.php
│ ├── MakeCommand.php
│ ├── MigrateCommand.php
│ ├── RefreshCommand.php
│ ├── ResetCommand.php
│ ├── RollbackCommand.php
│ ├── StatusCommand.php
│ └── stubs
│ │ └── migration.blank.stub
├── Facades
│ └── Index.php
├── Factories
│ └── MigrationFactory.php
├── Filesystem
│ ├── MigrationFile.php
│ └── MigrationStorage.php
├── IndexManagerInterface.php
├── MigrationInterface.php
├── Migrator.php
├── ReadinessInterface.php
├── Repositories
│ └── MigrationRepository.php
├── ServiceProvider.php
└── helpers.php
└── tests
├── Integration
├── Adapters
│ └── IndexManagerAdapterTest.php
├── Console
│ ├── FreshCommandTest.php
│ ├── MakeCommandTest.php
│ ├── MigrateCommandTest.php
│ ├── RefreshCommandTest.php
│ ├── ResetCommandTest.php
│ ├── RollbackCommandTest.php
│ └── StatusCommandTest.php
├── Facades
│ └── IndexTest.php
├── Factories
│ └── MigrationFactoryTest.php
├── Filesystem
│ └── MigrationStorageTest.php
├── MigratorTest.php
├── Repositories
│ └── MigrationRepositoryTest.php
└── TestCase.php
├── Unit
└── Filesystem
│ └── MigrationFileTest.php
└── migrations
├── 2018_12_01_081000_create_test_index.php
├── 2019_08_10_142230_update_test_index_mapping.php
├── archive
└── 2017_11_11_100000_create_test_alias.php
└── non_migration_file
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: ivanbabenko
2 | custom: ['https://paypal.me/babenkoi']
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/---bug-report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F41E Bug report"
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 |
15 |
16 | | Software | Version
17 | | ------------- | ---------------
18 | | PHP | x.y.z
19 | | Elasticsearch | x.y.z
20 | | Laravel | x.y.z
21 |
22 | **Describe the bug**
23 |
24 |
25 | **To Reproduce**
26 |
27 |
28 | **Current behavior**
29 |
30 |
31 | **Expected behavior**
32 |
33 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/---feature-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F4A1 Feature request"
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: feature
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/workflows/code-style.yml:
--------------------------------------------------------------------------------
1 | name: Code style
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | style-check:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Checkout code
10 | uses: actions/checkout@v2
11 |
12 | - name: Install php and composer
13 | uses: shivammathur/setup-php@v2
14 | with:
15 | php-version: 8.2
16 | coverage: none
17 | tools: composer:v2
18 |
19 | - name: Get composer cache directory
20 | id: composer-cache
21 | run: echo "::set-output name=dir::$(composer config cache-files-dir)"
22 |
23 | - name: Restore composer cache
24 | uses: actions/cache@v4
25 | with:
26 | path: ${{ steps.composer-cache.outputs.dir }}
27 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
28 | restore-keys: ${{ runner.os }}-composer-
29 |
30 | - name: Install dependencies
31 | run: composer install --no-interaction
32 |
33 | - name: Check code style
34 | run: make style-check
35 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: Close stale issues and pull requests
2 |
3 | on:
4 | schedule:
5 | - cron: "0 0 * * *"
6 |
7 | jobs:
8 | stale:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/stale@v3
12 | with:
13 | repo-token: ${{ secrets.GITHUB_TOKEN }}
14 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days'
15 | stale-pr-message: 'This pull request is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days'
16 | stale-issue-label: 'stale'
17 | stale-pr-label: 'stale'
18 | days-before-stale: 30
19 | days-before-close: 7
20 |
--------------------------------------------------------------------------------
/.github/workflows/static-analysis.yml:
--------------------------------------------------------------------------------
1 | name: Static analysis
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | static-analysis:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Checkout code
10 | uses: actions/checkout@v2
11 |
12 | - name: Install php and composer
13 | uses: shivammathur/setup-php@v2
14 | with:
15 | php-version: 8.2
16 | coverage: none
17 | tools: composer:v2
18 |
19 | - name: Get composer cache directory
20 | id: composer-cache
21 | run: echo "::set-output name=dir::$(composer config cache-files-dir)"
22 |
23 | - name: Restore composer cache
24 | uses: actions/cache@v4
25 | with:
26 | path: ${{ steps.composer-cache.outputs.dir }}
27 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
28 | restore-keys: ${{ runner.os }}-composer-
29 |
30 | - name: Install dependencies
31 | run: composer install --no-interaction
32 |
33 | - name: Analyse code
34 | run: make static-analysis
35 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | php: [8.2]
11 | include:
12 | - php: 8.2
13 | testbench: 9.0
14 | phpunit: 11.0
15 | steps:
16 | - name: Checkout code
17 | uses: actions/checkout@v2
18 |
19 | - name: Install php and composer
20 | uses: shivammathur/setup-php@v2
21 | with:
22 | php-version: ${{ matrix.php }}
23 | coverage: none
24 | tools: composer:v2
25 |
26 | - name: Get composer cache directory
27 | id: composer-cache
28 | run: echo "::set-output name=dir::$(composer config cache-files-dir)"
29 |
30 | - name: Restore composer cache
31 | uses: actions/cache@v4
32 | with:
33 | path: ${{ steps.composer-cache.outputs.dir }}
34 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
35 | restore-keys: ${{ runner.os }}-composer-
36 |
37 | - name: Install dependencies
38 | run: composer require --no-interaction --dev orchestra/testbench:^${{ matrix.testbench }} phpunit/phpunit:^${{ matrix.phpunit }}
39 |
40 | - name: Run tests
41 | run: make test
42 |
43 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /bin
3 | /vendor
4 | /composer.lock
5 | /phpunit.xml
6 | /.phpunit.cache
7 | /.phpunit.result.cache
8 | /.php_cs
9 | /.php_cs.cache
10 | /.php-cs-fixer.cache
11 | /phpstan.neon
12 |
--------------------------------------------------------------------------------
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | in(__DIR__ . '/src')
8 | ->in(__DIR__ . '/tests')
9 | ->name('*.php');
10 |
11 | return (new Config())
12 | ->setFinder($finder)
13 | ->setRules([
14 | '@PSR2' => true,
15 | 'array_syntax' => ['syntax' => 'short'],
16 | 'compact_nullable_type_declaration' => true,
17 | 'concat_space' => ['spacing' => 'one'],
18 | 'declare_strict_types' => true,
19 | 'dir_constant' => true,
20 | 'self_static_accessor' => false,
21 | 'fully_qualified_strict_types' => true,
22 | 'function_to_constant' => true,
23 | 'type_declaration_spaces' => true,
24 | 'header_comment' => false,
25 | 'list_syntax' => ['syntax' => 'short'],
26 | 'lowercase_cast' => true,
27 | 'magic_method_casing' => true,
28 | 'modernize_types_casting' => true,
29 | 'multiline_comment_opening_closing' => true,
30 | 'native_constant_invocation' => true,
31 | 'no_alias_functions' => true,
32 | 'no_alternative_syntax' => true,
33 | 'no_blank_lines_after_phpdoc' => true,
34 | 'no_empty_comment' => true,
35 | 'no_empty_phpdoc' => true,
36 | 'no_extra_blank_lines' => true,
37 | 'no_leading_import_slash' => true,
38 | 'no_leading_namespace_whitespace' => true,
39 | 'no_spaces_around_offset' => true,
40 | 'no_superfluous_phpdoc_tags' => ['allow_mixed' => true],
41 | 'no_trailing_comma_in_singleline' => true,
42 | 'no_unneeded_control_parentheses' => true,
43 | 'no_unset_cast' => true,
44 | 'no_unused_imports' => true,
45 | 'no_useless_else' => true,
46 | 'no_useless_return' => true,
47 | 'no_whitespace_in_blank_line' => true,
48 | 'normalize_index_brace' => true,
49 | 'ordered_imports' => true,
50 | 'php_unit_construct' => true,
51 | 'php_unit_dedicate_assert' => ['target' => 'newest'],
52 | 'php_unit_dedicate_assert_internal_type' => ['target' => 'newest'],
53 | 'php_unit_expectation' => ['target' => 'newest'],
54 | 'php_unit_mock' => ['target' => 'newest'],
55 | 'php_unit_mock_short_will_return' => true,
56 | 'php_unit_no_expectation_annotation' => ['target' => 'newest'],
57 | 'php_unit_test_annotation' => ['style' => 'prefix'],
58 | 'php_unit_test_case_static_method_calls' => ['call_type' => 'this'],
59 | 'phpdoc_align' => ['align' => 'vertical'],
60 | 'phpdoc_line_span' => ['method' => 'multi', 'property' => 'multi'],
61 | 'phpdoc_no_package' => true,
62 | 'phpdoc_no_useless_inheritdoc' => true,
63 | 'phpdoc_scalar' => true,
64 | 'phpdoc_separation' => true,
65 | 'phpdoc_single_line_var_spacing' => true,
66 | 'phpdoc_trim' => true,
67 | 'phpdoc_trim_consecutive_blank_line_separation' => true,
68 | 'phpdoc_types' => true,
69 | 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'],
70 | 'phpdoc_var_without_name' => true,
71 | 'return_assignment' => true,
72 | 'short_scalar_cast' => true,
73 | 'single_trait_insert_per_statement' => true,
74 | 'standardize_not_equals' => true,
75 | 'static_lambda' => true,
76 | 'ternary_to_null_coalescing' => true,
77 | 'trim_array_spaces' => true,
78 | 'array_indentation' => true,
79 | 'trailing_comma_in_multiline' => true,
80 | 'visibility_required' => true,
81 | 'yoda_style' => false,
82 | 'use_arrow_functions' => true,
83 | 'phpdoc_to_property_type' => false
84 | ]);
85 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at babenko.i.a@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Workflow
2 |
3 | * Fork the project and clone it locally
4 | * Create a new branch for every new feature or a bug fix
5 | * Do the necessary code changes
6 | * Cover the new or fixed code with tests
7 | * Write a comprehensive commit message in a format `Add the xxx feature` or `Fix the xxx bug`
8 | * Push to the forked repository
9 | * Create a Pull Request to the master branch of the original repository
10 | * Make a new commit with a fix if one or more checks are failing (code analysis, tests, etc.)
11 |
12 | ## Pull Request Requirements
13 |
14 | * Follow [PSR-2 coding style standard](https://www.php-fig.org/psr/psr-2/)
15 | * Write tests
16 | * Document every new feature or an interface change in the README file
17 | * Make one Pull Request per feature / bug fix
18 |
19 | ## Running the Test Suite
20 |
21 | To run tests locally you need PHP (7.2 or higher), [Composer](https://getcomposer.org/download/) and [SQLite 3](https://www.sqlite.org/download.html).
22 |
23 | Install the project dependencies:
24 | ```
25 | composer install
26 | ```
27 |
28 | Run the test suite:
29 | ```
30 | make test
31 | ```
32 |
33 | ## Code Analysis
34 |
35 | To ensure, that your code follows PSR-2 standards you can run:
36 | ```
37 | make style-check
38 | ```
39 |
40 | It is also recommended to perform static code analysis before opening a PR:
41 | ```
42 | make static-analysis
43 | ```
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Ivan Babenko
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: test coverage style-check static-analysis help
2 |
3 | .DEFAULT_GOAL := help
4 |
5 | test: ## Run tests
6 | @printf "\033[93m→ Running tests\033[0m\n"
7 | @bin/phpunit --testdox
8 | @printf "\n\033[92m✔︎ Tests are completed\033[0m\n"
9 |
10 | coverage: ## Run tests and generate the code coverage report
11 | @printf "\033[93m→ Running tests and generating the code coverage report\033[0m\n"
12 | @XDEBUG_MODE=coverage bin/phpunit --testdox --coverage-text
13 | @printf "\n\033[92m✔︎ Tests are completed and the report is generated\033[0m\n"
14 |
15 | style-check: ## Check the code style
16 | @printf "\033[93m→ Checking the code style\033[0m\n"
17 | @bin/php-cs-fixer fix --allow-risky=yes --dry-run --diff --show-progress=dots --verbose
18 | @printf "\n\033[92m✔︎ Code style is checked\033[0m\n"
19 |
20 | static-analysis: ## Do static code analysis
21 | @printf "\033[93m→ Analysing the code\033[0m\n"
22 | @php -d memory_limit=-1 bin/phpstan analyse
23 | @printf "\n\033[92m✔︎ Code static analysis is completed\033[0m\n"
24 |
25 | help: ## Show help
26 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Elastic Migrations
2 |
3 | [](https://packagist.org/packages/babenkoivan/elastic-migrations)
4 | [](https://packagist.org/packages/babenkoivan/elastic-migrations)
5 | [](https://packagist.org/packages/babenkoivan/elastic-migrations)
6 | [](https://github.com/babenkoivan/elastic-migrations/actions?query=workflow%3ATests)
7 | [](https://github.com/babenkoivan/elastic-migrations/actions?query=workflow%3A%22Code+style%22)
8 | [](https://github.com/babenkoivan/elastic-migrations/actions?query=workflow%3A%22Static+analysis%22)
9 | [](https://paypal.me/babenkoi)
10 |
11 |
12 |
13 |
14 |
15 | ---
16 |
17 | Elastic Migrations for Laravel allow you to easily modify and share indices schema across the application's environments.
18 |
19 | ## Contents
20 |
21 | * [Compatibility](#compatibility)
22 | * [Installation](#installation)
23 | * [Configuration](#configuration)
24 | * [Writing Migrations](#writing-migrations)
25 | * [Running Migrations](#running-migrations)
26 | * [Reverting Migrations](#reverting-migrations)
27 | * [Starting Over](#starting-over)
28 | * [Migration Status](#migration-status)
29 | * [Zero Downtime Migration](#zero-downtime-migration)
30 | * [Troubleshooting](#migration-status)
31 |
32 | ## Compatibility
33 |
34 | The current version of Elastic Migrations has been tested with the following configuration:
35 |
36 | * PHP 8.2
37 | * Elasticsearch 8.x
38 | * Laravel 11.x
39 |
40 | If your project uses older Laravel (or PHP) version check [the previous major version](https://github.com/babenkoivan/elastic-migrations/tree/v3.4.1#compatibility) of the package.
41 |
42 | ## Installation
43 |
44 | The library can be installed via Composer:
45 |
46 | ```bash
47 | composer require babenkoivan/elastic-migrations
48 | ```
49 |
50 | If you want to use Elastic Migrations with [Lumen framework](https://lumen.laravel.com/) check [this guide](https://github.com/babenkoivan/elastic-migrations/wiki/Lumen-Installation).
51 |
52 | ## Configuration
53 |
54 | Elastic Migrations uses [babenkoivan/elastic-client](https://github.com/babenkoivan/elastic-client) as a dependency.
55 | To change the client settings you need to publish the configuration file first:
56 |
57 | ```bash
58 | php artisan vendor:publish --provider="Elastic\Client\ServiceProvider"
59 | ```
60 |
61 | In the newly created `config/elastic.client.php` file you can define the default connection name and describe multiple
62 | connections using configuration hashes. Please, refer to the [elastic-client documentation](https://github.com/babenkoivan/elastic-client) for more details.
63 |
64 | It is recommended to publish Elastic Migrations settings as well:
65 |
66 | ```bash
67 | php artisan vendor:publish --provider="Elastic\Migrations\ServiceProvider"
68 | ```
69 |
70 | This will create the `config/elastic.migrations.php` file, which allows you to configure the following options:
71 |
72 | * `storage.default_path` - the default location of your migration files
73 | * `database.table` - the table name that holds executed migration names
74 | * `database.connection` - the database connection you wish to use
75 | * `prefixes.index` - the prefix of your indices
76 | * `prefixes.alias` - the prefix of your aliases
77 |
78 | If you store some migration files outside the default path and want them to be visible by the package, you may use
79 | `registerPaths` method to inform Elastic Migrations how to load them:
80 |
81 | ```php
82 | class MyAppServiceProvider extends Illuminate\Support\ServiceProvider
83 | {
84 | public function boot()
85 | {
86 | resolve(MigrationStorage::class)->registerPaths([
87 | '/my_app/elastic/migrations1',
88 | '/my_app/elastic/migrations2',
89 | ]);
90 | }
91 | }
92 | ```
93 |
94 |
95 | Finally, don't forget to run Laravel database migrations to create Elastic Migrations table:
96 |
97 | ```bash
98 | php artisan migrate
99 | ```
100 |
101 | ## Writing Migrations
102 |
103 | You can effortlessly create a new migration file using an Artisan console command:
104 |
105 | ```bash
106 | // create a migration file with "create_my_index.php" name in the default directory
107 | php artisan elastic:make:migration create_my_index
108 |
109 | // create a migration file with "create_my_index.php" name in "/my_path" directory
110 | // note, that you need to specify the full path to the file in this case
111 | php artisan elastic:make:migration /my_path/create_my_index.php
112 | ```
113 |
114 | Every migration has two methods: `up` and `down`. `up` is used to alternate the index schema and `down` is used to revert that action.
115 |
116 | You can use `Elastic\Migrations\Facades\Index` facade to perform basic operations over Elasticsearch indices:
117 |
118 | #### Create Index
119 |
120 | You can create an index with the default settings:
121 |
122 | ```php
123 | Index::create('my-index');
124 | ```
125 |
126 | You can use a modifier to configure mapping and settings:
127 |
128 | ```php
129 | Index::create('my-index', function (Mapping $mapping, Settings $settings) {
130 | // to add a new field to the mapping use method name as a field type (in Camel Case),
131 | // first argument as a field name and optional second argument for additional field parameters
132 | $mapping->text('title', ['boost' => 2]);
133 | $mapping->float('price');
134 |
135 | // you can define a dynamic template as follows
136 | $mapping->dynamicTemplate('my_template_name', [
137 | 'match_mapping_type' => 'long',
138 | 'mapping' => [
139 | 'type' => 'integer',
140 | ],
141 | ]);
142 |
143 | // you can also change the index settings and the analysis configuration
144 | $settings->index([
145 | 'number_of_replicas' => 2,
146 | 'refresh_interval' => -1
147 | ]);
148 |
149 | $settings->analysis([
150 | 'analyzer' => [
151 | 'title' => [
152 | 'type' => 'custom',
153 | 'tokenizer' => 'whitespace'
154 | ]
155 | ]
156 | ]);
157 | });
158 | ```
159 |
160 | There is also the `createRaw` method in your disposal:
161 |
162 | ```php
163 | $mapping = [
164 | 'properties' => [
165 | 'title' => [
166 | 'type' => 'text'
167 | ]
168 | ]
169 | ];
170 |
171 | $settings = [
172 | 'number_of_replicas' => 2
173 | ];
174 |
175 | Index::createRaw('my-index', $mapping, $settings);
176 | ```
177 |
178 | Finally, it is possible to create an index only if it doesn't exist:
179 |
180 | ```php
181 | // you can use a modifier as shown above
182 | Index::createIfNotExists('my-index', $modifier);
183 | // or you can use raw mapping and settings
184 | Index::createIfNotExistsRaw('my-index', $mapping, $settings);
185 | ```
186 |
187 | #### Update Mapping
188 |
189 | You can use a modifier to adjust the mapping:
190 |
191 | ```php
192 | Index::putMapping('my-index', function (Mapping $mapping) {
193 | $mapping->text('title', ['boost' => 2]);
194 | $mapping->float('price');
195 | });
196 | ```
197 |
198 | Alternatively, you can use the `putMappingRaw` method as follows:
199 |
200 | ```php
201 | Index::putMappingRaw('my-index', [
202 | 'properties' => [
203 | 'title' => [
204 | 'type' => 'text',
205 | 'boost' => 2
206 | ],
207 | 'price' => [
208 | 'price' => 'float'
209 | ]
210 | ]
211 | ]);
212 | ```
213 |
214 | #### Update Settings
215 |
216 | You can use a modifier to change an index configuration:
217 |
218 | ```php
219 | Index::putSettings('my-index', function (Settings $settings) {
220 | $settings->index([
221 | 'number_of_replicas' => 2,
222 | 'refresh_interval' => -1
223 | ]);
224 | });
225 | ```
226 |
227 | The same result can be achieved with the `putSettingsRaw` method:
228 |
229 | ```php
230 | Index::putSettingsRaw('my-index', [
231 | 'index' => [
232 | 'number_of_replicas' => 2,
233 | 'refresh_interval' => -1
234 | ]
235 | ]);
236 | ```
237 |
238 | It is possible to update analysis settings only on closed indices. The `pushSettings` method closes the index,
239 | updates the configuration and opens the index again:
240 |
241 | ```php
242 | Index::pushSettings('my-index', function (Settings $settings) {
243 | $settings->analysis([
244 | 'analyzer' => [
245 | 'title' => [
246 | 'type' => 'custom',
247 | 'tokenizer' => 'whitespace'
248 | ]
249 | ]
250 | ]);
251 | });
252 | ```
253 |
254 | The same can be done with the `pushSettingsRaw` method:
255 |
256 | ```php
257 | Index::pushSettingsRaw('my-index', [
258 | 'analysis' => [
259 | 'analyzer' => [
260 | 'title' => [
261 | 'type' => 'custom',
262 | 'tokenizer' => 'whitespace'
263 | ]
264 | ]
265 | ]
266 | ]);
267 | ```
268 |
269 | #### Drop Index
270 |
271 | You can unconditionally delete the index:
272 |
273 | ```php
274 | Index::drop('my-index');
275 | ```
276 |
277 | or delete it only if it exists:
278 |
279 | ```php
280 | Index::dropIfExists('my-index');
281 | ```
282 |
283 | #### Create Alias
284 |
285 | You can create an alias with optional filter query:
286 |
287 | ```php
288 | Index::putAlias('my-index', 'my-alias', [
289 | 'is_write_index' => true,
290 | 'filter' => [
291 | 'term' => [
292 | 'user_id' => 1,
293 | ],
294 | ],
295 | ]);
296 | ```
297 |
298 | #### Delete Alias
299 |
300 | You can delete an alias by its name:
301 |
302 | ```php
303 | Index::deleteAlias('my-index', 'my-alias');
304 | ```
305 |
306 | #### Multiple Connections
307 |
308 | You can configure multiple connections to Elasticsearch in the [client's configuration file](https://github.com/babenkoivan/elastic-client/tree/master#configuration),
309 | and then use a different connection for every operation:
310 |
311 | ```php
312 | Index::connection('my-connection')->drop('my-index');
313 | ```
314 |
315 | #### More
316 |
317 | Finally, you are free to inject `Elastic\Elasticsearch\Client` in the migration constructor and execute any supported by client actions.
318 |
319 | ## Running Migrations
320 |
321 | You can either run all migrations:
322 |
323 | ```bash
324 | php artisan elastic:migrate
325 | ```
326 |
327 | or run a specific one:
328 |
329 | ```bash
330 | // execute a migration located in one of the registered paths
331 | php artisan elastic:migrate 2018_12_01_081000_create_my_index
332 |
333 | // execute a migration located in "/my_path" directory
334 | // note, that you need to specify the full path to the file in this case
335 | php artisan elastic:migrate /my_path/2018_12_01_081000_create_my_index.php
336 | ```
337 |
338 | Use the `--force` option if you want to execute migrations on production environment:
339 |
340 | ```bash
341 | php artisan elastic:migrate --force
342 | ```
343 |
344 | ## Reverting Migrations
345 |
346 | You can either revert the last executed migrations:
347 |
348 | ```bash
349 | php artisan elastic:migrate:rollback
350 | ```
351 |
352 | or rollback a specific one:
353 |
354 | ```bash
355 | // rollback a migration located in one of the registered paths
356 | php artisan elastic:migrate:rollback 2018_12_01_081000_create_my_index
357 |
358 | // rollback a migration located in "/my_path" directory
359 | // note, that you need to specify the full path to the file in this case
360 | php artisan elastic:migrate:rollback /my_path/2018_12_01_081000_create_my_index
361 | ```
362 |
363 | Use the `elastic:migrate:reset` command if you want to revert all previously migrated files:
364 |
365 | ```bash
366 | php artisan elastic:migrate:reset
367 | ```
368 |
369 | ## Starting Over
370 |
371 | Sometimes you just want to start over, rollback all the changes and apply them again:
372 |
373 | ```bash
374 | php artisan elastic:migrate:refresh
375 | ```
376 |
377 | Alternatively you can also drop all existing indices and rerun the migrations:
378 |
379 | ```bash
380 | php artisan elastic:migrate:fresh
381 | ```
382 |
383 | **Note** that this command uses wildcards to delete indices. This requires setting [action.destructive_requires_name](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-management-settings.html#action-destructive-requires-name) to `false`.
384 |
385 | ## Migration Status
386 |
387 | You can always check which files have been already migrated and what can be reverted by the `elastic:migrate:rollback` command (the last batch):
388 |
389 | ```bash
390 | php artisan elastic:migrate:status
391 | ```
392 |
393 | It is also possible to display only pending migrations:
394 |
395 | ```bash
396 | php artisan elastic:migrate:status --pending
397 | ```
398 |
399 | ## Zero Downtime Migration
400 |
401 | Changing an index mapping with zero downtime is not a trivial process and might vary from one project to another.
402 | Elastic Migrations library doesn't include such feature out of the box, but you can implement it in your project by [following this guide](https://github.com/babenkoivan/elastic-migrations/wiki/Changing-Mapping-with-Zero-Downtime).
403 |
404 | ## Troubleshooting
405 |
406 | If you see one of the messages below, follow the instructions:
407 |
408 | * `Migration table is not yet created` - run the `php artisan migrate` command
409 | * `Migration directory is not yet created` - create a migration file using the `elastic:make:migration` command or
410 | create `migrations` directory manually
411 |
412 | In case one of the commands doesn't work as expected, try to publish configuration:
413 |
414 | ```bash
415 | php artisan vendor:publish --provider="Elastic\Migrations\ServiceProvider"
416 | ```
417 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "babenkoivan/elastic-migrations",
3 | "description": "Elasticsearch migrations for Laravel",
4 | "keywords": [
5 | "laravel",
6 | "migrations",
7 | "elastic",
8 | "elasticsearch",
9 | "php"
10 | ],
11 | "type": "library",
12 | "license": "MIT",
13 | "authors": [
14 | {
15 | "name": "Ivan Babenko",
16 | "email": "babenko.i.a@gmail.com"
17 | }
18 | ],
19 | "funding": [
20 | {
21 | "type": "ko-fi",
22 | "url": "https://ko-fi.com/ivanbabenko"
23 | },
24 | {
25 | "type": "paypal",
26 | "url": "https://paypal.me/babenkoi"
27 | }
28 | ],
29 | "autoload": {
30 | "psr-4": {
31 | "Elastic\\Migrations\\": "src"
32 | },
33 | "files": [
34 | "src/helpers.php"
35 | ]
36 | },
37 | "autoload-dev": {
38 | "psr-4": {
39 | "Elastic\\Migrations\\Tests\\": "tests"
40 | }
41 | },
42 | "require": {
43 | "php": "^8.2",
44 | "babenkoivan/elastic-adapter": "^4.0"
45 | },
46 | "require-dev": {
47 | "phpunit/phpunit": "^11.0",
48 | "orchestra/testbench": "^9.0",
49 | "friendsofphp/php-cs-fixer": "^3.14",
50 | "phpstan/phpstan": "^1.10",
51 | "dg/bypass-finals": "^1.7"
52 | },
53 | "config": {
54 | "bin-dir": "bin",
55 | "allow-plugins": {
56 | "php-http/discovery": true
57 | }
58 | },
59 | "extra": {
60 | "laravel": {
61 | "providers": [
62 | "Elastic\\Migrations\\ServiceProvider"
63 | ]
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/config/elastic.migrations.php:
--------------------------------------------------------------------------------
1 | [
5 | 'default_path' => env('ELASTIC_MIGRATIONS_DEFAULT_PATH', base_path('elastic/migrations'))
6 | ],
7 | 'database' => [
8 | 'table' => env('ELASTIC_MIGRATIONS_TABLE', 'elastic_migrations'),
9 | 'connection' => env('ELASTIC_MIGRATIONS_CONNECTION'),
10 | ],
11 | 'prefixes' => [
12 | 'index' => env('ELASTIC_MIGRATIONS_INDEX_PREFIX', env('SCOUT_PREFIX', '')),
13 | 'alias' => env('ELASTIC_MIGRATIONS_ALIAS_PREFIX', env('SCOUT_PREFIX', '')),
14 | ],
15 | ];
16 |
--------------------------------------------------------------------------------
/database/migrations/2019_15_12_112000_create_elastic_migrations_table.php:
--------------------------------------------------------------------------------
1 | table = config('elastic.migrations.database.table');
14 | }
15 |
16 | public function up(): void
17 | {
18 | if (!Schema::hasTable($this->table)) {
19 | Schema::create($this->table, static function (Blueprint $table) {
20 | $table->string('migration')->primary();
21 | $table->integer('batch');
22 | });
23 | }
24 | }
25 |
26 | public function down(): void
27 | {
28 | Schema::dropIfExists($this->table);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/ide.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "$schema": "https://laravel-ide.com/schema/laravel-ide-v2.json",
4 | "codeGenerations": [
5 | {
6 | "id": "babenkoivan.create-elastic-migration",
7 | "name": "Create Elastic Migration",
8 | "inputFilter": "elastic",
9 | "files": [
10 | {
11 | "directory": "/elastic/migrations",
12 | "name": "${CURRENT_TIME|format:yyyy_MM_dd_HHmmss}_${INPUT_CLASS|className|snakeCase}.php",
13 | "template": {
14 | "type": "stub",
15 | "path": "src/Console/stubs/migration.blank.stub",
16 | "parameters": {
17 | "DummyClass": "${INPUT_FQN|className}"
18 | }
19 | }
20 | }
21 | ]
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/phpstan-baseline.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | ignoreErrors:
3 | -
4 | message: "#^Parameter \\#1 \\$path of method Elastic\\\\Migrations\\\\Filesystem\\\\MigrationStorage\\:\\:makeFilePath\\(\\) expects string, mixed given\\.$#"
5 | count: 1
6 | path: src/Filesystem/MigrationStorage.php
7 |
8 | -
9 | message: "#^Property Elastic\\\\Migrations\\\\Filesystem\\\\MigrationStorage\\:\\:\\$defaultPath \\(string\\) does not accept mixed\\.$#"
10 | count: 1
11 | path: src/Filesystem/MigrationStorage.php
12 |
13 | -
14 | message: "#^Property Elastic\\\\Migrations\\\\Repositories\\\\MigrationRepository\\:\\:\\$connection \\(string\\|null\\) does not accept mixed\\.$#"
15 | count: 1
16 | path: src/Repositories/MigrationRepository.php
17 |
18 | -
19 | message: "#^Property Elastic\\\\Migrations\\\\Repositories\\\\MigrationRepository\\:\\:\\$table \\(string\\) does not accept mixed\\.$#"
20 | count: 1
21 | path: src/Repositories/MigrationRepository.php
22 |
--------------------------------------------------------------------------------
/phpstan.neon.dist:
--------------------------------------------------------------------------------
1 | includes:
2 | - phpstan-baseline.neon
3 |
4 | parameters:
5 | level: max
6 | paths:
7 | - src
8 | ignoreErrors:
9 | - identifier: missingType.iterableValue
10 | - identifier: missingType.generics
11 | - '#Parameter .+? of method Illuminate\\Support\\Collection<.+?>::.+?\(\) expects .+? given#'
12 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 | tests/Unit
13 |
14 |
15 | tests/Integration
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | src
25 |
26 |
27 | src/ServiceProvider.php
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/Adapters/IndexManagerAdapter.php:
--------------------------------------------------------------------------------
1 | indexManager = $indexManager;
20 | }
21 |
22 | public function create(string $indexName, ?callable $modifier = null): IndexManagerInterface
23 | {
24 | $prefixedIndexName = prefix_index_name($indexName);
25 |
26 | if (isset($modifier)) {
27 | $mapping = new Mapping();
28 | $settings = new Settings();
29 |
30 | $modifier($mapping, $settings);
31 |
32 | $index = new Index($prefixedIndexName, $mapping, $settings);
33 | } else {
34 | $index = new Index($prefixedIndexName);
35 | }
36 |
37 | $this->indexManager->create($index);
38 |
39 | return $this;
40 | }
41 |
42 | public function createRaw(string $indexName, ?array $mapping = null, ?array $settings = null): IndexManagerInterface
43 | {
44 | $prefixedIndexName = prefix_index_name($indexName);
45 |
46 | $this->indexManager->createRaw($prefixedIndexName, $mapping, $settings);
47 |
48 | return $this;
49 | }
50 |
51 | public function createIfNotExists(string $indexName, ?callable $modifier = null): IndexManagerInterface
52 | {
53 | $prefixedIndexName = prefix_index_name($indexName);
54 |
55 | if (!$this->indexManager->exists($prefixedIndexName)) {
56 | $this->create($indexName, $modifier);
57 | }
58 |
59 | return $this;
60 | }
61 |
62 | public function createIfNotExistsRaw(
63 | string $indexName,
64 | ?array $mapping = null,
65 | ?array $settings = null
66 | ): IndexManagerInterface {
67 | $prefixedIndexName = prefix_index_name($indexName);
68 |
69 | if (!$this->indexManager->exists($prefixedIndexName)) {
70 | $this->createRaw($indexName, $mapping, $settings);
71 | }
72 |
73 | return $this;
74 | }
75 |
76 | public function putMapping(string $indexName, callable $modifier): IndexManagerInterface
77 | {
78 | $prefixedIndexName = prefix_index_name($indexName);
79 |
80 | $mapping = new Mapping();
81 | $modifier($mapping);
82 |
83 | $this->indexManager->putMapping($prefixedIndexName, $mapping);
84 |
85 | return $this;
86 | }
87 |
88 | public function putMappingRaw(string $indexName, array $mapping): IndexManagerInterface
89 | {
90 | $prefixedIndexName = prefix_index_name($indexName);
91 |
92 | $this->indexManager->putMappingRaw($prefixedIndexName, $mapping);
93 |
94 | return $this;
95 | }
96 |
97 | public function putSettings(string $indexName, callable $modifier): IndexManagerInterface
98 | {
99 | $prefixedIndexName = prefix_index_name($indexName);
100 |
101 | $settings = new Settings();
102 | $modifier($settings);
103 |
104 | $this->indexManager->putSettings($prefixedIndexName, $settings);
105 |
106 | return $this;
107 | }
108 |
109 | public function putSettingsRaw(string $indexName, array $settings): IndexManagerInterface
110 | {
111 | $prefixedIndexName = prefix_index_name($indexName);
112 |
113 | $this->indexManager->putSettingsRaw($prefixedIndexName, $settings);
114 |
115 | return $this;
116 | }
117 |
118 | public function pushSettings(string $indexName, callable $modifier): IndexManagerInterface
119 | {
120 | $prefixedIndexName = prefix_index_name($indexName);
121 |
122 | $this->indexManager->close($prefixedIndexName);
123 | $this->putSettings($indexName, $modifier);
124 | $this->indexManager->open($prefixedIndexName);
125 |
126 | return $this;
127 | }
128 |
129 | public function pushSettingsRaw(string $indexName, array $settings): IndexManagerInterface
130 | {
131 | $prefixedIndexName = prefix_index_name($indexName);
132 |
133 | $this->indexManager->close($prefixedIndexName);
134 | $this->putSettingsRaw($indexName, $settings);
135 | $this->indexManager->open($prefixedIndexName);
136 |
137 | return $this;
138 | }
139 |
140 | public function drop(string $indexName): IndexManagerInterface
141 | {
142 | $prefixedIndexName = prefix_index_name($indexName);
143 |
144 | $this->indexManager->drop($prefixedIndexName);
145 |
146 | return $this;
147 | }
148 |
149 | public function dropIfExists(string $indexName): IndexManagerInterface
150 | {
151 | $prefixedIndexName = prefix_index_name($indexName);
152 |
153 | if ($this->indexManager->exists($prefixedIndexName)) {
154 | $this->drop($indexName);
155 | }
156 |
157 | return $this;
158 | }
159 |
160 | public function putAlias(string $indexName, string $aliasName, ?array $settings = null): IndexManagerInterface
161 | {
162 | $prefixedIndexName = prefix_index_name($indexName);
163 | $prefixedAliasName = prefix_alias_name($aliasName);
164 |
165 | $this->indexManager->putAliasRaw($prefixedIndexName, $prefixedAliasName, $settings);
166 |
167 | return $this;
168 | }
169 |
170 | public function deleteAlias(string $indexName, string $aliasName): IndexManagerInterface
171 | {
172 | $prefixedIndexName = prefix_index_name($indexName);
173 | $prefixedAliasName = prefix_alias_name($aliasName);
174 |
175 | $this->indexManager->deleteAlias($prefixedIndexName, $prefixedAliasName);
176 |
177 | return $this;
178 | }
179 |
180 | public function connection(string $connection): IndexManagerInterface
181 | {
182 | $self = clone $this;
183 | $self->indexManager = $self->indexManager->connection($connection);
184 | return $self;
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/src/Console/FreshCommand.php:
--------------------------------------------------------------------------------
1 | setOutput($this->output);
31 |
32 | if (!$this->confirmToProceed() || !$migrator->isReady()) {
33 | return 1;
34 | }
35 |
36 | $indexManager->drop('*');
37 | $migrationRepository->purge();
38 | $migrator->migrateAll();
39 |
40 | return 0;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Console/MakeCommand.php:
--------------------------------------------------------------------------------
1 | argument('name');
27 | $fileName = sprintf('%s_%s', (new Carbon())->format('Y_m_d_His'), Str::snake(trim($name)));
28 | $className = Str::studly(trim($name));
29 |
30 | $stub = $filesystem->get(__DIR__ . '/stubs/migration.blank.stub');
31 | $content = str_replace('DummyClass', $className, $stub);
32 |
33 | $migrationStorage->create($fileName, $content);
34 |
35 | $this->output->writeln('Created migration: ' . $fileName);
36 |
37 | return 0;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Console/MigrateCommand.php:
--------------------------------------------------------------------------------
1 | setOutput($this->output);
27 |
28 | if (!$this->confirmToProceed() || !$migrator->isReady()) {
29 | return 1;
30 | }
31 |
32 | /** @var ?string $name */
33 | $name = $this->argument('name');
34 |
35 | if (isset($name)) {
36 | $migrator->migrateOne(trim($name));
37 | } else {
38 | $migrator->migrateAll();
39 | }
40 |
41 | return 0;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Console/RefreshCommand.php:
--------------------------------------------------------------------------------
1 | setOutput($this->output);
26 |
27 | if (!$this->confirmToProceed() || !$migrator->isReady()) {
28 | return 1;
29 | }
30 |
31 | $migrator->rollbackAll();
32 | $migrator->migrateAll();
33 |
34 | return 0;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Console/ResetCommand.php:
--------------------------------------------------------------------------------
1 | setOutput($this->output);
26 |
27 | if (!$this->confirmToProceed() || !$migrator->isReady()) {
28 | return 1;
29 | }
30 |
31 | $migrator->rollbackAll();
32 |
33 | return 0;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Console/RollbackCommand.php:
--------------------------------------------------------------------------------
1 | setOutput($this->output);
27 |
28 | if (!$this->confirmToProceed() || !$migrator->isReady()) {
29 | return 1;
30 | }
31 |
32 | /** @var ?string $name */
33 | $name = $this->argument('name');
34 |
35 | if (isset($name)) {
36 | $migrator->rollbackOne(trim($name));
37 | } else {
38 | $migrator->rollbackLastBatch();
39 | }
40 |
41 | return 0;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Console/StatusCommand.php:
--------------------------------------------------------------------------------
1 | setOutput($this->output);
22 |
23 | if (!$migrator->isReady()) {
24 | return 1;
25 | }
26 |
27 | $onlyPending = (bool)$this->option('pending');
28 | $migrator->showStatus($onlyPending);
29 |
30 | return 0;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Console/stubs/migration.blank.stub:
--------------------------------------------------------------------------------
1 | path();
14 |
15 | $className = Str::studly(implode('_', array_slice(explode('_', $file->name()), 4)));
16 | /** @var MigrationInterface $migration */
17 | $migration = resolve($className);
18 |
19 | return $migration;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Filesystem/MigrationFile.php:
--------------------------------------------------------------------------------
1 | filePath = $filePath;
14 | }
15 |
16 | public function name(): string
17 | {
18 | return basename($this->filePath, static::FILE_EXTENSION);
19 | }
20 |
21 | public function path(): string
22 | {
23 | return $this->filePath;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Filesystem/MigrationStorage.php:
--------------------------------------------------------------------------------
1 | filesystem = $filesystem;
21 | $this->defaultPath = config('elastic.migrations.storage.default_path', '');
22 | $this->paths = collect([$this->defaultPath]);
23 | }
24 |
25 | public function create(string $fileName, string $content): MigrationFile
26 | {
27 | if ($this->isPath($fileName)) {
28 | $this->filesystem->put($fileName, $content);
29 | return new MigrationFile($fileName);
30 | }
31 |
32 | if (!$this->filesystem->isDirectory($this->defaultPath)) {
33 | $this->filesystem->makeDirectory($this->defaultPath, static::DIRECTORY_PERMISSIONS, true);
34 | }
35 |
36 | $filePath = $this->makeFilePath($this->defaultPath, $fileName);
37 | $this->filesystem->put($filePath, $content);
38 | return new MigrationFile($filePath);
39 | }
40 |
41 | public function whereName(string $fileName): ?MigrationFile
42 | {
43 | if ($this->isPath($fileName)) {
44 | return $this->filesystem->exists($fileName) ? new MigrationFile($fileName) : null;
45 | }
46 |
47 | foreach ($this->paths as $path) {
48 | $filePath = $this->makeFilePath($path, $fileName);
49 |
50 | if ($this->filesystem->exists($filePath)) {
51 | return new MigrationFile($filePath);
52 | }
53 | }
54 |
55 | return null;
56 | }
57 |
58 | public function all(): Collection
59 | {
60 | return $this->paths->flatMap(
61 | fn (string $path) => $this->filesystem->glob($path . '/*_*' . MigrationFile::FILE_EXTENSION)
62 | )->filter()->mapWithKeys(
63 | static function (string $filePath) {
64 | $file = new MigrationFile($filePath);
65 | return [$file->name() => $file];
66 | }
67 | )->sortKeys()->values();
68 | }
69 |
70 | public function registerPaths(array $paths): self
71 | {
72 | $this->paths = $this->paths->merge($paths)->filter()->unique()->values();
73 | return $this;
74 | }
75 |
76 | public function isReady(): bool
77 | {
78 | return $this->filesystem->isDirectory($this->defaultPath);
79 | }
80 |
81 | private function isPath(string $path): bool
82 | {
83 | return strpos($path, DIRECTORY_SEPARATOR) !== false;
84 | }
85 |
86 | private function makeFilePath(string $path, string $fileName): string
87 | {
88 | return $path . DIRECTORY_SEPARATOR . $fileName . MigrationFile::FILE_EXTENSION;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/IndexManagerInterface.php:
--------------------------------------------------------------------------------
1 | migrationStorage = $migrationStorage;
25 | $this->migrationRepository = $migrationRepository;
26 | $this->migrationFactory = $migrationFactory;
27 | }
28 |
29 | public function setOutput(OutputStyle $output): self
30 | {
31 | $this->output = $output;
32 | return $this;
33 | }
34 |
35 | public function migrateOne(string $fileName): self
36 | {
37 | $file = $this->migrationStorage->whereName($fileName);
38 |
39 | if (is_null($file)) {
40 | $this->output->writeln('Migration is not found: ' . $fileName);
41 | } else {
42 | $this->migrate(collect([$file]));
43 | }
44 |
45 | return $this;
46 | }
47 |
48 | public function migrateAll(): self
49 | {
50 | $files = $this->migrationStorage->all();
51 | $migratedFileNames = $this->migrationRepository->all();
52 |
53 | $nonMigratedFiles = $files->filter(
54 | static fn (MigrationFile $file) => !$migratedFileNames->contains($file->name())
55 | );
56 |
57 | $this->migrate($nonMigratedFiles);
58 |
59 | return $this;
60 | }
61 |
62 | public function rollbackOne(string $fileName): self
63 | {
64 | $file = $this->migrationStorage->whereName($fileName);
65 |
66 | if (is_null($file)) {
67 | $this->output->writeln('Migration is not found: ' . $fileName);
68 | } elseif (!$this->migrationRepository->exists($file->name())) {
69 | $this->output->writeln('Migration is not yet migrated: ' . $file->name());
70 | } else {
71 | $this->rollback(collect([$file->name()]));
72 | }
73 |
74 | return $this;
75 | }
76 |
77 | public function rollbackLastBatch(): self
78 | {
79 | $fileNames = $this->migrationRepository->lastBatch();
80 |
81 | $this->rollback($fileNames);
82 |
83 | return $this;
84 | }
85 |
86 | public function rollbackAll(): self
87 | {
88 | $fileNames = $this->migrationRepository->all();
89 |
90 | $this->rollback($fileNames);
91 |
92 | return $this;
93 | }
94 |
95 | public function showStatus(bool $onlyPending = false): self
96 | {
97 | $files = $this->migrationStorage->all();
98 | $migratedFiles = $this->migrationRepository->all();
99 | $lastBatch = $this->migrationRepository->lastBatch();
100 |
101 | $rows = $files->map(
102 | static fn (MigrationFile $file) => [
103 | $file->name(),
104 | $migratedFiles->contains($file->name())
105 | ? 'Ran>' . ($lastBatch->contains($file->name()) ? ' (last batch)>' : '')
106 | : 'Pending>',
107 | ]
108 | )->when($onlyPending, static fn (Collection $rows) => $rows->filter(
109 | static fn (array $row) => strpos($row[1], 'Pending') !== false
110 | )->values())->toArray();
111 |
112 | if ($rows !== []) {
113 | $headers = ['Migration name>', 'Status>'];
114 | $this->output->table($headers, $rows);
115 | return $this;
116 | }
117 |
118 | $this->output->writeln(
119 | sprintf('%s>', $onlyPending ? 'No pending migrations' : 'No migrations found')
120 | );
121 |
122 | return $this;
123 | }
124 |
125 | private function migrate(Collection $files): self
126 | {
127 | if ($files->isEmpty()) {
128 | $this->output->writeln('Nothing to migrate');
129 | return $this;
130 | }
131 |
132 | $nextBatchNumber = $this->migrationRepository->lastBatchNumber() + 1;
133 |
134 | $files->each(function (MigrationFile $file) use ($nextBatchNumber) {
135 | $this->output->writeln('Migrating: ' . $file->name());
136 |
137 | $migration = $this->migrationFactory->makeFromFile($file);
138 | $migration->up();
139 |
140 | $this->migrationRepository->insert($file->name(), $nextBatchNumber);
141 |
142 | $this->output->writeln('Migrated: ' . $file->name());
143 | });
144 |
145 | return $this;
146 | }
147 |
148 | private function rollback(Collection $fileNames): self
149 | {
150 | $files = $fileNames->map(
151 | fn (string $fileName) => $this->migrationStorage->whereName($fileName)
152 | )->filter();
153 |
154 | if ($fileNames->isEmpty()) {
155 | $this->output->writeln('Nothing to roll back');
156 | return $this;
157 | }
158 |
159 | if ($fileNames->count() !== $files->count()) {
160 | $this->output->writeln(
161 | 'Migration is not found: ' .
162 | implode(
163 | ',',
164 | $fileNames->diff($files->map(static fn (MigrationFile $file) => $file->name()))->toArray()
165 | )
166 | );
167 |
168 | return $this;
169 | }
170 |
171 | $files->each(function (MigrationFile $file) {
172 | $this->output->writeln('Rolling back: ' . $file->name());
173 |
174 | $migration = $this->migrationFactory->makeFromFile($file);
175 | $migration->down();
176 |
177 | $this->migrationRepository->delete($file->name());
178 |
179 | $this->output->writeln('Rolled back: ' . $file->name());
180 | });
181 |
182 | return $this;
183 | }
184 |
185 | public function isReady(): bool
186 | {
187 | if (!$isMigrationRepositoryReady = $this->migrationRepository->isReady()) {
188 | $this->output->writeln('Migration table is not yet created');
189 | }
190 |
191 | if (!$isMigrationStorageReady = $this->migrationStorage->isReady()) {
192 | $this->output->writeln('Default migration path is not yet created');
193 | }
194 |
195 | return $isMigrationRepositoryReady && $isMigrationStorageReady;
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/src/ReadinessInterface.php:
--------------------------------------------------------------------------------
1 | table = config('elastic.migrations.database.table');
20 | $this->connection = config('elastic.migrations.database.connection');
21 | }
22 |
23 | public function insert(string $fileName, int $batchNumber): bool
24 | {
25 | return $this->table()->insert([
26 | 'migration' => $fileName,
27 | 'batch' => $batchNumber,
28 | ]);
29 | }
30 |
31 | public function exists(string $fileName): bool
32 | {
33 | return $this->table()
34 | ->where('migration', $fileName)
35 | ->exists();
36 | }
37 |
38 | public function delete(string $fileName): bool
39 | {
40 | return (bool)$this->table()
41 | ->where('migration', $fileName)
42 | ->delete();
43 | }
44 |
45 | public function purge(): void
46 | {
47 | $this->table()->delete();
48 | }
49 |
50 | public function lastBatchNumber(): ?int
51 | {
52 | /** @var stdClass|null $record */
53 | $record = $this->table()
54 | ->select('batch')
55 | ->orderBy('batch', 'desc')
56 | ->first();
57 |
58 | return isset($record) ? (int)$record->batch : null;
59 | }
60 |
61 | public function lastBatch(): Collection
62 | {
63 | return $this->table()
64 | ->where('batch', $this->lastBatchNumber())
65 | ->orderBy('migration', 'desc')
66 | ->pluck('migration');
67 | }
68 |
69 | public function all(): Collection
70 | {
71 | return $this->table()
72 | ->orderBy('migration', 'desc')
73 | ->pluck('migration');
74 | }
75 |
76 | public function isReady(): bool
77 | {
78 | return Schema::connection($this->connection)->hasTable($this->table);
79 | }
80 |
81 | private function table(): Builder
82 | {
83 | return DB::connection($this->connection)->table($this->table);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/ServiceProvider.php:
--------------------------------------------------------------------------------
1 | configPath = dirname(__DIR__) . '/config/elastic.migrations.php';
39 | $this->migrationsPath = dirname(__DIR__) . '/database/migrations';
40 | }
41 |
42 | /**
43 | * {@inheritDoc}
44 | */
45 | public function register()
46 | {
47 | $this->mergeConfigFrom(
48 | $this->configPath,
49 | basename($this->configPath, '.php')
50 | );
51 |
52 | $this->app->singletonIf(MigrationStorage::class);
53 | $this->app->bindIf(IndexManagerInterface::class, IndexManagerAdapter::class);
54 | }
55 |
56 | /**
57 | * @return void
58 | */
59 | public function boot()
60 | {
61 | $this->publishes([
62 | $this->configPath => config_path(basename($this->configPath)),
63 | ]);
64 |
65 | $this->loadMigrationsFrom($this->migrationsPath);
66 |
67 | $this->commands($this->commands);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/helpers.php:
--------------------------------------------------------------------------------
1 | indexManagerMock = $this->createMock(IndexManager::class);
26 | $this->indexManagerAdapter = new IndexManagerAdapter($this->indexManagerMock);
27 | }
28 |
29 | #[DataProvider('prefixProvider')]
30 | public function test_index_can_be_created_without_modifier(string $indexNamePrefix): void
31 | {
32 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix);
33 |
34 | $indexName = 'test';
35 |
36 | $this->indexManagerMock
37 | ->expects($this->once())
38 | ->method('create')
39 | ->with(new Index($indexNamePrefix . $indexName));
40 |
41 | $this->indexManagerAdapter->create($indexName);
42 | }
43 |
44 | #[DataProvider('prefixProvider')]
45 | public function test_index_can_be_created_with_modifier(string $indexNamePrefix): void
46 | {
47 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix);
48 |
49 | $indexName = 'test';
50 |
51 | $modifier = static function (Mapping $mapping, Settings $settings) {
52 | $mapping->text('title');
53 | $settings->index(['number_of_replicas' => 2]);
54 | };
55 |
56 | $this->indexManagerMock
57 | ->expects($this->once())
58 | ->method('create')
59 | ->with(new Index(
60 | $indexNamePrefix . $indexName,
61 | (new Mapping())->text('title'),
62 | (new Settings())->index(['number_of_replicas' => 2])
63 | ));
64 |
65 | $this->indexManagerAdapter->create($indexName, $modifier);
66 | }
67 |
68 | #[DataProvider('prefixProvider')]
69 | public function test_index_can_be_created_with_raw_mapping(string $indexNamePrefix): void
70 | {
71 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix);
72 |
73 | $indexName = 'test';
74 |
75 | $mapping = [
76 | 'properties' => [
77 | 'title' => [
78 | 'type' => 'text',
79 | ],
80 | ],
81 | ];
82 |
83 | $this->indexManagerMock
84 | ->expects($this->once())
85 | ->method('createRaw')
86 | ->with($indexNamePrefix . $indexName, $mapping);
87 |
88 | $this->indexManagerAdapter->createRaw($indexName, $mapping);
89 | }
90 |
91 | #[DataProvider('prefixProvider')]
92 | public function test_index_with_modifier_can_be_created_only_if_it_does_not_exist(string $indexNamePrefix): void
93 | {
94 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix);
95 |
96 | $indexName = 'test';
97 |
98 | $this->indexManagerMock
99 | ->expects($this->once())
100 | ->method('exists')
101 | ->with($indexNamePrefix . $indexName)
102 | ->willReturn(false);
103 |
104 | $this->indexManagerMock
105 | ->expects($this->once())
106 | ->method('create')
107 | ->with(new Index($indexNamePrefix . $indexName));
108 |
109 | $this->indexManagerAdapter->createIfNotExists($indexName);
110 | }
111 |
112 | #[DataProvider('prefixProvider')]
113 | public function test_index_with_raw_mapping_can_be_created_only_if_it_does_not_exist(string $indexNamePrefix): void
114 | {
115 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix);
116 |
117 | $indexName = 'test';
118 |
119 | $mapping = [
120 | 'properties' => [
121 | 'title' => [
122 | 'type' => 'text',
123 | ],
124 | ],
125 | ];
126 |
127 | $this->indexManagerMock
128 | ->expects($this->once())
129 | ->method('exists')
130 | ->with($indexNamePrefix . $indexName)
131 | ->willReturn(false);
132 |
133 | $this->indexManagerMock
134 | ->expects($this->once())
135 | ->method('createRaw')
136 | ->with($indexNamePrefix . $indexName, $mapping);
137 |
138 | $this->indexManagerAdapter->createIfNotExistsRaw($indexName, $mapping);
139 | }
140 |
141 | #[DataProvider('prefixProvider')]
142 | public function test_mapping_can_be_updated_using_modifier(string $indexNamePrefix): void
143 | {
144 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix);
145 |
146 | $indexName = 'test';
147 |
148 | $modifier = static function (Mapping $mapping) {
149 | $mapping->disableSource()->text('title');
150 | };
151 |
152 | $this->indexManagerMock
153 | ->expects($this->once())
154 | ->method('putMapping')
155 | ->with(
156 | $indexNamePrefix . $indexName,
157 | (new Mapping())->disableSource()->text('title')
158 | );
159 |
160 | $this->indexManagerAdapter->putMapping($indexName, $modifier);
161 | }
162 |
163 | #[DataProvider('prefixProvider')]
164 | public function test_mapping_can_be_updated_using_raw_input(string $indexNamePrefix): void
165 | {
166 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix);
167 |
168 | $indexName = 'test';
169 |
170 | $mapping = [
171 | 'properties' => [
172 | 'title' => ['type' => 'text'],
173 | ],
174 | ];
175 |
176 | $this->indexManagerMock
177 | ->expects($this->once())
178 | ->method('putMappingRaw')
179 | ->with($indexNamePrefix . $indexName, $mapping);
180 |
181 | $this->indexManagerAdapter->putMappingRaw($indexName, $mapping);
182 | }
183 |
184 | #[DataProvider('prefixProvider')]
185 | public function test_settings_can_be_updated_using_modifier(string $indexNamePrefix): void
186 | {
187 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix);
188 |
189 | $indexName = 'test';
190 |
191 | $modifier = static function (Settings $settings) {
192 | $settings->index(['number_of_replicas' => 2, 'refresh_interval' => -1]);
193 | };
194 |
195 | $this->indexManagerMock
196 | ->expects($this->once())
197 | ->method('putSettings')
198 | ->with(
199 | $indexNamePrefix . $indexName,
200 | (new Settings())->index(['number_of_replicas' => 2, 'refresh_interval' => -1])
201 | );
202 |
203 | $this->indexManagerAdapter->putSettings($indexName, $modifier);
204 | }
205 |
206 | #[DataProvider('prefixProvider')]
207 | public function test_settings_can_be_updated_using_raw_input(string $indexNamePrefix): void
208 | {
209 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix);
210 |
211 | $indexName = 'test';
212 | $settings = ['number_of_replicas' => 2];
213 |
214 | $this->indexManagerMock
215 | ->expects($this->once())
216 | ->method('putSettingsRaw')
217 | ->with($indexNamePrefix . $indexName, $settings);
218 |
219 | $this->indexManagerAdapter->putSettingsRaw($indexName, $settings);
220 | }
221 |
222 | #[DataProvider('prefixProvider')]
223 | public function test_settings_can_be_pushed_using_modifier(string $indexNamePrefix): void
224 | {
225 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix);
226 |
227 | $indexName = 'test';
228 |
229 | $modifier = static function (Settings $settings) {
230 | $settings->index(['number_of_replicas' => 2]);
231 | };
232 |
233 | $this->indexManagerMock
234 | ->expects($this->once())
235 | ->method('close')
236 | ->with($indexNamePrefix . $indexName);
237 |
238 | $this->indexManagerMock
239 | ->expects($this->once())
240 | ->method('putSettings')
241 | ->with(
242 | $indexNamePrefix . $indexName,
243 | (new Settings())->index(['number_of_replicas' => 2])
244 | );
245 |
246 | $this->indexManagerMock
247 | ->expects($this->once())
248 | ->method('open')
249 | ->with($indexNamePrefix . $indexName);
250 |
251 | $this->indexManagerAdapter->pushSettings($indexName, $modifier);
252 | }
253 |
254 | #[DataProvider('prefixProvider')]
255 | public function test_settings_can_be_pushed_using_raw_input(string $indexNamePrefix): void
256 | {
257 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix);
258 |
259 | $indexName = 'test';
260 | $settings = ['number_of_replicas' => 2];
261 |
262 | $this->indexManagerMock
263 | ->expects($this->once())
264 | ->method('close')
265 | ->with($indexNamePrefix . $indexName);
266 |
267 | $this->indexManagerMock
268 | ->expects($this->once())
269 | ->method('putSettingsRaw')
270 | ->with($indexNamePrefix . $indexName, $settings);
271 |
272 | $this->indexManagerMock
273 | ->expects($this->once())
274 | ->method('open')
275 | ->with($indexNamePrefix . $indexName);
276 |
277 | $this->indexManagerAdapter->pushSettingsRaw($indexName, $settings);
278 | }
279 |
280 | #[DataProvider('prefixProvider')]
281 | public function test_index_can_be_dropped(string $indexNamePrefix): void
282 | {
283 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix);
284 |
285 | $indexName = 'test';
286 |
287 | $this->indexManagerMock
288 | ->expects($this->once())
289 | ->method('drop')
290 | ->with($indexNamePrefix . $indexName);
291 |
292 | $this->indexManagerAdapter->drop($indexName);
293 | }
294 |
295 | #[DataProvider('prefixProvider')]
296 | public function test_index_can_be_dropped_only_if_exists(string $indexNamePrefix): void
297 | {
298 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix);
299 |
300 | $indexName = 'test';
301 |
302 | $this->indexManagerMock
303 | ->expects($this->once())
304 | ->method('exists')
305 | ->with($indexNamePrefix . $indexName)
306 | ->willReturn(true);
307 |
308 | $this->indexManagerMock
309 | ->expects($this->once())
310 | ->method('drop')
311 | ->with($indexNamePrefix . $indexName);
312 |
313 | $this->indexManagerAdapter->dropIfExists($indexName);
314 | }
315 |
316 | #[DataProvider('prefixProvider')]
317 | public function test_alias_can_be_created(string $aliasNamePrefix): void
318 | {
319 | $this->config->set('elastic.migrations.prefixes.alias', $aliasNamePrefix);
320 |
321 | $indexName = 'foo';
322 | $aliasName = 'bar';
323 |
324 | $this->indexManagerMock
325 | ->expects($this->once())
326 | ->method('putAliasRaw')
327 | ->with($indexName, $aliasNamePrefix . $aliasName);
328 |
329 | $this->indexManagerAdapter->putAlias($indexName, $aliasName);
330 | }
331 |
332 | #[DataProvider('prefixProvider')]
333 | public function test_alias_can_be_deleted(string $aliasNamePrefix): void
334 | {
335 | $this->config->set('elastic.migrations.prefixes.alias', $aliasNamePrefix);
336 |
337 | $indexName = 'foo';
338 | $aliasName = 'bar';
339 |
340 | $this->indexManagerMock
341 | ->expects($this->once())
342 | ->method('deleteAlias')
343 | ->with($indexName, $aliasNamePrefix . $aliasName);
344 |
345 | $this->indexManagerAdapter->deleteAlias($indexName, $aliasName);
346 | }
347 |
348 | public function test_connection_can_be_changed(): void
349 | {
350 | $connection = 'test';
351 |
352 | $this->indexManagerMock
353 | ->expects($this->once())
354 | ->method('connection')
355 | ->with($connection);
356 |
357 | $this->indexManagerAdapter->connection($connection);
358 | }
359 |
360 | public static function prefixProvider(): array
361 | {
362 | return [
363 | 'no prefix' => [''],
364 | 'short prefix' => ['foo_'],
365 | 'long prefix' => ['foo_bar_'],
366 | ];
367 | }
368 | }
369 |
--------------------------------------------------------------------------------
/tests/Integration/Console/FreshCommandTest.php:
--------------------------------------------------------------------------------
1 | migrator = $this->createMock(Migrator::class);
28 | $this->app->instance(Migrator::class, $this->migrator);
29 |
30 | $this->migrationRepository = $this->createMock(MigrationRepository::class);
31 | $this->app->instance(MigrationRepository::class, $this->migrationRepository);
32 |
33 | $this->indexManager = $this->createMock(IndexManagerInterface::class);
34 | $this->app->instance(IndexManagerInterface::class, $this->indexManager);
35 |
36 | $this->command = new FreshCommand();
37 | $this->command->setLaravel($this->app);
38 | }
39 |
40 | public function test_does_nothing_if_migrator_is_not_ready(): void
41 | {
42 | $this->migrator
43 | ->expects($this->once())
44 | ->method('isReady')
45 | ->willReturn(false);
46 |
47 | $this->indexManager
48 | ->expects($this->never())
49 | ->method('drop');
50 |
51 | $this->migrationRepository
52 | ->expects($this->never())
53 | ->method('purge');
54 |
55 | $this->migrator
56 | ->expects($this->never())
57 | ->method('migrateAll');
58 |
59 | $result = $this->command->run(
60 | new ArrayInput(['--force' => true]),
61 | new NullOutput()
62 | );
63 |
64 | $this->assertSame(1, $result);
65 | }
66 |
67 | public function test_drops_indices_and_migration(): void
68 | {
69 | $this->migrator
70 | ->expects($this->once())
71 | ->method('isReady')
72 | ->willReturn(true);
73 |
74 | $this->indexManager
75 | ->expects($this->once())
76 | ->method('drop')
77 | ->with('*');
78 |
79 | $this->migrationRepository
80 | ->expects($this->once())
81 | ->method('purge');
82 |
83 | $this->migrator
84 | ->expects($this->once())
85 | ->method('migrateAll');
86 |
87 | $result = $this->command->run(
88 | new ArrayInput(['--force' => true]),
89 | new NullOutput()
90 | );
91 |
92 | $this->assertSame(0, $result);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/tests/Integration/Console/MakeCommandTest.php:
--------------------------------------------------------------------------------
1 | createMock(MigrationStorage::class);
18 | $this->app->instance(MigrationStorage::class, $migrationStorageMock);
19 |
20 | /** @var string $migrationStub */
21 | $migrationStub = file_get_contents(dirname(__DIR__, 3) . '/src/Console/stubs/migration.blank.stub');
22 |
23 | $migrationStorageMock
24 | ->expects($this->once())
25 | ->method('create')
26 | ->with(
27 | $this->stringEndsWith('_test_migration_creation'),
28 | str_replace('DummyClass', 'TestMigrationCreation', $migrationStub)
29 | );
30 |
31 | $command = new MakeCommand();
32 | $command->setLaravel($this->app);
33 |
34 | $input = new ArrayInput(['name' => 'test_migration_creation']);
35 | $output = new BufferedOutput();
36 |
37 | $resultCode = $command->run($input, $output);
38 | $resultMessage = $output->fetch();
39 |
40 | $this->assertSame(0, $resultCode);
41 |
42 | $this->assertStringContainsString('Created migration', $resultMessage);
43 | $this->assertStringContainsString('_test_migration_creation', $resultMessage);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/Integration/Console/MigrateCommandTest.php:
--------------------------------------------------------------------------------
1 | migrator = $this->createMock(Migrator::class);
24 | $this->app->instance(Migrator::class, $this->migrator);
25 |
26 | $this->command = new MigrateCommand();
27 | $this->command->setLaravel($this->app);
28 | }
29 |
30 | public function test_does_nothing_if_migrator_is_not_ready(): void
31 | {
32 | $this->migrator
33 | ->expects($this->once())
34 | ->method('isReady')
35 | ->willReturn(false);
36 |
37 | $this->migrator
38 | ->expects($this->never())
39 | ->method('migrateOne');
40 |
41 | $this->migrator
42 | ->expects($this->never())
43 | ->method('migrateAll');
44 |
45 | $result = $this->command->run(
46 | new ArrayInput(['--force' => true]),
47 | new NullOutput()
48 | );
49 |
50 | $this->assertSame(1, $result);
51 | }
52 |
53 | public function test_runs_one_migration_if_file_name_is_provided(): void
54 | {
55 | $this->migrator
56 | ->expects($this->once())
57 | ->method('isReady')
58 | ->willReturn(true);
59 |
60 | $this->migrator
61 | ->expects($this->once())
62 | ->method('migrateOne')
63 | ->with('test_file_name');
64 |
65 | $result = $this->command->run(
66 | new ArrayInput(['--force' => true, 'name' => 'test_file_name']),
67 | new NullOutput()
68 | );
69 |
70 | $this->assertSame(0, $result);
71 | }
72 |
73 | public function test_runs_all_migrations_if_file_name_is_not_provided(): void
74 | {
75 | $this->migrator
76 | ->expects($this->once())
77 | ->method('isReady')
78 | ->willReturn(true);
79 |
80 | $this->migrator
81 | ->expects($this->once())
82 | ->method('migrateAll');
83 |
84 | $result = $this->command->run(
85 | new ArrayInput(['--force' => true]),
86 | new NullOutput()
87 | );
88 |
89 | $this->assertSame(0, $result);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/tests/Integration/Console/RefreshCommandTest.php:
--------------------------------------------------------------------------------
1 | migrator = $this->createMock(Migrator::class);
24 | $this->app->instance(Migrator::class, $this->migrator);
25 |
26 | $this->command = new RefreshCommand();
27 | $this->command->setLaravel($this->app);
28 | }
29 |
30 | public function test_does_nothing_if_migrator_is_not_ready(): void
31 | {
32 | $this->migrator
33 | ->expects($this->once())
34 | ->method('isReady')
35 | ->willReturn(false);
36 |
37 | $this->migrator
38 | ->expects($this->never())
39 | ->method('rollbackAll');
40 |
41 | $this->migrator
42 | ->expects($this->never())
43 | ->method('migrateAll');
44 |
45 | $result = $this->command->run(
46 | new ArrayInput(['--force' => true]),
47 | new NullOutput()
48 | );
49 |
50 | $this->assertSame(1, $result);
51 | }
52 |
53 | public function test_resets_and_reruns_all_migrations_if_migrator_is_ready(): void
54 | {
55 | $this->migrator
56 | ->expects($this->once())
57 | ->method('isReady')
58 | ->willReturn(true);
59 |
60 | $this->migrator
61 | ->expects($this->once())
62 | ->method('rollbackAll');
63 |
64 | $this->migrator
65 | ->expects($this->once())
66 | ->method('migrateAll');
67 |
68 | $result = $this->command->run(
69 | new ArrayInput(['--force' => true]),
70 | new NullOutput()
71 | );
72 |
73 | $this->assertSame(0, $result);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/tests/Integration/Console/ResetCommandTest.php:
--------------------------------------------------------------------------------
1 | migrator = $this->createMock(Migrator::class);
24 | $this->app->instance(Migrator::class, $this->migrator);
25 |
26 | $this->command = new ResetCommand();
27 | $this->command->setLaravel($this->app);
28 | }
29 |
30 | public function test_does_nothing_if_migrator_is_not_ready(): void
31 | {
32 | $this->migrator
33 | ->expects($this->once())
34 | ->method('isReady')
35 | ->willReturn(false);
36 |
37 | $this->migrator
38 | ->expects($this->never())
39 | ->method('rollbackAll');
40 |
41 | $result = $this->command->run(
42 | new ArrayInput(['--force' => true]),
43 | new NullOutput()
44 | );
45 |
46 | $this->assertSame(1, $result);
47 | }
48 |
49 | public function test_rollbacks_all_migrations_if_migrator_is_ready(): void
50 | {
51 | $this->migrator
52 | ->expects($this->once())
53 | ->method('isReady')
54 | ->willReturn(true);
55 |
56 | $this->migrator
57 | ->expects($this->once())
58 | ->method('rollbackAll');
59 |
60 | $result = $this->command->run(
61 | new ArrayInput(['--force' => true]),
62 | new NullOutput()
63 | );
64 |
65 | $this->assertSame(0, $result);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/tests/Integration/Console/RollbackCommandTest.php:
--------------------------------------------------------------------------------
1 | migrator = $this->createMock(Migrator::class);
24 | $this->app->instance(Migrator::class, $this->migrator);
25 |
26 | $this->command = new RollbackCommand();
27 | $this->command->setLaravel($this->app);
28 | }
29 |
30 | public function test_does_nothing_if_migrator_is_not_ready(): void
31 | {
32 | $this->migrator
33 | ->expects($this->once())
34 | ->method('isReady')
35 | ->willReturn(false);
36 |
37 | $this->migrator
38 | ->expects($this->never())
39 | ->method('rollbackOne');
40 |
41 | $this->migrator
42 | ->expects($this->never())
43 | ->method('rollbackLastBatch');
44 |
45 | $result = $this->command->run(
46 | new ArrayInput(['--force' => true]),
47 | new NullOutput()
48 | );
49 |
50 | $this->assertSame(1, $result);
51 | }
52 |
53 | public function test_rollbacks_one_migration_if_file_name_is_provided(): void
54 | {
55 | $this->migrator
56 | ->expects($this->once())
57 | ->method('isReady')
58 | ->willReturn(true);
59 |
60 | $this->migrator
61 | ->expects($this->once())
62 | ->method('rollbackOne')
63 | ->with('test_file_name');
64 |
65 | $result = $this->command->run(
66 | new ArrayInput(['--force' => true, 'name' => 'test_file_name']),
67 | new NullOutput()
68 | );
69 |
70 | $this->assertSame(0, $result);
71 | }
72 |
73 | public function test_rollbacks_last_batch_if_file_name_is_not_provided(): void
74 | {
75 | $this->migrator
76 | ->expects($this->once())
77 | ->method('isReady')
78 | ->willReturn(true);
79 |
80 | $this->migrator
81 | ->expects($this->once())
82 | ->method('rollbackLastBatch');
83 |
84 | $result = $this->command->run(
85 | new ArrayInput(['--force' => true]),
86 | new NullOutput()
87 | );
88 |
89 | $this->assertSame(0, $result);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/tests/Integration/Console/StatusCommandTest.php:
--------------------------------------------------------------------------------
1 | migrator = $this->createMock(Migrator::class);
24 | $this->app->instance(Migrator::class, $this->migrator);
25 |
26 | $this->command = new StatusCommand();
27 | $this->command->setLaravel($this->app);
28 | }
29 |
30 | public function test_does_nothing_if_migrator_is_not_ready(): void
31 | {
32 | $this->migrator
33 | ->expects($this->once())
34 | ->method('isReady')
35 | ->willReturn(false);
36 |
37 | $this->migrator
38 | ->expects($this->never())
39 | ->method('showStatus');
40 |
41 | $result = $this->command->run(
42 | new ArrayInput([]),
43 | new NullOutput()
44 | );
45 |
46 | $this->assertSame(1, $result);
47 | }
48 |
49 | public function test_displays_each_migration_status_if_migrator_is_ready(): void
50 | {
51 | $this->migrator
52 | ->expects($this->once())
53 | ->method('isReady')
54 | ->willReturn(true);
55 |
56 | $this->migrator
57 | ->expects($this->once())
58 | ->method('showStatus');
59 |
60 | $result = $this->command->run(
61 | new ArrayInput([]),
62 | new NullOutput()
63 | );
64 |
65 | $this->assertSame(0, $result);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/tests/Integration/Facades/IndexTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(IndexManagerInterface::class, Index::getFacadeRoot());
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tests/Integration/Factories/MigrationFactoryTest.php:
--------------------------------------------------------------------------------
1 | migrationFactory = resolve(MigrationFactory::class);
24 | $this->migrationStorage = resolve(MigrationStorage::class);
25 | }
26 |
27 | #[TestWith(['2018_12_01_081000_create_test_index'])]
28 | #[TestWith(['2019_08_10_142230_update_test_index_mapping'])]
29 | public function test_migration_can_be_created_from_file(string $fileName): void
30 | {
31 | /** @var MigrationFile $file */
32 | $file = $this->migrationStorage->whereName($fileName);
33 |
34 | $this->assertInstanceOf(
35 | MigrationInterface::class,
36 | $this->migrationFactory->makeFromFile($file)
37 | );
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/Integration/Filesystem/MigrationStorageTest.php:
--------------------------------------------------------------------------------
1 | migrationStorage = resolve(MigrationStorage::class);
21 | $this->migrationStorage->registerPaths([__DIR__ . '/../../migrations/archive']);
22 | }
23 |
24 | #[TestWith(['2022_06_01_223400_create_new_index'])]
25 | #[TestWith([__DIR__ . '/../../migrations/archive/2022_06_01_223400_create_new_index.php'])]
26 | public function test_file_can_be_created(string $fileName): void
27 | {
28 | $file = $this->migrationStorage->create($fileName, 'content');
29 |
30 | $this->assertFileExists($file->path());
31 | $this->assertStringEqualsFile($file->path(), 'content');
32 |
33 | @unlink($file->path());
34 | }
35 |
36 | public function test_directory_is_created_along_with_file(): void
37 | {
38 | $defaultPath = __DIR__ . '/../../migrations/tmp';
39 | $this->config->set('elastic.migrations.storage.default_path', $defaultPath);
40 |
41 | // create a new instance to apply the new config
42 | $this->app->forgetInstance(MigrationStorage::class);
43 | $migrationStorage = resolve(MigrationStorage::class);
44 |
45 | $file = $migrationStorage->create('test', 'content');
46 |
47 | $this->assertDirectoryExists($defaultPath);
48 |
49 | @unlink($file->path());
50 | @rmdir($defaultPath);
51 | }
52 |
53 | #[TestWith(['2018_12_01_081000_create_test_index'])]
54 | #[TestWith(['2019_08_10_142230_update_test_index_mapping'])]
55 | #[TestWith([__DIR__ . '/../../migrations/archive/2017_11_11_100000_create_test_alias.php'])]
56 | public function test_file_can_be_retrieved_if_exists(string $fileName): void
57 | {
58 | /** @var MigrationFile $file */
59 | $file = $this->migrationStorage->whereName($fileName);
60 |
61 | $this->assertSame(basename($fileName, MigrationFile::FILE_EXTENSION), $file->name());
62 | }
63 |
64 | #[TestWith(['3030_01_01_000000_non_existing_file'])]
65 | #[TestWith(['test'])]
66 | #[TestWith([''])]
67 | #[TestWith([__DIR__ . '/../../migrations/archive/3030_01_01_000000_non_existing_file.php'])]
68 | public function test_file_can_not_be_retrieved_if_it_does_not_exist(string $fileName): void
69 | {
70 | $file = $this->migrationStorage->whereName($fileName);
71 |
72 | $this->assertNull($file);
73 | }
74 |
75 | public function test_all_files_within_migrations_directory_can_be_retrieved(): void
76 | {
77 | $files = $this->migrationStorage->all();
78 |
79 | $this->assertSame(
80 | [
81 | '2017_11_11_100000_create_test_alias',
82 | '2018_12_01_081000_create_test_index',
83 | '2019_08_10_142230_update_test_index_mapping',
84 | ],
85 | $files->map(static fn (MigrationFile $file) => $file->name())->toArray()
86 | );
87 | }
88 |
89 | public function test_storage_is_ready_when_default_path_exists(): void
90 | {
91 | $this->assertTrue($this->migrationStorage->isReady());
92 | }
93 |
94 | public function test_storage_is_not_ready_when_default_path_does_not_exist(): void
95 | {
96 | $this->config->set('elastic.migrations.storage.default_path', '/non_existing_directory');
97 |
98 | // create a new instance to apply the new config
99 | $this->app->forgetInstance(MigrationStorage::class);
100 | $migrationStorage = resolve(MigrationStorage::class);
101 |
102 | $this->assertFalse($migrationStorage->isReady());
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/tests/Integration/MigratorTest.php:
--------------------------------------------------------------------------------
1 | table = $this->config->get('elastic.migrations.database.table');
30 | $this->output = $this->createMock(OutputStyle::class);
31 | $this->migrator = resolve(Migrator::class)->setOutput($this->output);
32 |
33 | // create fixtures
34 | DB::table($this->table)->insert([
35 | ['migration' => '2018_12_01_081000_create_test_index', 'batch' => 1],
36 | ]);
37 | }
38 |
39 | public function test_single_migration_can_not_be_executed_if_file_does_not_exist(): void
40 | {
41 | $this->output
42 | ->expects($this->once())
43 | ->method('writeln')
44 | ->with('Migration is not found: 3020_11_01_045023_drop_test_index');
45 |
46 | $this->assertSame(
47 | $this->migrator,
48 | $this->migrator->migrateOne('3020_11_01_045023_drop_test_index')
49 | );
50 | }
51 |
52 | public function test_single_migration_can_be_executed_if_file_exists(): void
53 | {
54 | Index::shouldReceive('putMapping')->once();
55 |
56 | $this->output
57 | ->expects($this->exactly(2))
58 | ->method('writeln')
59 | ->with(
60 | $this->callback(static fn (string $message) => in_array($message, [
61 | 'Migrating: 2019_08_10_142230_update_test_index_mapping',
62 | 'Migrated: 2019_08_10_142230_update_test_index_mapping',
63 | ]))
64 | );
65 |
66 | $this->assertSame(
67 | $this->migrator,
68 | $this->migrator->migrateOne('2019_08_10_142230_update_test_index_mapping')
69 | );
70 |
71 | $this->assertDatabaseHas($this->table, [
72 | 'migration' => '2019_08_10_142230_update_test_index_mapping',
73 | 'batch' => 2,
74 | ]);
75 | }
76 |
77 | public function test_all_migrations_can_not_be_executed_if_directory_is_empty(): void
78 | {
79 | // create a temporary empty directory and reconfigure the package to use it
80 | $tmpDirectory = $this->config->get('elastic.migrations.storage.default_path') . '/tmp';
81 | @mkdir($tmpDirectory);
82 | $this->config->set('elastic.migrations.storage.default_path', $tmpDirectory);
83 |
84 | // create a new instance to apply the new config
85 | $this->app->forgetInstance(MigrationStorage::class);
86 | $migrator = resolve(Migrator::class)->setOutput($this->output);
87 |
88 | // check that there is nothing to migrate
89 | $this->output
90 | ->expects($this->once())
91 | ->method('writeln')
92 | ->with('Nothing to migrate');
93 |
94 | $this->assertSame($migrator, $migrator->migrateAll());
95 |
96 | // remove the temporary directory
97 | @rmdir($tmpDirectory);
98 | }
99 |
100 | public function test_all_migrations_can_be_executed_if_directory_is_not_empty(): void
101 | {
102 | Index::shouldReceive('putMapping')->once();
103 |
104 | $this->output
105 | ->expects($this->exactly(2))
106 | ->method('writeln')
107 | ->with(
108 | $this->callback(static fn (string $message) => in_array($message, [
109 | 'Migrating: 2019_08_10_142230_update_test_index_mapping',
110 | 'Migrated: 2019_08_10_142230_update_test_index_mapping',
111 | ]))
112 | );
113 |
114 | $this->assertSame(
115 | $this->migrator,
116 | $this->migrator->migrateAll()
117 | );
118 |
119 | $this->assertDatabaseHas($this->table, [
120 | 'migration' => '2019_08_10_142230_update_test_index_mapping',
121 | 'batch' => 2,
122 | ]);
123 | }
124 |
125 | public function test_single_migration_can_not_be_rolled_back_if_file_does_not_exist(): void
126 | {
127 | $this->output
128 | ->expects($this->once())
129 | ->method('writeln')
130 | ->with('Migration is not found: 3020_11_01_045023_drop_test_index');
131 |
132 | $this->assertSame(
133 | $this->migrator,
134 | $this->migrator->rollbackOne('3020_11_01_045023_drop_test_index')
135 | );
136 | }
137 |
138 | public function test_single_migration_can_not_be_rolled_back_if_file_is_not_yet_migrated(): void
139 | {
140 | $this->output
141 | ->expects($this->once())
142 | ->method('writeln')
143 | ->with('Migration is not yet migrated: 2019_08_10_142230_update_test_index_mapping');
144 |
145 | $this->assertSame(
146 | $this->migrator,
147 | $this->migrator->rollbackOne('2019_08_10_142230_update_test_index_mapping')
148 | );
149 | }
150 |
151 | public function test_single_migration_can_be_rolled_back_if_file_exists_and_is_migrated(): void
152 | {
153 | Index::shouldReceive('drop')->once();
154 |
155 | $this->output
156 | ->expects($this->exactly(2))
157 | ->method('writeln')
158 | ->with(
159 | $this->callback(static fn (string $message) => in_array($message, [
160 | 'Rolling back: 2018_12_01_081000_create_test_index',
161 | 'Rolled back: 2018_12_01_081000_create_test_index',
162 | ]))
163 | );
164 |
165 | $this->assertSame(
166 | $this->migrator,
167 | $this->migrator->rollbackOne('2018_12_01_081000_create_test_index')
168 | );
169 |
170 | $this->assertDatabaseMissing($this->table, [
171 | 'migration' => '2018_12_01_081000_create_test_index',
172 | 'batch' => 1,
173 | ]);
174 | }
175 |
176 | public function test_last_batch_can_not_be_rolled_back_if_some_files_are_missing(): void
177 | {
178 | // imitate, that migration has already been migrated
179 | DB::table($this->table)->insert([
180 | ['migration' => '2019_03_10_101500_create_test_index', 'batch' => 2],
181 | ]);
182 |
183 | $this->output
184 | ->expects($this->once())
185 | ->method('writeln')
186 | ->with('Migration is not found: 2019_03_10_101500_create_test_index');
187 |
188 | $this->assertSame(
189 | $this->migrator,
190 | $this->migrator->rollbackLastBatch()
191 | );
192 | }
193 |
194 | public function test_last_batch_can_be_rolled_back_if_all_files_are_present(): void
195 | {
196 | // imitate, that migration has already been migrated
197 | DB::table($this->table)->insert([
198 | ['migration' => '2019_08_10_142230_update_test_index_mapping', 'batch' => 4],
199 | ]);
200 |
201 | Index::shouldReceive('putMapping')->once();
202 |
203 | $this->output
204 | ->expects($this->exactly(2))
205 | ->method('writeln')
206 | ->with(
207 | $this->callback(static fn (string $message) => in_array($message, [
208 | 'Rolling back: 2019_08_10_142230_update_test_index_mapping',
209 | 'Rolled back: 2019_08_10_142230_update_test_index_mapping',
210 | ]))
211 | );
212 |
213 | $this->assertSame(
214 | $this->migrator,
215 | $this->migrator->rollbackLastBatch()
216 | );
217 |
218 | $this->assertDatabaseMissing($this->table, [
219 | 'migration' => '2019_08_10_142230_update_test_index_mapping',
220 | 'batch' => 4,
221 | ]);
222 | }
223 |
224 | public function test_all_migrations_can_not_be_rolled_back_if_some_files_are_missing(): void
225 | {
226 | // imitate, that migrations have already been migrated
227 | DB::table($this->table)->insert([
228 | ['migration' => '2019_03_10_101500_create_test_index', 'batch' => 2],
229 | ['migration' => '2019_01_01_053550_drop_test_index', 'batch' => 2],
230 | ]);
231 |
232 | $this->output
233 | ->expects($this->once())
234 | ->method('writeln')
235 | ->with(
236 | 'Migration is not found: 2019_03_10_101500_create_test_index,2019_01_01_053550_drop_test_index'
237 | );
238 |
239 | $this->assertSame(
240 | $this->migrator,
241 | $this->migrator->rollbackAll()
242 | );
243 | }
244 |
245 | public function test_all_migrations_can_be_rolled_back_if_all_files_are_present(): void
246 | {
247 | // imitate, that migration has already been migrated
248 | DB::table($this->table)->insert([
249 | ['migration' => '2019_08_10_142230_update_test_index_mapping', 'batch' => 2],
250 | ]);
251 |
252 | Index::shouldReceive('putMapping')->once();
253 | Index::shouldReceive('drop')->once();
254 |
255 | $this->output
256 | ->expects($this->exactly(4))
257 | ->method('writeln')
258 | ->with(
259 | $this->callback(static fn (string $message) => in_array($message, [
260 | 'Rolling back: 2019_08_10_142230_update_test_index_mapping',
261 | 'Rolled back: 2019_08_10_142230_update_test_index_mapping',
262 | 'Rolling back: 2018_12_01_081000_create_test_index',
263 | 'Rolled back: 2018_12_01_081000_create_test_index',
264 | ]))
265 | );
266 |
267 | $this->assertSame(
268 | $this->migrator,
269 | $this->migrator->rollbackAll()
270 | );
271 |
272 | $this->assertDatabaseMissing($this->table, [
273 | 'migration' => '2019_08_10_142230_update_test_index_mapping',
274 | 'batch' => 2,
275 | ]);
276 |
277 | $this->assertDatabaseMissing($this->table, [
278 | 'migration' => '2018_12_01_081000_create_test_index',
279 | 'batch' => 1,
280 | ]);
281 | }
282 |
283 | public static function statusDataProvider(): array
284 | {
285 | return [
286 | 'all migrations' => [
287 | 'onlyPending' => false,
288 | 'expectedOutput' => [
289 | ['2018_12_01_081000_create_test_index', 'Ran> (last batch)>'],
290 | ['2019_08_10_142230_update_test_index_mapping', 'Pending>'],
291 | ],
292 | ],
293 | 'pending migrations' => [
294 | 'onlyPending' => true,
295 | 'expectedOutput' => [
296 | ['2019_08_10_142230_update_test_index_mapping', 'Pending>'],
297 | ],
298 | ],
299 | ];
300 | }
301 |
302 | #[DataProvider('statusDataProvider')]
303 | public function test_status_is_displayed_correctly(bool $onlyPending, array $expectedOutput): void
304 | {
305 | $this->output
306 | ->expects($this->once())
307 | ->method('table')
308 | ->with(['Migration name>', 'Status>'], $expectedOutput);
309 |
310 | $this->assertSame(
311 | $this->migrator,
312 | $this->migrator->showStatus($onlyPending)
313 | );
314 | }
315 |
316 | public function test_migrator_is_ready_when_repository_and_storage_are_ready(): void
317 | {
318 | $this->assertTrue($this->migrator->isReady());
319 | }
320 |
321 | public function test_migrator_is_not_ready_when_repository_is_not_ready(): void
322 | {
323 | Schema::drop($this->table);
324 |
325 | $this->output
326 | ->expects($this->once())
327 | ->method('writeln')
328 | ->with('Migration table is not yet created');
329 |
330 | $this->assertFalse($this->migrator->isReady());
331 | }
332 |
333 | public function test_migrator_is_not_ready_when_storage_is_not_ready(): void
334 | {
335 | $this->config->set('elastic.migrations.storage.default_path', '/non_existing_directory');
336 |
337 | // create a new instance to apply the new config
338 | $this->app->forgetInstance(MigrationStorage::class);
339 | $migrator = $this->app->make(Migrator::class)->setOutput($this->output);
340 |
341 | $this->output
342 | ->expects($this->once())
343 | ->method('writeln')
344 | ->with('Default migration path is not yet created');
345 |
346 | $this->assertFalse($migrator->isReady());
347 | }
348 | }
349 |
--------------------------------------------------------------------------------
/tests/Integration/Repositories/MigrationRepositoryTest.php:
--------------------------------------------------------------------------------
1 | table = $this->config->get('elastic.migrations.database.table');
25 |
26 | // create fixtures
27 | DB::table($this->table)->insert([
28 | ['migration' => '2019_08_10_142230_update_test_index_mapping', 'batch' => 2],
29 | ['migration' => '2018_12_01_081000_create_test_index', 'batch' => 1],
30 | ]);
31 |
32 | $this->migrationRepository = new MigrationRepository();
33 | }
34 |
35 | public function test_record_can_be_inserted(): void
36 | {
37 | $this->migrationRepository->insert('2019_12_12_201657_update_test_index_settings', 3);
38 |
39 | $this->assertDatabaseHas(
40 | $this->table,
41 | ['migration' => '2019_12_12_201657_update_test_index_settings', 'batch' => 3]
42 | );
43 | }
44 |
45 | public function test_record_passes_existence_check(): void
46 | {
47 | $this->assertTrue($this->migrationRepository->exists('2018_12_01_081000_create_test_index'));
48 | $this->assertFalse($this->migrationRepository->exists('2019_12_05_092345_drop_test_index'));
49 | }
50 |
51 | public function test_record_can_be_deleted(): void
52 | {
53 | $this->migrationRepository->delete('2019_12_01_081000_create_test_index');
54 |
55 | $this->assertDatabaseMissing(
56 | $this->table,
57 | ['migration' => '2019_12_01_081000_create_test_index', 'batch' => 1]
58 | );
59 | }
60 |
61 | public function test_all_records_can_be_retrieved(): void
62 | {
63 | $this->assertSame(
64 | $this->migrationRepository->all()->toArray(),
65 | [
66 | '2019_08_10_142230_update_test_index_mapping',
67 | '2018_12_01_081000_create_test_index',
68 | ]
69 | );
70 | }
71 |
72 | public function test_last_batch_number_can_be_retrieved(): void
73 | {
74 | $this->assertSame(2, $this->migrationRepository->lastBatchNumber());
75 |
76 | DB::table($this->table)->delete();
77 | $this->assertNull($this->migrationRepository->lastBatchNumber());
78 | }
79 |
80 | public function test_last_record_batch_can_be_retrieved(): void
81 | {
82 | $this->assertSame(
83 | $this->migrationRepository->lastBatch()->toArray(),
84 | [
85 | '2019_08_10_142230_update_test_index_mapping',
86 | ]
87 | );
88 | }
89 |
90 | public function test_repository_is_ready_when_table_exists(): void
91 | {
92 | $this->assertTrue($this->migrationRepository->isReady());
93 | }
94 |
95 | public function test_repository_is_not_ready_when_table_does_not_exist(): void
96 | {
97 | Schema::drop($this->table);
98 |
99 | $this->assertFalse($this->migrationRepository->isReady());
100 | }
101 |
102 | public function test_repository_can_delete_all_records(): void
103 | {
104 | $this->assertCount(2, $this->migrationRepository->all());
105 |
106 | $this->migrationRepository->purge();
107 |
108 | $this->assertCount(0, $this->migrationRepository->all());
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/tests/Integration/TestCase.php:
--------------------------------------------------------------------------------
1 | config = $app['config'];
30 | $this->config->set('elastic.migrations.database.table', 'test_elastic_migrations');
31 | $this->config->set('elastic.migrations.storage.default_path', realpath(__DIR__ . '/../migrations'));
32 |
33 | $app->singleton(Client::class, function () {
34 | $httpClientMock = $this->createMock(ClientInterface::class);
35 |
36 | return ClientBuilder::create()
37 | ->setHttpClient($httpClientMock)
38 | ->build();
39 | });
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tests/Unit/Filesystem/MigrationFileTest.php:
--------------------------------------------------------------------------------
1 | assertSame(
17 | self::FULL_PATH,
18 | (new MigrationFile(self::FULL_PATH))->path()
19 | );
20 | }
21 |
22 | public function test_name_getter(): void
23 | {
24 | $this->assertSame(
25 | basename(self::FULL_PATH, MigrationFile::FILE_EXTENSION),
26 | (new MigrationFile(self::FULL_PATH))->name()
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/migrations/2018_12_01_081000_create_test_index.php:
--------------------------------------------------------------------------------
1 | client = $client;
14 | }
15 |
16 | public function up(): void
17 | {
18 | Index::create('test');
19 |
20 | $this->client->indices()->clearCache([
21 | 'index' => 'test',
22 | ]);
23 | }
24 |
25 | public function down(): void
26 | {
27 | Index::drop('test');
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/migrations/2019_08_10_142230_update_test_index_mapping.php:
--------------------------------------------------------------------------------
1 | enableSource();
13 | $mapping->text('title');
14 | });
15 | }
16 |
17 | public function down(): void
18 | {
19 | Index::putMapping('test', static function (Mapping $mapping) {
20 | $mapping->disableSource();
21 | });
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/migrations/archive/2017_11_11_100000_create_test_alias.php:
--------------------------------------------------------------------------------
1 |