GenerateDynamicBundleCommand::getTemplateHash()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 0
cts 8
cp 0
rs 9.9
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 6
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\BundleGenerator;
13
use Graviton\GeneratorBundle\Generator\DynamicBundleBundleGenerator;
14
use Graviton\GeneratorBundle\Definition\Loader\LoaderInterface;
15
use Graviton\GeneratorBundle\Generator\ResourceGenerator;
16
use JMS\Serializer\SerializerInterface;
17
use Symfony\Component\Console\Command\Command;
18
use Symfony\Component\Console\Input\InputInterface;
19
use Symfony\Component\Console\Input\InputOption;
20
use Symfony\Component\Console\Output\OutputInterface;
21
use Symfony\Component\Filesystem\Filesystem;
22
use Symfony\Component\Finder\Finder;
23
use Symfony\Component\Finder\SplFileInfo;
24
25
/**
26
 * Here, we generate all "dynamic" Graviton bundles..
27
 *
28
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
29
 * @license  https://opensource.org/licenses/MIT MIT License
30
 * @link     http://swisscom.ch
31
 */
32
class GenerateDynamicBundleCommand extends Command
33
{
34
35
    /** @var  string */
36
    const BUNDLE_NAMESPACE = 'GravitonDyn';
37
38
    /** @var  string */
39
    const BUNDLE_NAME_MASK = self::BUNDLE_NAMESPACE.'/%sBundle';
40
41
    /** @var  string */
42
    const GENERATION_HASHFILE_FILENAME = 'genhash';
43
44
    /** @var  string */
45
    private $bundleBundleNamespace;
46
47
    /** @var  string */
48
    private $bundleBundleDir;
49
50
    /** @var  string */
51
    private $bundleBundleClassname;
52
53
    /** @var  string */
54
    private $bundleBundleClassfile;
55
56
    /** @var  array */
57
    private $bundleBundleList = [];
58
59
    /** @var array|null */
60
    private $bundleAdditions = null;
61
62
    /** @var array|null */
63
    private $serviceWhitelist = null;
64
    /**
65
     * @var CommandRunner
66
     */
67
    private $runner;
68
    /**
69
     * @var LoaderInterface
70
     */
71
    private $definitionLoader;
72
    /**
73
     * @var SerializerInterface
74
     */
75
    private $serializer;
76
    /**
77
     * @var Filesystem
78
     */
79
    private $fs;
80
    /**
81
     * @var BundleGenerator
82
     */
83
    private $bundleGenerator;
84
    /**
85
     * @var ResourceGenerator
86
     */
87
    private $resourceGenerator;
88
    /**
89
     * @var DynamicBundleBundleGenerator
90
     */
91
    private $bundleBundleGenerator;
92
93
    /**
94
     * @param LoaderInterface              $definitionLoader      JSON definition loader
95
     * @param BundleGenerator              $bundleGenerator       bundle generator
96
     * @param ResourceGenerator            $resourceGenerator     resource generator
97
     * @param DynamicBundleBundleGenerator $bundleBundleGenerator bundlebundle generator
98
     * @param SerializerInterface          $serializer            Serializer
99
     * @param string|null                  $bundleAdditions       Additional bundles list in JSON format
100
     * @param string|null                  $serviceWhitelist      Service whitelist in JSON format
101
     * @param string|null                  $name                  name
102
     */
103
    public function __construct(
104
        LoaderInterface     $definitionLoader,
105
        BundleGenerator $bundleGenerator,
106
        ResourceGenerator $resourceGenerator,
107
        DynamicBundleBundleGenerator $bundleBundleGenerator,
108
        SerializerInterface $serializer,
109
        $bundleAdditions = null,
110
        $serviceWhitelist = null,
111
        $name = null
112
    ) {
113
        parent::__construct($name);
114
115
        $this->definitionLoader = $definitionLoader;
116
        $this->bundleGenerator = $bundleGenerator;
117
        $this->resourceGenerator = $resourceGenerator;
118
        $this->bundleBundleGenerator = $bundleBundleGenerator;
119
        $this->serializer = $serializer;
120
        $this->fs = new Filesystem();
121
122
        if ($bundleAdditions !== null && $bundleAdditions !== '') {
123
            $this->bundleAdditions = $bundleAdditions;
124
        }
125
        if ($serviceWhitelist !== null && $serviceWhitelist !== '') {
126
            $this->serviceWhitelist = $serviceWhitelist;
127
        }
128
    }
129
130
    /**
131
     * {@inheritDoc}
132
     *
133
     * @return void
134
     */
135
    protected function configure()
136
    {
137
        parent::configure();
138
139
        $this->addOption(
140
            'json',
141
            '',
142
            InputOption::VALUE_OPTIONAL,
143
            'Path to the json definition.'
144
        )
145
            ->addOption(
146
                'srcDir',
147
                '',
148
                InputOption::VALUE_OPTIONAL,
149
                'Src Dir',
150
                dirname(__FILE__) . '/../../../'
151
            )
152
            ->addOption(
153
                'bundleBundleName',
154
                '',
155
                InputOption::VALUE_OPTIONAL,
156
                'Which BundleBundle to manipulate to add our stuff',
157
                'GravitonDynBundleBundle'
158
            )
159
            ->setName('graviton:generate:dynamicbundles')
160
            ->setDescription(
161
                'Generates all dynamic bundles in the GravitonDyn namespace. Either give a path '.
162
                'to a single JSON file or a directory path containing multiple files.'
163
            );
164
    }
165
166
    /**
167
     * {@inheritDoc}
168
     *
169
     * @param InputInterface  $input  input
170
     * @param OutputInterface $output output
171
     *
172
     * @return void
173
     */
174
    protected function execute(InputInterface $input, OutputInterface $output)
175
    {
176
        /**
177
         * GENERATE THE BUNDLEBUNDLE
178
         */
179
        $bundleBundleDir = sprintf(self::BUNDLE_NAME_MASK, 'Bundle');
180
181
        // GravitonDynBundleBundle
182
        $bundleName = str_replace('/', '', $bundleBundleDir);
183
184
        // bundlebundle stuff..
185
        $this->bundleBundleNamespace = $bundleBundleDir;
186
        $this->bundleBundleDir = $input->getOption('srcDir') . $bundleBundleDir;
187
        $this->bundleBundleClassname = $bundleName;
188
        $this->bundleBundleClassfile = $this->bundleBundleDir . '/' . $this->bundleBundleClassname . '.php';
189
190
        $filesToWorkOn = $this->definitionLoader->load($input->getOption('json'));
191
192
        if (count($filesToWorkOn) < 1) {
193
            throw new \LogicException("Could not find any usable JSON files.");
194
        }
195
196
        $templateHash = $this->getTemplateHash();
197
        $existingBundles = $this->getExistingBundleHashes($input->getOption('srcDir'));
198
199
        /**
200
         * GENERATE THE BUNDLE(S)
201
         */
202
        foreach ($filesToWorkOn as $jsonDef) {
203
            $thisIdName = $jsonDef->getId();
204
            $namespace = sprintf(self::BUNDLE_NAME_MASK, $thisIdName);
205
206
            // make sure bundle is in bundlebundle
207
            $this->bundleBundleList[] = $namespace;
208
209
            $jsonDef->setNamespace($namespace);
210
211
            $bundleName = str_replace('/', '', $namespace);
212
            $bundleDir = $input->getOption('srcDir').$namespace;
213
            $bundleNamespace = str_replace('/', '\\', $namespace).'\\';
214
215
            try {
216
                $thisHash = sha1($templateHash.PATH_SEPARATOR.serialize($jsonDef));
217
218
                $needsGeneration = true;
219
                if (isset($existingBundles[$bundleDir])) {
220
                    if ($existingBundles[$bundleDir] == $thisHash) {
221
                        $needsGeneration = false;
222
                    }
223
                    unset($existingBundles[$bundleDir]);
224
                }
225
226
                if ($needsGeneration) {
227
                    $this->generateBundle($bundleNamespace, $bundleName, $input->getOption('srcDir'));
228
                    $this->generateGenerationHashFile($bundleDir, $thisHash);
229
                }
230
231
                if ($needsGeneration) {
232
                    $this->generateResources(
233
                        $jsonDef,
234
                        $bundleName,
235
                        $bundleDir,
236
                        $bundleNamespace
237
                    );
238
239
                    $output->write(
240
                        PHP_EOL.
241
                        sprintf('<info>Generated "%s" from definition %s</info>', $bundleName, $jsonDef->getId()).
242
                        PHP_EOL
243
                    );
244
                } else {
245
                    $output->write(
246
                        PHP_EOL.
247
                        sprintf('<info>Using pre-existing "%s"</info>', $bundleName).
248
                        PHP_EOL
249
                    );
250
                }
251
            } catch (\Exception $e) {
252
                $output->writeln(
253
                    sprintf('<error>%s</error>', $e->getMessage())
254
                );
255
256
                // remove failed bundle from list
257
                array_pop($this->bundleBundleList);
258
            }
259
        }
260
261
        // whatever is left in $existingBundles is not defined anymore and needs to be deleted..
262
        foreach ($existingBundles as $dirName => $hash) {
263
            $fileInfo = new \SplFileInfo($dirName);
264
            $bundleClassName = $this->getBundleClassnameFromFolder($fileInfo->getFilename());
265
266
            // remove from bundlebundle list
267
            unset($this->bundleBundleList[array_search($bundleClassName, $this->bundleBundleList)]);
268
269
            $this->fs->remove($dirName);
270
271
            $output->write(
272
                PHP_EOL.
273
                sprintf('<info>Deleted obsolete bundle "%s"</info>', $dirName).
274
                PHP_EOL
275
            );
276
        }
277
278
        // generate bundlebundle
279
        $this->generateBundleBundleClass();
280
    }
281
282
    /**
283
     * scans through all existing dynamic bundles, checks if there is a generation hash and collect that
284
     * all in an array that can be used for fast checking.
285
     *
286
     * @param string $baseDir base directory of dynamic bundles
287
     *
288
     * @return array key is bundlepath, value is the current hash
289
     */
290
    private function getExistingBundleHashes($baseDir)
291
    {
292
        $existingBundles = [];
293
        $fs = new Filesystem();
294
        $bundleBaseDir = $baseDir.self::BUNDLE_NAMESPACE;
295
296
        if (!$fs->exists($bundleBaseDir)) {
297
            return $existingBundles;
298
        }
299
300
        $bundleFinder = $this->getBundleFinder($baseDir);
301
302
        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...
303
            $genHash = '';
304
            $hashFileFinder = new Finder();
305
            $hashFileIterator = $hashFileFinder
306
                ->files()
307
                ->in($bundleDir->getPathname())
308
                ->name(self::GENERATION_HASHFILE_FILENAME)
309
                ->depth('== 0')
310
                ->getIterator();
311
312
            $hashFileIterator->rewind();
313
314
            $hashFile = $hashFileIterator->current();
315
            if ($hashFile instanceof SplFileInfo) {
316
                $genHash = $hashFile->getContents();
317
            }
318
319
            $existingBundles[$bundleDir->getPathname()] = $genHash;
320
        }
321
322
        return $existingBundles;
323
    }
