Completed
Push — master ( 714b6a...f9d007 )
by Narcotic
06:13
created

generateSubResources()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.9765

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 3
cts 8
cp 0.375
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 12
nc 2
nop 3
crap 2.9765
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 6
    public function __construct(
87
        CommandRunner       $runner,
88
        LoaderInterface     $definitionLoader,
89
        SerializerInterface $serializer,
90
        $bundleAdditions = null,
91
        $serviceWhitelist = null,
92
        $name = null
93
    ) {
94 6
        parent::__construct($name);
95
96 6
        $this->runner = $runner;
97 6
        $this->definitionLoader = $definitionLoader;
98 6
        $this->serializer = $serializer;
99
100 6
        if ($bundleAdditions !== null && $bundleAdditions !== '') {
101
            $this->bundleAdditions = $bundleAdditions;
102
        }
103 6
        if ($serviceWhitelist !== null && $serviceWhitelist !== '') {
104
            $this->serviceWhitelist = $serviceWhitelist;
105
        }
106 6
    }
107
108
    /**
109
     * {@inheritDoc}
110
     *
111
     * @return void
112
     */
113 6
    protected function configure()
114
    {
115 6
        parent::configure();
116
117 6
        $this->addOption(
118 6
            'json',
119 6
            '',
120 6
            InputOption::VALUE_OPTIONAL,
121 6
            'Path to the json definition.'
122
        )
123 6
            ->addOption(
124 6
                'srcDir',
125 6
                '',
126 6
                InputOption::VALUE_OPTIONAL,
127 6
                'Src Dir',
128 6
                dirname(__FILE__) . '/../../../'
129
            )
130 6
            ->addOption(
131 6
                'bundleBundleName',
132 6
                '',
133 6
                InputOption::VALUE_OPTIONAL,
134 6
                'Which BundleBundle to manipulate to add our stuff',
135 6
                'GravitonDynBundleBundle'
136
            )
137 6
            ->addOption(
138 6
                'bundleFormat',
139 6
                '',
140 6
                InputOption::VALUE_OPTIONAL,
141 6
                'Which format',
142 6
                'xml'
143
            )
144 6
            ->setName('graviton:generate:dynamicbundles')
145 6
            ->setDescription(
146
                'Generates all dynamic bundles in the GravitonDyn namespace. Either give a path '.
147 6
                'to a single JSON file or a directory path containing multiple files.'
148
            );
149 6
    }
150
151
    /**
152
     * {@inheritDoc}
153
     *
154
     * @param InputInterface  $input  input
155
     * @param OutputInterface $output output
156
     *
157
     * @return void
158
     */
159 2
    protected function execute(InputInterface $input, OutputInterface $output)
