Completed
Push — feature/serializer-groups ( 7eaea2 )
by Narcotic
63:05
created

GenerateDynamicBundleCommand::getBundleFinder()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 1
1
<?php
2
/**
3
 * generate dynamic bundles
4
 */
5
6
namespace Graviton\GeneratorBundle\Command;
7
8
use Graviton\GeneratorBundle\CommandRunner;
9
use Graviton\GeneratorBundle\Definition\JsonDefinition;
10
use Graviton\GeneratorBundle\Definition\JsonDefinitionArray;
11
use Graviton\GeneratorBundle\Definition\JsonDefinitionHash;
12
use Graviton\GeneratorBundle\Generator\DynamicBundleBundleGenerator;
13
use Graviton\GeneratorBundle\Definition\Loader\LoaderInterface;
14
use JMS\Serializer\SerializerInterface;
15
use Symfony\Component\Console\Command\Command;
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
use Symfony\Component\Finder\Finder;
21
use Symfony\Component\Finder\SplFileInfo;
22
23
/**
24
 * Here, we generate all "dynamic" Graviton bundles..
25
 *
26
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
27
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
28
 * @link     http://swisscom.ch
29
 */
30
class GenerateDynamicBundleCommand extends Command
31
{
32
33
    /** @var  string */
34
    const BUNDLE_NAMESPACE = 'GravitonDyn';
35
36
    /** @var  string */
37
    const BUNDLE_NAME_MASK = self::BUNDLE_NAMESPACE.'/%sBundle';
38
39
    /** @var  string */
40
    const GENERATION_HASHFILE_FILENAME = 'genhash';
41
42
    /** @var  string */
43
    private $bundleBundleNamespace;
44
45
    /** @var  string */
46
    private $bundleBundleDir;
47
48
    /** @var  string */
49
    private $bundleBundleClassname;
50
51
    /** @var  string */
52
    private $bundleBundleClassfile;
53
54
    /** @var  array */
55
    private $bundleBundleList = [];
56
57
    /** @var array|null */
58
    private $bundleAdditions = null;
59
60
    /** @var array|null */
61
    private $serviceWhitelist = null;
62
63
    /**
64
     * @var CommandRunner
65
     */
66
    private $runner;
67
    /**
68
     * @var LoaderInterface
69
     */
70
    private $definitionLoader;
71
    /**
72
     * @var SerializerInterface
73
     */
74
    private $serializer;
75
76
77
    /**
78
     * @param CommandRunner       $runner           Runs a console command.
79
     * @param LoaderInterface     $definitionLoader JSON definition loader
80
     * @param SerializerInterface $serializer       Serializer
81
     * @param string|null         $bundleAdditions  Additional bundles list in JSON format
82
     * @param string|null         $serviceWhitelist Service whitelist in JSON format
83
     * @param string|null         $name             The name of the command; passing null means it must be set in
84
     *                                              configure()
85
     */
86
    public function __construct(
87
        CommandRunner       $runner,
88
        LoaderInterface     $definitionLoader,
89
        SerializerInterface $serializer,
90
        $bundleAdditions = null,
91
        $serviceWhitelist = null,
92
        $name = null
93
    ) {
94
        parent::__construct($name);
95
96
        $this->runner = $runner;
97
        $this->definitionLoader = $definitionLoader;
98
        $this->serializer = $serializer;
99
100
        if ($bundleAdditions !== null && $bundleAdditions !== '') {
101
            $this->bundleAdditions = $bundleAdditions;
102
        }
103
        if ($serviceWhitelist !== null && $serviceWhitelist !== '') {
104
            $this->serviceWhitelist = $serviceWhitelist;
105
        }
106
    }
107
108
    /**
109
     * {@inheritDoc}
110
     *
111
     * @return void
112
     */
113
    protected function configure()
114
    {
115
        parent::configure();
116
117
        $this->addOption(
118
            'json',
119
            '',
120
            InputOption::VALUE_OPTIONAL,
121
            'Path to the json definition.'
122
        )
123
            ->addOption(
124
                'srcDir',
125
                '',
126
                InputOption::VALUE_OPTIONAL,
127
                'Src Dir',
128
                dirname(__FILE__) . '/../../../'
129
            )
130
            ->addOption(
131
                'bundleBundleName',
132
                '',
133
                InputOption::VALUE_OPTIONAL,
134
                'Which BundleBundle to manipulate to add our stuff',
135
                'GravitonDynBundleBundle'
136
            )
137
            ->addOption(
138
                'bundleFormat',
139
                '',
140
                InputOption::VALUE_OPTIONAL,
141
                'Which format',
142
                'xml'
143
            )
144
            ->setName('graviton:generate:dynamicbundles')
145
            ->setDescription(
146
                'Generates all dynamic bundles in the GravitonDyn namespace. Either give a path '.
147
                'to a single JSON file or a directory path containing multiple files.'
148
            );
149
    }
150
151
    /**
152
     * {@inheritDoc}
153
     *
154
     * @param InputInterface  $input  input
155
     * @param OutputInterface $output output
156
     *
157
     * @return void
158
     */
159
    protected function execute(InputInterface $input, OutputInterface $output)
160
    {
161
        /**
162
         * GENERATE THE BUNDLEBUNDLE
163
         */
164
        $namespace = sprintf(self::BUNDLE_NAME_MASK, 'Bundle');
165
166
        // GravitonDynBundleBundle
167
        $bundleName = str_replace('/', '', $namespace);
168
169
        // bundlebundle stuff..
170
        $this->bundleBundleNamespace = $namespace;
171
        $this->bundleBundleDir = $input->getOption('srcDir') . $namespace;
172
        $this->bundleBundleClassname = $bundleName;
173
        $this->bundleBundleClassfile = $this->bundleBundleDir . '/' . $this->bundleBundleClassname . '.php';
174
175
        $filesToWorkOn = $this->definitionLoader->load($input->getOption('json'));
176
177
        if (count($filesToWorkOn) < 1) {
178
            throw new \LogicException("Could not find any usable JSON files.");
179
        }
180
181
        $fs = new Filesystem();
182
183
        $this->createInitialBundleBundle($input->getOption('srcDir'));
184
185
        $templateHash = $this->getTemplateHash();
186
        $existingBundles = $this->getExistingBundleHashes($input->getOption('srcDir'));
187
188
        /**
189
         * GENERATE THE BUNDLE(S)
190
         */
191
        foreach ($filesToWorkOn as $jsonDef) {
192
            $thisIdName = $jsonDef->getId();
193
            $namespace = sprintf(self::BUNDLE_NAME_MASK, $thisIdName);
194
195
            $jsonDef->setNamespace($namespace);
196
197
            $bundleName = str_replace('/', '', $namespace);
198
            $this->bundleBundleList[] = $namespace;
199
200
            try {
201
                $bundleDir = $input->getOption('srcDir').$namespace;
202
                $thisHash = sha1($templateHash.PATH_SEPARATOR.serialize($jsonDef));
203
204
                $needsGeneration = true;
205
                if (isset($existingBundles[$bundleDir])) {
206
                    if ($existingBundles[$bundleDir] == $thisHash) {
207
                        $needsGeneration = false;
208
                    }
209
                    unset($existingBundles[$bundleDir]);
210
                }
211
212
                if ($needsGeneration) {
213
                    $this->generateBundle($namespace, $bundleName, $input, $output, $bundleDir);
214
                    $this->generateGenerationHashFile($bundleDir, $thisHash);
215
                }
216
217
                $this->generateBundleBundleClass();
218
219
                if ($needsGeneration) {
220
                    $this->generateSubResources($output, $jsonDef, $bundleName);
221
                    $this->generateMainResource($output, $jsonDef, $bundleName);
222
223
                    $output->write(
224
                        PHP_EOL.
225
                        sprintf('<info>Generated "%s" from definition %s</info>', $bundleName, $jsonDef->getId()).
226
                        PHP_EOL
227
                    );
228
                } else {
229
                    $output->write(
230
                        PHP_EOL.
231
                        sprintf('<info>Using pre-existing "%s"</info>', $bundleName).
232
                        PHP_EOL
233
                    );
234
                }
235
            } catch (\Exception $e) {
236
                $output->writeln(
237
                    sprintf('<error>%s</error>', $e->getMessage())
238
                );
239
240
                // remove failed bundle from list
241
                array_pop($this->bundleBundleList);
242
            }
243
        }
244
245
        // whatever is left in $existingBundles is not defined anymore and needs to be deleted..
246
        foreach ($existingBundles as $dirName => $hash) {
247
            $fileInfo = new \SplFileInfo($dirName);
248
            $bundleClassName = $this->getBundleClassnameFromFolder($fileInfo->getFilename());
249
250
            // remove from bundlebundle list
251
            unset($this->bundleBundleList[array_search($bundleClassName, $this->bundleBundleList)]);
252
253
            $fs->remove($dirName);
254
255
            $output->write(
256
                PHP_EOL.
257
                sprintf('<info>Deleted obsolete bundle "%s"</info>', $dirName).
258
                PHP_EOL
259
            );
260
        }
261
262
        $this->generateBundleBundleClass();
263
    }
264
265
    /**
266
     * scans through all existing dynamic bundles, checks if there is a generation hash and collect that
267
     * all in an array that can be used for fast checking.
268
     *
269
     * @param string $baseDir base directory of dynamic bundles
270
     *
271
     * @return array key is bundlepath, value is the current hash
272
     */
273
    private function getExistingBundleHashes($baseDir)
274
    {
275
        $existingBundles = [];
276
        $fs = new Filesystem();
277
        $bundleBaseDir = $baseDir.self::BUNDLE_NAMESPACE;
278
279
        if (!$fs->exists($bundleBaseDir)) {
280
            return $existingBundles;
281
        }
282
283
        $bundleFinder = $this->getBundleFinder($baseDir);
284
285
        foreach ($bundleFinder as $bundleDir) {
0 ignored issues
show
Bug introduced by
The expression $bundleFinder of type null|object<Symfony\Component\Finder\Finder> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
286
            $genHash = '';
287
            $hashFileFinder = new Finder();
288
            $hashFileIterator = $hashFileFinder
289
                ->files()
290
                ->in($bundleDir->getPathname())
291
                ->name(self::GENERATION_HASHFILE_FILENAME)
292
                ->depth('== 0')
293
                ->getIterator();
294
295
            $hashFileIterator->rewind();
296
297
            $hashFile = $hashFileIterator->current();
298
            if ($hashFile instanceof SplFileInfo) {
299
                $genHash = $hashFile->getContents();
300
            }
301
302
            $existingBundles[$bundleDir->getPathname()] = $genHash;
303
        }
304
305
        return $existingBundles;
306
    }
307
308
    /**
309
     * we cannot just delete the BundleBundle at the beginning, we need to prefill
310
     * it with all existing dynamic bundles..
311
     *
312
     * @param string $baseDir base dir
313
     *
314
     * @return void
315
     */
316
    private function createInitialBundleBundle($baseDir)
317
    {
318
        $bundleFinder = $this->getBundleFinder($baseDir);
319
320
        if (!$bundleFinder) {
321
            return;
322
        }
323
324
        foreach ($bundleFinder as $bundleDir) {
325
            $this->bundleBundleList[] = $this->getBundleClassnameFromFolder($bundleDir->getFilename());
326
        }
327
328
        $this->generateBundleBundleClass();
329
    }
330
331
    /**
332
     * from a name of a folder of a bundle, this function returns the corresponding class name
333
     *
334
     * @param string $folderName folder name
335
     *
336
     * @return string
337
     */
338
    private function getBundleClassnameFromFolder($folderName)
339
    {
340
        if (substr($folderName, -6) == 'Bundle') {
341
            $folderName = substr($folderName, 0, -6);
342
        }
343
344
        return sprintf(self::BUNDLE_NAME_MASK, $folderName);
345
    }
346
347
    /**
348
     * returns a finder that iterates all bundle directories
349
     *
350
     * @param string $baseDir the base dir to search
351
     *
352
     * @return Finder|null finder or null if basedir does not exist
353
     */
354
    private function getBundleFinder($baseDir)
355
    {
356
        $bundleBaseDir = $baseDir.self::BUNDLE_NAMESPACE;
357
358
        if (!(new Filesystem())->exists($bundleBaseDir)) {
359
            return null;
360
        }
361
362
        $bundleFinder = new Finder();
363
        $bundleFinder->directories()->in($bundleBaseDir)->depth('== 0')->notName('BundleBundle');
364
365
        return $bundleFinder;
366
    }
367
368
    /**
369
     * Calculates a hash of all templates that generator uses to output it's file.
370
     * That way a regeneration will be triggered when one of them changes..
371
     *
372
     * @return string hash
373
     */
374
    private function getTemplateHash()
375
    {
376
        $templateDir = __DIR__ . '/../Resources/skeleton';
377
        $resourceFinder = new Finder();
378
        $resourceFinder->in($templateDir)->files()->sortByName();
379
        $templateTimes = '';
380
        foreach ($resourceFinder as $file) {
381
            $templateTimes .= PATH_SEPARATOR . sha1_file($file->getPathname());
382
        }
383
        return sha1($templateTimes);
384
    }
385
386
    /**
387
     * Generate Bundle entities
388
     *
389
     * @param OutputInterface $output     Instance to sent text to be displayed on stout.
390
     * @param JsonDefinition  $jsonDef    Configuration to be generated the entity from.
391
     * @param string          $bundleName Name of the bundle the entity shall be generated for.
392
     *
393
     * @return void
394
     * @throws \Exception
395
     */
396
    protected function generateSubResources(
397
        OutputInterface $output,
398
        JsonDefinition $jsonDef,
399
        $bundleName
400
    ) {
401
        foreach ($this->getSubResources($jsonDef) as $subRecource) {
402
            $arguments = [
403
                'graviton:generate:resource',
404
                '--no-debug' => null,
405
                '--entity' => $bundleName . ':' . $subRecource->getId(),
406
                '--format' => 'xml',
407
                '--json' => $this->serializer->serialize($subRecource->getDef(), 'json'),
408
                '--fields' => $this->getFieldString($subRecource),
409
                '--no-controller' => 'true',
410
            ];
411
            $this->generateResource($arguments, $output, $jsonDef);
412
        }
413
    }
414
415
    /**
416
     * Generate the actual Bundle
417
     *
418
     * @param OutputInterface $output     Instance to sent text to be displayed on stout.
419
     * @param JsonDefinition  $jsonDef    Configuration to be generated the entity from.
420
     * @param string          $bundleName Name of the bundle the entity shall be generated for.
421
     *
422
     * @return void
423
     */
424
    protected function generateMainResource(OutputInterface $output, JsonDefinition $jsonDef, $bundleName)
425
    {
426
        $fields = $jsonDef->getFields();
427
        if (!empty($fields)) {
428
            $arguments = array(
429
                'graviton:generate:resource',
430
                '--no-debug' => null,
431
                '--entity' => $bundleName . ':' . $jsonDef->getId(),
432
                '--json' => $this->serializer->serialize($jsonDef->getDef(), 'json'),
433
                '--format' => 'xml',
434
                '--fields' => $this->getFieldString($jsonDef)
435
            );
436
437
            $this->generateResource($arguments, $output, $jsonDef);
438
        }
439
    }
440
441
    /**
442
     * Get all sub hashes
443
     *
444
     * @param JsonDefinition $definition Main JSON definition
445
     * @return JsonDefinition[]
446
     */
447
    protected function getSubResources(JsonDefinition $definition)
448
    {
449
        $resources = [];
450
        foreach ($definition->getFields() as $field) {
451
            while ($field instanceof JsonDefinitionArray) {
452
                $field = $field->getElement();
453
            }
454
            if (!$field instanceof JsonDefinitionHash) {
455
                continue;
456
            }
457
458
            $subDefiniton = $field->getJsonDefinition();
459
460
            $resources = array_merge($this->getSubResources($subDefiniton), $resources);
461
            $resources[] = $subDefiniton;
462
        }
463
464
        return $resources;
465
    }
466
467
    /**
468
     * Gathers data for the command to run.
469
     *
470
     * @param array           $arguments Set of cli arguments passed to the command
471
     * @param OutputInterface $output    Output channel to send messages to.
472
     * @param JsonDefinition  $jsonDef   Configuration of the service
473
     *
474
     * @return void
475
     * @throws \LogicException
476
     */
477
    private function generateResource(array $arguments, OutputInterface $output, JsonDefinition $jsonDef)
478
    {
479
        // controller?
480
        $routerBase = $jsonDef->getRouterBase();
481
        if ($routerBase === false || $this->isNotWhitelistedController($routerBase)) {
482
            $arguments['--no-controller'] = 'true';
483
        }
484
485
        $this->runner->executeCommand($arguments, $output, 'Create resource call failed, see above. Exiting.');
486
    }
487
488
    /**
489
     * Generates a Bundle via command line (wrapping graviton:generate:bundle)
490
     *
491
     * @param string          $namespace    Namespace
492
     * @param string          $bundleName   Name of bundle
493
     * @param InputInterface  $input        Input
494
     * @param OutputInterface $output       Output
495
     * @param string          $deleteBefore Delete before directory
0 ignored issues
show
Documentation introduced by
Should the type for parameter $deleteBefore not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
496
     *
497
     * @return void
498
     *
499
     * @throws \LogicException
500
     */
501
    private function generateBundle(
502
        $namespace,
503
        $bundleName,
504
        InputInterface $input,
505
        OutputInterface $output,
506
        $deleteBefore = null
507
    ) {
508
        // first, create the bundle
509
        $arguments = array(
510
            'graviton:generate:bundle',
511
            '--no-debug' => null,
512
            '--namespace' => $namespace,
513
            '--bundle-name' => $bundleName,
514
            '--dir' => $input->getOption('srcDir'),
515
            '--format' => $input->getOption('bundleFormat'),
516
            '--loaderBundleName' => $input->getOption('bundleBundleName'),
517
        );
518
519
        if (!is_null($deleteBefore)) {
520
            $arguments['--deleteBefore'] = $deleteBefore;
521
        }
522
523
        $this->runner->executeCommand(
524
            $arguments,
525
            $output,
526
            'Create bundle call failed, see above. Exiting.'
527
        );
528
    }
529
530
    /**
531
     * Generates our BundleBundle for dynamic bundles.
532
     * It basically replaces the Bundle main class that got generated
533
     * by the Sensio bundle task and it includes all of our bundles there.
534
     *
535
     * @return void
536
     */
537
    private function generateBundleBundleClass()
538
    {
539
        $dbbGenerator = new DynamicBundleBundleGenerator();
540
541
        // add optional bundles if defined by parameter.
542
        if ($this->bundleAdditions !== null) {
543
            $dbbGenerator->setAdditions($this->bundleAdditions);
544
        }
545
546
        $dbbGenerator->generate(
547
            $this->bundleBundleList,
548
            $this->bundleBundleNamespace,
549
            $this->bundleBundleClassname,
550
            $this->bundleBundleClassfile
551
        );
552
    }
553
554
    /**
555
     * Returns the field string as described in the json file
556
     *
557
     * @param JsonDefinition $jsonDef The json def
558
     *
559
     * @return string CommandLine string for the generator command
560
     */
561
    private function getFieldString(JsonDefinition $jsonDef)
562
    {
563
        $ret = array();
564
565
        foreach ($jsonDef->getFields() as $field) {
566
            // don't add 'id' field it seems..
567
            if ($field->getName() != 'id') {
568
                $ret[] = $field->getName() . ':' . $field->getTypeDoctrine();
569
            }
570
        }
571
572
        return implode(
573
            ' ',
574
            $ret
575
        );
576
    }
577
578
    /**
579
     * Checks an optional environment setting if this $routerBase is whitelisted there.
580
     * If something is 'not whitelisted' (return true) means that the controller should not be generated.
581
     * This serves as a lowlevel possibility to disable the generation of certain controllers.
582
     * If we have no whitelist defined, we consider that all services should be generated (default).
583
     *
584
     * @param string $routerBase router base
585
     *
586
     * @return bool true if yes, false if not
587
     */
588
    private function isNotWhitelistedController($routerBase)
589
    {
590
        if ($this->serviceWhitelist === null) {
591
            return false;
592
        }
593
594
        return !in_array($routerBase, $this->serviceWhitelist, true);
595
    }
596
597
    /**
598
     * Generates the file containing the hash to determine if this bundle needs regeneration
599
     *
600
     * @param string $bundleDir directory of the bundle
601
     * @param string $hash      the hash to save
602
     *
603
     * @return void
604
     */
605
    private function generateGenerationHashFile($bundleDir, $hash)
606
    {
607
        $fs = new Filesystem();
608
        if ($fs->exists($bundleDir)) {
609
            $fs->dumpFile($bundleDir.DIRECTORY_SEPARATOR.self::GENERATION_HASHFILE_FILENAME, $hash);
610
        }
611
    }
612
}
613