324
325
    /**
326
     * from a name of a folder of a bundle, this function returns the corresponding class name
327
     *
328
     * @param string $folderName folder name
329
     *
330
     * @return string
331
     */
332
    private function getBundleClassnameFromFolder($folderName)
333
    {
334
        if (substr($folderName, -6) == 'Bundle') {
335
            $folderName = substr($folderName, 0, -6);
336
        }
337
338
        return sprintf(self::BUNDLE_NAME_MASK, $folderName);
339
    }
340
341
    /**
342
     * returns a finder that iterates all bundle directories
343
     *
344
     * @param string $baseDir the base dir to search
345
     *
346
     * @return Finder|null finder or null if basedir does not exist
347
     */
348
    private function getBundleFinder($baseDir)
349
    {
350
        $bundleBaseDir = $baseDir.self::BUNDLE_NAMESPACE;
351
352
        if (!(new Filesystem())->exists($bundleBaseDir)) {
353
            return null;
354
        }
355
356
        $bundleFinder = new Finder();
357
        $bundleFinder->directories()->in($bundleBaseDir)->depth('== 0')->notName('BundleBundle');
358
359
        return $bundleFinder;
360
    }
361
362
    /**
363
     * Calculates a hash of all templates that generator uses to output it's file.
364
     * That way a regeneration will be triggered when one of them changes..
365
     *
366
     * @return string hash
367
     */
