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