Completed
Pull Request — develop (#349)
by Bastian
07:34
created

generateSubResources()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 27
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3
Metric Value
dl 0
loc 27
ccs 3
cts 3
cp 1
rs 8.8571
cc 3
eloc 19
nc 3
nop 5
crap 3
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
21
/**
22
 * Here, we generate all "dynamic" Graviton bundles..
23
 *
24
 * @todo     create a new Application in-situ
25
 * @todo     see if we can get rid of container dependency..
26
 *
27
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
28
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
29
 * @link     http://swisscom.ch
30
 */
31
class GenerateDynamicBundleCommand extends Command
32
{
33
    const BUNDLE_NAME_MASK = 'GravitonDyn/%sBundle';
34
35
    /** @var  string */
36
    private $bundleBundleNamespace;
37
38
    /** @var  string */
39
    private $bundleBundleDir;
40
41
    /** @var  string */
42
    private $bundleBundleClassname;
43
44
    /** @var  string */
45
    private $bundleBundleClassfile;
46
47
    /** @var  array */
48
    private $bundleBundleList = [];
49
50
    /** @var array|null */
51
    private $bundleAdditions = null;
52
53
    /** @var array|null */
54
    private $serviceWhitelist = null;
55
56
    /**
57
     * @var CommandRunner
58
     */
59
    private $runner;
60
    /**
61
     * @var LoaderInterface
62
     */
63
    private $definitionLoader;
64
    /**
65
     * @var XmlManipulator
66
     */
67
    private $xmlManipulator;
68
    /**
69
     * @var SerializerInterface
70
     */
71
    private $serializer;
72
73
74
    /**
75
     * @param CommandRunner       $runner           Runs a console command.
76
     * @param XmlManipulator      $xmlManipulator   Helper to change the content of a xml file.
77
     * @param LoaderInterface     $definitionLoader JSON definition loader
78
     * @param SerializerInterface $serializer       Serializer
79
     * @param string|null         $bundleAdditions  Additional bundles list in JSON format
80
     * @param string|null         $serviceWhitelist Service whitelist in JSON format
81
     * @param string|null         $name             The name of the command; passing null means it must be set in
82
     *                                              configure()
83
     */
84 1
    public function __construct(
85
        CommandRunner       $runner,
86
        XmlManipulator      $xmlManipulator,
87
        LoaderInterface     $definitionLoader,
88
        SerializerInterface $serializer,
89
        $bundleAdditions = null,
90
        $serviceWhitelist = null,
91
        $name = null
92
    ) {
93 1
        parent::__construct($name);
94
95 1
        $this->runner = $runner;
96 1
        $this->xmlManipulator = $xmlManipulator;
97 1
        $this->definitionLoader = $definitionLoader;
98 1
        $this->serializer = $serializer;
99
100 1
        if ($bundleAdditions !== null && $bundleAdditions !== '') {
101
            $this->bundleAdditions = $serializer->deserialize(
102
                $bundleAdditions,
103
                'array<string>',
104
                'json'
105
            );
106
        }
107 1
        if ($serviceWhitelist !== null && $serviceWhitelist !== '') {
108
            $this->serviceWhitelist = $serializer->deserialize(
109
                $serviceWhitelist,
110
                'array<string>',
111
                'json'
112
            );
113
        }
114 1
    }
115
116
    /**
117
     * {@inheritDoc}
118
     *
119
     * @return void
120
     */
121 1
    protected function configure()
122
    {
123 1
        parent::configure();
124
125 1
        $this->addOption(
126 1
            'json',
127 1
            '',
128 1
            InputOption::VALUE_OPTIONAL,
129 1
            'Path to the json definition.'
130
        )
131 1
            ->addOption(
132 1
                'srcDir',
133 1
                '',
134 1
                InputOption::VALUE_OPTIONAL,
135 1
                'Src Dir',
136 1
                dirname(__FILE__) . '/../../../'
137
            )
138 1
            ->addOption(
139 1
                'bundleBundleName',
140 1
                '',
141 1
                InputOption::VALUE_OPTIONAL,
142 1
                'Which BundleBundle to manipulate to add our stuff',
143 1
                'GravitonDynBundleBundle'
144
            )
145 1
            ->addOption(
146 1
                'bundleFormat',
147 1
                '',
148 1
                InputOption::VALUE_OPTIONAL,
149 1
                'Which format',
150 1
                'xml'
151
            )
152 1
            ->setName('graviton:generate:dynamicbundles')
153 1
            ->setDescription(
154
                'Generates all dynamic bundles in the GravitonDyn namespace. Either give a path
155 1
                    to a single JSON file or a directory path containing multiple files.'
156
            );
157 1
    }
158
159
    /**
160
     * {@inheritDoc}
161
     *
162
     * @param InputInterface  $input  input
163
     * @param OutputInterface $output output
164
     *
165
     * @return void
166
     */
167 1
    protected function execute(InputInterface $input, OutputInterface $output)
168
    {
169
        /**
170
         * GENERATE THE BUNDLEBUNDLE
171
         */
172 1
        $namespace = sprintf(self::BUNDLE_NAME_MASK, 'Bundle');
173
174
        // GravitonDynBundleBundle
175 1
        $bundleName = str_replace('/', '', $namespace);
176
177
        // bundlebundle stuff..
178 1
        $this->bundleBundleNamespace = $namespace;
179 1
        $this->bundleBundleDir = $input->getOption('srcDir') . $namespace;
180 1
        $this->bundleBundleClassname = $bundleName;
181 1
        $this->bundleBundleClassfile = $this->bundleBundleDir . '/' . $this->bundleBundleClassname . '.php';
182
183 1
        $filesToWorkOn = $this->definitionLoader->load($input->getOption('json'));
184
185 1
        if (count($filesToWorkOn) < 1) {
186 1
            throw new \LogicException("Could not find any usable JSON files.");
187
        }
188
189
        /**
190
         * GENERATE THE BUNDLE(S)
191
         */
192
        foreach ($filesToWorkOn as $jsonDef) {
193
            $thisIdName = $jsonDef->getId();
194
            $namespace = sprintf(self::BUNDLE_NAME_MASK, $thisIdName);
195
196
            $jsonDef->setNamespace($namespace);
197
198
            $bundleName = str_replace('/', '', $namespace);
199
            $this->bundleBundleList[] = $namespace;
200
201
            try {
202
                $this->generateBundle($namespace, $bundleName, $input, $output);
203
                $this->generateBundleBundleClass();
204
                $this->generateSubResources($output, $jsonDef, $this->xmlManipulator, $bundleName, $namespace);
205
                $this->generateMainResource($output, $jsonDef, $bundleName);
206
                $this->generateValidationXml($this->xmlManipulator, $this->getGeneratedValidationXmlPath($namespace));
207
208
                $output->writeln('');
209
                $output->writeln(
210
                    sprintf('<info>Generated "%s" from definition %s</info>', $bundleName, $jsonDef->getId())
211
                );
212
                $output->writeln('');
213
            } catch (\Exception $e) {
214
                $output->writeln(
215
                    sprintf('<error>%s</error>', $e->getMessage())
216
                );
217
218
                // remove failed bundle from list
219
                array_pop($this->bundleBundleList);
220
            }
221
222
            $this->xmlManipulator->reset();
223
        }
224
    }
225
226
    /**
227
     * Generate Bundle entities
228
     *
229
     * @param OutputInterface $output         Instance to sent text to be displayed on stout.
230
     * @param JsonDefinition  $jsonDef        Configuration to be generated the entity from.
231
     * @param XmlManipulator  $xmlManipulator Helper to safe the validation xml file.
232
     * @param string          $bundleName     Name of the bundle the entity shall be generated for.
233
     * @param string          $namespace      Absolute path to the bundle root dir.
234
     *
235
     * @return void
236
     * @throws \Exception
237
     */
238 2
    protected function generateSubResources(
239
        OutputInterface $output,
240
        JsonDefinition $jsonDef,
241
        XmlManipulator $xmlManipulator,
242
        $bundleName,
243
        $namespace
244
    ) {
245 2
        foreach ($this->getSubResources($jsonDef) as $subRecource) {
246
            $arguments = [
247
                'graviton:generate:resource',
248
                '--entity' => $bundleName . ':' . $subRecource->getId(),
249
                '--format' => 'xml',
250
                '--json' => $this->serializer->serialize($subRecource->getDef(), 'json'),
251
                '--fields' => $this->getFieldString($subRecource),
252
                '--with-repository' => null,
253
                '--no-controller' => 'true',
254
            ];
255
            $this->generateResource($arguments, $output, $jsonDef);
256
257
            // look for validation.xml and save it from over-writing ;-)
258
            // we basically get the xml content that was generated in order to save them later..
259
            $validationXml = $this->getGeneratedValidationXmlPath($namespace);
260
            if (file_exists($validationXml)) {
261
                $xmlManipulator->addNodes(file_get_contents($validationXml));
262
            }
263
        }
264 2
    }
265
266
    /**
267
     * Generate the actual Bundle
268
     *
269
     * @param OutputInterface $output     Instance to sent text to be displayed on stout.
270
     * @param JsonDefinition  $jsonDef    Configuration to be generated the entity from.
271
     * @param string          $bundleName Name of the bundle the entity shall be generated for.
272
     *
273
     * @return void
274
     */
275
    protected function generateMainResource(OutputInterface $output, JsonDefinition $jsonDef, $bundleName)
276
    {
277
        $fields = $jsonDef->getFields();
278
        if (!empty($fields)) {
279
            $arguments = array(
280
                'graviton:generate:resource',
281
                '--entity' => $bundleName . ':' . $jsonDef->getId(),
282
                '--json' => $this->serializer->serialize($jsonDef->getDef(), 'json'),
283
                '--format' => 'xml',
284
                '--fields' => $this->getFieldString($jsonDef),
285
                '--with-repository' => null
286
            );
287
288
            $this->generateResource($arguments, $output, $jsonDef);
289
        }
290
    }
291
292
    /**
293
     * Get all sub hashes
294
     *
295
     * @param JsonDefinition $definition Main JSON definition
296
     * @return JsonDefinition[]
297
     */
298 3
    protected function getSubResources(JsonDefinition $definition)
299
    {
300 3
        $resources = [];
301 3
        foreach ($definition->getFields() as $field) {
302 2
            while ($field instanceof JsonDefinitionArray) {
303 2
                $field = $field->getElement();
304
            }
305 2
            if (!$field instanceof JsonDefinitionHash) {
306 2
                continue;
307
            }
308
309 1
            $subDefiniton = $field->getJsonDefinition();
310
311 1
            $resources = array_merge($this->getSubResources($subDefiniton), $resources);
312 1
            $resources[] = $subDefiniton;
313
        }
314
315 3
        return $resources;
316
    }
317
318
    /**
319
     * Gathers data for the command to run.
320
     *
321
     * @param array           $arguments Set of cli arguments passed to the command
322
     * @param OutputInterface $output    Output channel to send messages to.
323
     * @param JsonDefinition  $jsonDef   Configuration of the service
324
     *
325
     * @return void
326
     * @throws \LogicException
327
     */
328
    private function generateResource(array $arguments, OutputInterface $output, JsonDefinition $jsonDef)
329
    {
330
        // controller?
331
        $routerBase = $jsonDef->getRouterBase();
332
        if ($routerBase === false || $this->isNotWhitelistedController($routerBase)) {
333
            $arguments['--no-controller'] = 'true';
334
        }
335
336
        $this->runner->executeCommand($arguments, $output, 'Create resource call failed, see above. Exiting.');
337
    }
338
339
    /**
340
     * Generates a Bundle via command line (wrapping graviton:generate:bundle)
341
     *
342
     * @param string          $namespace  Namespace
343
     * @param string          $bundleName Name of bundle
344
     * @param InputInterface  $input      Input
345
     * @param OutputInterface $output     Output
346
     *
347
     * @return void
348
     *
349
     * @throws \LogicException
350
     */
351
    private function generateBundle(
352
        $namespace,
353
        $bundleName,
354
        InputInterface $input,
355
        OutputInterface $output
356
    ) {
357
        // first, create the bundle
358
        $arguments = array(
359
            'graviton:generate:bundle',
360
            '--namespace' => $namespace,
361
            '--bundle-name' => $bundleName,
362
            '--dir' => $input->getOption('srcDir'),
363
            '--format' => $input->getOption('bundleFormat'),
364
            '--doUpdateKernel' => 'false',
365
            '--loaderBundleName' => $input->getOption('bundleBundleName'),
366
            '--structure' => null
367
        );
368
369
        $this->runner->executeCommand(
370
            $arguments,
371
            $output,
372
            'Create bundle call failed, see above. Exiting.'
373
        );
374
    }
375
376
    /**
377
     * Generates our BundleBundle for dynamic bundles.
378
     * It basically replaces the Bundle main class that got generated
379
     * by the Sensio bundle task and it includes all of our bundles there.
380
     *
381
     * @return void
382
     */
383
    private function generateBundleBundleClass()
384
    {
385
        $dbbGenerator = new DynamicBundleBundleGenerator();
386
387
        // add optional bundles if defined by parameter.
388
        if ($this->bundleAdditions !== null) {
389
            $dbbGenerator->setAdditions($this->bundleAdditions);
390
        }
391
392
        $dbbGenerator->generate(
393
            $this->bundleBundleList,
394
            $this->bundleBundleNamespace,
395
            $this->bundleBundleClassname,
396
            $this->bundleBundleClassfile
397
        );
398
    }
399
400
    /**
401
     * Returns the path to the generated validation.xml
402
     *
403
     * @param string $namespace Namespace
404
     *
405
     * @return string path
406
     */
407
    private function getGeneratedValidationXmlPath($namespace)
408
    {
409
        return dirname(__FILE__) . '/../../../' . $namespace . '/Resources/config/validation.xml';
410
    }
411
412
    /**
413
     * Returns the field string as described in the json file
414
     *
415
     * @param JsonDefinition $jsonDef The json def
416
     *
417
     * @return string CommandLine string for the generator command
418
     */
419
    private function getFieldString(JsonDefinition $jsonDef)
420
    {
421
        $ret = array();
422
423
        foreach ($jsonDef->getFields() as $field) {
424
            // don't add 'id' field it seems..
425
            if ($field->getName() != 'id') {
426
                $ret[] = $field->getName() . ':' . $field->getTypeDoctrine();
427
            }
428
        }
429
430
        return implode(
431
            ' ',
432
            $ret
433
        );
434
    }
435
436
    /**
437
     * Checks an optional environment setting if this $routerBase is whitelisted there.
438
     * If something is 'not whitelisted' (return true) means that the controller should not be generated.
439
     * This serves as a lowlevel possibility to disable the generation of certain controllers.
440
     * If we have no whitelist defined, we consider that all services should be generated (default).
441
     *
442
     * @param string $routerBase router base
443
     *
444
     * @return bool true if yes, false if not
445
     */
446
    private function isNotWhitelistedController($routerBase)
447
    {
448
        if ($this->serviceWhitelist === null) {
449
            return false;
450
        }
451
452
        return !in_array($routerBase, $this->serviceWhitelist, true);
453
    }
454
455
    /**
456
     * renders and stores the validation.xml file of a bundle.
457
     *
458
     * what are we doing here?
459
     * well, when we started to generate our subclasses (hashes in our own service) as own
460
     * Document classes, i had the problem that the validation.xml always got overwritten by the
461
     * console task. sadly, validation.xml is one file for all classes in the bundle.
462
     * so here we merge the generated validation.xml we saved in the loop before back into the
463
     * final validation.xml again. the final result should be one validation.xml including all
464
     * the validation rules for all the documents in this bundle.
465
     *
466
     * @todo we might just make this an option to the resource generator, i need to grok why this was an issue
467
     *
468
     * @param XmlManipulator $xmlManipulator Helper to safe the validation xml file.
469
     * @param string         $location       Location where to store the file.
470
     *
471
     * @return void
472
     */
473
    private function generateValidationXml(XmlManipulator $xmlManipulator, $location)
474
    {
475
        if (file_exists($location)) {
476
            $xmlManipulator
477
                ->renderDocument(file_get_contents($location))
478
                ->saveDocument($location);
479
        }
480
    }
481
482
    /**
483
     * Returns an XMLElement from a generated validation.xml that was generated during Resources generation.
484
     *
485
     * @param string $namespace Namespace, ie GravitonDyn\ShowcaseBundle
486
     *
487
     * @return \SimpleXMLElement The element
488
     *
489
     * @deprecated is this really used?
490
     */
491
    public function getGeneratedValidationXml($namespace)
492
    {
493
        $validationXmlPath = $this->getGeneratedValidationXmlPath($namespace);
494
        if (file_exists($validationXmlPath)) {
495
            $validationXml = new \SimpleXMLElement(file_get_contents($validationXmlPath));
496
            $validationXml->registerXPathNamespace('sy', 'http://symfony.com/schema/dic/constraint-mapping');
497
        } else {
498
            throw new \LogicException('Could not find ' . $validationXmlPath . ' that should be generated.');
499
        }
500
501
        return $validationXml;
502
    }
503
}
504