Issues (590)

src/Console/HydratorGenerationCommand.php (1 issue)

Severity
1
<?php
2
3
namespace Bdf\Prime\Console;
4
5
use Bdf\Prime\Entity\Hydrator\Exception\HydratorGenerationException;
6
use Bdf\Prime\Entity\Hydrator\HydratorGeneratedInterface;
7
use Bdf\Prime\Entity\Hydrator\HydratorGenerator;
8
use Bdf\Prime\Mapper\Mapper;
9
use Bdf\Prime\ServiceLocator;
10
use Bdf\Util\Console\BdfStyle;
11
use Bdf\Util\File\ClassFileLocator;
12
use Bdf\Util\File\PhpClassFile;
13
use Symfony\Component\Console\Attribute\AsCommand;
14
use Symfony\Component\Console\Command\Command;
15
use Symfony\Component\Console\Input\InputArgument;
16
use Symfony\Component\Console\Input\InputInterface;
17
use Symfony\Component\Console\Input\InputOption;
18
use Symfony\Component\Console\Output\OutputInterface;
19
use Symfony\Component\Filesystem\Filesystem;
20
21
/**
22
 * HydratorGenerationCommand
23
 */
24
#[AsCommand('prime:hydrator', 'Generate optimized hydrator classes for entities')]
25
class HydratorGenerationCommand extends Command
26
{
27
    protected static $defaultName = 'prime:hydrator';
28
29
    /**
30
     * @var ServiceLocator
31
     */
32
    private $locator;
33
34
    /**
35
     * @var string
36
     */
37
    private $outputDir;
38
39
    /**
40
     * @var string
41
     */
42
    private $loaderFile;
43
44
    /**
45
     * HydratorGenerationCommand constructor.
46
     *
47
     * @param ServiceLocator $locator
48
     * @param string $loaderFile
49
     */
50
    public function __construct(ServiceLocator $locator, string $loaderFile = null)
51
    {
52
        $this->locator = $locator;
53
        $this->loaderFile = $loaderFile;
54
55
        parent::__construct(static::$defaultName);
56
    }
57
58
    /**
59
     * {@inheritdoc}
60
     */
61
    protected function configure()
62
    {
63
        $this
64
            ->setDescription('Generate optimized hydrator classes for entities')
65
            ->addArgument('path', InputArgument::OPTIONAL, 'The entities path. If omitted, regenerate the loader file')
66
            ->addOption('loader', 'l', InputOption::VALUE_REQUIRED, 'The hydrator loader file')
67
            ->addOption('include', 'i', InputOption::VALUE_NONE, 'Add includes into loader file ?')
68
        ;
69
    }
70
71
    /**
72
     * {@inheritdoc}
73
     */
74
    protected function execute(InputInterface $input, OutputInterface $output): int
75
    {
76
        $io = new BdfStyle($input, $output);
77
78
        $this->configureOutputs($io);
79
        $path = realpath($io->argument('path'));
80
81
        $cache = $this->locator->mappers()->getMetadataCache();
82
        $this->locator->mappers()->setMetadataCache(null);
83
84
        if ($io->argument('path')) {
85
            $io->info('Generating hydrators...');
86
87
            foreach ($this->getClassIterator($path) as $classInfo) {
88
                $className = $classInfo->getClass();
89
90
                if (!$this->locator->mappers()->isEntity($className)) {
91
                    $io->debug("'{$className}' is not an entity class");
92
                    continue;
93
                }
94
95
                $mapper = $this->locator->mappers()->build($this->locator, $className);
96
97
                $this->generateHydrator($io, $className, $mapper);
98
            }
99
        }
100
101
        $this->generateLoader($io);
102
103
        $this->locator->mappers()->setMetadataCache($cache);
104
        return 0;
105
    }
106
107
    /**
108
     * Configure output file and directory from options
109
     *
110
     * @param BdfStyle $io
111
     *
112
     * @return void
113
     */
114
    private function configureOutputs($io): void
115
    {
116
        if ($io->option('loader') !== null) {
117
            $this->loaderFile = $io->option('loader');
118
        }
119
120
        if (empty($this->loaderFile)) {
121
            $io->block('You should configure "prime.hydrators.loader" or set the loader file with option -l');
122
            exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
123
        }
124
125
        $this->outputDir = dirname($this->loaderFile).DIRECTORY_SEPARATOR;
126
        (new Filesystem())->mkdir($this->outputDir);
127
    }
128
129
    /**
130
     * Generate the entity hydrator file
131
     *
132
     * @param BdfStyle $io
133
     * @param string $className
134
     * @param Mapper $mapper
135
     *
136
     * @return void
137
     */
138
    private function generateHydrator($io, $className, Mapper $mapper): void
139
    {
140
        $io->inline("Generate hydrator for {$className} ");
141
142
        try {
143
            $generator = new HydratorGenerator($this->locator, $mapper, $className);
144
145
            file_put_contents(
146
                $this->getHydratorFilename($generator->hydratorClassName()),
147
                $generator->generate()
148
            );
149
150
            $io->info('[OK]');
151
        } catch (HydratorGenerationException $exception) {
152
            $io->error($exception->getMessage());
153
        }
154
    }
155
156
    /**
157
     * Generate the hydrators loader
158
     *
159
     * @param BdfStyle $io
160
     *
161
     * @return void
162
     */
163
    private function generateLoader($io): void
164
    {
165
        $io->info('Generating loader file...');
166
167
        $includes = [];
168
        $hydrators = [];
169
        $factories = [];
170
171
        foreach ($this->getClassIterator($this->outputDir) as $classInfo) {
172
            $className = $classInfo->getClass();
173
174
            if (!class_exists($className)) {
175
                require_once $classInfo->getPathname();
176
            }
177
178
            if (!is_subclass_of($className, HydratorGeneratedInterface::class)) {
179
                $io->debug("$className is not an hydrator");
180
                continue;
181
            }
182
183
            $includes[] = $classInfo->getFilename();
184
185
            $embeddeds = $className::embeddedPrimeClasses();
186
187
            if (empty($embeddeds)) {
188
                $hydrators[] = "'{$className::supportedPrimeClassName()}' => new {$className}(),";
189
            } else {
190
                $arguments = [];
191
192
                foreach ($embeddeds as $entity) {
193
                    $arguments[] = "\$registry->get('{$entity}')";
194
                }
195
196
                $arguments = implode(', ', $arguments);
197
                $closure = "function(\$registry) {return new {$className}({$arguments});}";
198
199
                $factories[] = "'{$className::supportedPrimeClassName()}' => {$closure},";
200
            }
201
        }
202
203
        $content = "<?php\n";
204
205
        if ($io->option('include')) {
206
            foreach ($includes as $file) {
207
                $content .= "require_once __DIR__.DIRECTORY_SEPARATOR.'{$file}';\n";
208
            }
209
        } else {
210
            $io->block([
211
                'Loader file do not contains includes. Don\'t forget to add the hydrators directory to composer, or specify "-i" option to the command :',
212
                <<<JSON
213
"autoload": {
214
    /* ... */
215
    "classmap": [
216
        /* ... */
217
        "{$this->outputDir}"
218
    ]
219
}
220
JSON
221
            ], 'comment');
222
        }
223
224
        $content .= "\$registry->setHydrators([\n".implode("\n", $hydrators)."\n]);\n";
225
        $content .= "\$registry->setFactories([\n".implode("\n", $factories)."\n]);\n";
226
227
        file_put_contents($this->loaderFile, $content);
228
    }
229
230
    /**
231
     * @param string $hydratorClassname
232
     *
233
     * @return string
234
     */
235
    private function getHydratorFilename($hydratorClassname)
236
    {
237
        return $this->outputDir.$hydratorClassname.'.php';
238
    }
239
240
    /**
241
     * @param string $path
242
     *
243
     * @return ClassFileLocator|array
244
     */
245
    private function getClassIterator($path)
246
    {
247
        if (is_dir($path)) {
248
            return new ClassFileLocator($path);
249
        } else {
250
            $classFile = new PhpClassFile($path);
251
            $classFile->extractClassInfo();
252
253
            return [$classFile];
254
        }
255
    }
256
}
257