Completed
Push — master ( 8d7f0a...89a77c )
by
unknown
25:09 queued 15:59
created

GenerateDynamicBundleCommand::getTemplateHash()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 0
cts 9
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 8
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\DynamicBundleBundleGenerator;
13
use Graviton\GeneratorBundle\Manipulator\File\XmlManipulator;
14
use Graviton\GeneratorBundle\Definition\Loader\LoaderInterface;
15
use JMS\Serializer\SerializerInterface;
16
use Symfony\Component\Console\Command\Command;
17
use Symfony\Component\Console\Input\InputInterface;
18
use Symfony\Component\Console\Input\InputOption;
19
use Symfony\Component\Console\Output\OutputInterface;
20
use Symfony\Component\Filesystem\Filesystem;
21
use Symfony\Component\Finder\Finder;
22
use Symfony\Component\Finder\SplFileInfo;
23
24
/**
25
 * Here, we generate all "dynamic" Graviton bundles..
26
 *
27
 * @todo     create a new Application in-situ
28
 * @todo     see if we can get rid of container dependency..
29
 *
30
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
31
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
32
 * @link     http://swisscom.ch
33
 */
34
class GenerateDynamicBundleCommand extends Command
35
{
36
37
    /** @var  string */
38
    const BUNDLE_NAMESPACE = 'GravitonDyn';
39
40
    /** @var  string */
41
    const BUNDLE_NAME_MASK = self::BUNDLE_NAMESPACE.'/%sBundle';
42
43
    /** @var  string */
44
    const GENERATION_HASHFILE_FILENAME = 'genhash';
45
46
    /** @var  string */
47
    private $bundleBundleNamespace;
48
49
    /** @var  string */
50
    private $bundleBundleDir;
51
52
    /** @var  string */
53
    private $bundleBundleClassname;
54
55
    /** @var  string */
56
    private $bundleBundleClassfile;
57
58
    /** @var  array */
59
    private $bundleBundleList = [];
60
61
    /** @var array|null */
62
    private $bundleAdditions = null;
63
64
    /** @var array|null */
65
    private $serviceWhitelist = null;
66
67
    /**
68
     * @var CommandRunner
69
     */
70
    private $runner;
71
    /**
72
     * @var LoaderInterface
73
     */
74
    private $definitionLoader;
75
    /**
76
     * @var XmlManipulator
77
     */
78
    private $xmlManipulator;
79
    /**
80
     * @var SerializerInterface
81
     */
82
    private $serializer;
83
84
85
    /**
86
     * @param CommandRunner       $runner           Runs a console command.
87
     * @param XmlManipulator      $xmlManipulator   Helper to change the content of a xml file.
88
     * @param LoaderInterface     $definitionLoader JSON definition loader
89
     * @param SerializerInterface $serializer       Serializer
90
     * @param string|null         $bundleAdditions  Additional bundles list in JSON format
91
     * @param string|null         $serviceWhitelist Service whitelist in JSON format
92
     * @param string|null         $name             The name of the command; passing null means it must be set in
93
     *                                              configure()
94
     */
95 6
    public function __construct(
96
        CommandRunner       $runner,
97
        XmlManipulator      $xmlManipulator,
98
        LoaderInterface     $definitionLoader,
99
        SerializerInterface $serializer,
100
        $bundleAdditions = null,
101
        $serviceWhitelist = null,
102
        $name = null
103
    ) {
104 6
        parent::__construct($name);
105
106 6
        $this->runner = $runner;
107 6
        $this->xmlManipulator = $xmlManipulator;
108 6
        $this->definitionLoader = $definitionLoader;
109 6
        $this->serializer = $serializer;
110
111 6
        if ($bundleAdditions !== null && $bundleAdditions !== '') {
112
            $this->bundleAdditions = $bundleAdditions;
0 ignored issues
show
Documentation Bug introduced by
It seems like $bundleAdditions of type string is incompatible with the declared type array|null of property $bundleAdditions.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
113
        }
114 6
        if ($serviceWhitelist !== null && $serviceWhitelist !== '') {
115
            $this->serviceWhitelist = $serviceWhitelist;
0 ignored issues
show
Documentation Bug introduced by
It seems like $serviceWhitelist of type string is incompatible with the declared type array|null of property $serviceWhitelist.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
116
        }
117 6
    }
118
119
    /**
120
     * {@inheritDoc}
121
     *
122
     * @return void
123
     */
124 6
    protected function configure()
125
    {
126 6
        parent::configure();
127
128 6
        $this->addOption(
129 6
            'json',
130 6
            '',
131 6
            InputOption::VALUE_OPTIONAL,
132 3
            'Path to the json definition.'
133 3
        )
134 6
            ->addOption(
135 6
                'srcDir',
136 6
                '',
137 6
                InputOption::VALUE_OPTIONAL,
138 6
                'Src Dir',
139 6
                dirname(__FILE__) . '/../../../'
140 3
            )
141 6
            ->addOption(
142 6
                'bundleBundleName',
143 6
                '',
144 6
                InputOption::VALUE_OPTIONAL,
145 6
                'Which BundleBundle to manipulate to add our stuff',
146 3
                'GravitonDynBundleBundle'
147 3
            )
148 6
            ->addOption(
149 6
                'bundleFormat',
150 6
                '',
151 6
                InputOption::VALUE_OPTIONAL,
152 6
                'Which format',
153 3
                'xml'
154 3
            )
155 6
            ->setName('graviton:generate:dynamicbundles')
156 6
            ->setDescription(
157
                'Generates all dynamic bundles in the GravitonDyn namespace. Either give a path
158 3
                    to a single JSON file or a directory path containing multiple files.'
159 3
            );
160 6
    }
161
162
    /**
163
     * {@inheritDoc}
164
     *
165
     * @param InputInterface  $input  input
166
     * @param OutputInterface $output output
167
     *
168
     * @return void
169
     */
170 2
    protected function execute(InputInterface $input, OutputInterface $output)
171
    {
172
        /**
173
         * GENERATE THE BUNDLEBUNDLE
174
         */
175 2
        $namespace = sprintf(self::BUNDLE_NAME_MASK, 'Bundle');
176
177
        // GravitonDynBundleBundle
178 2
        $bundleName = str_replace('/', '', $namespace);
179
180
        // bundlebundle stuff..
181 2
        $this->bundleBundleNamespace = $namespace;
182 2
        $this->bundleBundleDir = $input->getOption('srcDir') . $namespace;
183 2
        $this->bundleBundleClassname = $bundleName;
184 2
        $this->bundleBundleClassfile = $this->bundleBundleDir . '/' . $this->bundleBundleClassname . '.php';
185
186 2
        $filesToWorkOn = $this->definitionLoader->load($input->getOption('json'));
187
188 2
        if (count($filesToWorkOn) < 1) {
189 2
            throw new \LogicException("Could not find any usable JSON files.");
190
        }
191
192
        $fs = new Filesystem();
193
194
        if ($fs->exists($this->bundleBundleClassfile)) {
195
            $fs->remove($this->bundleBundleClassfile);
196
        }
197
198
        $templateHash = $this->getTemplateHash();
199
        $existingBundles = $this->getExistingBundleHashes($input->getOption('srcDir'));
200
201
        /**
202
         * GENERATE THE BUNDLE(S)
203
         */
204
        foreach ($filesToWorkOn as $jsonDef) {
205
            $thisIdName = $jsonDef->getId();
206
            $namespace = sprintf(self::BUNDLE_NAME_MASK, $thisIdName);
207
208
            $jsonDef->setNamespace($namespace);
209
210
            $bundleName = str_replace('/', '', $namespace);
211
            $this->bundleBundleList[] = $namespace;
212
213
            try {
214
                $bundleDir = $input->getOption('srcDir').$namespace;
215
                $thisHash = sha1($templateHash.PATH_SEPARATOR.serialize($jsonDef));
216
217
                $needsGeneration = true;
218
                if (isset($existingBundles[$bundleDir])) {
219
                    if ($existingBundles[$bundleDir] == $thisHash) {
220
                        $needsGeneration = false;
221
                    }
222
                    unset($existingBundles[$bundleDir]);
223
                }
224
225
                if ($needsGeneration) {
226
                    $fs->remove($bundleDir);
227
                    $this->generateBundle($namespace, $bundleName, $input, $output);
228
                    $this->generateGenerationHashFile($bundleDir, $thisHash);
229
                }
230
231
                $this->generateBundleBundleClass();
232
233
                if ($needsGeneration) {
234
                    $this->generateSubResources($output, $jsonDef, $this->xmlManipulator, $bundleName, $namespace);
235
                    $this->generateMainResource($output, $jsonDef, $bundleName);
236
                    $this->generateValidationXml(
237
                        $this->xmlManipulator,
238
                        $this->getGeneratedValidationXmlPath($namespace)
239
                    );
240
241
                    $output->write(
242
                        PHP_EOL.
243
                        sprintf('<info>Generated "%s" from definition %s</info>', $bundleName, $jsonDef->getId()).
244
                        PHP_EOL
245
                    );
246
                } else {
247
                    $output->write(
248
                        PHP_EOL.
249
                        sprintf('<info>Using pre-existing "%s"</info>', $bundleName).
250
                        PHP_EOL
251
                    );
252
                }
253
            } catch (\Exception $e) {
254
                $output->writeln(
255
                    sprintf('<error>%s</error>', $e->getMessage())
256
                );
257
258
                // remove failed bundle from list
259
                array_pop($this->bundleBundleList);
260
            }
261
262
            $this->xmlManipulator->reset();
263
        }
264
265
        // whatever is left in $existingBundles is not defined anymore and needs to be deleted..
266
        foreach ($existingBundles as $dirName => $hash) {
267
            $fs->remove($dirName);
268
            $output->write(
269
                PHP_EOL.
270
                sprintf('<info>Deleted obsolete bundle "%s"</info>', $dirName).
271
                PHP_EOL
272
            );
273
        }
274
    }
275
276
    /**
277
     * scans through all existing dynamic bundles, checks if there is a generation hash and collect that
278
     * all in an array that can be used for fast checking.
279
     *
280
     * @param string $baseDir base directory of dynamic bundles
281
     *
282
     * @return array key is bundlepath, value is the current hash
283
     */
284
    private function getExistingBundleHashes($baseDir)
285
    {
286
        $existingBundles = [];
287
        $fs = new Filesystem();
288
        $bundleBaseDir = $baseDir.self::BUNDLE_NAMESPACE;
289
290
        if (!$fs->exists($bundleBaseDir)) {
291
            return $existingBundles;
292
        }
293
294
        $bundleFinder = new Finder();
295
        $bundleFinder->directories()->in($bundleBaseDir)->depth('== 0')->notName('BundleBundle');
296
297
        foreach ($bundleFinder as $bundleDir) {
298
            $genHash = '';
299
            $hashFileFinder = new Finder();
300
            $hashFileIterator = $hashFileFinder
301
                ->files()
302
                ->in($bundleDir->getPathname())
303
                ->name(self::GENERATION_HASHFILE_FILENAME)
304
                ->depth('== 0')
305
                ->getIterator();
306
307
            $hashFileIterator->rewind();
308
309
            $hashFile = $hashFileIterator->current();
310
            if ($hashFile instanceof SplFileInfo) {
311
                $genHash = $hashFile->getContents();
312
            }
313
314
            $existingBundles[$bundleDir->getPathname()] = $genHash;
315
        }
316
317
        return $existingBundles;
318
    }
319
320
    /**
321
     * Calculates a hash of all templates that generator uses to output it's file.
322
     * That way a regeneration will be triggered when one of them changes..
323
     *
324
     * @return string hash
325
     */
326
    private function getTemplateHash()
327
    {
328
        $templateDir = __DIR__ . '/../Resources/skeleton';
329
        $resourceFinder = new Finder();
330
        $resourceFinder->in($templateDir)->files()->sortByName();
331
        $templateTimes = '';
332
        foreach ($resourceFinder as $file) {
333
            $templateTimes .= PATH_SEPARATOR . $file->getMTime();
334
        }
335
        return sha1($templateTimes);
336
    }
337
338
    /**
339
     * Generate Bundle entities
340
     *
341
     * @param OutputInterface $output         Instance to sent text to be displayed on stout.
342
     * @param JsonDefinition  $jsonDef        Configuration to be generated the entity from.
343
     * @param XmlManipulator  $xmlManipulator Helper to safe the validation xml file.
344
     * @param string          $bundleName     Name of the bundle the entity shall be generated for.
345
     * @param string          $namespace      Absolute path to the bundle root dir.
346
     *
347
     * @return void
348
     * @throws \Exception
349
     */
350 4
    protected function generateSubResources(
351
        OutputInterface $output,
352
        JsonDefinition $jsonDef,
353
        XmlManipulator $xmlManipulator,
354
        $bundleName,
355
        $namespace
356
    ) {
357 4
        foreach ($this->getSubResources($jsonDef) as $subRecource) {
358
            $arguments = [
359
                'graviton:generate:resource',
360
                '--entity' => $bundleName . ':' . $subRecource->getId(),
361
                '--format' => 'xml',
362
                '--json' => $this->serializer->serialize($subRecource->getDef(), 'json'),
363
                '--fields' => $this->getFieldString($subRecource),
364
                '--no-controller' => 'true',
365
            ];
366
            $this->generateResource($arguments, $output, $jsonDef);
367
368
            // look for validation.xml and save it from over-writing ;-)
369
            // we basically get the xml content that was generated in order to save them later..
370
            $validationXml = $this->getGeneratedValidationXmlPath($namespace);
371
            if (file_exists($validationXml)) {
372
                $xmlManipulator->addNodes(file_get_contents($validationXml));
373
            }
374 2
        }
375 4
    }
376
377
    /**
378
     * Generate the actual Bundle
379
     *
380
     * @param OutputInterface $output     Instance to sent text to be displayed on stout.
381
     * @param JsonDefinition  $jsonDef    Configuration to be generated the entity from.
382
     * @param string          $bundleName Name of the bundle the entity shall be generated for.
383
     *
384
     * @return void
385
     */
386
    protected function generateMainResource(OutputInterface $output, JsonDefinition $jsonDef, $bundleName)
387
    {
388
        $fields = $jsonDef->getFields();
389
        if (!empty($fields)) {
390
            $arguments = array(
391
                'graviton:generate:resource',
392
                '--entity' => $bundleName . ':' . $jsonDef->getId(),
393
                '--json' => $this->serializer->serialize($jsonDef->getDef(), 'json'),
394
                '--format' => 'xml',
395
                '--fields' => $this->getFieldString($jsonDef)
396
            );
397
398
            $this->generateResource($arguments, $output, $jsonDef);
399
        }
400
    }
401
402
    /**
403
     * Get all sub hashes
404
     *
405
     * @param JsonDefinition $definition Main JSON definition
406
     * @return JsonDefinition[]
407
     */
408 6
    protected function getSubResources(JsonDefinition $definition)
409
    {
410 6
        $resources = [];
411 6
        foreach ($definition->getFields() as $field) {
412 4
            while ($field instanceof JsonDefinitionArray) {
413 4
                $field = $field->getElement();
414 2
            }
415 4
            if (!$field instanceof JsonDefinitionHash) {
416 4
                continue;
417
            }
418
419 2
            $subDefiniton = $field->getJsonDefinition();
420
421 2
            $resources = array_merge($this->getSubResources($subDefiniton), $resources);
422 2
            $resources[] = $subDefiniton;
423 3
        }
424
425 6
        return $resources;
426
    }
427
428
    /**
429
     * Gathers data for the command to run.
430
     *
431
     * @param array           $arguments Set of cli arguments passed to the command
432
     * @param OutputInterface $output    Output channel to send messages to.
433
     * @param JsonDefinition  $jsonDef   Configuration of the service
434
     *
435
     * @return void
436
     * @throws \LogicException
437
     */
438
    private function generateResource(array $arguments, OutputInterface $output, JsonDefinition $jsonDef)
439
    {
440
        // controller?
441
        $routerBase = $jsonDef->getRouterBase();
442
        if ($routerBase === false || $this->isNotWhitelistedController($routerBase)) {
443
            $arguments['--no-controller'] = 'true';
444
        }
445
446
        $this->runner->executeCommand($arguments, $output, 'Create resource call failed, see above. Exiting.');
447
    }
448
449
    /**
450
     * Generates a Bundle via command line (wrapping graviton:generate:bundle)
451
     *
452
     * @param string          $namespace  Namespace
453
     * @param string          $bundleName Name of bundle
454
     * @param InputInterface  $input      Input
455
     * @param OutputInterface $output     Output
456
     *
457
     * @return void
458
     *
459
     * @throws \LogicException
460
     */
461
    private function generateBundle(
462
        $namespace,
463
        $bundleName,
464
        InputInterface $input,
465
        OutputInterface $output
466
    ) {
467
        // first, create the bundle
468
        $arguments = array(
469
            'graviton:generate:bundle',
470
            '--namespace' => $namespace,
471
            '--bundle-name' => $bundleName,
472
            '--dir' => $input->getOption('srcDir'),
473
            '--format' => $input->getOption('bundleFormat'),
474
            '--doUpdateKernel' => 'false',
475
            '--loaderBundleName' => $input->getOption('bundleBundleName'),
476
        );
477
478
        $this->runner->executeCommand(
479
            $arguments,
480
            $output,
481
            'Create bundle call failed, see above. Exiting.'
482
        );
483
    }
484
485
    /**
486
     * Generates our BundleBundle for dynamic bundles.
487
     * It basically replaces the Bundle main class that got generated
488
     * by the Sensio bundle task and it includes all of our bundles there.
489
     *
490
     * @return void
491
     */
492
    private function generateBundleBundleClass()
493
    {
494
        $dbbGenerator = new DynamicBundleBundleGenerator();
495
496
        // add optional bundles if defined by parameter.
497
        if ($this->bundleAdditions !== null) {
498
            $dbbGenerator->setAdditions($this->bundleAdditions);
499
        }
500
501
        $dbbGenerator->generate(
502
            $this->bundleBundleList,
503
            $this->bundleBundleNamespace,
504
            $this->bundleBundleClassname,
505
            $this->bundleBundleClassfile
506
        );
507
    }
508
509
    /**
510
     * Returns the path to the generated validation.xml
511
     *
512
     * @param string $namespace Namespace
513
     *
514
     * @return string path
515
     */
516
    private function getGeneratedValidationXmlPath($namespace)
517
    {
518
        return dirname(__FILE__) . '/../../../' . $namespace . '/Resources/config/validation.xml';
519
    }
520
521
    /**
522
     * Returns the field string as described in the json file
523
     *
524
     * @param JsonDefinition $jsonDef The json def
525
     *
526
     * @return string CommandLine string for the generator command
527
     */
528
    private function getFieldString(JsonDefinition $jsonDef)
529
    {
530
        $ret = array();
531
532
        foreach ($jsonDef->getFields() as $field) {
533
            // don't add 'id' field it seems..
534
            if ($field->getName() != 'id') {
535
                $ret[] = $field->getName() . ':' . $field->getTypeDoctrine();
536
            }
537
        }
538
539
        return implode(
540
            ' ',
541
            $ret
542
        );
543
    }
544
545
    /**
546
     * Checks an optional environment setting if this $routerBase is whitelisted there.
547
     * If something is 'not whitelisted' (return true) means that the controller should not be generated.
548
     * This serves as a lowlevel possibility to disable the generation of certain controllers.
549
     * If we have no whitelist defined, we consider that all services should be generated (default).
550
     *
551
     * @param string $routerBase router base
552
     *
553
     * @return bool true if yes, false if not
554
     */
555
    private function isNotWhitelistedController($routerBase)
556
    {
557
        if ($this->serviceWhitelist === null) {
558
            return false;
559
        }
560
561
        return !in_array($routerBase, $this->serviceWhitelist, true);
562
    }
563
564
    /**
565
     * renders and stores the validation.xml file of a bundle.
566
     *
567
     * what are we doing here?
568
     * well, when we started to generate our subclasses (hashes in our own service) as own
569
     * Document classes, i had the problem that the validation.xml always got overwritten by the
570
     * console task. sadly, validation.xml is one file for all classes in the bundle.
571
     * so here we merge the generated validation.xml we saved in the loop before back into the
572
     * final validation.xml again. the final result should be one validation.xml including all
573
     * the validation rules for all the documents in this bundle.
574
     *
575
     * @todo we might just make this an option to the resource generator, i need to grok why this was an issue
576
     *
577
     * @param XmlManipulator $xmlManipulator Helper to safe the validation xml file.
578
     * @param string         $location       Location where to store the file.
579
     *
580
     * @return void
581
     */
582
    private function generateValidationXml(XmlManipulator $xmlManipulator, $location)
583
    {
584
        if (file_exists($location)) {
585
            $xmlManipulator
586
                ->renderDocument(file_get_contents($location))
587
                ->saveDocument($location);
588
        }
589
    }
590
591
    /**
592
     * Generates the file containing the hash to determine if this bundle needs regeneration
593
     *
594
     * @param string $bundleDir directory of the bundle
595
     * @param string $hash      the hash to save
596
     *
597
     * @return void
598
     */
599
    private function generateGenerationHashFile($bundleDir, $hash)
600
    {
601
        $fs = new Filesystem();
602
        if ($fs->exists($bundleDir)) {
603
            $fs->dumpFile($bundleDir.DIRECTORY_SEPARATOR.self::GENERATION_HASHFILE_FILENAME, $hash);
604
        }
605
    }
606
607
    /**
608
     * Returns an XMLElement from a generated validation.xml that was generated during Resources generation.
609
     *
610
     * @param string $namespace Namespace, ie GravitonDyn\ShowcaseBundle
611
     *
612
     * @return \SimpleXMLElement The element
613
     *
614
     * @deprecated is this really used?
615
     */
616
    public function getGeneratedValidationXml($namespace)
617
    {
618
        $validationXmlPath = $this->getGeneratedValidationXmlPath($namespace);
619
        if (file_exists($validationXmlPath)) {
620
            $validationXml = new \SimpleXMLElement(file_get_contents($validationXmlPath));
621
            $validationXml->registerXPathNamespace('sy', 'http://symfony.com/schema/dic/constraint-mapping');
622
        } else {
623
            throw new \LogicException('Could not find ' . $validationXmlPath . ' that should be generated.');
624
        }
625
626
        return $validationXml;
627
    }
628
}
629