368
    private function getTemplateHash()
369
    {
370
        $templateDir = __DIR__ . '/../Resources/skeleton';
371
        $resourceFinder = new Finder();
372
        $resourceFinder->in($templateDir)->files()->sortByName();
373
        $templateTimes = '';
374
        foreach ($resourceFinder as $file) {
375
            $templateTimes .= PATH_SEPARATOR . sha1_file($file->getPathname());
376
        }
377
        return sha1($templateTimes);
378
    }
379
380
    /**
381
     * Generate Bundle entities
382
     *
383
     * @param OutputInterface $output          Instance to sent text to be displayed on stout.
384
     * @param JsonDefinition  $jsonDef         Configuration to be generated the entity from.
385
     * @param string          $bundleName      Name of the bundle the entity shall be generated for.
386
     * @param string          $bundleClassName class name
387
     *
388
     * @return void
389
     * @throws \Exception
390
     */
391
    protected function generateSubResources(
392
        OutputInterface $output,
393
        JsonDefinition $jsonDef,
394
        $bundleName,
395
        $bundleClassName
396
    ) {
397
        foreach ($this->getSubResources($jsonDef) as $subRecource) {
398
            $arguments = [
399
                'graviton:generate:resource',
400
                '--no-debug' => null,
401
                '--entity' => $bundleName . ':' . $subRecource->getId(),
402
                '--bundleClassName' => $bundleClassName,
403
                '--json' => $this->serializer->serialize($subRecource->getDef(), 'json'),
404
                '--no-controller' => 'true',
405
            ];
406
            $this->generateResource($arguments, $output, $jsonDef);
407
        }
408
    }
