├── .gitignore
├── LICENSE.md
├── README.md
├── composer.json
├── phpunit.xml
├── pint.json
├── src
├── FactoryGenerator.php
├── FactoryGeneratorServiceProvider.php
├── GenerateCommand.php
└── TypeGuesser.php
├── stubs
└── factory.stub
└── tests
├── Fixtures
└── Models
│ ├── Book.php
│ ├── Car.php
│ ├── Habit.php
│ ├── NotAModel.php
│ └── User.php
├── GenerateCommandTest.php
├── TestCase.php
├── TypeGuesserTest.php
└── migrations
├── 0000_00_00_000001_create_users_table.php
├── 0000_00_00_000002_create_habits_table.php
└── 0000_00_00_000003_create_cars_table.php
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea/
2 | /vendor
3 | .env
4 | .phpunit.result.cache
5 | composer.lock
6 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Laravel Shift
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Laravel Model Factory Generator
2 | This package generates model factories from existing models using the new [class-based factories](https://laravel.com/docs/8.x/database-testing#writing-factories) introduced in Laravel 8.
3 |
4 |
5 | ## Installation
6 | You may install this package via composer by running:
7 |
8 | ```sh
9 | composer require --dev laravel-shift/factory-generator
10 | ```
11 |
12 | The package will automatically register itself using Laravel's package discovery.
13 |
14 |
15 | ## Usage
16 | This package adds an artisan command for generating model factories.
17 |
18 | Without any arguments, this command will generate model factories for all existing models within your Laravel application:
19 |
20 | ```sh
21 | php artisan generate:factory
22 | ```
23 |
24 | Similar to Laravel, this will search for models within the `app/Models` folder, or if that folder does not exist, within the `app` folder.
25 |
26 | To generate factories for models within a different folder, you may pass the `--path` option (or `-p`).
27 |
28 | ```sh
29 | php artisan generate:factory --path=some/Other/Path
30 | ```
31 |
32 | To generate a factory for a single model, you may pass the model name:
33 |
34 | ```sh
35 | php artisan generate:factory User
36 | ```
37 |
38 | By default _nullable_ columns are not included in the factory definition. If you want to include _nullable_ columns you may set the `--include-nullable` option (or `-i`).
39 |
40 | ```sh
41 | php artisan generate:factory -i User
42 | ```
43 |
44 |
45 | ## Attribution
46 | This package was original forked from [Naoray/laravel-factory-prefill](https://github.com/Naoray/laravel-factory-prefill) by [Krishan König](https://github.com/Naoray).
47 |
48 | It has diverged to support the latest version of Laravel and to power part of the automation by the [Tests Generator](https://laravelshift.com/laravel-test-generator).
49 |
50 |
51 | ## Contributing
52 | Contributions should be submitted to the `master` branch. Any submissions should be complete with tests and adhere to the [Laravel code style](https://www.php-fig.org/psr/psr-2/). You may also contribute by [opening an issue](https://github.com/laravel-shift/factory-generator/issues).
53 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "laravel-shift/factory-generator",
3 | "description": "Generate factories from existing models",
4 | "type": "package",
5 | "license": "MIT",
6 | "keywords": [
7 | "laravel",
8 | "factory",
9 | "model",
10 | "testing"
11 | ],
12 | "require": {
13 | "php": "^8.1",
14 | "illuminate/support": "^10.0|^11.0|^12.0",
15 | "fakerphp/faker": "^1.9.1"
16 | },
17 | "require-dev": {
18 | "orchestra/testbench": "^8.0|^9.0|^10.0",
19 | "phpunit/phpunit": "^10.5|^11.5.3"
20 | },
21 | "autoload": {
22 | "psr-4": {
23 | "Shift\\FactoryGenerator\\": "src"
24 | }
25 | },
26 | "autoload-dev": {
27 | "psr-4": {
28 | "Tests\\": "tests",
29 | "App\\": "vendor/orchestra/testbench-core/laravel/app"
30 | },
31 | "exclude-from-classmap": [
32 | "tests/migrations"
33 | ]
34 | },
35 | "extra": {
36 | "laravel": {
37 | "providers": [
38 | "Shift\\FactoryGenerator\\FactoryGeneratorServiceProvider"
39 | ]
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | src/
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/pint.json:
--------------------------------------------------------------------------------
1 | {
2 | "preset": "laravel",
3 | "notPaths": []
4 | }
5 |
--------------------------------------------------------------------------------
/src/FactoryGenerator.php:
--------------------------------------------------------------------------------
1 | typeGuesser = $guesser;
24 | $this->includeNullableColumns = $nullables;
25 | $this->overwrite = $overwrite;
26 | }
27 |
28 | public function generate($model): ?string
29 | {
30 | if (! $modelClass = $this->modelExists($model)) {
31 | return null;
32 | }
33 |
34 | $factoryPath = $this->factoryPath($modelClass);
35 |
36 | if (! $this->overwrite && $this->factoryExists($factoryPath)) {
37 | return null;
38 | }
39 |
40 | $this->modelInstance = new $modelClass();
41 |
42 | $code = Artisan::call('model:show', ['model' => $modelClass, '--json' => true]);
43 | if ($code !== 0) {
44 | return null;
45 | }
46 |
47 | $json = json_decode(Artisan::output(), true);
48 | if (! $json) {
49 | return null;
50 | }
51 |
52 | $foreign_keys = $this->modelInstance->getConnection()->getSchemaBuilder()->getForeignKeys($json['table']);
53 |
54 | collect($this->columns($json['attributes'], $foreign_keys))
55 | ->merge($this->relationships($json['relations']))
56 | ->filter()
57 | ->unique()
58 | ->values()
59 | ->pipe(function ($properties) use ($factoryPath, $modelClass) {
60 | $this->writeFactoryFile($factoryPath, $properties->all(), $modelClass);
61 | $this->addFactoryTrait($modelClass);
62 | });
63 |
64 | return $factoryPath;
65 | }
66 |
67 | protected function addFactoryTrait($modelClass)
68 | {
69 | $traits = class_uses_recursive($modelClass);
70 | if (in_array('Illuminate\\Database\\Eloquent\\Factories\\HasFactory', $traits)) {
71 | return;
72 | }
73 |
74 | $path = (new \ReflectionClass($modelClass))->getFileName();
75 |
76 | $contents = File::get($path);
77 |
78 | $tokens = collect(\PhpToken::tokenize($contents));
79 |
80 | $class = $tokens->first(fn (\PhpToken $token) => $token->id === T_CLASS);
81 | $import = $tokens->first(fn (\PhpToken $token) => $token->id === T_USE);
82 |
83 | $pos = strpos($contents, '{', $class->pos) + 1;
84 | $replacement = PHP_EOL.' use HasFactory;'.PHP_EOL;
85 | $contents = substr_replace($contents, $replacement, $pos, 0);
86 |
87 | $anchor = $import ?? $class;
88 |
89 | $contents = substr_replace(
90 | $contents,
91 | 'use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;'.PHP_EOL,
92 | $anchor->pos,
93 | 0
94 | );
95 |
96 | File::put($path, $contents);
97 | }
98 |
99 | /**
100 | * Check if factory already exists.
101 | *
102 | * @param string $name
103 | * @return bool|string
104 | */
105 | protected function factoryExists($path): bool
106 | {
107 | return File::exists($path);
108 | }
109 |
110 | /**
111 | * Map database table column definition to faker value.
112 | */
113 | protected function mapColumn(array $column): array
114 | {
115 | $key = $column['name'];
116 |
117 | if (! $this->shouldBeIncluded($column)) {
118 | return $this->factoryTuple($key);
119 | }
120 |
121 | // TODO: probably belongs elsewhere...
122 | if ($key === 'password') {
123 | return $this->factoryTuple($key, "Hash::make('password')");
124 | }
125 |
126 | $value = $column['unique']
127 | ? '$this->faker->unique()->'
128 | : '$this->faker->';
129 |
130 | return $this->factoryTuple($key, $value.$this->mapToFaker($column));
131 | }
132 |
133 | protected function factoryTuple($key, $value = null): array
134 | {
135 | return [
136 | $key => is_null($value) ? $value : "'{$key}' => $value",
137 | ];
138 | }
139 |
140 | /**
141 | * Map name to faker method.
142 | */
143 | protected function mapToFaker(array $column): string
144 | {
145 | return $this->typeGuesser->guess(
146 | $column['name'],
147 | $column['type'],
148 | $column['length']
149 | );
150 | }
151 |
152 | /**
153 | * Check if the given model exists.
154 | */
155 | protected function modelExists(string $name): string
156 | {
157 | if (class_exists($modelClass = $this->qualifyClass($name))) {
158 | return $modelClass;
159 | }
160 |
161 | // TODO: this check should happen before calling this service class...
162 | throw new \UnexpectedValueException('could not find model ['.$name.']');
163 | }
164 |
165 | /**
166 | * Parse the class name and format according to the root namespace.
167 | */
168 | protected function qualifyClass(string $name): string
169 | {
170 | $name = ltrim($name, '\\/');
171 |
172 | $rootNamespace = app()->getNamespace();
173 |
174 | if (Str::startsWith($name, $rootNamespace)) {
175 | return $name;
176 | }
177 |
178 | $name = str_replace('/', '\\', $name);
179 |
180 | return $this->qualifyClass(
181 | trim($rootNamespace, '\\').'\\'.$name
182 | );
183 | }
184 |
185 | /**
186 | * Get properties for relationships where we can build
187 | * other factories. Currently, that's simply BelongsTo.
188 | */
189 | protected function relationships(array $relationships): Collection
190 | {
191 | return collect($relationships)
192 | ->filter(fn ($relationship) => $relationship['type'] === 'BelongsTo')
193 | ->mapWithKeys(function ($relationship) {
194 | $property = $this->modelInstance->{$relationship['name']}()->getForeignKeyName();
195 |
196 | return [$property => "'$property' => \\".$relationship['related'].'::factory()'];
197 | });
198 | }
199 |
200 | /**
201 | * Check if a given column should be included in the factory.
202 | */
203 | protected function shouldBeIncluded(array $column): bool
204 | {
205 | $shouldBeIncluded = (! $column['nullable'] || $this->includeNullableColumns)
206 | && ! $column['increments']
207 | && ! $column['foreign']
208 | && $column['name'] !== $this->modelInstance->getKeyName();
209 |
210 | if (! $this->modelInstance->usesTimestamps()) {
211 | return $shouldBeIncluded;
212 | }
213 |
214 | $timestamps = [
215 | $this->modelInstance->getCreatedAtColumn(),
216 | $this->modelInstance->getUpdatedAtColumn(),
217 | ];
218 |
219 | if (method_exists($this->modelInstance, 'getDeletedAtColumn')) {
220 | $timestamps[] = $this->modelInstance->getDeletedAtColumn();
221 | }
222 |
223 | return $shouldBeIncluded
224 | && ! in_array($column['name'], $timestamps);
225 | }
226 |
227 | /**
228 | * Write the model factory file using the given definition and path.
229 | */
230 | protected function writeFactoryFile(string $path, array $data, string $modelClass): void
231 | {
232 | File::ensureDirectoryExists(dirname($path));
233 |
234 | $factoryQualifiedName = \Illuminate\Database\Eloquent\Factories\Factory::resolveFactoryName($modelClass);
235 | $factoryNamespace = Str::beforeLast($factoryQualifiedName, '\\');
236 | $contents = File::get(__DIR__.'/../stubs/factory.stub');
237 | $contents = str_replace('{{ factoryNamespace }}', $factoryNamespace, $contents);
238 | $contents = str_replace('{{ namespacedModel }}', $modelClass, $contents);
239 | $contents = str_replace('{{ model }}', class_basename($modelClass), $contents);
240 | $definitions = array_map(fn ($value) => ' '.$value.',', $data);
241 | $contents = str_replace(' //', implode(PHP_EOL, $definitions), $contents);
242 |
243 | File::put($path, $contents);
244 | }
245 |
246 | private function appendColumnData(array $column, array $foreignKeys): array
247 | {
248 | $column['foreign'] = in_array($column['name'], $foreignKeys);
249 | $column['length'] = null;
250 |
251 | if (str_contains($column['type'], '(')) {
252 | $column['length'] = Str::between($column['type'], '(', ')');
253 | $column['type'] = Str::before($column['type'], '(');
254 | }
255 |
256 | return $column;
257 | }
258 |
259 | private function columns(array $attributes, array $foreignKeys): Collection
260 | {
261 | return collect($attributes)
262 | ->reject(fn ($column) => is_null($column['type']))
263 | ->map(fn ($column) => $this->appendColumnData($column, $foreignKeys))
264 | ->mapWithKeys(fn ($column) => $this->mapColumn($column));
265 | }
266 |
267 | private function factoryPath($model): string
268 | {
269 | $subDirectory = Str::of($model)
270 | ->replaceFirst('App\\Models\\', '')
271 | ->replaceFirst('App\\', '');
272 |
273 | return database_path('factories/'.str_replace('\\', '/', $subDirectory).'Factory.php');
274 | }
275 | }
276 |
--------------------------------------------------------------------------------
/src/FactoryGeneratorServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->runningInConsole()) {
12 | $this->commands([
13 | GenerateCommand::class,
14 | ]);
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/GenerateCommand.php:
--------------------------------------------------------------------------------
1 | resolveModelPath();
25 | $models = $this->argument('models');
26 |
27 | if (! File::exists($directory)) {
28 | $this->error("Path does not exist [$directory]");
29 |
30 | return self::FAILURE;
31 | }
32 |
33 | $generator = resolve(FactoryGenerator::class, ['nullables' => $this->option('include-nullable'), 'overwrite' => $this->option('force')]);
34 |
35 | $this->loadModels($directory, $models)
36 | ->filter(function ($model) {
37 | $model = new ReflectionClass($model);
38 |
39 | return $model->isSubclassOf(Model::class) && ! $model->isAbstract();
40 | })
41 | ->each(function ($model) use ($generator) {
42 | $factory = $generator->generate($model);
43 |
44 | if ($factory) {
45 | $this->line('Model factory created: '.$factory);
46 | } else {
47 | $this->line('Failed to create factory for model: '.$model);
48 | }
49 | });
50 |
51 | return self::SUCCESS;
52 | }
53 |
54 | protected function loadModels(string $directory, array $models = []): Collection
55 | {
56 | if (! empty($models)) {
57 | $dir = str_replace(app_path(), '', $directory);
58 |
59 | return collect($models)->map(function ($name) use ($dir) {
60 | if (strpos($name, '\\') !== false) {
61 | return $name;
62 | }
63 |
64 | return str_replace(
65 | [DIRECTORY_SEPARATOR, basename($this->laravel->path()).'\\'],
66 | ['\\', $this->laravel->getNamespace()],
67 | basename($this->laravel->path()).$dir.DIRECTORY_SEPARATOR.$name
68 | );
69 | });
70 | }
71 |
72 | return collect(File::allFiles($directory))->map(function (SplFileInfo $file) {
73 | if (! preg_match('/^namespace\s+([^;]+)/m', $file->getContents(), $matches)) {
74 | return null;
75 | }
76 |
77 | return $matches[1].'\\'.$file->getBasename('.php');
78 | })->filter();
79 | }
80 |
81 | protected function resolveModelPath(): string
82 | {
83 | $path = $this->option('path');
84 | if (! is_null($path)) {
85 | return base_path($path);
86 | }
87 |
88 | if (File::isDirectory(app_path('Models'))) {
89 | return app_path('Models');
90 | }
91 |
92 | return app_path();
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/TypeGuesser.php:
--------------------------------------------------------------------------------
1 | generator = $generator;
22 | }
23 |
24 | public function guess(string $name, string $type, ?string $size = null): string
25 | {
26 | $name = Str::of($name)->lower();
27 |
28 | if ($name->endsWith('_id')) {
29 | return 'randomDigitNotNull()';
30 | }
31 |
32 | if ($typeNameGuess = $this->guessBasedOnName($name->__toString(), $size)) {
33 | return $typeNameGuess;
34 | }
35 |
36 | if ($nativeName = $this->nativeNameFor($name->replace('_', ''))) {
37 | return $nativeName.'()';
38 | }
39 |
40 | if ($name->endsWith('_url')) {
41 | return 'url()';
42 | }
43 |
44 | return $this->guessBasedOnType($type, $size);
45 | }
46 |
47 | /**
48 | * Get type guess.
49 | */
50 | protected function guessBasedOnName(string $name, ?string $size = null): ?string
51 | {
52 | if (str_ends_with($name, '_token')) {
53 | return 'sha1()';
54 | }
55 |
56 | return match ($name) {
57 | 'login' => 'userName()',
58 | 'email_address', 'emailaddress' => 'email()',
59 | 'phone', 'telephone', 'telnumber' => 'phoneNumber()',
60 | 'town' => 'city()',
61 | 'postalcode', 'postal_code', 'zipcode', 'zip_code' => 'postcode()',
62 | 'province', 'county' => $this->predictCountyType(),
63 | 'country' => $this->predictCountryType($size),
64 | 'currency' => 'currencyCode()',
65 | 'website' => 'url()',
66 | 'companyname', 'company_name', 'employer' => 'company()',
67 | 'title' => $this->predictTitleType($size),
68 | default => null,
69 | };
70 | }
71 |
72 | /**
73 | * Get native name for the given string.
74 | */
75 | protected function nativeNameFor(string $lookup): ?string
76 | {
77 | static $fakerMethodNames = [];
78 |
79 | if (empty($fakerMethodNames)) {
80 | $fakerMethodNames = collect($this->generator->getProviders())
81 | ->flatMap(function (Base $provider) {
82 | return $this->getNamesFromProvider($provider);
83 | })
84 | ->unique()
85 | ->toArray();
86 | }
87 |
88 | if (isset($fakerMethodNames[$lookup])) {
89 | return $fakerMethodNames[$lookup];
90 | }
91 |
92 | return null;
93 | }
94 |
95 | /**
96 | * Get public methods as a lookup pair.
97 | */
98 | protected function getNamesFromProvider(Base $provider): array
99 | {
100 | return collect(get_class_methods($provider))
101 | ->reject(fn (string $methodName) => Str::startsWith($methodName, '__'))
102 | ->mapWithKeys(fn (string $methodName) => [Str::lower($methodName) => $methodName])
103 | ->all();
104 | }
105 |
106 | /**
107 | * Try to guess the right faker method for the given type.
108 | */
109 | protected function guessBasedOnType(string $type, ?string $size): string
110 | {
111 | $precision = 0;
112 | if (str_contains($size, ',')) {
113 | [$size, $precision] = explode(',', $size, 2);
114 | }
115 |
116 | $type = match ($type) {
117 | 'tinyint(1)', 'bit', 'varbit', 'boolean', 'bool' => 'boolean',
118 | 'varchar(max)', 'nvarchar(max)', 'text', 'ntext', 'tinytext', 'mediumtext', 'longtext' => 'text',
119 | 'integer', 'int', 'int4', 'smallint', 'int2', 'tinyint', 'mediumint', 'bigint', 'int8' => 'integer',
120 | 'date' => 'date',
121 | 'decimal', 'float', 'real', 'float4', 'double', 'float8' => 'float',
122 | 'time', 'timetz' => 'time',
123 | 'datetime', 'datetime2', 'smalldatetime','datetimeoffset' => 'datetime',
124 | 'timestamp', 'timestamptz' => 'timestamp',
125 | 'json', 'jsonb' => 'json',
126 | 'uuid', 'uniqueidentifier' => 'uuid',
127 | 'inet', 'inet4', 'cidr' => 'ip_address',
128 | 'macaddr', 'macaddr8' => 'mac_address',
129 | 'year' => 'year',
130 | 'char', 'bpchar', 'nchar' => 'char',
131 | 'varchar', 'nvarchar' => 'string',
132 | 'binary', 'varbinary', 'bytea', 'image', 'blob', 'tinyblob', 'mediumblob', 'longblob' => 'binary',
133 | 'geometry', 'geometrycollection', 'linestring', 'multilinestring', 'point', 'multipoint', 'polygon', 'multipolygon' => 'geometry',
134 | 'geography' => 'geography',
135 |
136 | // 'enum => 'enum',
137 | // 'set' => 'set',
138 | // 'money', 'smallmoney' => 'money',
139 | // 'xml' => 'xml',
140 | // 'interval' => 'interval',
141 | // 'box', 'circle', 'line', 'lseg', 'path' => 'geometry',
142 | // 'tsvector', 'tsquery' => 'text',
143 | default => $type,
144 | };
145 |
146 | if ($type === 'float' && $precision == 0) {
147 | $type = 'integer';
148 | }
149 |
150 | return match ($type) {
151 | 'boolean' => 'boolean()',
152 | 'char' => 'randomLetter()',
153 | 'date' => 'date()',
154 | 'datetime' => 'dateTime()',
155 | 'float' => 'randomFloat('.$precision.')',
156 | 'inet6' => 'ipv6()',
157 | 'integer', 'number' => 'randomNumber('.$size.')',
158 | 'ip_address' => 'ipv4()',
159 | 'mac_address' => 'macAddress()',
160 | 'text' => 'text()',
161 | 'time' => 'time()',
162 | 'timestamp' => 'unixTime()',
163 | 'uuid' => 'uuid()',
164 | 'year' => 'year()',
165 | default => 'word()',
166 | };
167 | }
168 |
169 | /**
170 | * Predicts county type by locale.
171 | */
172 | protected function predictCountyType(): string
173 | {
174 | if ($this->generator->locale == 'en_US') {
175 | return "sprintf('%s County', \$faker->city())";
176 | }
177 |
178 | return 'state()';
179 | }
180 |
181 | /**
182 | * Predicts country code based on $size.
183 | */
184 | protected function predictCountryType(?int $size): string
185 | {
186 | return match ($size) {
187 | 2 => 'countryCode()',
188 | 3 => 'countryISOAlpha3()',
189 | 5, 6 => 'locale()',
190 | default => 'country()',
191 | };
192 | }
193 |
194 | /**
195 | * Predicts type of title by $size.
196 | */
197 | protected function predictTitleType(?int $size): string
198 | {
199 | if ($size === null || $size <= 10) {
200 | return 'title()';
201 | }
202 |
203 | return 'sentence()';
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/stubs/factory.stub:
--------------------------------------------------------------------------------
1 |
14 | */
15 | public function definition(): array
16 | {
17 | return [
18 | //
19 | ];
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/Fixtures/Models/Book.php:
--------------------------------------------------------------------------------
1 | belongsTo(User::class);
22 | }
23 |
24 | /**
25 | * Get my previous owner.
26 | *
27 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
28 | */
29 | public function previousOwner()
30 | {
31 | return $this->belongsTo(User::class);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/Fixtures/Models/Habit.php:
--------------------------------------------------------------------------------
1 | belongsTo(User::class);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/Fixtures/Models/NotAModel.php:
--------------------------------------------------------------------------------
1 | artisan('make:factory', ['name' => 'ExistsFactory']);
21 | $this->artisan('make:model', ['name' => 'Exists']);
22 |
23 | $this->artisan('generate:factory', ['models' => ['Exists']])
24 | ->expectsOutput('Factory already exists for model [Exists]')
25 | ->run();
26 | }
27 |
28 | /** @test */
29 | public function it_asks_if_a_model_shall_be_created_if_it_does_not_yet_exist()
30 | {
31 | $this->artisan('generate:factory', ['models' => ['App\\NonExistent']])
32 | ->expectsOutput('Model created successfully.')
33 | ->run();
34 | }
35 |
36 | /** @test */
37 | public function it_can_associate_models_through_their_relationship()
38 | {
39 | $this->artisan('generate:factory', [
40 | 'models' => [Habit::class],
41 | '--no-interaction' => true,
42 | ])
43 | ->expectsOutput('Factory blueprint created!')
44 | ->run();
45 |
46 | $this->assertFileExists(database_path('factories/HabitFactory.php'));
47 | $this->assertTrue(Str::contains(
48 | File::get(database_path('factories/HabitFactory.php')),
49 | "'user_id' => factory(Tests\Fixtures\Models\User::class)->lazy(),"
50 | ));
51 | }
52 |
53 | /** @test */
54 | public function it_can_associate_models_through_their_relationship_methods_without_touching_the_db()
55 | {
56 | $this->artisan('generate:factory', [
57 | 'models' => [Car::class],
58 | '--no-interaction' => true,
59 | ])
60 | ->expectsOutput('Factory blueprint created!')
61 | ->run();
62 |
63 | $this->assertFileExists(database_path('factories/CarFactory.php'));
64 | $this->assertTrue(Str::contains(
65 | File::get(database_path('factories/CarFactory.php')),
66 | "'owner_id' => factory(Tests\Fixtures\Models\User::class)->lazy(),"
67 | ));
68 | }
69 |
70 | /** @test */
71 | public function it_can_correctly_prefill_password_columns()
72 | {
73 | $this->artisan('generate:factory', [
74 | 'models' => [User::class],
75 | '--no-interaction' => true,
76 | ])
77 | ->expectsOutput('Factory blueprint created!')
78 | ->run();
79 |
80 | $this->assertFileExists(database_path('factories/UserFactory.php'));
81 | $this->assertTrue(Str::contains(
82 | File::get(database_path('factories/UserFactory.php')),
83 | "'password' => bcrypt('password'),"
84 | ));
85 | }
86 |
87 | /** @test */
88 | public function it_can_create_prefilled_factories_for_a_model()
89 | {
90 | $this->artisan('generate:factory', [
91 | 'models' => [Habit::class],
92 | '--no-interaction' => true,
93 | ])
94 | ->expectsOutput('Factory blueprint created!')
95 | ->run();
96 |
97 | $this->assertFileExists(database_path('factories/HabitFactory.php'));
98 | }
99 |
100 | /** @test */
101 | public function it_can_create_prefilled_factories_for_all_models()
102 | {
103 | $this->artisan('generate:factory', [
104 | '--no-interaction' => true,
105 | '--path' => __DIR__.'/Fixtures/Models',
106 | '--include-nullable' => true,
107 | ])
108 | ->expectsOutput('3 factories created')
109 | ->run();
110 |
111 | $this->assertFileExists(database_path('factories/CarFactory.php'));
112 | $this->assertFileExists(database_path('factories/HabitFactory.php'));
113 | $this->assertFileExists(database_path('factories/UserFactory.php'));
114 | }
115 |
116 | /** @test */
117 | public function it_can_create_prefilled_factories_for_defined_models_only_with_including_namespace()
118 | {
119 | $this->artisan('generate:factory', [
120 | 'models' => [
121 | '\Tests\Fixtures\Models\Car',
122 | '\Tests\Fixtures\Models\Habit',
123 | ],
124 | '--no-interaction' => true,
125 | '--include-nullable' => true,
126 | ])
127 | ->expectsOutput('2 factories created')
128 | ->run();
129 |
130 | $this->assertFileExists(database_path('factories/CarFactory.php'));
131 | $this->assertFileExists(database_path('factories/HabitFactory.php'));
132 | }
133 |
134 | /** @test */
135 | public function it_can_include_nullable_properties_in_factories()
136 | {
137 | $this->artisan('generate:factory', [
138 | 'models' => [Car::class],
139 | '--no-interaction' => true,
140 | '--include-nullable' => true,
141 | ])
142 | ->expectsOutput('Factory created!')
143 | ->run();
144 |
145 | $this->assertFileExists(database_path('factories/CarFactory.php'));
146 |
147 | $this->assertTrue(Str::contains(
148 | File::get(database_path('factories/CarFactory.php')),
149 | "'factory_year' => \$faker->randomNumber,"
150 | ));
151 | }
152 |
153 | /** @test */
154 | public function it_does_not_include_the_models_created_at_updated_at_or_deleted_at_timestamps_even_nullable_values_are_requested()
155 | {
156 | $this->artisan('generate:factory', [
157 | 'models' => [Car::class],
158 | '--no-interaction' => true,
159 | '--include-nullable' => true,
160 | ])
161 | ->expectsOutput('Factory blueprint created!')
162 | ->run();
163 |
164 | $this->assertFileExists(database_path('factories/CarFactory.php'));
165 | $this->assertFalse(Str::contains(
166 | File::get(database_path('factories/CarFactory.php')),
167 | "'created_at' => \$faker,"
168 | ));
169 | $this->assertFalse(Str::contains(
170 | File::get(database_path('factories/CarFactory.php')),
171 | "'updated_at' => \$faker,"
172 | ));
173 | $this->assertFalse(Str::contains(
174 | File::get(database_path('factories/CarFactory.php')),
175 | "'deleted_at' => \$faker,"
176 | ));
177 | }
178 |
179 | /** @test */
180 | public function it_identifies_belongs_to_relations_through_relation_methods()
181 | {
182 | $this->artisan('generate:factory', [
183 | 'models' => [Car::class],
184 | '--no-interaction' => true,
185 | '--include-nullable' => true,
186 | ])
187 | ->expectsOutput('Factory blueprint created!')
188 | ->run();
189 |
190 | $this->assertFileExists($path = database_path('factories/CarFactory.php'));
191 | $this->assertTrue(Str::contains(
192 | File::get($path),
193 | "'previous_owner_id' => factory(".User::class.'::class)->lazy(),'
194 | ));
195 | }
196 |
197 | /** @test */
198 | public function it_prints_an_error_if_no_database_info_could_be_found()
199 | {
200 | $this->artisan('generate:factory', [
201 | 'models' => [Book::class],
202 | '--no-interaction' => true,
203 | ])
204 | ->expectsOutput('We could not find any data for your factory. Did you `php artisan migrate` already?')
205 | ->run();
206 | }
207 |
208 | /** @test */
209 | public function it_returns_a_no_files_found_error_if_no_files_were_found_in_the_given_directory()
210 | {
211 | $this->artisan('generate:factory', [
212 | '--no-interaction' => true,
213 | '--path' => $directory = __DIR__.'/Fixtures/NonExistent',
214 | '--include-nullable' => true,
215 | ])
216 | ->expectsOutput("No files in [$directory] were found!")
217 | ->assertExitCode(1)
218 | ->run();
219 | }
220 |
221 | /**
222 | * Define database migrations.
223 | *
224 | * @return void
225 | */
226 | protected function defineDatabaseMigrations()
227 | {
228 | $this->loadMigrationsFrom(realpath(__DIR__.'/migrations'));
229 | }
230 |
231 | protected function setUp(): void
232 | {
233 | parent::setUp();
234 |
235 | $this->beforeApplicationDestroyed(function () {
236 | File::cleanDirectory(app_path());
237 | File::cleanDirectory(database_path('factories'));
238 | });
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | typeGuesser = resolve(TypeGuesser::class);
19 | }
20 |
21 | /** @test */
22 | public function it_can_guess_boolean_values_by_type()
23 | {
24 | $this->assertEquals('boolean()', $this->typeGuesser->guess('is_verified', 'boolean'));
25 | }
26 |
27 | /** @test */
28 | public function it_can_guess_random_integer_values_by_type()
29 | {
30 | $this->assertEquals('randomNumber()', $this->typeGuesser->guess('integer', 'integer'));
31 | $this->assertEquals('randomNumber(10)', $this->typeGuesser->guess('integer', 'integer', 10));
32 |
33 | $this->assertEquals('randomNumber()', $this->typeGuesser->guess('big_int', 'bigint'));
34 | $this->assertEquals('randomNumber(10)', $this->typeGuesser->guess('big_int', 'bigint', 10));
35 |
36 | $this->assertEquals('randomNumber()', $this->typeGuesser->guess('small_int', 'smallint'));
37 | $this->assertEquals('randomNumber(10)', $this->typeGuesser->guess('small_int', 'smallint', 10));
38 | }
39 |
40 | /** @test */
41 | public function it_can_guess_random_decimal_values_by_type()
42 | {
43 | $this->assertEquals('randomFloat()', $this->typeGuesser->guess('decimal_value', 'decimal'));
44 | $this->assertEquals('randomFloat(10)', $this->typeGuesser->guess('decimal_value', 'decimal', 10));
45 | }
46 |
47 | /** @test */
48 | public function it_can_guess_random_float_values_by_type()
49 | {
50 | $this->assertEquals('randomFloat()', $this->typeGuesser->guess('float_value', 'float'));
51 | $this->assertEquals('randomFloat(10)', $this->typeGuesser->guess('float_value', 'float', 10));
52 | }
53 |
54 | /** @test */
55 | public function it_can_guess_date_time_values_by_type()
56 | {
57 | $this->assertEquals('dateTime()', $this->typeGuesser->guess('done_at', 'datetime'));
58 | $this->assertEquals('date()', $this->typeGuesser->guess('birthdate', $this->getType(Types::DATE_IMMUTABLE)));
59 | $this->assertEquals('time()', $this->typeGuesser->guess('closing_at', $this->getType(Types::TIME_IMMUTABLE)));
60 | }
61 |
62 | /** @test */
63 | public function it_can_guess_text_values_by_type()
64 | {
65 | $this->assertEquals('text()', $this->typeGuesser->guess('body', 'text'));
66 | }
67 |
68 | /** @test */
69 | public function it_can_guess_name_values()
70 | {
71 | $this->assertEquals('name()', $this->typeGuesser->guess('name', 'no-op'));
72 | }
73 |
74 | /** @test */
75 | public function it_can_guess_first_name_values()
76 | {
77 | $this->assertEquals('firstName()', $this->typeGuesser->guess('first_name', 'no-op'));
78 | $this->assertEquals('firstName()', $this->typeGuesser->guess('firstname', 'no-op'));
79 | }
80 |
81 | /** @test */
82 | public function it_can_guess_last_name_values()
83 | {
84 | $this->assertEquals('lastName()', $this->typeGuesser->guess('last_name', 'no-op'));
85 | $this->assertEquals('lastName()', $this->typeGuesser->guess('lastname', 'no-op'));
86 | }
87 |
88 | /** @test */
89 | public function it_can_guess_user_name_values()
90 | {
91 | $this->assertEquals('userName()', $this->typeGuesser->guess('username', 'no-op'));
92 | $this->assertEquals('userName()', $this->typeGuesser->guess('user_name', 'no-op'));
93 | $this->assertEquals('userName()', $this->typeGuesser->guess('login', 'no-op'));
94 | }
95 |
96 | /** @test */
97 | public function it_can_guess_email_values()
98 | {
99 | $this->assertEquals('email()', $this->typeGuesser->guess('email', 'no-op'));
100 | $this->assertEquals('email()', $this->typeGuesser->guess('emailaddress', 'no-op'));
101 | $this->assertEquals('email()', $this->typeGuesser->guess('email_address', 'no-op'));
102 | }
103 |
104 | /** @test */
105 | public function it_can_guess_phone_number_values()
106 | {
107 | $this->assertEquals('phoneNumber()', $this->typeGuesser->guess('phonenumber', 'no-op'));
108 | $this->assertEquals('phoneNumber()', $this->typeGuesser->guess('phone_number', 'no-op'));
109 | $this->assertEquals('phoneNumber()', $this->typeGuesser->guess('phone', 'no-op'));
110 | $this->assertEquals('phoneNumber()', $this->typeGuesser->guess('telephone', 'no-op'));
111 | $this->assertEquals('phoneNumber()', $this->typeGuesser->guess('telnumber', 'no-op'));
112 | }
113 |
114 | /** @test */
115 | public function it_can_guess_address_values()
116 | {
117 | $this->assertEquals('address()', $this->typeGuesser->guess('address', 'no-op'));
118 | }
119 |
120 | /** @test */
121 | public function it_can_guess_city_values()
122 | {
123 | $this->assertEquals('city()', $this->typeGuesser->guess('city', 'no-op'));
124 | $this->assertEquals('city()', $this->typeGuesser->guess('town', 'no-op'));
125 | }
126 |
127 | /** @test */
128 | public function it_can_guess_street_address_values()
129 | {
130 | $this->assertEquals('streetAddress()', $this->typeGuesser->guess('street_address', 'no-op'));
131 | $this->assertEquals('streetAddress()', $this->typeGuesser->guess('streetAddress', 'no-op'));
132 | }
133 |
134 | /** @test */
135 | public function it_can_guess_postcode_values()
136 | {
137 | $this->assertEquals('postcode()', $this->typeGuesser->guess('postcode', 'no-op'));
138 | $this->assertEquals('postcode()', $this->typeGuesser->guess('zipcode', 'no-op'));
139 | $this->assertEquals('postcode()', $this->typeGuesser->guess('postalcode', 'no-op'));
140 | $this->assertEquals('postcode()', $this->typeGuesser->guess('postal_code', 'no-op'));
141 | $this->assertEquals('postcode()', $this->typeGuesser->guess('postalCode', 'no-op'));
142 | }
143 |
144 | /** @test */
145 | public function it_can_guess_state_values()
146 | {
147 | $this->assertEquals('state()', $this->typeGuesser->guess('state', 'no-op'));
148 | $this->assertEquals('state()', $this->typeGuesser->guess('province', 'no-op'));
149 | $this->assertEquals('state()', $this->typeGuesser->guess('county', 'no-op'));
150 | }
151 |
152 | /** @test */
153 | public function it_can_guess_country_values()
154 | {
155 | $this->assertEquals('countryCode()', $this->typeGuesser->guess('country', 'no-op', 2));
156 | $this->assertEquals('countryISOAlpha3()', $this->typeGuesser->guess('country', 'no-op', 3));
157 | $this->assertEquals('country()', $this->typeGuesser->guess('country', 'no-op'));
158 | }
159 |
160 | /** @test */
161 | public function it_can_guess_locale_values()
162 | {
163 | $this->assertEquals('locale()', $this->typeGuesser->guess('country', 'no-op', 5));
164 | $this->assertEquals('locale()', $this->typeGuesser->guess('country', 'no-op', 6));
165 | $this->assertEquals('locale()', $this->typeGuesser->guess('locale', 'no-op'));
166 | }
167 |
168 | /** @test */
169 | public function it_can_guess_currency_code_values()
170 | {
171 | $this->assertEquals('currencyCode()', $this->typeGuesser->guess('currency', 'no-op'));
172 | $this->assertEquals('currencyCode()', $this->typeGuesser->guess('currencycode', 'no-op'));
173 | $this->assertEquals('currencyCode()', $this->typeGuesser->guess('currency_code', 'no-op'));
174 | }
175 |
176 | /** @test */
177 | public function it_can_guess_url_values()
178 | {
179 | $this->assertEquals('url()', $this->typeGuesser->guess('website', 'no-op'));
180 | $this->assertEquals('url()', $this->typeGuesser->guess('url', 'no-op'));
181 | $this->assertEquals('url()', $this->typeGuesser->guess('twitter_url', 'no-op'));
182 | $this->assertEquals('url()', $this->typeGuesser->guess('endpoint_url', 'no-op'));
183 | }
184 |
185 | /** @test */
186 | public function it_can_guess_image_url_values()
187 | {
188 | $this->assertEquals('imageUrl()', $this->typeGuesser->guess('image_url', 'no-op'));
189 | }
190 |
191 | /** @test */
192 | public function it_can_guess_company_values()
193 | {
194 | $this->assertEquals('company()', $this->typeGuesser->guess('company', 'no-op'));
195 | $this->assertEquals('company()', $this->typeGuesser->guess('companyname', 'no-op'));
196 | $this->assertEquals('company()', $this->typeGuesser->guess('company_name', 'no-op'));
197 | $this->assertEquals('company()', $this->typeGuesser->guess('employer', 'no-op'));
198 | }
199 |
200 | /** @test */
201 | public function it_can_guess_title_values()
202 | {
203 | $this->assertEquals('title()', $this->typeGuesser->guess('title', 'no-op', 10));
204 | $this->assertEquals('title()', $this->typeGuesser->guess('title', 'no-op'));
205 | }
206 |
207 | /** @test */
208 | public function it_can_guess_sentence_values()
209 | {
210 | $this->assertEquals('sentence()', $this->typeGuesser->guess('title', 'no-op', 15));
211 | }
212 |
213 | /** @test */
214 | public function it_can_guess_password_values()
215 | {
216 | $this->assertEquals('password()', $this->typeGuesser->guess('password', 'no-op'));
217 | }
218 |
219 | /** @test */
220 | public function it_can_guess_coordinates_based_on_their_names()
221 | {
222 | $this->assertEquals('latitude()', $this->typeGuesser->guess('latitude', 'no-op'));
223 | $this->assertEquals('longitude()', $this->typeGuesser->guess('longitude', 'no-op'));
224 | }
225 |
226 | /** @test */
227 | public function it_returns_word_as_default_value()
228 | {
229 | $this->assertEquals('word()', $this->typeGuesser->guess('not_guessable', 'no-op'));
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/tests/migrations/0000_00_00_000001_create_users_table.php:
--------------------------------------------------------------------------------
1 | increments('id');
16 | $table->string('name');
17 | $table->string('email')->unique()->nullable();
18 | $table->string('password');
19 |
20 | $table->timestamps();
21 | });
22 | }
23 |
24 | /**
25 | * Reverse the migrations.
26 | */
27 | public function down()
28 | {
29 | Schema::dropIfExists('users');
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tests/migrations/0000_00_00_000002_create_habits_table.php:
--------------------------------------------------------------------------------
1 | increments('id');
16 | $table->integer('user_id')->unsigned();
17 | $table->string('name');
18 | $table->timestamps();
19 |
20 | $table->foreign('user_id')
21 | ->references('id')
22 | ->on('users');
23 | });
24 | }
25 |
26 | /**
27 | * Reverse the migrations.
28 | */
29 | public function down()
30 | {
31 | Schema::dropIfExists('habits');
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/migrations/0000_00_00_000003_create_cars_table.php:
--------------------------------------------------------------------------------
1 | increments('id');
16 | $table->integer('owner_id')->unsigned();
17 | $table->unsignedInteger('previous_owner_id');
18 | $table->string('brand');
19 | $table->integer('factory_year')->nullable();
20 | $table->timestamps();
21 |
22 | $table->foreign('owner_id')
23 | ->references('id')
24 | ->on('users');
25 | });
26 | }
27 |
28 | /**
29 | * Reverse the migrations.
30 | */
31 | public function down()
32 | {
33 | Schema::dropIfExists('cars');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------