├── .gitignore ├── composer.json ├── phpunit.xml ├── readme.md ├── src ├── Utilities │ ├── Commands │ │ └── TestDB.php │ ├── Exceptions │ │ └── InvalidSQLiteConnectionException.php │ ├── Repositories │ │ ├── BaseRepository.php │ │ └── BaseRepositoryInterface.php │ ├── Traits │ │ └── TestingDatabaseTrait.php │ └── UtilitiesServiceProvider.php └── config │ └── .gitkeep └── tests └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | composer.phar 4 | composer.lock 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "srlabs/laravel-testing-utilities", 3 | "description": "Helper utilities for testing Laravel Applications", 4 | "license": "MIT", 5 | "keywords": [ 6 | "laravel", 7 | "testing" 8 | ], 9 | "authors": [ 10 | { 11 | "name": "Ryan C. Durham", 12 | "email": "ryan@stagerightlabs.com" 13 | } 14 | ], 15 | "require": { 16 | "php": "^8.0", 17 | "illuminate/support": "^9.0", 18 | "illuminate/contracts": "^9.0", 19 | "illuminate/database": "^9.0" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "SRLabs\\Utilities\\": "src/Utilities" 24 | } 25 | }, 26 | "minimum-stability": "stable" 27 | } 28 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Laravel Utilities 2 | 3 | This package is intended to be a collection of helpful utilities to assist with the development and maintenance of Laravel Applications. 4 | 5 | | Laravel Version | Package Version | Packagist Branch | 6 | |---|---|---| 7 | | 7.* | 10.* | ```"srlabs/laravel-testing-utilities": "~10"``` | 8 | | 8.* | 11.* | ```"srlabs/laravel-testing-utilities": "~11"``` | 9 | | 9.* | 12.* | ```"srlabs/laravel-testing-utilities": "~12"``` | 10 | 11 | To install this package, run 12 | ```bash 13 | $ composer require srlabs/laravel-testing-utilities 14 | ``` 15 | 16 | and then add the service provider to your service providers listing in ```app/config/app.php``` 17 | 18 | ```php 19 | 'providers' => [ 20 | // ... 21 | 'SRLabs\Utilities\UtilitiesServiceProvider' 22 | // ... 23 | ], 24 | ``` 25 | 26 | ## Testing Assistant 27 | 28 | This utility makes it easy to implement the testing stratgey described by [Chris Duell](https://github.com/duellsy) in his blog post *[Speeding up PHP unit tests 15 times](http://www.chrisduell.com/blog/development/speeding-up-unit-tests-in-php/)*. When running tests that require the use of a database persistence layer, running migrations and seeding the database for each test can take a very long time. Chris instead suggests creating a sqlite database file ahead of time, and making a new copy of that file for each test instead. 29 | 30 | This package provides an artisan command ('utility:testdb') that will run your migrations and seeds and save them to a pre-defined sqlite file. Once that is complete, there is a companion trait that you add to your tests which will copy the staging database file to the testing database location when running tests. 31 | 32 | You need to define a sqlite database connection in your ```config/database.php``` file. The connection name can be whatever you would like, but the package will assume a name of 'staging' if you don't provide one. 33 | 34 | Default 'staging' connection: 35 | 36 | ```bash 37 | $ php artisan utility:testdb 38 | ``` 39 | 40 | Custom 'sqlite_testing' connection: 41 | 42 | ```bash 43 | $ php artisan utility:testdb sqlite_testing 44 | ``` 45 | 46 | You can also specify a Database Seeder class: 47 | 48 | ```bash 49 | $ php artisan utility:testdb sqlite_testing --class="SentinelDatabaseSeeder" 50 | ``` 51 | 52 | This command will migrate and seed the sqlite database you have specified. 53 | 54 | When you are ready to use this new sqlite file in your tests, add the ```TestingDatabase``` trait to your test class, and use it as such: 55 | 56 | ```php 57 | 58 | use SRLabs\Utilities\Traits\TestingDatabaseTrait; 59 | 60 | class FooTest extends TestCase { 61 | 62 | use TestingDatabaseTrait; 63 | 64 | public function setUp() 65 | { 66 | parent::setUp(); 67 | 68 | $this->prepareDatabase('staging', 'testing'); 69 | } 70 | 71 | public function testSomethingIsTrue() 72 | { 73 | $this->assertTrue(true); 74 | } 75 | 76 | } 77 | ``` 78 | In this example "staging" is the Database connection that represents the pre-compiled sqlite file, and "testing" is a separate connection that represents the database that will be used by the tests. Each time the ```setUp()``` method is called, the testing sqlite file will be replaced by the staging sqlite file, effectively resetting your database to a clean starting point. 79 | You may need to do some extra configuration to have phpunit use your "testing" database. 80 | 81 | ## (Optional) Run Automatically When Running PHPUnit 82 | 83 | Using this method will have `artisan utility:testdb` execute before any tests are ran **only** if there are new migration changes. 84 | 85 | #### 1. Create file `bootstrap/testing.php`: 86 | 87 | ```php 88 | 118 | ... 119 | 120 | ... 121 | 122 | 123 | 124 | 125 | ``` 126 | -------------------------------------------------------------------------------- /src/Utilities/Commands/TestDB.php: -------------------------------------------------------------------------------- 1 | file = $file; 35 | } 36 | 37 | /* 38 | * Don't allow this command to be run in a production environment 39 | */ 40 | use ConfirmableTrait; 41 | 42 | /** 43 | * This command prepares a sqlite testing database, to allow for the 44 | * technique described by Chris Duell here: 45 | * http://www.chrisduell.com/blog/development/speeding-up-unit-tests-in-php/ 46 | */ 47 | public function handle() 48 | { 49 | // Don't allow this command to run in a production environment 50 | if ( ! $this->confirmToProceed()) return; 51 | 52 | // First check that we are using sqlite as the testing database 53 | $this->prepareDatabaseConnection(); 54 | 55 | // Confirm DB file exists 56 | $this->prepareSQLiteFile(); 57 | 58 | // Gather arguments 59 | $name = $this->argument('connection'); 60 | $connection = config('database.connections.' . $name, []); 61 | 62 | // Gather options 63 | $seeder = $this->option('class'); 64 | 65 | // Clear existing database, if necessary 66 | if (is_readable($connection['database'])) 67 | { 68 | unlink($connection['database']); 69 | touch($connection['database']); 70 | } 71 | 72 | // Everything is in order - we can proceed. 73 | $this->call('migrate', array('--database' => $name)); 74 | 75 | // If a seeder class was specified, pass that to the seed command 76 | $this->call('db:seed', array('--database' => $name, '--class' => $seeder)); 77 | 78 | // Send a completion message to the user 79 | $this->info($connection['database'] . " has been refreshed."); 80 | } 81 | 82 | /** 83 | * This whole endeavor is pointless if there is no testing environment configuration available. 84 | */ 85 | protected function prepareDatabaseConnection() 86 | { 87 | $this->connection = config('database.connections.' . $this->argument('connection'), []); 88 | 89 | if (empty($this->connection) || !array_key_exists('database', $this->connection)) 90 | { 91 | $this->error('SQLite DB connection "' . $this->argument('connection') . '" not found in config.' ); 92 | exit(); 93 | } 94 | 95 | if ($this->connection['driver'] != 'sqlite') 96 | { 97 | $this->error( "This technique is not intended to be used on a non-sqlite database." ); 98 | exit(); 99 | } 100 | 101 | // Save the path to the sqlite file 102 | $this->dbpath = $this->connection['database']; 103 | } 104 | 105 | /** 106 | * We want to start with a clean slate, i.e. an empty sqlite file. 107 | */ 108 | protected function prepareSQLiteFile() 109 | { 110 | // First remove the old database file 111 | $this->file->delete($this->dbpath); 112 | 113 | // Now create an empty target database file 114 | touch($this->dbpath); 115 | 116 | // Double check that the file exists before moving on 117 | if (! $this->file->exists($this->dbpath)) 118 | { 119 | $this->error( 'SQlite file not found.' ); 120 | exit(); 121 | } 122 | } 123 | 124 | /** 125 | * Get the console command arguments. 126 | * 127 | * @return array 128 | */ 129 | protected function getArguments() 130 | { 131 | return array( 132 | array('connection', InputArgument::OPTIONAL, 'Testing DB Connection Name', 'staging'), 133 | ); 134 | } 135 | 136 | /** 137 | * Get the console command options. 138 | * 139 | * @return array 140 | */ 141 | protected function getOptions() 142 | { 143 | return array( 144 | array('class', null, InputOption::VALUE_OPTIONAL, 'The class to be used for seeding.', 'DatabaseSeeder'), 145 | ); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Utilities/Exceptions/InvalidSQLiteConnectionException.php: -------------------------------------------------------------------------------- 1 | code}] : {$this->message}\n"; 14 | } 15 | } -------------------------------------------------------------------------------- /src/Utilities/Repositories/BaseRepository.php: -------------------------------------------------------------------------------- 1 | model = $model; 46 | $this->cache = $cache; 47 | $this->dispatcher = $dispatcher; 48 | $this->logger = $logger; 49 | 50 | // Set the Resource Name 51 | $this->resourceName = $this->model->getTable(); 52 | $this->className = get_class($model); 53 | } 54 | 55 | /** 56 | * Create a new model instance and store it in the database 57 | * 58 | * @param array $data 59 | * @return static 60 | */ 61 | public function store(array $data) 62 | { 63 | // Do we need to create a reference id? 64 | if ($this->model->isFillable('ref') && !isset($data['ref'])) { 65 | $data['ref'] = $this->generateReferenceId(); 66 | } 67 | 68 | // Create the new model object 69 | $model = $this->model->create($data); 70 | 71 | // Do we need to set a hash? 72 | if ($this->model->isFillable('hash')) { 73 | $model->hash = \Hashids::encode($model->id); 74 | $model->save(); 75 | } 76 | 77 | // Return the new model object 78 | return $model; 79 | } 80 | 81 | /** 82 | * Update a Model Object Instance 83 | * 84 | * @param int|string $id 85 | * @param array $data 86 | * @return \Illuminate\Support\Collection|null|static 87 | */ 88 | public function update($id, array $data) 89 | { 90 | // If the first parameter is a string, it is a Hashids hash 91 | if (is_string($id)) { 92 | $id = $this->decodeHash($id); 93 | } 94 | 95 | // Fetch the Model Object 96 | $model = $this->byId($id); 97 | 98 | // Set the new values on the Model Object 99 | foreach ($data as $key => $value) { 100 | if ($this->model->isFillable($key)) { 101 | $model->$key = $value; 102 | } 103 | } 104 | 105 | // Save the changes to the database 106 | $model->save(); 107 | 108 | // Flush the cache 109 | $this->flush($model); 110 | 111 | // Return the updated model 112 | return $model; 113 | } 114 | 115 | /** 116 | * Delete a model object 117 | * 118 | * @param string|Model 119 | * 120 | * @return boolean 121 | */ 122 | public function delete($model) 123 | { 124 | // Resolve the hash string if necessary 125 | $model = $this->resolveHash($model); 126 | 127 | //Flush the cache 128 | $this->flush($model); 129 | 130 | // Delete the order 131 | return $model->delete(); 132 | } 133 | 134 | /** 135 | * Retrieve a single model object, using its id 136 | * 137 | * @param integer $id 138 | * @return null|Model 139 | */ 140 | public function byId($id) 141 | { 142 | $key = $this->resourceName . '.id.' . (string)$id; 143 | 144 | return $this->cache->remember($key, 10, function () use ($id) { 145 | return $this->model->find($id); 146 | }); 147 | } 148 | 149 | /** 150 | * Retrieve a single model, using its hash value 151 | * 152 | * @param $hash 153 | * @return null|Model 154 | */ 155 | public function byHash($hash) 156 | { 157 | $id = $this->decodeHash($hash); 158 | 159 | if ($id) { 160 | $key = $this->resourceName . '.hash.' . $hash; 161 | 162 | return $this->cache->remember($key, 10, function() use ($id) { 163 | return $this->model->find($id); 164 | }); 165 | } 166 | 167 | return null; 168 | } 169 | 170 | /** 171 | * Retrieve a model object by its reference value, if it has one 172 | * 173 | * @param $reference 174 | * @return null 175 | */ 176 | public function byReference($reference) 177 | { 178 | if ($this->model->isFillable('ref')) { 179 | $key = $this->resourceName . '.ref.' . $reference; 180 | 181 | return $this->cache->remember($key, 10, function () use ($reference) { 182 | return $this->model->where('ref', $reference)->first(); 183 | }); 184 | } else { 185 | return null; 186 | } 187 | } 188 | 189 | /** 190 | * Determine if there is already an instance of a model with the given attributes 191 | * 192 | * @param array $attributes 193 | * @return bool 194 | */ 195 | public function exists(array $attributes) 196 | { 197 | return $this->model->where($attributes)->exists(); 198 | } 199 | 200 | /** 201 | * Flush the cache for this Model Object instance 202 | * 203 | * @param Model $model 204 | * @return void 205 | */ 206 | public function flush(Model $model) 207 | { 208 | // Assemble Cache Keys 209 | $keys[] = $this->resourceName . '.hash.' . $model->hash; 210 | $keys[] = $this->resourceName . '.id.' . $model->id; 211 | 212 | // Some keys will not be available on all models 213 | if ($this->model->isFillable('ref')) { 214 | $keys[] = $this->resourceName . '.ref.' . $model->ref; 215 | } 216 | 217 | // Clear the cache for the given keys 218 | foreach ($keys as $key) { 219 | $this->cache->forget($key); 220 | } 221 | } 222 | 223 | /** 224 | * Return the Repository Model instance 225 | * @return Model 226 | */ 227 | public function getModel() 228 | { 229 | return $this->model; 230 | } 231 | 232 | /** 233 | * A helper function for decoding Hashids 234 | * 235 | * @param $hash 236 | * @return null 237 | */ 238 | protected function decodeHash($hash) 239 | { 240 | $decoded = \Hashids::decode($hash); 241 | 242 | if (is_array($decoded)) { 243 | return $decoded[0]; 244 | } else { 245 | return null; 246 | } 247 | } 248 | 249 | /** 250 | * Convert hash string to model object, if necessary 251 | * 252 | * @param $model 253 | * @return Model|null 254 | */ 255 | protected function resolveHash($model) 256 | { 257 | if (!($model instanceof $this->className)) { 258 | return $this->byHash($model); 259 | } 260 | 261 | return $model; 262 | } 263 | 264 | /** 265 | * Each repository will be responsible for implementing its own reference generator. 266 | * 267 | * @return string 268 | */ 269 | abstract function generateReferenceId(); 270 | } -------------------------------------------------------------------------------- /src/Utilities/Repositories/BaseRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 16 | $this->commands( 17 | [ 18 | TestDB::class, 19 | ] 20 | ); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagerightlabs/laravel-testing-utilities/4840a95e7c363c842f6d250533f3217c8c570eb3/src/config/.gitkeep -------------------------------------------------------------------------------- /tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagerightlabs/laravel-testing-utilities/4840a95e7c363c842f6d250533f3217c8c570eb3/tests/.gitkeep --------------------------------------------------------------------------------