├── .github ├── LICENSE.md └── README.md ├── .gitignore ├── .php-cs-fixer.dist.php ├── LICENSE ├── composer.json ├── phpunit.xml └── src ├── Data └── FastRefreshDatabaseState.php └── Traits └── FastRefreshDatabase.php /.github/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Plannr Technologies Ltd 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 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | # FastRefreshDatabase for Laravel 🚀 2 | 3 | Have you ever come across an issue where the traditional `RefreshDatabase` trait takes ages to run tests when you have lots of migrations? If so, you may be after this package! 4 | 5 | ## The Problem 6 | Traditionally, the `RefreshDatabase` trait will run `php artisan migrate:fresh` every time you run tests. After the first test, it will use transactions to roll back the data and run the next one, so subsequent tests are fast, but the initial test is slow. This can be really annoying if you are used to running a single test, as it could take seconds to run a single test. 7 | 8 | ## The Solution 9 | You don't need to run `php artisan migrate:fresh` every time you run tests, only when you add a new migration or change an old one. The `FastRefreshDatabase` trait will create a checksum of your `migrations` folder as well as your current Git branch. It will then create a checksum file in your application's `storage/app` directory. When your migrations change or your branch changes, the checksum won't match the cached one and `php artisan migrate:fresh` is run. 10 | 11 | When you don't make any changes, it will continue to use the same database without refreshing, which can speed up the test time by 100x! 12 | 13 | ## Benchmarks 14 | Running a single test, with about 400 migrations. 15 | 16 | | Processor | Before | After | 17 | |---------------|------------|-------| 18 | | Intel Core i5 | 30 seconds | 100 milliseconds | 19 | | Apple M1 Pro | 5 seconds | 100 milliseconds | 20 | 21 | ## Installation 22 | 23 | Install the package with Composer 24 | 25 | ```bash 26 | composer require plannr/laravel-fast-refresh-database --dev 27 | ``` 28 | 29 | ## Adding to your TestCase 30 | Next, just replace the existing `RefreshDatabase` trait you are using in your TestCase file with the `FastRefreshDatabase` trait 31 | 32 | ```diff 33 | in(__DIR__); 57 | uses(FastRefreshDatabase::class)->in('Feature'); 58 | ``` 59 | 60 | ## Deleting The Migration Checksum 61 | 62 | Sometimes you may wish to force-update database migrations, to do this, locate the `migration-checksum_{Database Name Slug}.txt` file within `storage/app`. 63 | 64 | ## Customising the checksum file location 65 | 66 | You may customise the migration checksum file location and name by extending the trait and overwriting the `getMigrationChecksumFile()` method. 67 | 68 | ```php 69 | protected function getMigrationChecksumFile(): string 70 | { 71 | return storage_path('custom/some-other-file.txt'); 72 | } 73 | ``` 74 | 75 | ### ParaTest Databases 76 | 77 | Parallel testing databases contain tokens that serve as unique identifiers for each test runner. This makes the trait inherently able to support parallel testing without any extra effort, because the database name is stored in the checksum file name. 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | .phpunit.result.cache 4 | .php-cs-fixer.cache 5 | composer.lock 6 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__, 6 | ]) 7 | ->name('*.php') 8 | ->notName('*.blade.php') 9 | ->ignoreDotFiles(true) 10 | ->ignoreVCS(true) 11 | ->exclude(['vendor', 'node_modules']); 12 | 13 | $config = new PhpCsFixer\Config(); 14 | 15 | // Rules from: https://cs.symfony.com/doc/rules/index.html 16 | 17 | return $config->setRules([ 18 | '@PSR2' => true, 19 | 'array_syntax' => ['syntax' => 'short'], 20 | 'ordered_imports' => ['sort_algorithm' => 'length'], 21 | 'no_unused_imports' => true, 22 | 'not_operator_with_successor_space' => true, 23 | 'trailing_comma_in_multiline' => true, 24 | 'single_quote' => ['strings_containing_single_quote_chars' => true], 25 | 'phpdoc_scalar' => true, 26 | 'unary_operator_spaces' => true, 27 | 'binary_operator_spaces' => true, 28 | 'blank_line_before_statement' => [ 29 | 'statements' => ['declare', 'return', 'throw', 'try'], 30 | ], 31 | 'phpdoc_single_line_var_spacing' => true, 32 | 'phpdoc_var_without_name' => true, 33 | 'method_argument_space' => [ 34 | 'on_multiline' => 'ensure_fully_multiline', 35 | 'keep_multiple_spaces_after_comma' => true, 36 | ], 37 | 'return_type_declaration' => [ 38 | 'space_before' => 'none' 39 | ], 40 | ])->setFinder($finder); 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Plannr Technologies Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plannr/laravel-fast-refresh-database", 3 | "description": "Refresh your database faster than you've ever seen before 🚀", 4 | "license": "MIT", 5 | "type": "library", 6 | "authors": [ 7 | { 8 | "name": "Sam Carré", 9 | "email": "29132017+Sammyjo20@users.noreply.github.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^8.1", 14 | "symfony/process": "^6.0 || ^7.0" 15 | }, 16 | "require-dev": { 17 | "friendsofphp/php-cs-fixer": "^3.13", 18 | "orchestra/testbench": "^9.0" 19 | }, 20 | "minimum-stability": "stable", 21 | "autoload": { 22 | "psr-4": { 23 | "Plannr\\Laravel\\FastRefreshDatabase\\": "src/" 24 | } 25 | }, 26 | "scripts": { 27 | "fix-code": [ 28 | "./vendor/bin/php-cs-fixer fix" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests 10 | 11 | 12 | 13 | 14 | ./app 15 | ./src 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Data/FastRefreshDatabaseState.php: -------------------------------------------------------------------------------- 1 | getCachedMigrationChecksum(); 29 | $currentChecksum = FastRefreshDatabaseState::$currentChecksum ??= $this->calculateMigrationChecksum(); 30 | 31 | if ($cachedChecksum !== $currentChecksum) { 32 | $this->artisan('migrate:fresh', $this->migrateFreshUsing()); 33 | 34 | $this->app[Kernel::class]->setArtisan(null); 35 | 36 | $this->storeMigrationChecksum($currentChecksum); 37 | } 38 | 39 | RefreshDatabaseState::$migrated = true; 40 | } 41 | 42 | $this->beginDatabaseTransaction(); 43 | } 44 | 45 | /** 46 | * Calculate a checksum based on the migrations name and last modified date 47 | * 48 | * @return string 49 | * @throws \JsonException 50 | */ 51 | protected function calculateMigrationChecksum(): string 52 | { 53 | $finder = Finder::create() 54 | ->in(database_path('migrations')) 55 | ->name('*.php') 56 | ->ignoreDotFiles(true) 57 | ->ignoreVCS(true) 58 | ->files(); 59 | 60 | $migrations = array_map(static function (SplFileInfo $fileInfo) { 61 | return [$fileInfo->getMTime(), $fileInfo->getPath()]; 62 | }, iterator_to_array($finder)); 63 | 64 | // Reset the array keys so there is less data 65 | 66 | $migrations = array_values($migrations); 67 | 68 | // Add the current git branch 69 | 70 | $checkBranch = new Process(['git', 'branch', '--show-current']); 71 | $checkBranch->run(); 72 | 73 | $migrations['gitBranch'] = trim($checkBranch->getOutput()); 74 | 75 | // Create a hash 76 | 77 | return hash('sha256', json_encode($migrations, JSON_THROW_ON_ERROR)); 78 | } 79 | 80 | /** 81 | * Get the cached migration checksum 82 | * 83 | * @return string|null 84 | */ 85 | protected function getCachedMigrationChecksum(): ?string 86 | { 87 | return rescue(fn () => file_get_contents($this->getMigrationChecksumFile()), null, false); 88 | } 89 | 90 | /** 91 | * Store the migration checksum 92 | * 93 | * @param string $checksum 94 | * @return void 95 | */ 96 | protected function storeMigrationChecksum(string $checksum): void 97 | { 98 | file_put_contents($this->getMigrationChecksumFile(), $checksum); 99 | } 100 | 101 | /** 102 | * Provides a configurable migration checksum file path 103 | * 104 | * @return string 105 | */ 106 | protected function getMigrationChecksumFile(): string 107 | { 108 | $connection = $this->app[ConnectionInterface::class]; 109 | 110 | $databaseNameSlug = Str::slug($connection->getDatabaseName()); 111 | 112 | return storage_path("app/migration-checksum_{$databaseNameSlug}.txt"); 113 | } 114 | } 115 | --------------------------------------------------------------------------------