├── .gitignore
├── README.md
├── composer.json
├── resources
└── views
│ └── factory.blade.php
└── src
├── Console
└── GenerateCommand.php
├── TestFactoryHelperServiceProvider.php
└── Types
└── EnumType.php
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | composer.lock
3 | .idea
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Laravel Test Factory Generator
2 |
3 | `php artisan generate:model-factory`
4 |
5 | This package will generate [factories](https://laravel.com/docs/master/database-testing#writing-factories) from your existing models so you can get started with testing your Laravel application more quickly.
6 |
7 | ### Example output
8 |
9 | #### Migration and Model
10 | ```php
11 | Schema::create('users', function (Blueprint $table) {
12 | $table->increments('id');
13 | $table->string('name');
14 | $table->string('username');
15 | $table->string('email')->unique();
16 | $table->string('password', 60);
17 | $table->integer('company_id');
18 | $table->rememberToken();
19 | $table->timestamps();
20 | });
21 |
22 | class User extends Model {
23 | public function company()
24 | {
25 | return $this->belongsTo(Company::class);
26 | }
27 | }
28 | ```
29 |
30 | #### Factory Result
31 |
32 | ```php
33 | $factory->define(App\User::class, function (Faker\Generator $faker) {
34 | return [
35 | 'name' => $faker->name,
36 | 'username' => $faker->userName,
37 | 'email' => $faker->safeEmail,
38 | 'password' => bcrypt($faker->password),
39 | 'company_id' => factory(App\Company::class),
40 | 'remember_token' => Str::random(10),
41 | ];
42 | });
43 | ```
44 |
45 |
46 | ### Install
47 |
48 | Require this package with composer using the following command:
49 |
50 | ```bash
51 | composer require --dev mpociot/laravel-test-factory-helper
52 | ```
53 |
54 | ### Usage
55 |
56 | To generate multiple factories at once, run the artisan command:
57 |
58 | `php artisan generate:model-factory`
59 |
60 | This command will find all models within your application and create test factories. By default, this will not overwrite any existing model factories. You can _force_ overwriting existing model factories by using the `--force` option.
61 |
62 | To generate a factory for specific model or models, run the artisan command:
63 |
64 | `php artisan generate:model-factory User Team`
65 |
66 | By default, this command will search under the `app` folder for models. If your models are within a different folder, for example `app/Models`, you can specify this using `--dir` option. In this case, run the artisan command:
67 |
68 | `php artisan generate:model-factory --dir app/Models -- User Team`
69 |
70 | ### License
71 |
72 | The Laravel Test Factory Helper is free software licensed under the MIT license.
73 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mpociot/laravel-test-factory-helper",
3 | "description": "Generate Laravel test factories from your existing models",
4 | "keywords": ["Laravel", "Tests", "Factory"],
5 | "license": "MIT",
6 | "require": {
7 | "php": "^7.2.5",
8 | "illuminate/support": "^6.0|^7.0",
9 | "illuminate/console": "^6.0|^7.0",
10 | "illuminate/filesystem": "^6.0|^7.0",
11 | "doctrine/dbal": "^2.9"
12 | },
13 | "autoload": {
14 | "psr-4": {
15 | "Mpociot\\LaravelTestFactoryHelper\\": "src/"
16 | }
17 | },
18 | "extra": {
19 | "laravel": {
20 | "providers": [
21 | "Mpociot\\LaravelTestFactoryHelper\\TestFactoryHelperServiceProvider"
22 | ]
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/resources/views/factory.blade.php:
--------------------------------------------------------------------------------
1 | /* @@var $factory \Illuminate\Database\Eloquent\Factory */
2 |
3 | use Faker\Generator as Faker;
4 |
5 | $factory->define({{ $reflection->getName() }}::class, function (Faker $faker) {
6 | return [
7 | @foreach($properties as $name => $property)
8 | '{{$name}}' => {!! $property !!},
9 | @endforeach
10 | ];
11 | });
12 |
--------------------------------------------------------------------------------
/src/Console/GenerateCommand.php:
--------------------------------------------------------------------------------
1 | files = $files;
68 | $this->view = $view;
69 | }
70 |
71 | /**
72 | * Execute the console command.
73 | *
74 | * @return void
75 | */
76 | public function handle()
77 | {
78 | Type::addType('customEnum', EnumType::class);
79 | $this->dir = $this->option('dir');
80 | $this->force = $this->option('force');
81 |
82 | $models = $this->loadModels($this->argument('model'));
83 |
84 | foreach ($models as $model) {
85 | $filename = 'database/factories/' . class_basename($model) . 'Factory.php';
86 |
87 | if ($this->files->exists($filename) && !$this->force) {
88 | $this->line('Model factory exists, use --force to overwrite: ' . $filename);
89 |
90 | continue;
91 | }
92 |
93 | $result = $this->generateFactory($model);
94 |
95 | if ($result === false) {
96 | continue;
97 | }
98 |
99 | $written = $this->files->put($filename, $result);
100 | if ($written !== false) {
101 | $this->line('Model factory created: ' . $filename);
102 | } else {
103 | $this->line('Failed to create model factory: ' . $filename);
104 | }
105 | }
106 | }
107 |
108 |
109 | /**
110 | * Get the console command arguments.
111 | *
112 | * @return array
113 | */
114 | protected function getArguments()
115 | {
116 | return array(
117 | array('model', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Which models to include', array()),
118 | );
119 | }
120 |
121 | /**
122 | * Get the console command options.
123 | *
124 | * @return array
125 | */
126 | protected function getOptions()
127 | {
128 | return [
129 | ['dir', 'D', InputOption::VALUE_OPTIONAL, 'The model directory', $this->dir],
130 | ['force', 'F', InputOption::VALUE_NONE, 'Overwrite any existing model factory'],
131 | ];
132 | }
133 |
134 | protected function generateFactory($model)
135 | {
136 | $output = 'properties = [];
139 | if (!class_exists($model)) {
140 | if ($this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
141 | $this->error("Unable to find '$model' class");
142 | }
143 | return false;
144 | }
145 |
146 | try {
147 | // handle abstract classes, interfaces, ...
148 | $reflectionClass = new \ReflectionClass($model);
149 |
150 | if (!$reflectionClass->isSubclassOf('Illuminate\Database\Eloquent\Model')) {
151 | return false;
152 | }
153 |
154 | if ($this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
155 | $this->comment("Loading model '$model'");
156 | }
157 |
158 | if (!$reflectionClass->IsInstantiable()) {
159 | // ignore abstract class or interface
160 | return false;
161 | }
162 |
163 | $model = $this->laravel->make($model);
164 |
165 | $this->getPropertiesFromTable($model);
166 | $this->getPropertiesFromMethods($model);
167 |
168 | $output .= $this->createFactory($model);
169 | } catch (\Exception $e) {
170 | $this->error("Exception: " . $e->getMessage() . "\nCould not analyze class $model.");
171 | }
172 |
173 | return $output;
174 | }
175 |
176 |
177 | protected function loadModels($models = [])
178 | {
179 | if (!empty($models)) {
180 | return array_map(function ($name) {
181 | if (strpos($name, '\\') !== false) {
182 | return $name;
183 | }
184 |
185 | return str_replace(
186 | [DIRECTORY_SEPARATOR, basename($this->laravel->path()) . '\\'],
187 | ['\\', $this->laravel->getNamespace()],
188 | $this->dir . DIRECTORY_SEPARATOR . $name
189 | );
190 | }, $models);
191 | }
192 |
193 |
194 | $dir = base_path($this->dir);
195 | if (!file_exists($dir)) {
196 | return [];
197 | }
198 |
199 | return array_map(function (\SplFIleInfo $file) {
200 | return str_replace(
201 | [DIRECTORY_SEPARATOR, basename($this->laravel->path()) . '\\'],
202 | ['\\', $this->laravel->getNamespace()],
203 | $file->getPath() . DIRECTORY_SEPARATOR . basename($file->getFilename(), '.php')
204 | );
205 | }, $this->files->allFiles($this->dir));
206 | }
207 |
208 | /**
209 | * Load the properties from the database table.
210 | *
211 | * @param \Illuminate\Database\Eloquent\Model $model
212 | */
213 | protected function getPropertiesFromTable($model)
214 | {
215 | $table = $model->getConnection()->getTablePrefix() . $model->getTable();
216 | $schema = $model->getConnection()->getDoctrineSchemaManager($table);
217 | $databasePlatform = $schema->getDatabasePlatform();
218 | $databasePlatform->registerDoctrineTypeMapping('enum', 'customEnum');
219 |
220 | $platformName = $databasePlatform->getName();
221 | $customTypes = $this->laravel['config']->get("ide-helper.custom_db_types.{$platformName}", array());
222 | foreach ($customTypes as $yourTypeName => $doctrineTypeName) {
223 | $databasePlatform->registerDoctrineTypeMapping($yourTypeName, $doctrineTypeName);
224 | }
225 |
226 | $database = null;
227 | if (strpos($table, '.')) {
228 | list($database, $table) = explode('.', $table);
229 | }
230 |
231 | $columns = $schema->listTableColumns($table, $database);
232 |
233 | if ($columns) {
234 | foreach ($columns as $column) {
235 | $name = $column->getName();
236 | if (in_array($name, $model->getDates())) {
237 | $type = 'datetime';
238 | } else {
239 | $type = $column->getType()->getName();
240 | }
241 | if (!($model->incrementing && $model->getKeyName() === $name) &&
242 | $name !== $model::CREATED_AT &&
243 | $name !== $model::UPDATED_AT
244 | ) {
245 | if (!method_exists($model, 'getDeletedAtColumn') || (method_exists($model, 'getDeletedAtColumn') && $name !== $model->getDeletedAtColumn())) {
246 | $this->setProperty($name, $type, $table);
247 | }
248 | }
249 | }
250 | }
251 | }
252 |
253 |
254 | /**
255 | * @param \Illuminate\Database\Eloquent\Model $model
256 | */
257 | protected function getPropertiesFromMethods($model)
258 | {
259 | $methods = get_class_methods($model);
260 |
261 | foreach ($methods as $method) {
262 | if (!Str::startsWith($method, 'get') && !method_exists('Illuminate\Database\Eloquent\Model', $method)) {
263 | // Use reflection to inspect the code, based on Illuminate/Support/SerializableClosure.php
264 | $reflection = new \ReflectionMethod($model, $method);
265 | $file = new \SplFileObject($reflection->getFileName());
266 | $file->seek($reflection->getStartLine() - 1);
267 | $code = '';
268 | while ($file->key() < $reflection->getEndLine()) {
269 | $code .= $file->current();
270 | $file->next();
271 | }
272 | $code = trim(preg_replace('/\s\s+/', '', $code));
273 | $begin = strpos($code, 'function(');
274 | $code = substr($code, $begin, strrpos($code, '}') - $begin + 1);
275 | foreach (['belongsTo'] as $relation) {
276 | $search = '$this->' . $relation . '(';
277 | if ($pos = stripos($code, $search)) {
278 | $relationObj = $model->$method();
279 | if ($relationObj instanceof Relation) {
280 | $this->setProperty($relationObj->getForeignKeyName(), 'factory(' . get_class($relationObj->getRelated()) . '::class)');
281 | }
282 | }
283 | }
284 | }
285 | }
286 | }
287 |
288 | /**
289 | * @param string $name
290 | * @param string|null $type
291 | */
292 | protected function setProperty($name, $type = null, $table = null)
293 | {
294 | if ($type !== null && Str::startsWith($type, 'factory(')) {
295 | $this->properties[$name] = $type;
296 |
297 | return;
298 | }
299 |
300 | $fakeableTypes = [
301 | 'enum' => '$faker->randomElement(' . $this->enumValues($table, $name) . ')',
302 | 'string' => '$faker->word',
303 | 'text' => '$faker->text',
304 | 'date' => '$faker->date()',
305 | 'time' => '$faker->time()',
306 | 'guid' => '$faker->word',
307 | 'datetimetz' => '$faker->dateTime()',
308 | 'datetime' => '$faker->dateTime()',
309 | 'integer' => '$faker->randomNumber()',
310 | 'bigint' => '$faker->randomNumber()',
311 | 'smallint' => '$faker->randomNumber()',
312 | 'decimal' => '$faker->randomFloat()',
313 | 'float' => '$faker->randomFloat()',
314 | 'boolean' => '$faker->boolean'
315 | ];
316 |
317 | $fakeableNames = [
318 | 'city' => '$faker->city',
319 | 'company' => '$faker->company',
320 | 'country' => '$faker->country',
321 | 'description' => '$faker->text',
322 | 'email' => '$faker->safeEmail',
323 | 'first_name' => '$faker->firstName',
324 | 'firstname' => '$faker->firstName',
325 | 'guid' => '$faker->uuid',
326 | 'last_name' => '$faker->lastName',
327 | 'lastname' => '$faker->lastName',
328 | 'lat' => '$faker->latitude',
329 | 'latitude' => '$faker->latitude',
330 | 'lng' => '$faker->longitude',
331 | 'longitude' => '$faker->longitude',
332 | 'name' => '$faker->name',
333 | 'password' => 'bcrypt($faker->password)',
334 | 'phone' => '$faker->phoneNumber',
335 | 'phone_number' => '$faker->phoneNumber',
336 | 'postcode' => '$faker->postcode',
337 | 'postal_code' => '$faker->postcode',
338 | 'remember_token' => 'Str::random(10)',
339 | 'slug' => '$faker->slug',
340 | 'street' => '$faker->streetName',
341 | 'address1' => '$faker->streetAddress',
342 | 'address2' => '$faker->secondaryAddress',
343 | 'summary' => '$faker->text',
344 | 'url' => '$faker->url',
345 | 'user_name' => '$faker->userName',
346 | 'username' => '$faker->userName',
347 | 'uuid' => '$faker->uuid',
348 | 'zip' => '$faker->postcode',
349 | ];
350 |
351 | if (isset($fakeableNames[$name])) {
352 | $this->properties[$name] = $fakeableNames[$name];
353 |
354 | return;
355 | }
356 |
357 | if (isset($fakeableTypes[$type])) {
358 | $this->properties[$name] = $fakeableTypes[$type];
359 |
360 | return;
361 | }
362 |
363 | $this->properties[$name] = '$faker->word';
364 | }
365 |
366 | public static function enumValues($table, $name)
367 | {
368 | if ($table === null) {
369 | return "[]";
370 | }
371 |
372 | $type = DB::select(DB::raw('SHOW COLUMNS FROM ' . $table . ' WHERE Field = "' . $name . '"'))[0]->Type;
373 |
374 | preg_match_all("/'([^']+)'/", $type, $matches);
375 |
376 | $values = isset($matches[1]) ? $matches[1] : array();
377 |
378 | return "['" . implode("', '", $values) . "']";
379 | }
380 |
381 |
382 | /**
383 | * @param string $class
384 | * @return string
385 | */
386 | protected function createFactory($class)
387 | {
388 | $reflection = new \ReflectionClass($class);
389 |
390 | $content = $this->view->make('test-factory-helper::factory', [
391 | 'reflection' => $reflection,
392 | 'properties' => $this->properties,
393 | ])->render();
394 |
395 | return $content;
396 | }
397 |
398 | }
399 |
--------------------------------------------------------------------------------
/src/TestFactoryHelperServiceProvider.php:
--------------------------------------------------------------------------------
1 | loadViewsFrom($viewPath, 'test-factory-helper');
27 | }
28 |
29 | /**
30 | * Register the service provider.
31 | *
32 | * @return void
33 | */
34 | public function register()
35 | {
36 | $this->app->bind('command.test-factory-helper.generate',
37 | function ($app) {
38 | return new GenerateCommand($app['files'], $app['view']);
39 | }
40 | );
41 |
42 | $this->commands('command.test-factory-helper.generate');
43 | }
44 |
45 | /**
46 | * Get the services provided by the provider.
47 | *
48 | * @return array
49 | */
50 | public function provides()
51 | {
52 | return array('command.test-factory-helper.generate');
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/src/Types/EnumType.php:
--------------------------------------------------------------------------------
1 |