├── .github
└── workflows
│ └── tests.yml
├── .gitignore
├── LICENSE
├── README.md
├── composer.json
├── composer.lock
├── config
└── morpher.php
├── morpher-logo.png
├── phpunit.xml.dist
├── src
├── Commands
│ └── MakeCommand.php
├── Facades
│ └── Morpher.php
├── Inspection.php
├── Morph.php
├── Morpher.php
├── Providers
│ └── MorpherServiceProvider.php
└── Support
│ ├── Console.php
│ └── TestsMorphs.php
├── stubs
└── Morph.php.stub
└── tests
├── AnonymousMigrationsTest.php
├── MorphConsoleAccessTest.php
├── MorphDisabledTest.php
├── MorphInspectionTest.php
├── MorphTest.php
├── Support
└── ConsoleTest.php
├── TestCase.php
└── examples
├── Morphs
├── AnonymousMorph.php
├── CantRunMorph.php
├── ExampleMorph.php
└── PrepareExampleMorph.php
└── migrations
├── 0000_00_00_000001_create_example_table.php
├── 0000_00_01_000000_create_another_example_table.php
├── 0000_00_01_000000_create_console_table.php
└── automated
└── 0000_00_00_000002_create_anonymous_table.php
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | run:
11 | runs-on: ${{ matrix.os }}
12 | strategy:
13 | fail-fast: true
14 | matrix:
15 | os: [ubuntu-latest, windows-latest]
16 | php: ['7.4', '8.0']
17 |
18 | name: PHP ${{ matrix.php }} Test on ${{ matrix.os }}
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v2
22 |
23 | - name: Setup PHP
24 | uses: shivammathur/setup-php@v2
25 | with:
26 | php-version: ${{ matrix.php }}
27 | extensions: mbstring, intl, fileinfo, pdo_sqlite
28 | ini-values: post_max_size=256M, max_execution_time=180
29 | coverage: xdebug
30 | tools: php-cs-fixer, phpunit
31 |
32 | - name: Validate composer.json and composer.lock
33 | run: composer validate
34 |
35 | - name: Cache Composer packages
36 | id: composer-cache
37 | uses: actions/cache@v2
38 | with:
39 | path: vendor
40 | key: ${{ matrix.os }}-php-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
41 | restore-keys: |
42 | ${{ matrix.os }}-php-${{ matrix.php }}
43 |
44 | - name: Install dependencies
45 | if: steps.composer-cache.outputs.cache-hit != 'true'
46 | run: composer install --prefer-dist --no-progress --no-suggest
47 |
48 | - name: Run tests
49 | run: vendor/bin/phpunit
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | .phpunit.result.cache
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Ricorocks Digital Agency
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [](https://github.com/Ricorocks-Digital-Agency/morpher/actions/workflows/tests.yml)
6 |
7 | **We've all been there.** You have an application in production, and now one of the database tables
8 | needs a structural change. You can't manually go in and change all the affected database rows. So what do you do?
9 | Put the logic in a migration? Seems a little risky, no?
10 |
11 | Morpher is a Laravel package that provides a unified pattern of transforming data between database migrations.
12 | It allows you to keep your migration logic clean and terse and move responsibility for data manipulation to a more
13 | appropriate location. It also provides a robust way to write tests for these transformations, which otherwise
14 | proves to be a real challenge.
15 |
16 | ## TOC
17 | - [Installation](#installation)
18 | - [Usage Guide](#create-your-first-morph)
19 | - [Lifecycle of a Morph](#lifecycle)
20 | - [Testing Morphs](#testing-morphs)
21 | - [Disabling Morphs](#disabling-morphs)
22 | - [Notes and Considerations](#notes-and-considerations)
23 |
24 | ## Installation
25 |
26 | ```bash
27 | composer require ricorocks-digital-agency/morpher
28 | ```
29 |
30 | It's not required, but you might want to publish the config file:
31 |
32 | ```bash
33 | php artisan vendor:publish --tag=morpher
34 | ```
35 |
36 | ## Create your first Morph
37 |
38 | Let's set up an example scenario. Your users table has a single `name` column, but you now need to separate it out into
39 | `first_name` and `last_name` columns. Your application has been live for a little while, so there is going to be a need
40 | to perform a data transformation.
41 |
42 | You start by creating a migration:
43 |
44 | ```bash
45 | php artisan make:migration split_names_on_users_table
46 | ```
47 |
48 | That migration's `up` method might look something like this:
49 |
50 | ```php
51 | public function up()
52 | {
53 | Schema::table('users', function (Blueprint $table) {
54 | $table->dropColumn('name');
55 | $table->addColumn('first_name')->nullable();
56 | $table->addColumn('last_name')->nullable();
57 | });
58 | }
59 | ```
60 |
61 | So, how do we go about taking all of the existing names, preparing the data, and inserting it into our new table?
62 |
63 | Let's start by creating out first Morph:
64 |
65 | ```bash
66 | php artisan make:morph SplitUserNames
67 | ```
68 |
69 | This will create a new class in `database/morphs` called `SplitUserNames`. Our next step is to link our migration to our
70 | new Morph. We can do this using the `$migration` property in the morph class:
71 |
72 | ```php
73 | protected static $migration = SplitNamesOnUsersTable::class;
74 | ```
75 |
76 | If you need more complex logic, you can instead override the `migration` method and return a migration class name that way.
77 |
78 | > :zap: Working with anonymous migrations? You can instead use the filename of the migration as
79 | > the value of the `$migration` property. For example: `protected static $migration = "2021_05_01_000000_create_some_anonymous_table"`;
80 |
81 | Our next task is to describe our Morph. In the `app/Morphs/SplitUserNames` class, we need to do the following:
82 |
83 | 1. Retrieve the current names prior to the migration being run.
84 | 2. Split the names into first and last names.
85 | 3. Insert the names after the migration has finished.
86 |
87 | To accomplish this, our `Morph` might look as follows:
88 |
89 | ```php
90 | class SplitUserNames extends Morph
91 | {
92 | protected static $migration = SplitNamesOnUsersTable::class;
93 | protected $newNames;
94 |
95 | public function prepare()
96 | {
97 | // Get all of the names along with their ID
98 | $names = DB::table('users')->select(['id', 'name'])->get();
99 |
100 | // Set a class property with the mapped version of the names
101 | $this->newNames = $names->map(function($data) {
102 | $nameParts = $this->splitName($data->name);
103 | return ['id' => $data->id, 'first_name' => $nameParts[0], 'last_name' => $nameParts[1]];
104 | });
105 | }
106 |
107 | protected function splitName($name)
108 | {
109 | // ...return some splitting logic here
110 | }
111 |
112 | public function run()
113 | {
114 | // Now we run the database query based on our transformed data
115 | DB::table('users')->upsert($this->newNames->toArray(), 'id');
116 | }
117 | }
118 | ```
119 |
120 | Now, when we run `php artisan migrate`, this Morph will run automatically.
121 |
122 | ## Lifecycle
123 |
124 | It helps to understand the lifecycle that a `Morph` goes through in order to make full use it.
125 |
126 | When a `Morph` is linked to a migration, and that migration's `up` method is run (usually from migrating the database),
127 | the following happens (in order):
128 |
129 | 1. The `prepare` method will be called on the `Morph` class. You can do anything you need to prepare data here.
130 | 2. The migration will run.
131 | 3. The `canRun` method will be called on the `Morph` class. Returning false in this method will stop the process here.
132 | 4. The `run` method will be called on the `Morph` class. This is where you should perform your data transformations.
133 |
134 | ## Testing Morphs
135 |
136 | One of the biggest challenges presented by data morphing is writing feature tests. It becomes very tricky to insert
137 | data to test on prior to the morph taking place. And yet, automated tests are so important when the code you're running
138 | will be modifying real data. Morpher makes the process of testing data a breeze so that you no longer have to compromise.
139 |
140 | To get started, we recommend creating a separate test case (or more than one test case) per Morph you'd like to write
141 | tests for. Add the `TestsMorphs` trait to that test class, and add the `supportMorphs` call to end of the `setUp`
142 | method.
143 |
144 | ```php
145 | use RicorocksDigitalAgency\Morpher\Support\TestsMorphs;
146 |
147 | class UserMorphTest extends TestCase {
148 |
149 | use TestsMorphs;
150 |
151 | protected function setUp(): void
152 | {
153 | parent::setUp();
154 | $this->supportMorphs();
155 | }
156 |
157 | }
158 | ```
159 |
160 | > :warning: The `TestsMorphs` trait conflicts with other database traits, such as `RefreshDatabase` or `DatabaseTransactions`.
161 | > As such, ensure that your morph test cases are isolated (in separate test classes) from other tests in your suite.
162 |
163 | With that done, you can get to work writing your tests! In order to do this, we provide a robust inspection API to
164 | facilitate Morph tests.
165 |
166 | ```php
167 | use RicorocksDigitalAgency\Morpher\Facades\Morpher;
168 |
169 | class UserMorphTest extends TestCase {
170 |
171 | // ...After setup
172 |
173 | public function test_it_translates_the_user_names_correctly() {
174 | Morpher::test(UserMorph::class)
175 | ->beforeThisMigration(function($morph) {
176 | /**
177 | * We use the `beforeMigrating` hook to allow for "old"
178 | * data creation. In our user names example, we'll
179 | * create users with combined forename and surname.
180 | */
181 | DB::table('users')->insert([['name' => 'Joe Bloggs'], ['name' => 'Luke Downing']]);
182 | })
183 | ->before(function($morph) {
184 | /**
185 | * We use the `before` hook to perform any expectations
186 | * after the migration has run but before the Morph
187 | * has been executed.
188 | */
189 | $this->assertCount(2, User::all());
190 | })
191 | ->after(function($morph) {
192 | /**
193 | * We use the `after` hook to perform any expectations
194 | * after the morph has finished running. For example,
195 | * we would expect data to have been transformed.
196 | */
197 | [$joe, $luke] = User::all();
198 |
199 | $this->assertEquals("Joe", $joe->forename);
200 | $this->assertEquals("Bloggs", $joe->surname);
201 |
202 | $this->assertEquals("Luke", $luke->forename);
203 | $this->assertEquals("Downing", $luke->surname);
204 | });
205 | }
206 |
207 | }
208 | ```
209 |
210 | As you can see, there are several inspections methods we can make use of to fully test our Morphs.
211 | Note that you only need to use the inspections relevant to your particular Morph.
212 |
213 | ### `beforeThisMigration`
214 |
215 | This method is run prior to the migration connected to the `Morph` being run on the database.
216 | It is also run prior to the `prepare` method on your Morph being called.
217 | Seen as your tests won't have "old" data for your Morph to alter, you can use this method to
218 | create fake data ready for your Morph to use.
219 |
220 | > Note that in most cases, your Laravel Factories will likely be outdated, so you may have to
221 | > resort to manual methods such as the `DB` Facade. You could also create a versioned
222 | > Factory that uses the old data structure.
223 |
224 | ### `before`
225 |
226 | This method is executed prior to the `run` method being called on your Morph, but after the
227 | prepare method. You could use this as an opportunity to make sure your prepare method
228 | has collected the expected data and stored it on the Morph object, if your Morph
229 | needs to perform that step.
230 |
231 | ### `after`
232 |
233 | This method is executed after the `run` method has been called on your Morph. You should
234 | use this to check that the data migration has run successfully and that your data has
235 | actually been transformed.
236 |
237 | ## Disabling Morphs
238 |
239 | It may be helpful, particularly in local development where you destroy and rebuild the database regularly, to disable
240 | Morphs from running. To do this, add the following to your environment file:
241 |
242 | ```dotenv
243 | RUN_MORPHS=false
244 | ```
245 |
246 | ## Notes and Considerations
247 |
248 | * Everything in the `run` method is encapsulated in a database transaction. This means that if there is an exception
249 | whilst running your morph, no data changes will be persisted.
250 | * It's important to remember that this package isn't magic. If you do something stupid to the data in your database, there
251 | is no going back. **Back up your data before migrating.**
252 | * You can override the `canRun` method to stop a faulty data set ruining your database. Perform any checks you want in this
253 | method, and just return a boolean to tell us if we should go ahead.
254 | * Want to write your progress to the console during a Morph? You can do so using the `$this->console` property on
255 | the Morph class!
256 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ricorocks-digital-agency/morpher",
3 | "description": "A Laravel package for managing data changes during database migrations.",
4 | "type": "library",
5 | "require": {
6 | "php": "^7.4|^8.0",
7 | "illuminate/support": "^8.0"
8 | },
9 | "require-dev": {
10 | "phpunit/phpunit": "^9.5",
11 | "orchestra/testbench": "^6.13",
12 | "pestphp/pest-plugin-expectations": "^1.0",
13 | "spatie/macroable": "^1.0",
14 | "spatie/laravel-ray": "^1.17"
15 | },
16 | "autoload": {
17 | "psr-4": {
18 | "RicorocksDigitalAgency\\Morpher\\": "src/"
19 | }
20 | },
21 | "autoload-dev": {
22 | "psr-4": {
23 | "RicorocksDigitalAgency\\Morpher\\Tests\\": "tests/"
24 | }
25 | },
26 | "license": "MIT",
27 | "authors": [
28 | {
29 | "name": "luke",
30 | "email": "lukeraymonddowning@gmail.com"
31 | }
32 | ],
33 | "extra": {
34 | "laravel": {
35 | "providers": [
36 | "RicorocksDigitalAgency\\Morpher\\Providers\\MorpherServiceProvider"
37 | ],
38 | "aliases": {
39 | "Morpher": "RicorocksDigitalAgency\\Morpher\\Facades\\Morpher"
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/config/morpher.php:
--------------------------------------------------------------------------------
1 | env("RUN_MORPHS", true),
11 |
12 | /**
13 | * Define any paths that contain Morph classes.
14 | * We'll load them in during migrations and
15 | * run relevant Morphs during the process.
16 | */
17 | 'paths' => [
18 | database_path('morphs'),
19 | ],
20 |
21 | ];
22 |
--------------------------------------------------------------------------------
/morpher-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ricorocks-Digital-Agency/morpher/f4d468ee9d717d924e3c7b471f93900110ef5be4/morpher-logo.png
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | tests
15 |
16 |
17 |
18 |
19 | src/
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/Commands/MakeCommand.php:
--------------------------------------------------------------------------------
1 | argument('class'), $stub);
22 |
23 | File::ensureDirectoryExists(database_path('morphs/'));
24 |
25 | $path = database_path('morphs/' . Str::finish($this->argument('class'), ".php"));
26 | File::put($path, $class);
27 |
28 | $this->info("Morph created at $path");
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/src/Facades/Morpher.php:
--------------------------------------------------------------------------------
1 | beforeMigratingHooks = collect();
16 | $this->beforeHooks = collect();
17 | $this->afterHooks = collect();
18 | }
19 |
20 | public function beforeThisMigration(callable $closure): self
21 | {
22 | return tap($this, fn() => $this->beforeMigratingHooks->push($closure));
23 | }
24 |
25 | public function before(callable $closure): self
26 | {
27 | return tap($this, fn() => $this->beforeHooks->push($closure));
28 | }
29 |
30 | public function after(callable $closure): self
31 | {
32 | return tap($this, fn() => $this->afterHooks->push($closure));
33 | }
34 |
35 | public function runBeforeThisMigration(Morph $morph)
36 | {
37 | $this->beforeMigratingHooks->each(fn($hook) => call_user_func($hook, $morph));
38 | }
39 |
40 | public function runBefore(Morph $morph)
41 | {
42 | $this->beforeHooks->each(fn($hook) => call_user_func($hook, $morph));
43 | }
44 |
45 | public function runAfter(Morph $morph)
46 | {
47 | $this->afterHooks->each(fn($hook) => call_user_func($hook, $morph));
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Morph.php:
--------------------------------------------------------------------------------
1 | console = $console;
15 | }
16 |
17 | public function prepare()
18 | {
19 | }
20 |
21 | public function canRun()
22 | {
23 | return true;
24 | }
25 |
26 | public static function migration()
27 | {
28 | return static::$migration;
29 | }
30 |
31 | public abstract function run();
32 | }
33 |
--------------------------------------------------------------------------------
/src/Morpher.php:
--------------------------------------------------------------------------------
1 | console = $console;
24 | }
25 |
26 | public function test(string $morph)
27 | {
28 | return tap(new Inspection(), fn($inspection) => $this->inspections[$morph][] = $inspection);
29 | }
30 |
31 | public function setup()
32 | {
33 | if (!config('morpher.enabled')) {
34 | return;
35 | }
36 |
37 | Event::listen(MigrationStarted::class, fn($event) => $this->prepareMorphs($event));
38 | Event::listen(MigrationEnded::class, fn($event) => $this->runMorphs($event));
39 | }
40 |
41 | protected function prepareMorphs($event)
42 | {
43 | if (!$this->isBuildingDatabase($event)) {
44 | return;
45 | }
46 |
47 | $this->getMorphs($event->migration)
48 | ->each(fn($morph) => $morph->withConsole($this->console))
49 | ->each(fn($morph) => $this->inspectionsForMorph($morph)->each->runBeforeThisMigration($morph))
50 | ->each(fn($morph) => $morph->prepare());
51 | }
52 |
53 | protected function isBuildingDatabase($event)
54 | {
55 | return $event->method == "up";
56 | }
57 |
58 | protected function getMorphs($migration)
59 | {
60 | return $this->morphs[get_class($migration)] ??= $this->allMorphs()
61 | ->filter(fn($morph) => Str::of(get_class($migration))->contains($morph::migration()))
62 | ->map(fn($morph) => app()->make($morph));
63 | }
64 |
65 | protected function allMorphs()
66 | {
67 | if ($this->allMorphs) {
68 | return $this->allMorphs;
69 | }
70 |
71 | static::includeMorphClasses();
72 |
73 | $this->allMorphs = collect(get_declared_classes())
74 | ->filter(fn($className) => is_subclass_of($className, Morph::class));
75 |
76 | return $this->allMorphs;
77 | }
78 |
79 | protected static function includeMorphClasses()
80 | {
81 | collect(config('morpher.paths', []))
82 | ->flatMap(fn($directory) => File::allFiles($directory))
83 | ->each(fn(SplFileInfo $fileInfo) => include_once $fileInfo->getRealPath());
84 | }
85 |
86 | protected function runMorphs($event)
87 | {
88 | if (!$this->isBuildingDatabase($event)) {
89 | return;
90 | }
91 |
92 | DB::transaction(
93 | fn() => $this->getMorphs($event->migration)
94 | ->filter(fn($morph) => $morph->canRun())
95 | ->each(
96 | function ($morph) use ($event) {
97 | $inspections = $this->inspectionsForMorph($morph);
98 | $inspections->each->runBefore($morph);
99 | $morph->run($event);
100 | $inspections->each->runAfter($morph);
101 | }
102 | )
103 | );
104 | }
105 |
106 | protected function inspectionsForMorph($morph)
107 | {
108 | return collect(data_get($this->inspections, get_class($morph)));
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/Providers/MorpherServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->singleton(Console::class, fn() => new Console(new ConsoleOutput));
17 | $this->app->singleton('morpher', Morpher::class);
18 | }
19 |
20 | public function boot()
21 | {
22 | $this->mergeConfigFrom(__DIR__ . '/../../config/morpher.php', 'morpher');
23 |
24 | if ($this->app->runningInConsole()) {
25 | $this->console();
26 | }
27 | }
28 |
29 | protected function console()
30 | {
31 | $this->commands(MakeCommand::class);
32 | MorpherFacade::setup();
33 | $this->publishes([__DIR__ . '/../../config/morpher.php' => config_path('morpher.php')], 'morpher');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Support/Console.php:
--------------------------------------------------------------------------------
1 | output = $output;
16 | Event::listen(CommandStarting::class, fn($event) => $this->output = $event->output);
17 | }
18 |
19 | public function info($message)
20 | {
21 | $this->output->writeln("$message>");
22 | }
23 |
24 | public function warning($message)
25 | {
26 | $this->output->writeln("$message>");
27 | }
28 |
29 | public function error($message)
30 | {
31 | $this->output->writeln("$message>");
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Support/TestsMorphs.php:
--------------------------------------------------------------------------------
1 | beforeApplicationDestroyed(function () {
15 | $this->artisan('migrate:fresh');
16 | $this->app[Kernel::class]->setArtisan(null);
17 |
18 | $this->artisan('migrate:rollback');
19 |
20 | RefreshDatabaseState::$migrated = false;
21 | });
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/stubs/Morph.php.stub:
--------------------------------------------------------------------------------
1 | expectException(Exception::class);
16 | $this->expectExceptionMessage("This exception came from an anonymous migration morph");
17 |
18 | $this->loadMigrationsFrom(__DIR__ . '/examples/migrations/automated');
19 | $this->artisan('migrate', ['--database' => 'testbench'])->run();
20 |
21 | $this->beforeApplicationDestroyed(function () {
22 | $this->artisan('migrate:rollback', ['--database' => 'testbench'])->run();
23 | });
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/tests/MorphConsoleAccessTest.php:
--------------------------------------------------------------------------------
1 | app->bind(
23 | EvaluatesConsoles::class,
24 | fn($app) => new EvaluatesConsoles($app->make(Console::class))
25 | );
26 |
27 | Event::dispatch(new MigrationStarted(app(CreateConsoleTable::class), 'up'));
28 | Event::dispatch(new MigrationEnded(app(CreateConsoleTable::class), 'up'));
29 | }
30 | }
31 |
32 | class EvaluatesConsoles extends Morph
33 | {
34 | protected static $migration = CreateConsoleTable::class;
35 | protected $parentConsole;
36 |
37 | public function __construct($console = null)
38 | {
39 | $this->parentConsole = $console;
40 | }
41 |
42 | public function canRun()
43 | {
44 | return true;
45 | }
46 |
47 | public function run()
48 | {
49 | expect($this->console)->toBe($this->parentConsole);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/tests/MorphDisabledTest.php:
--------------------------------------------------------------------------------
1 | set('morpher.enabled', false);
21 | }
22 |
23 | /** @test */
24 | public function it_does_not_run_when_disabled()
25 | {
26 | app(CreateExampleTable::class)->up();
27 |
28 | DB::table('examples')->insert([['name' => 'Bob'], ['name' => 'Barry']]);
29 |
30 | Event::dispatch(new MigrationEnded(app(CreateExampleTable::class), 'up'));
31 |
32 | expect(DB::table('examples')->find(1)->name)->toEqual('Bob');
33 | expect(DB::table('examples')->find(2)->name)->toEqual('Barry');
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/tests/MorphInspectionTest.php:
--------------------------------------------------------------------------------
1 | beforeThisMigration(function() {
27 | expect(Schema::hasTable('examples'))->toBeFalse();
28 | })
29 | ->before(function() {
30 | DB::table('examples')->insert([['name' => 'Bob']]);
31 | })
32 | ->after(function() {
33 | expect(DB::table('examples')->find(1)->name)->toEqual('Foo');
34 | });
35 |
36 | Event::dispatch(new MigrationStarted(app(CreateExampleTable::class), 'up'));
37 | app(CreateExampleTable::class)->up();
38 | Event::dispatch(new MigrationEnded(app(CreateExampleTable::class), 'up'));
39 | }
40 |
41 | /** @test */
42 | public function it_can_run_an_inspection_prior_to_running_the_morph()
43 | {
44 | Morpher::test(ExampleMorph::class)->before(function() {
45 | expect(DB::table('examples')->find(1)->name)->toEqual('Bob');
46 | expect(DB::table('examples')->find(2)->name)->toEqual('Barry');
47 | });
48 |
49 | app(CreateExampleTable::class)->up();
50 | // Add some fake data to change
51 | DB::table('examples')->insert([['name' => 'Bob'], ['name' => 'Barry']]);
52 |
53 | Event::dispatch(new MigrationEnded(app(CreateExampleTable::class), 'up'));
54 | }
55 |
56 | /** @test */
57 | public function it_can_run_an_inspection_after_running_the_morph()
58 | {
59 | Morpher::test(ExampleMorph::class)->after(function() {
60 | expect(DB::table('examples')->find(1)->name)->toEqual('Foo');
61 | expect(DB::table('examples')->find(2)->name)->toEqual('Foo');
62 | });
63 |
64 | app(CreateExampleTable::class)->up();
65 | // Add some fake data to change
66 | DB::table('examples')->insert([['name' => 'Bob'], ['name' => 'Barry']]);
67 |
68 | Event::dispatch(new MigrationEnded(app(CreateExampleTable::class), 'up'));
69 | }
70 |
71 | /** @test */
72 | public function multiple_before_inspections_on_the_same_morph_can_be_run()
73 | {
74 | $this->expectException(\Exception::class);
75 |
76 | Morpher::test(ExampleMorph::class)->before(function() {
77 | throw new \Exception("This should throw");
78 | });
79 |
80 | Morpher::test(ExampleMorph::class)->before(function() {
81 | expect(DB::table('examples')->find(1)->name)->toEqual('Bob');
82 | });
83 |
84 | app(CreateExampleTable::class)->up();
85 | DB::table('examples')->insert([['name' => 'Bob'], ['name' => 'Barry']]);
86 |
87 | Event::dispatch(new MigrationEnded(app(CreateExampleTable::class), 'up'));
88 | }
89 |
90 | /** @test */
91 | public function multiple_after_inspections_on_the_same_morph_can_be_run()
92 | {
93 | $this->expectException(\Exception::class);
94 |
95 | Morpher::test(ExampleMorph::class)->after(function() {
96 | throw new \Exception("This should throw");
97 | });
98 |
99 | Morpher::test(ExampleMorph::class)->after(function() {
100 | expect(DB::table('examples')->find(1)->name)->toEqual('Foo');
101 | });
102 |
103 | app(CreateExampleTable::class)->up();
104 | DB::table('examples')->insert([['name' => 'Bob'], ['name' => 'Barry']]);
105 |
106 | Event::dispatch(new MigrationEnded(app(CreateExampleTable::class), 'up'));
107 | }
108 |
109 | /** @test */
110 | public function multiple_inspections_can_be_declared_on_the_same_inspection()
111 | {
112 | $counter = 0;
113 |
114 | Morpher::test(ExampleMorph::class)
115 | ->before(function() use(&$counter) {
116 | $counter += 1;
117 | })
118 | ->before(function() use(&$counter) {
119 | $counter += 1;
120 | })
121 | ->after(function() use(&$counter) {
122 | $counter += 1;
123 | })
124 | ->after(function() use(&$counter) {
125 | $counter += 1;
126 | });
127 |
128 | app(CreateExampleTable::class)->up();
129 | DB::table('examples')->insert([['name' => 'Bob'], ['name' => 'Barry']]);
130 |
131 | Event::dispatch(new MigrationEnded(app(CreateExampleTable::class), 'up'));
132 |
133 | expect($counter)->toEqual(4);
134 | }
135 |
136 | }
137 |
--------------------------------------------------------------------------------
/tests/MorphTest.php:
--------------------------------------------------------------------------------
1 | up();
22 |
23 | // Add some fake data to change
24 | DB::table('examples')->insert([['name' => 'Bob'], ['name' => 'Barry']]);
25 |
26 | Event::dispatch(new MigrationEnded(app(CreateExampleTable::class), 'up'));
27 |
28 | expect(DB::table('examples')->find(1)->name)->toEqual('Foo');
29 | expect(DB::table('examples')->find(2)->name)->toEqual('Foo');
30 | }
31 |
32 | /** @test */
33 | public function it_doesnt_run_on_down_ended_events()
34 | {
35 | app(CreateExampleTable::class)->down();
36 | Event::dispatch(new MigrationEnded(app(CreateExampleTable::class), 'down'));
37 |
38 | expect(Schema::hasTable('examples'))->toBeFalse();
39 | }
40 |
41 | /** @test */
42 | public function it_runs_a_prepare_method_prior_to_migrating()
43 | {
44 | app(CreateExampleTable::class)->up();
45 | DB::table('examples')->insert([['name' => 'Bob'], ['name' => 'Barry']]);
46 |
47 |
48 | Event::dispatch(new MigrationStarted(app(CreateAnotherExampleTable::class), 'up'));
49 | app(CreateAnotherExampleTable::class)->up();
50 | Event::dispatch(new MigrationEnded(app(CreateAnotherExampleTable::class), 'up'));
51 |
52 | expect(DB::table('other_examples')->find(1)->name)->toEqual('Bob');
53 | expect(DB::table('other_examples')->find(2)->name)->toEqual('Barry');
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/tests/Support/ConsoleTest.php:
--------------------------------------------------------------------------------
1 | expect(true)->toBeTrue()));
19 |
20 | $console->info('Hey there!');
21 | }
22 |
23 | /** @test */
24 | public function if_there_is_a_command_starting_event_this_output_is_used_instead()
25 | {
26 | $console = new Console(new RunsCallbackOnWriteLn(fn() => $this->fail('This console instance was not overwritten')));
27 |
28 | Event::dispatch(new CommandStarting(
29 | 'migrate',
30 | new ArgvInput(),
31 | new RunsCallbackOnWriteLn(fn() => expect(true)->toBeTrue())
32 | ));
33 |
34 | $console->info('Hey there!');
35 | }
36 | }
37 |
38 | class RunsCallbackOnWriteLn implements OutputInterface
39 | {
40 | protected $callback;
41 |
42 | public function __construct($callback)
43 | {
44 | $this->callback = $callback;
45 | }
46 |
47 | public function write($messages, bool $newline = false, int $options = 0) { }
48 |
49 | public function writeln($messages, int $options = 0)
50 | {
51 | value($this->callback);
52 | }
53 |
54 | public function setVerbosity(int $level) { }
55 |
56 | public function getVerbosity() { }
57 |
58 | public function isQuiet() { }
59 |
60 | public function isVerbose() { }
61 |
62 | public function isVeryVerbose() { }
63 |
64 | public function isDebug() { }
65 |
66 | public function setDecorated(bool $decorated) { }
67 |
68 | public function isDecorated() { }
69 |
70 | public function setFormatter(OutputFormatterInterface $formatter) { }
71 |
72 | public function getFormatter() { }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | set('database.default', 'testbench');
21 | $app['config']->set(
22 | 'database.connections.testbench',
23 | [
24 | 'driver' => 'sqlite',
25 | 'database' => ':memory:',
26 | 'prefix' => '',
27 | ]
28 | );
29 |
30 | $app['config']->set('morpher.paths', [__DIR__ . '/examples/Morphs']);
31 | }
32 |
33 | protected function getPackageProviders($app)
34 | {
35 | return [
36 | RayServiceProvider::class,
37 | MorpherServiceProvider::class,
38 | ];
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/tests/examples/Morphs/AnonymousMorph.php:
--------------------------------------------------------------------------------
1 | where('id', '>', 0)->update(['name' => 'Foo']);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/examples/Morphs/PrepareExampleMorph.php:
--------------------------------------------------------------------------------
1 | values = DB::table('examples')->select('name')->get();
14 | }
15 |
16 | public function run()
17 | {
18 | DB::table('other_examples')->insert($this->values->map(fn($data) => ['name' => $data->name])->toArray());
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/examples/migrations/0000_00_00_000001_create_example_table.php:
--------------------------------------------------------------------------------
1 | id();
12 | $table->string('name')->nullable();
13 | $table->timestamps();
14 | });
15 | }
16 |
17 | public function down()
18 | {
19 | Schema::table('examples', function (Blueprint $table) {
20 | $table->dropIfExists();
21 | });
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/tests/examples/migrations/0000_00_01_000000_create_another_example_table.php:
--------------------------------------------------------------------------------
1 | id();
12 | $table->string('name')->nullable();
13 | $table->timestamps();
14 | });
15 | }
16 |
17 | public function down()
18 | {
19 | Schema::table('other_examples', function (Blueprint $table) {
20 | $table->dropIfExists();
21 | });
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/tests/examples/migrations/0000_00_01_000000_create_console_table.php:
--------------------------------------------------------------------------------
1 | id();
13 | $table->string('name')->nullable();
14 | $table->timestamps();
15 | });
16 | }
17 |
18 | public function down()
19 | {
20 | Schema::table('console', function (Blueprint $table) {
21 | $table->dropIfExists();
22 | });
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/tests/examples/migrations/automated/0000_00_00_000002_create_anonymous_table.php:
--------------------------------------------------------------------------------
1 | id();
12 | $table->string('name')->nullable();
13 | $table->timestamps();
14 | });
15 | }
16 |
17 | public function down()
18 | {
19 | Schema::table('anonymous_table', function (Blueprint $table) {
20 | $table->dropIfExists();
21 | });
22 | }
23 |
24 | };
25 |
--------------------------------------------------------------------------------