Passed
Pull Request — master (#221)
by
unknown
02:48
created

MakeFactory::defaultPropertiesFor()   C

Complexity

Conditions 13
Paths 29

Size

Total Lines 64
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 182

Importance

Changes 17
Bugs 3 Features 1
Metric Value
eloc 34
c 17
b 3
f 1
dl 0
loc 64
ccs 0
cts 0
cp 0
rs 6.6166
cc 13
nc 29
nop 3
crap 182

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Zenstruck\Foundry\Bundle\Maker;
4
5
use Doctrine\ORM\EntityManagerInterface;
6
use Doctrine\Persistence\ManagerRegistry;
7
use Symfony\Bundle\MakerBundle\ConsoleStyle;
8
use Symfony\Bundle\MakerBundle\DependencyBuilder;
9
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
10
use Symfony\Bundle\MakerBundle\Generator;
11
use Symfony\Bundle\MakerBundle\InputConfiguration;
12
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
13
use Symfony\Component\Console\Command\Command;
14
use Symfony\Component\Console\Input\InputArgument;
15
use Symfony\Component\Console\Input\InputInterface;
16
use Symfony\Component\Console\Input\InputOption;
17
use Symfony\Component\Finder\Finder;
18
use Zenstruck\Foundry\ModelFactory;
19
20
/**
21
 * @author Kevin Bond <[email protected]>
22
 */
23
final class MakeFactory extends AbstractMaker
24
{
25 20
    private const ORM_DEFAULTS = [
26
        'ARRAY' => '[],',
27 20
        'ASCII_STRING' => 'self::faker()->text(),',
28 20
        'BIGINT' => 'self::faker()->randomNumber(),',
29
        'BLOB' => 'self::faker()->text(),',
30 4
        'BOOLEAN' => 'self::faker()->boolean(),',
31
        'DATE' => 'self::faker()->datetime(),',
32 4
        'DATE_MUTABLE' => 'self::faker()->datetime(),',
33
        'DATE_IMMUTABLE' => 'self::faker()->datetime(),',
34
        'DATETIME_MUTABLE' => 'self::faker()->datetime(),',
35 20
        'DATETIME_IMMUTABLE' => 'self::faker()->datetime(),',
36
        'DATETIMETZ_MUTABLE' => 'self::faker()->datetime(),',
37
        'DATETIMETZ_IMMUTABLE' => 'self::faker()->datetime(),',
38 20
        'DECIMAL' => 'self::faker()->randomFloat(),',
39 20
        'FLOAT' => 'self::faker()->randomFloat(),',
40 20
        'INTEGER' => 'self::faker()->randomNumber(),',
41
        'JSON' => '[],',
42
        'JSON_ARRAY' => '[],',
43 20
        'SIMPLE_ARRAY' => '[],',
44 20
        'SMALLINT' => 'self::faker()->randomNumber(1, 32767),',
45
        'STRING' => 'self::faker()->text(),',
46 20
        'TEXT' => 'self::faker()->text(),',
47
        'TIME_MUTABLE' => 'self::faker()->datetime(),',
48 20
        'TIME_IMMUTABLE' => 'self::faker()->datetime(),',
49 12
    ];
50
51
    /** @var ManagerRegistry */
52 8
    private $managerRegistry;
53 4
54 4
    /** @var string[] */
55
    private $entitiesWithFactories;
56
57 8
    public function __construct(ManagerRegistry $managerRegistry, \Traversable $factories)
58 8
    {
59
        $this->managerRegistry = $managerRegistry;
60 8
        $this->entitiesWithFactories = \array_map(
61 8
            static function(ModelFactory $factory) {
62
                return $factory::getEntityClass();
63 20
            },
64
            \iterator_to_array($factories)
65 20
        );
66
    }
67 20
68 4
    public static function getCommandName(): string
69
    {
70
        return 'make:factory';
71 20
    }
72 4
73
    public static function getCommandDescription(): string
74
    {
75 16
        return 'Creates a Foundry model factory for a Doctrine entity class';
76 16
    }
77 16
78 16
    public function configureCommand(Command $command, InputConfiguration $inputConfig): void
79 16
    {
80
        $command
81
            ->setDescription(self::getCommandDescription())
82 16
            ->addArgument('entity', InputArgument::OPTIONAL, 'Entity class to create a factory for')
83
            ->addOption('namespace', null, InputOption::VALUE_REQUIRED, 'Customize the namespace for generated factories', 'Factory')
84 16
            ->addOption('test', null, InputOption::VALUE_NONE, 'Create in <fg=yellow>tests/</> instead of <fg=yellow>src/</>')
85
            ->addOption('all-fields', null, InputOption::VALUE_NONE, 'Create defaults for all entity fields, not only required fields')
86 16
        ;
87
88
        $inputConfig->setArgumentAsNonInteractive('entity');
89 16
    }
90 16
91 16
    public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
92
    {
93 16
        if ($input->getArgument('entity')) {
94 16
            return;
95
        }
96
97
        if (!$input->getOption('test')) {
98 16
            $io->text('// Note: pass <fg=yellow>--test</> if you want to generate factories in your <fg=yellow>tests/</> directory');
99
            $io->newLine();
100 16
        }
101
102 16
        if (!$input->getOption('all-fields')) {
103 16
            $io->text('// Note: pass <fg=yellow>--all-fields</> if you want to generate default values for all fields, not only required fields');
104
            $io->newLine();
105
        }
106 16
107
        $argument = $command->getDefinition()->getArgument('entity');
108 20
        $entity = $io->choice($argument->getDescription(), $this->entityChoices());
109
110
        $input->setArgument('entity', $entity);
111 20
    }
112
113 8
    public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
114
    {
115 8
        $class = $input->getArgument('entity');
116
117 8
        if (!\class_exists($class)) {
118 8
            $class = $generator->createClassNameDetails($class, 'Entity\\')->getFullName();
119 8
        }
120
121
        if (!\class_exists($class)) {
122
            throw new RuntimeCommandException(\sprintf('Entity "%s" not found.', $input->getArgument('entity')));
123 8
        }
124
125 8
        $namespace = $input->getOption('namespace');
126
127
        // strip maker's root namespace if set
128
        if (0 === \mb_strpos($namespace, $generator->getRootNamespace())) {
129
            $namespace = \mb_substr($namespace, \mb_strlen($generator->getRootNamespace()));
130
        }
131
132
        $namespace = \trim($namespace, '\\');
133
134
        // if creating in tests dir, ensure namespace prefixed with Tests\
135
        if ($input->getOption('test') && 0 !== \mb_strpos($namespace, 'Tests\\')) {
136
            $namespace = 'Tests\\'.$namespace;
137
        }
138
139
        $entity = new \ReflectionClass($class);
140
        $factory = $generator->createClassNameDetails($entity->getShortName(), $namespace, 'Factory');
141
142
        $repository = new \ReflectionClass($this->managerRegistry->getRepository($entity->getName()));
143
144
        if (0 !== \mb_strpos($repository->getName(), $generator->getRootNamespace())) {
145
            // not using a custom repository
146
            $repository = null;
147
        }
148
149
        $generator->generateClass(
150
            $factory->getFullName(),
151
            __DIR__.'/../Resources/skeleton/Factory.tpl.php',
152
            [
153
                'entity' => $entity,
154
                'defaultProperties' => $this->defaultPropertiesFor($entity->getName(), $input->getOption('all-fields'), $io),
155
                'repository' => $repository,
156
            ]
157
        );
158
159
        $generator->writeChanges();
160
161
        $this->writeSuccessMessage($io);
162
163
        $io->text([
164
            'Next: Open your new factory and set default values/states.',
165
            'Find the documentation at https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories',
166
        ]);
167
    }
168
169
    public function configureDependencies(DependencyBuilder $dependencies): void
170
    {
171
        // noop
172
    }
173
174
    private function entityChoices(): array
175
    {
176
        $choices = [];
177
178
        foreach ($this->managerRegistry->getManagers() as $manager) {
179
            foreach ($manager->getMetadataFactory()->getAllMetadata() as $metadata) {
180
                if (!\in_array($metadata->getName(), $this->entitiesWithFactories, true)) {
181
                    $choices[] = $metadata->getName();
182
                }
183
            }
184
        }
185
186
        \sort($choices);
187
188
        if (empty($choices)) {
189
            throw new RuntimeCommandException('No entities found.');
190
        }
191
192
        return $choices;
193
    }
194
195
    private function defaultPropertiesFor(string $class, bool $allFields, ConsoleStyle $io): iterable
196
    {
197
        $em = $this->managerRegistry->getManagerForClass($class);
198
199
        if (!$em instanceof EntityManagerInterface) {
200
            return [];
201
        }
202
203
        $metadata = $em->getClassMetadata($class);
204
        $ids = $metadata->getIdentifierFieldNames();
205
206
        // If Factory exist for related entities populate too with auto defaults
207
        $relatedEntities = $metadata->associationMappings;
208
        foreach ($relatedEntities as $item) {
209
            // if joinColumns is not written entity is default nullable ($nullable = true;)
210
            // @see vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/JoinColumn.php LINE 28
211
            if (!\array_key_exists('joinColumns', $item)) {
212
                continue;
213
            }
214
215
            if (!\array_key_exists('nullable', $item['joinColumns'][0])) {
216
                continue;
217
            }
218
219
            if (true === $item['joinColumns'][0]['nullable']) {
220
                continue;
221
            }
222
223
            $fieldName = $item['fieldName'];
224
225
            $targetEntity = \explode('\\', $item['targetEntity']);
226
            $targetEntity = \end($targetEntity);
227
            $factory = \ucfirst($targetEntity).'Factory';
228
229
            if ($this->hasFactory($factory)) {
230
                yield \lcfirst($fieldName) => \ucfirst($factory).'::new(),';
231
            } else {
232
                // TODO ELSE: ask user to create missing factory?
233
                $io->text('// Note: <fg=yellow>'.$factory.'</> is missing.');
234
                $io->newLine();
235
                $question = $io->ask('Do you want create this Factory too?', 'yes|no');
236
                if ('yes' === $question) {
237
                    $io->text('// Note: <fg=yellow>'.$factory.'</> will be created');
238
                    $io->newLine();
239
240
                    // TODO create ...
241
                }
242
            }
243
        }
244
245
        foreach ($metadata->fieldMappings as $property) {
246
            // ignore identifiers and nullable fields
247
            if ((!$allFields && ($property['nullable'] ?? false)) || \in_array($property['fieldName'], $ids, true)) {
248
                continue;
249
            }
250
251
            $type = \mb_strtoupper($property['type']);
252
            $value = "null, // TODO add {$type} ORM type manually";
253
254
            if (\array_key_exists($type, self::ORM_DEFAULTS)) {
255
                $value = self::ORM_DEFAULTS[$type];
256
            }
257
258
            yield $property['fieldName'] => $value;
259
        }
260
    }
261
262
    private function hasFactory($factory)
263
    {
264
        // Github CI
265
        if (\class_exists('Zenstruck\Foundry\Tests\Fixtures\Factories\\'.$factory)) {
266
            return true;
267
        }
268
269
        $dirs = [];
270
271
        if (\is_dir('src')) {
272
            $dirs[] = 'src/';
273
        }
274
275
        if (\is_dir('tests')) {
276
            $dirs[] = 'tests/';
277
        }
278
279
        $finder = new Finder();
280
        $finder->in($dirs)->files()->name($factory.'.php');
281
282
        if (\count(\iterator_to_array($finder)) > 0) {
283
            return true;
284
        }
285
286
        return false;
287
    }
288
}
289