Passed
Pull Request — master (#174)
by Kevin
03:03
created

MakeFactory::defaultPropertiesFor()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
eloc 13
c 0
b 0
f 0
dl 0
loc 25
ccs 0
cts 0
cp 0
rs 9.2222
cc 6
nc 5
nop 1
crap 42
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 Zenstruck\Foundry\ModelFactory;
18
19
/**
20
 * @author Kevin Bond <[email protected]>
21
 */
22
final class MakeFactory extends AbstractMaker
23
{
24
    private const ORM_DEFAULTS = [
25 20
        'ARRAY' => '[],',
26
        'ASCII_STRING' => 'self::faker()->text(),',
27 20
        'BIGINT' => 'self::faker()->randomNumber(),',
28 20
        'BLOB' => 'self::faker()->text(),',
29
        'BOOLEAN' => 'self::faker()->boolean(),',
30 4
        'DATE' => 'self::faker()->datetime(),',
31
        'DATE_MUTABLE' => 'self::faker()->datetime(),',
32 4
        'DATE_IMMUTABLE' => 'self::faker()->datetime(),',
33
        'DATETIME_MUTABLE' => 'self::faker()->datetime(),',
34
        'DATETIME_IMMUTABLE' => 'self::faker()->datetime(),',
35 20
        'DATETIMETZ_MUTABLE' => 'self::faker()->datetime(),',
36
        'DATETIMETZ_IMMUTABLE' => 'self::faker()->datetime(),',
37
        'DECIMAL' => 'self::faker()->randomFloat(),',
38 20
        'FLOAT' => 'self::faker()->randomFloat(),',
39 20
        'INTEGER' => 'self::faker()->randomNumber(),',
40 20
        'JSON' => '[],',
41
        'JSON_ARRAY' => '[],',
42
        'SIMPLE_ARRAY' => '[],',
43 20
        'SMALLINT' => 'self::faker()->randomNumber(1, 32767),',
44 20
        'STRING' => 'self::faker()->text(),',
45
        'TEXT' => 'self::faker()->text(),',
46 20
        'TIME_MUTABLE' => 'self::faker()->datetime(),',
47
        'TIME_IMMUTABLE' => 'self::faker()->datetime(),',
48 20
    ];
49 12
50
    /** @var ManagerRegistry */
51
    private $managerRegistry;
52 8
53 4
    /** @var string[] */
54 4
    private $entitiesWithFactories;
55
56
    public function __construct(ManagerRegistry $managerRegistry, \Traversable $factories)
57 8
    {
58 8
        $this->managerRegistry = $managerRegistry;
59
        $this->entitiesWithFactories = \array_map(
60 8
            static function(ModelFactory $factory) {
61 8
                return $factory::getEntityClass();
62
            },
63 20
            \iterator_to_array($factories)
64
        );
65 20
    }
66
67 20
    public static function getCommandName(): string
68 4
    {
69
        return 'make:factory';
70
    }
71 20
72 4
    public static function getCommandDescription(): string
73
    {
74
        return 'Creates a Foundry model factory for a Doctrine entity class';
75 16
    }
76 16
77 16
    public function configureCommand(Command $command, InputConfiguration $inputConfig): void
78 16
    {
79 16
        $command
80
            ->setDescription(self::getCommandDescription())
81
            ->addArgument('entity', InputArgument::OPTIONAL, 'Entity class to create a factory for')
82 16
            ->addOption('namespace', null, InputOption::VALUE_REQUIRED, 'Customize the namespace for generated factories', 'Factory')
83
            ->addOption('test', null, InputOption::VALUE_NONE, 'Create in <fg=yellow>tests/</> instead of <fg=yellow>src/</>')
84 16
        ;
85
86 16
        $inputConfig->setArgumentAsNonInteractive('entity');
87
    }
88
89 16
    public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
90 16
    {
91 16
        if ($input->getArgument('entity')) {
92
            return;
93 16
        }
94 16
95
        if (!$input->getOption('test')) {
96
            $io->text('// Note: pass <fg=yellow>--test</> if you want to generate factories in your <fg=yellow>tests/</> directory');
97
            $io->newLine();
98 16
        }
99
100 16
        $argument = $command->getDefinition()->getArgument('entity');
101
        $entity = $io->choice($argument->getDescription(), $this->entityChoices());
102 16
103 16
        $input->setArgument('entity', $entity);
104
    }
105
106 16
    public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
107
    {
108 20
        $class = $input->getArgument('entity');
109
110
        if (!\class_exists($class)) {
0 ignored issues
show
Bug introduced by
It seems like $class can also be of type null and string[]; however, parameter $class of class_exists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

110
        if (!\class_exists(/** @scrutinizer ignore-type */ $class)) {
Loading history...
111 20
            $class = $generator->createClassNameDetails($class, 'Entity\\')->getFullName();
0 ignored issues
show
Bug introduced by
It seems like $class can also be of type null and string[]; however, parameter $name of Symfony\Bundle\MakerBund...reateClassNameDetails() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

111
            $class = $generator->createClassNameDetails(/** @scrutinizer ignore-type */ $class, 'Entity\\')->getFullName();
Loading history...
112
        }
113 8
114
        if (!\class_exists($class)) {
115 8
            throw new RuntimeCommandException(\sprintf('Entity "%s" not found.', $input->getArgument('entity')));
0 ignored issues
show
Bug introduced by
It seems like $input->getArgument('entity') can also be of type string[]; however, parameter $values of sprintf() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

115
            throw new RuntimeCommandException(\sprintf('Entity "%s" not found.', /** @scrutinizer ignore-type */ $input->getArgument('entity')));
Loading history...
116
        }
117 8
118 8
        $namespace = $input->getOption('namespace');
119 8
120
        // strip maker's root namespace if set
121
        if (0 === \mb_strpos($namespace, $generator->getRootNamespace())) {
122
            $namespace = \mb_substr($namespace, \mb_strlen($generator->getRootNamespace()));
123 8
        }
124
125 8
        $namespace = \trim($namespace, '\\');
126
127
        // if creating in tests dir, ensure namespace prefixed with Tests\
128
        if ($input->getOption('test') && 0 !== \mb_strpos($namespace, 'Tests\\')) {
129
            $namespace = 'Tests\\'.$namespace;
130
        }
131
132
        $entity = new \ReflectionClass($class);
0 ignored issues
show
Bug introduced by
It seems like $class can also be of type string[]; however, parameter $objectOrClass of ReflectionClass::__construct() does only seem to accept object|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

132
        $entity = new \ReflectionClass(/** @scrutinizer ignore-type */ $class);
Loading history...
133
        $factory = $generator->createClassNameDetails($entity->getShortName(), $namespace, 'Factory');
134
135
        $repository = new \ReflectionClass($this->managerRegistry->getRepository($entity->getName()));
136
137
        if (0 !== \mb_strpos($repository->getName(), $generator->getRootNamespace())) {
138
            // not using a custom repository
139
            $repository = null;
140
        }
141
142
        $generator->generateClass(
143
            $factory->getFullName(),
144
            __DIR__.'/../Resources/skeleton/Factory.tpl.php',
145
            [
146
                'entity' => $entity,
147
                'defaultProperties' => $this->defaultPropertiesFor($entity->getName()),
148
                'repository' => $repository,
149
            ]
150
        );
151
152
        $generator->writeChanges();
153
154
        $this->writeSuccessMessage($io);
155
156
        $io->text([
157
            'Next: Open your new factory and set default values/states.',
158
            'Find the documentation at https://github.com/zenstruck/foundry#model-factories',
159
        ]);
160
    }
161
162
    public function configureDependencies(DependencyBuilder $dependencies): void
163
    {
164
        // noop
165
    }
166
167
    private function entityChoices(): array
168
    {
169
        $choices = [];
170
171
        foreach ($this->managerRegistry->getManagers() as $manager) {
172
            foreach ($manager->getMetadataFactory()->getAllMetadata() as $metadata) {
173
                if (!\in_array($metadata->getName(), $this->entitiesWithFactories, true)) {
174
                    $choices[] = $metadata->getName();
175
                }
176
            }
177
        }
178
179
        \sort($choices);
180
181
        if (empty($choices)) {
182
            throw new RuntimeCommandException('No entities found.');
183
        }
184
185
        return $choices;
186
    }
187
188
    private function defaultPropertiesFor(string $class): iterable
189
    {
190
        $em = $this->managerRegistry->getManagerForClass($class);
191
192
        if (!$em instanceof EntityManagerInterface) {
193
            return [];
194
        }
195
196
        $metadata = $em->getClassMetadata($class);
197
        $ids = $metadata->getIdentifierFieldNames();
198
199
        foreach ($metadata->fieldMappings as $property) {
200
            // ignore identifiers and nullable fields
201
            if ($property['nullable'] || \in_array($property['fieldName'], $ids, true)) {
202
                continue;
203
            }
204
205
            $type = \mb_strtoupper($property['type']);
206
            $value = "null, // TODO add {$type} ORM type manually";
207
208
            if (\array_key_exists($type, self::ORM_DEFAULTS)) {
209
                $value = self::ORM_DEFAULTS[$type];
210
            }
211
212
            yield $property['fieldName'] => $value;
213
        }
214
    }
215
}
216