160
    {
161
        /**
162
         * GENERATE THE BUNDLEBUNDLE
163
         */
164 2
        $namespace = sprintf(self::BUNDLE_NAME_MASK, 'Bundle');
165
166
        // GravitonDynBundleBundle
167 2
        $bundleName = str_replace('/', '', $namespace);
168
169
        // bundlebundle stuff..
170 2
        $this->bundleBundleNamespace = $namespace;
171 2
        $this->bundleBundleDir = $input->getOption('srcDir') . $namespace;
172 2
        $this->bundleBundleClassname = $bundleName;
173 2
        $this->bundleBundleClassfile = $this->bundleBundleDir . '/' . $this->bundleBundleClassname . '.php';
174
175 2
        $filesToWorkOn = $this->definitionLoader->load($input->getOption('json'));
176
177 2
        if (count($filesToWorkOn) < 1) {
178 2
            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 4
    protected function generateSubResources(
397
        OutputInterface $output,
398
        JsonDefinition $jsonDef,
399
        $bundleName
400
    ) {
401 4
        foreach ($this->getSubResources($jsonDef) as $subRecource) {
402
            $arguments = [
403
                'graviton:generate:resource',
404
                '--no-debug' => null,
405
                '--entity' => $bundleName . ':' . $subRecource->getId(),
406
                '--json' => $this->serializer->serialize($subRecource->getDef(), 'json'),
407
                '--no-controller' => 'true',
408
            ];
409
            $this->generateResource($arguments, $output, $jsonDef);
410
        }
411 4
    }
412
413
    /**
414
     * Generate the actual Bundle
415
     *
416
     * @param OutputInterface $output     Instance to sent text to be displayed on stout.
417
     * @param JsonDefinition  $jsonDef    Configuration to be generated the entity from.
418
     * @param string          $bundleName Name of the bundle the entity shall be generated for.
419
     *
420
     * @return void
421
     */
422
    protected function generateMainResource(OutputInterface $output, JsonDefinition $jsonDef, $bundleName)
423
    {
424
        if (!empty($jsonDef->getFields())) {
425
            $arguments = array(
426
                'graviton:generate:resource',
427
                '--no-debug' => null,
428
                '--entity' => $bundleName . ':' . $jsonDef->getId(),
429
                '--json' => $this->serializer->serialize($jsonDef->getDef(), 'json')
430
            );
431
432
            $this->generateResource($arguments, $output, $jsonDef);
433
        }
434
    }
435
436
    /**
437
     * Get all sub hashes
438
     *
439
     * @param JsonDefinition $definition Main JSON definition
440
     * @return JsonDefinition[]
441
     */
442 6
    protected function getSubResources(JsonDefinition $definition)
443
    {
444 6
        $resources = [];
445 6
        foreach ($definition->getFields() as $field) {
446 4
            while ($field instanceof JsonDefinitionArray) {
447 4
                $field = $field->getElement();
448
            }
449 4
            if (!$field instanceof JsonDefinitionHash) {
450 4
                continue;
451
            }
452
453 2
            $subDefiniton = $field->getJsonDefinition();
454
455 2
            $resources = array_merge($this->getSubResources($subDefiniton), $resources);
456 2
            $resources[] = $subDefiniton;
457
        }
458
459 6
        return $resources;
460
    }
461
462
    /**
463
     * Gathers data for the command to run.
464
     *
465
     * @param array           $arguments Set of cli arguments passed to the command
466
     * @param OutputInterface $output    Output channel to send messages to.
467
     * @param JsonDefinition  $jsonDef   Configuration of the service
468
     *
469
     * @return void
470
     * @throws \LogicException
471
     */
472
    private function generateResource(array $arguments, OutputInterface $output, JsonDefinition $jsonDef)
473
    {
474
        // controller?
475
        $routerBase = $jsonDef->getRouterBase();
476
        if ($routerBase === false || $this->isNotWhitelistedController($routerBase)) {
477
            $arguments['--no-controller'] = 'true';
478
        }
479
480
        $this->runner->executeCommand($arguments, $output, 'Create resource call failed, see above. Exiting.');
481
    }
482
483
    /**
484
     * Generates a Bundle via command line (wrapping graviton:generate:bundle)
485
     *
486
     * @param string          $namespace    Namespace
487
     * @param string          $bundleName   Name of bundle
488
     * @param InputInterface  $input        Input
489
     * @param OutputInterface $output       Output
490
     * @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...
491
     *
492
     * @return void
493
     *
494
     * @throws \LogicException
495
     */
496
    private function generateBundle(
497
        $namespace,
498
        $bundleName,
499
        InputInterface $input,
500
        OutputInterface $output,
501
        $deleteBefore = null
502
    ) {
503
        // first, create the bundle
504
        $arguments = array(
505
            'graviton:generate:bundle',
506
            '--no-debug' => null,
507
            '--namespace' => $namespace,
508
            '--bundle-name' => $bundleName,
509
            '--dir' => $input->getOption('srcDir'),
510
            '--format' => $input->getOption('bundleFormat'),
511
            '--loaderBundleName' => $input->getOption('bundleBundleName'),
512
        );
513
514
        if (!is_null($deleteBefore)) {
515
            $arguments['--deleteBefore'] = $deleteBefore;
516
        }
517
518
        $this->runner->executeCommand(
519
            $arguments,
520
            $output,
521
            'Create bundle call failed, see above. Exiting.'
522
        );
523
    }
524
525
    /**
526
     * Generates our BundleBundle for dynamic bundles.
527
     * It basically replaces the Bundle main class that got generated
528
     * by the Sensio bundle task and it includes all of our bundles there.
529
     *
530
     * @return void
531
     */
532
    private function generateBundleBundleClass()
533
    {
534
        $dbbGenerator = new DynamicBundleBundleGenerator();
535
536
        // add optional bundles if defined by parameter.
537
        if ($this->bundleAdditions !== null) {
538
            $dbbGenerator->setAdditions($this->bundleAdditions);
539
        }
540
541
        $dbbGenerator->generate(
542
            $this->bundleBundleList,
543
            $this->bundleBundleNamespace,
544
            $this->bundleBundleClassname,
545
            $this->bundleBundleClassfile
546
        );
547
    }
548
549
    /**
550
     * Checks an optional environment setting if this $routerBase is whitelisted there.
551
     * If something is 'not whitelisted' (return true) means that the controller should not be generated.
552
     * This serves as a lowlevel possibility to disable the generation of certain controllers.
553
     * If we have no whitelist defined, we consider that all services should be generated (default).
554
     *
555
     * @param string $routerBase router base
556
     *
557
     * @return bool true if yes, false if not
558
     */
559
    private function isNotWhitelistedController($routerBase)
560
    {
561
        if ($this->serviceWhitelist === null) {
562
            return false;
563
        }
564
565
        return !in_array($routerBase, $this->serviceWhitelist, true);
566
    }
567
568
    /**
569
     * Generates the file containing the hash to determine if this bundle needs regeneration
570
     *
571
     * @param string $bundleDir directory of the bundle
572
     * @param string $hash      the hash to save
573
     *
574
     * @return void
575
     */
576
    private function generateGenerationHashFile($bundleDir, $hash)
577
    {
578
        $fs = new Filesystem();
579
        if ($fs->exists($bundleDir)) {
580
            $fs->dumpFile($bundleDir.DIRECTORY_SEPARATOR.self::GENERATION_HASHFILE_FILENAME, $hash);
581
        }
582
    }
583
}
584