409
410
    /**
411
     * generates the resources of a bundle
412
     *
413
     * @param JsonDefinition $jsonDef         definition
414
     * @param string         $bundleName      name
415
     * @param string         $bundleDir       dir
416
     * @param string         $bundleNamespace namespace
417
     *
418
     * @return void
419
     */
420
    protected function generateResources(
421
        JsonDefinition $jsonDef,
422
        $bundleName,
423
        $bundleDir,
424
        $bundleNamespace
425
    ) {
426
427
        /** @var ResourceGenerator $generator */
428
        $generator = $this->resourceGenerator;
429
        $generator->setGenerateController(false);
430
431
        foreach ($this->getSubResources($jsonDef) as $subRecource) {
432
            $generator->setJson(new JsonDefinition($subRecource->getDef()->setIsSubDocument(true)));
433
            $generator->generate(
434
                $bundleDir,
435
                $bundleNamespace,
436
                $bundleName,
437
                $subRecource->getId()
438
            );
439
        }
440
441
        // main resources
442
        if (!empty($jsonDef->getFields())) {
443
            $generator->setGenerateController(true);
444
445
            $routerBase = $jsonDef->getRouterBase();
446
            if ($routerBase === false || $this->isNotWhitelistedController($routerBase)) {
447
                $generator->setGenerateController(false);
448
            }
449
450
            $generator->setJson(new JsonDefinition($jsonDef->getDef()));
451
            $generator->generate(
452
                $bundleDir,
453
                $bundleNamespace,
454
                $bundleName,
455
                $jsonDef->getId()
456
            );
457
        }
458
    }
459
460
    /**
461
     * Get all sub hashes
462
     *
463
     * @param JsonDefinition $definition Main JSON definition
464
     * @return JsonDefinition[]
465
     */
466
    protected function getSubResources(JsonDefinition $definition)
