├── .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 | Morpher 3 |

4 | 5 | [![Tests](https://github.com/Ricorocks-Digital-Agency/morpher/actions/workflows/tests.yml/badge.svg)](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 | --------------------------------------------------------------------------------