diff --git a/README.md b/README.md index ba0f24e8..88ce5df3 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,8 @@ * [Generating actions](#generating-actions) * [Running Actions](#running-actions) * [Forcing Actions To Run In Production](#forcing-actions-to-run-in-production) - * [Execution every time](#execution-every-time) + * [Execution Every Time](#execution-every-time) + * [Database Transactions](#database-transactions) * [Rolling Back Actions](#rolling-back-actions) * [Roll Back & Action Using A Single Command](#roll-back--action-using-a-single-command) * [Actions Status](#actions-status) @@ -110,7 +111,7 @@ database, you will be prompted for confirmation before the commands are executed php artisan migrate:actions --force ``` -#### Execution every time +#### Execution Every Time In some cases, you need to call the code every time you deploy the application. For example, to call reindexing. @@ -121,14 +122,6 @@ use Helldar\LaravelActions\Support\Actionable; class Reindex extends Actionable { - /** - * Determines the type of launch of the action. - * - * If true, then it will be executed once. - * If false, then the action will run every time the `migrate:actions` command is invoked. - * - * @var bool - */ protected $once = false; public function up(): void @@ -148,6 +141,39 @@ If the value is `$once = false`, the `up` method will be called every time the ` In this case, information about it will not be written to the `migration_actions` table and, therefore, the `down` method will not be called when the rollback command is called. +#### Database Transactions + +In some cases, it becomes necessary to undo previously performed actions in the database. For example, when code execution throws an error. To do this, the code +must be wrapped in a transaction. + +By setting the `$transactions = true` parameter, you will ensure that your code is wrapped in a transaction without having to manually call +the `DB::transaction()` method. This will reduce the time it takes to create the action. + +```php +use Helldar\LaravelActions\Support\Actionable; + +class AddSomeData extends Actionable +{ + protected $transactions = true; + + public function up(): void + { + // ... + + $post = Post::create([ + 'title' => 'Random Title' + ]); + + $post->tags()->sync($ids); + } + + public function down(): void + { + // + } +} +``` + ### Rolling Back Actions To roll back the latest action operation, you may use the `rollback` command. This command rolls back the last "batch" of actions, which may include multiple diff --git a/src/Support/Actionable.php b/src/Support/Actionable.php index b24ef5b0..be536aed 100644 --- a/src/Support/Actionable.php +++ b/src/Support/Actionable.php @@ -17,6 +17,15 @@ abstract class Actionable extends Migration implements Contract */ protected $once = true; + /** + * Determines a call to database transactions. + * + * By default, false. + * + * @var bool + */ + protected $transactions = false; + /** * Determines the type of launch of the action. * @@ -29,4 +38,14 @@ public function isOnce(): bool { return $this->once; } + + /** + * Determines a call to database transactions. + * + * @return bool + */ + public function enabledTransactions(): bool + { + return $this->transactions; + } } diff --git a/src/Support/Migrator.php b/src/Support/Migrator.php index 307d0430..5bedb530 100644 --- a/src/Support/Migrator.php +++ b/src/Support/Migrator.php @@ -4,6 +4,7 @@ use Helldar\LaravelActions\Traits\Infoable; use Illuminate\Database\Migrations\Migrator as BaseMigrator; +use Illuminate\Support\Facades\DB; final class Migrator extends BaseMigrator { @@ -58,6 +59,25 @@ protected function runUp($file, $batch, $pretend) $this->note("Migrated: {$name} ({$runTime}ms)"); } + /** + * Starts the execution of code, starting database transactions, if necessary. + * + * @param object $migration + * @param string $method + */ + protected function runMigration($migration, $method) + { + if ($this->enabledTransactions($migration)) { + DB::transaction(function () use ($migration, $method) { + parent::runMigration($migration, $method); + }); + + return; + } + + parent::runMigration($migration, $method); + } + /** * Whether it is necessary to record information about the execution in the database. * @@ -69,4 +89,16 @@ protected function allowLogging($migration): bool { return $migration->isOnce(); } + + /** + * Whether it is necessary to call database transactions at runtime. + * + * @param object $migration + * + * @return bool + */ + protected function enabledTransactions($migration): bool + { + return $migration->enabledTransactions(); + } } diff --git a/tests/Commands/MigrateTest.php b/tests/Commands/MigrateTest.php index aabdb847..fa3cb2ac 100644 --- a/tests/Commands/MigrateTest.php +++ b/tests/Commands/MigrateTest.php @@ -2,6 +2,7 @@ namespace Tests\Commands; +use Exception; use Tests\TestCase; final class MigrateTest extends TestCase @@ -22,7 +23,7 @@ public function testMigrationCommand() $this->assertDatabaseMigrationHas($this->table, 'test_migration'); } - public function testEveryTimeExecution() + public function testOnce() { $this->copyFiles(); @@ -32,27 +33,69 @@ public function testEveryTimeExecution() $this->assertDatabaseCount($table, 0); $this->assertDatabaseCount($this->table, 0); - $this->assertDatabaseMigrationDoesntLike($this->table, 'every_time'); + $this->assertDatabaseMigrationDoesntLike($this->table, $table); $this->artisan('migrate:actions')->run(); $this->assertDatabaseCount($table, 1); $this->assertDatabaseCount($this->table, 1); - $this->assertDatabaseMigrationDoesntLike($this->table, 'every_time'); + $this->assertDatabaseMigrationDoesntLike($this->table, $table); $this->artisan('migrate:actions')->run(); $this->assertDatabaseCount($table, 2); $this->assertDatabaseCount($this->table, 1); - $this->assertDatabaseMigrationDoesntLike($this->table, 'every_time'); + $this->assertDatabaseMigrationDoesntLike($this->table, $table); $this->artisan('migrate:actions')->run(); $this->assertDatabaseCount($table, 3); $this->assertDatabaseCount($this->table, 1); - $this->assertDatabaseMigrationDoesntLike($this->table, 'every_time'); + $this->assertDatabaseMigrationDoesntLike($this->table, $table); $this->artisan('migrate:actions')->run(); $this->assertDatabaseCount($table, 4); $this->assertDatabaseCount($this->table, 1); - $this->assertDatabaseMigrationDoesntLike($this->table, 'every_time'); + $this->assertDatabaseMigrationDoesntLike($this->table, $table); + } + + public function testSuccessTransaction() + { + $this->copySuccessTransaction(); + + $table = 'transactions'; + + $this->artisan('migrate:actions:install')->run(); + + $this->assertDatabaseCount($table, 0); + $this->assertDatabaseCount($this->table, 0); + $this->assertDatabaseMigrationDoesntLike($this->table, $table); + $this->artisan('migrate:actions')->run(); + + $this->assertDatabaseCount($table, 3); + $this->assertDatabaseCount($this->table, 1); + $this->assertDatabaseMigrationHas($this->table, $table); + } + + public function testFailedTransaction() + { + $this->copyFailedTransaction(); + + $table = 'transactions'; + + $this->artisan('migrate:actions:install')->run(); + + $this->assertDatabaseCount($table, 0); + $this->assertDatabaseCount($this->table, 0); + $this->assertDatabaseMigrationDoesntLike($this->table, $table); + + try { + $this->artisan('migrate:actions')->run(); + } catch (Exception $e) { + $this->assertSame(Exception::class, get_class($e)); + $this->assertSame('Random message', $e->getMessage()); + } + + $this->assertDatabaseCount($table, 0); + $this->assertDatabaseCount($this->table, 0); + $this->assertDatabaseMigrationDoesntLike($this->table, $table); } public function testMigrationNotFound() diff --git a/tests/Concerns/Files.php b/tests/Concerns/Files.php index 26ba383c..88333bca 100644 --- a/tests/Concerns/Files.php +++ b/tests/Concerns/Files.php @@ -9,7 +9,7 @@ trait Files protected function freshFiles(): void { File::deleteDirectory( - database_path('actions') + $this->targetDirectory() ); } @@ -17,7 +17,32 @@ protected function copyFiles(): void { File::copyDirectory( __DIR__ . '/../fixtures/actions', - database_path('actions') + $this->targetDirectory() ); } + + protected function copySuccessTransaction(): void + { + File::copy( + __DIR__ . '/../fixtures/stubs/2021_02_15_124237_test_success_transactions.stub', + $this->targetDirectory('2021_02_15_124237_test_success_transactions.php') + ); + } + + protected function copyFailedTransaction(): void + { + File::copy( + __DIR__ . '/../fixtures/stubs/2021_02_15_124852_test_failed_transactions.stub', + $this->targetDirectory('2021_02_15_124852_test_failed_transactions.php') + ); + } + + protected function targetDirectory(string $path = null): string + { + $dir = database_path('actions'); + + File::ensureDirectoryExists($dir); + + return rtrim($dir, '/\\') . '/' . ltrim($path, '/\\'); + } } diff --git a/tests/fixtures/migrations/2021_02_15_124419_create_transactions_table.php b/tests/fixtures/migrations/2021_02_15_124419_create_transactions_table.php new file mode 100644 index 00000000..d78fe66c --- /dev/null +++ b/tests/fixtures/migrations/2021_02_15_124419_create_transactions_table.php @@ -0,0 +1,20 @@ +uuid('value'); + }); + } + + public function down() + { + Schema::dropIfExists('transactions'); + } +} diff --git a/tests/fixtures/stubs/2021_02_15_124237_test_success_transactions.stub b/tests/fixtures/stubs/2021_02_15_124237_test_success_transactions.stub new file mode 100644 index 00000000..03bd0dbe --- /dev/null +++ b/tests/fixtures/stubs/2021_02_15_124237_test_success_transactions.stub @@ -0,0 +1,34 @@ +table()->insert([ + $this->value(), + $this->value(), + $this->value(), + ]); + } + + public function down(): void + { + // nothing + } + + protected function table() + { + return DB::table('transactions'); + } + + protected function value(): array + { + return ['value' => Uuid::uuid4()]; + } +} diff --git a/tests/fixtures/stubs/2021_02_15_124852_test_failed_transactions.stub b/tests/fixtures/stubs/2021_02_15_124852_test_failed_transactions.stub new file mode 100644 index 00000000..ead99db3 --- /dev/null +++ b/tests/fixtures/stubs/2021_02_15_124852_test_failed_transactions.stub @@ -0,0 +1,36 @@ +table()->insert([ + $this->value(), + $this->value(), + $this->value(), + ]); + + throw new Exception('Random message'); + } + + public function down(): void + { + // nothing + } + + protected function table() + { + return DB::table('transactions'); + } + + protected function value(): array + { + return ['value' => Uuid::uuid4()]; + } +}