467
    {
468
        $resources = [];
469
        foreach ($definition->getFields() as $field) {
470
            while ($field instanceof JsonDefinitionArray) {
471
                $field = $field->getElement();
472
            }
473
            if (!$field instanceof JsonDefinitionHash) {
474
                continue;
475
            }
476
477
            $subDefiniton = $field->getJsonDefinition();
478
479
            $resources = array_merge($this->getSubResources($subDefiniton), $resources);
480
            $resources[] = $subDefiniton;
481
        }
482
483
        return $resources;
484
    }
485
486
    /**
487
     * Gathers data for the command to run.
488
     *
489
     * @param array           $arguments Set of cli arguments passed to the command
490
     * @param OutputInterface $output    Output channel to send messages to.
491
     * @param JsonDefinition  $jsonDef   Configuration of the service
492
     *
493
     * @return void
494
     * @throws \LogicException
495
     */
496
    private function generateResource(array $arguments, OutputInterface $output, JsonDefinition $jsonDef)
497
    {
498
        // controller?
499
        $routerBase = $jsonDef->getRouterBase();
500
        if ($routerBase === false || $this->isNotWhitelistedController($routerBase)) {
501
            $arguments['--no-controller'] = 'true';
502
        }
503
504
        $this->runner->executeCommand($arguments, $output, 'Create resource call failed, see above. Exiting.');
505
    }
506
507
    /**
508
     * generates the basic bundle structure
509
     *
510
     * @param string $namespace  Namespace
511
     * @param string $bundleName Name of bundle
512
     * @param string $targetDir  target directory
513
     *
514
     * @return void
515
     *
516
     * @throws \LogicException
517
     */
518
    private function generateBundle(
519
        $namespace,
520
        $bundleName,
521
        $targetDir
522
    ) {
523
        $this->bundleGenerator->generate(
524
            $namespace,
525
            $bundleName,
526
            $targetDir,
527
            'yml'
528
        );
529
    }
530
531
    /**
532
     * Generates our BundleBundle for dynamic bundles.
533
     * It basically replaces the Bundle main class that got generated
534
     * by the Sensio bundle task and it includes all of our bundles there.
535
     *
536
     * @return void
537
     */
538
    private function generateBundleBundleClass()
539
    {
540
        // add optional bundles if defined by parameter.
541
        if ($this->bundleAdditions !== null) {
542
            $this->bundleBundleGenerator->setAdditions($this->bundleAdditions);
543
        } else {
544
            $this->bundleBundleGenerator->setAdditions([]);
545
        }
546
547
        $this->bundleBundleGenerator->generate(
548
            $this->bundleBundleList,
549
            $this->bundleBundleNamespace,
550
            $this->bundleBundleClassname,
551
            $this->bundleBundleClassfile
552
        );
553
    }
554
555
    /**
556
     * Checks an optional environment setting if this $routerBase is whitelisted there.
557
     * If something is 'not whitelisted' (return true) means that the controller should not be generated.
558
     * This serves as a lowlevel possibility to disable the generation of certain controllers.
559
     * If we have no whitelist defined, we consider that all services should be generated (default).
560
     *
561
     * @param string $routerBase router base
562
     *
563
     * @return bool true if yes, false if not
564
     */
565
    private function isNotWhitelistedController($routerBase)
566
    {
567
        if ($this->serviceWhitelist === null) {
568
            return false;
569
        }
570
571
        return !in_array($routerBase, $this->serviceWhitelist, true);
572
    }
573
574
    /**
575
     * Generates the file containing the hash to determine if this bundle needs regeneration
576
     *
577
     * @param string $bundleDir directory of the bundle
578
     * @param string $hash      the hash to save
579
     *
580
     * @return void
581
     */
582
    private function generateGenerationHashFile($bundleDir, $hash)
583
    {
584
        $fs = new Filesystem();
585
        if ($fs->exists($bundleDir)) {
586
            $fs->dumpFile($bundleDir.DIRECTORY_SEPARATOR.self::GENERATION_HASHFILE_FILENAME, $hash);
587
        }
588
    }
589
}
590