Completed
Pull Request — develop (#487)
by Lucas
100:12 queued 61:52
created

GenerateDynamicBundleCommand::__construct()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 23
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.7283

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 9
cts 13
cp 0.6923
rs 8.5906
c 0
b 0
f 0
cc 5
eloc 17
nc 4
nop 7
crap 5.7283
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 6
    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 6
        parent::__construct($name);
94
95 6
        $this->runner = $runner;
96 6
        $this->xmlManipulator = $xmlManipulator;
97 6
        $this->definitionLoader = $definitionLoader;
98 6
        $this->serializer = $serializer;
99
100 6
        if ($bundleAdditions !== null && $bundleAdditions !== '') {
101
            $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...
102
        }
103 6
        if ($serviceWhitelist !== null && $serviceWhitelist !== '') {
104
            $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...
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
            'Path to the json definition.'
122 6
        )
123 6
            ->addOption(
124 6
                'srcDir',
125 6
                '',
126 6
                InputOption::VALUE_OPTIONAL,
127 6
                'Src Dir',
128 6
                dirname(__FILE__) . '/../../../'
129 6
            )
130 6
            ->addOption(
131 6
                'bundleBundleName',
132 6
                '',
133 6
                InputOption::VALUE_OPTIONAL,
134 6
                'Which BundleBundle to manipulate to add our stuff',
135
                'GravitonDynBundleBundle'
136 6
            )
137 6
            ->addOption(
138 6
                'bundleFormat',
139 6
                '',
140 6
                InputOption::VALUE_OPTIONAL,
141 6
                'Which format',
142
                'xml'
143 6
            )
144 6
            ->setName('graviton:generate:dynamicbundles')
145 6
            ->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 6
            );
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
        /**
182
         * GENERATE THE BUNDLE(S)
183
         */
184
        foreach ($filesToWorkOn as $jsonDef) {
185
            $thisIdName = $jsonDef->getId();
186
            $namespace = sprintf(self::BUNDLE_NAME_MASK, $thisIdName);
187
188
            $jsonDef->setNamespace($namespace);
189
190
            $bundleName = str_replace('/', '', $namespace);
191
            $this->bundleBundleList[] = $namespace;
192
193
            try {
194
                $this->generateBundle($namespace, $bundleName, $input, $output);
195
                $this->generateBundleBundleClass();
196
                $this->generateSubResources($output, $jsonDef, $this->xmlManipulator, $bundleName, $namespace);
197
                $this->generateMainResource($output, $jsonDef, $bundleName);
198
                $this->generateValidationXml($this->xmlManipulator, $this->getGeneratedValidationXmlPath($namespace));
199
200
                $output->writeln('');
201
                $output->writeln(
202
                    sprintf('<info>Generated "%s" from definition %s</info>', $bundleName, $jsonDef->getId())
203
                );
204
                $output->writeln('');
205
            } catch (\Exception $e) {
206
                $output->writeln(
207
                    sprintf('<error>%s</error>', $e->getMessage())
208
                );
209
210
                // remove failed bundle from list
211
                array_pop($this->bundleBundleList);
212
            }
213
214
            $this->xmlManipulator->reset();
215
        }
216
    }
217
218
    /**
219
     * Generate Bundle entities
220
     *
221
     * @param OutputInterface $output         Instance to sent text to be displayed on stout.
222
     * @param JsonDefinition  $jsonDef        Configuration to be generated the entity from.
223
     * @param XmlManipulator  $xmlManipulator Helper to safe the validation xml file.
224
     * @param string          $bundleName     Name of the bundle the entity shall be generated for.
225
     * @param string          $namespace      Absolute path to the bundle root dir.
226
     *
227
     * @return void
228
     * @throws \Exception
229
     */
230 4
    protected function generateSubResources(
231
        OutputInterface $output,
232
        JsonDefinition $jsonDef,
233
        XmlManipulator $xmlManipulator,
234
        $bundleName,
235
        $namespace
236
    ) {
237 4
        foreach ($this->getSubResources($jsonDef) as $subRecource) {
238
            $arguments = [
239
                'graviton:generate:resource',
240
                '--entity' => $bundleName . ':' . $subRecource->getId(),
241
                '--format' => 'xml',
242
                '--json' => $this->serializer->serialize($subRecource->getDef(), 'json'),
243
                '--fields' => $this->getFieldString($subRecource),
244
                '--no-controller' => 'true',
245
            ];
246
            $this->generateResource($arguments, $output, $jsonDef);
247
248
            // look for validation.xml and save it from over-writing ;-)
249
            // we basically get the xml content that was generated in order to save them later..
250
            $validationXml = $this->getGeneratedValidationXmlPath($namespace);
251
            if (file_exists($validationXml)) {
252
                $xmlManipulator->addNodes(file_get_contents($validationXml));
253
            }
254 4
        }
255 4
    }
256
257
    /**
258
     * Generate the actual Bundle
259
     *
260
     * @param OutputInterface $output     Instance to sent text to be displayed on stout.
261
     * @param JsonDefinition  $jsonDef    Configuration to be generated the entity from.
262
     * @param string          $bundleName Name of the bundle the entity shall be generated for.
263
     *
264
     * @return void
265
     */
266
    protected function generateMainResource(OutputInterface $output, JsonDefinition $jsonDef, $bundleName)
267
    {
268
        $fields = $jsonDef->getFields();
269
        if (!empty($fields)) {
270
            $arguments = array(
271
                'graviton:generate:resource',
272
                '--entity' => $bundleName . ':' . $jsonDef->getId(),
273
                '--json' => $this->serializer->serialize($jsonDef->getDef(), 'json'),
274
                '--format' => 'xml',
275
                '--fields' => $this->getFieldString($jsonDef)
276
            );
277
278
            $this->generateResource($arguments, $output, $jsonDef);
279
        }
280
    }
281
282
    /**
283
     * Get all sub hashes
284
     *
285
     * @param JsonDefinition $definition Main JSON definition
286
     * @return JsonDefinition[]
287
     */
288 6
    protected function getSubResources(JsonDefinition $definition)
289
    {
290 6
        $resources = [];
291 6
        foreach ($definition->getFields() as $field) {
292 4
            while ($field instanceof JsonDefinitionArray) {
293 4
                $field = $field->getElement();
294 4
            }
295 4
            if (!$field instanceof JsonDefinitionHash) {
296 4
                continue;
297
            }
298
299 2
            $subDefiniton = $field->getJsonDefinition();
300
301 2
            $resources = array_merge($this->getSubResources($subDefiniton), $resources);
302 2
            $resources[] = $subDefiniton;
303 6
        }
304
305 6
        return $resources;
306
    }
307
308
    /**
309
     * Gathers data for the command to run.
310
     *
311
     * @param array           $arguments Set of cli arguments passed to the command
312
     * @param OutputInterface $output    Output channel to send messages to.
313
     * @param JsonDefinition  $jsonDef   Configuration of the service
314
     *
315
     * @return void
316
     * @throws \LogicException
317
     */
318
    private function generateResource(array $arguments, OutputInterface $output, JsonDefinition $jsonDef)
319
    {
320
        // controller?
321
        $routerBase = $jsonDef->getRouterBase();
322
        if ($routerBase === false || $this->isNotWhitelistedController($routerBase)) {
323
            $arguments['--no-controller'] = 'true';
324
        }
325
326
        $this->runner->executeCommand($arguments, $output, 'Create resource call failed, see above. Exiting.');
327
    }
328
329
    /**
330
     * Generates a Bundle via command line (wrapping graviton:generate:bundle)
331
     *
332
     * @param string          $namespace  Namespace
333
     * @param string          $bundleName Name of bundle
334
     * @param InputInterface  $input      Input
335
     * @param OutputInterface $output     Output
336
     *
337
     * @return void
338
     *
339
     * @throws \LogicException
340
     */
341
    private function generateBundle(
342
        $namespace,
343
        $bundleName,
344
        InputInterface $input,
345
        OutputInterface $output
346
    ) {
347
        // first, create the bundle
348
        $arguments = array(
349
            'graviton:generate:bundle',
350
            '--namespace' => $namespace,
351
            '--bundle-name' => $bundleName,
352
            '--dir' => $input->getOption('srcDir'),
353
            '--format' => $input->getOption('bundleFormat'),
354
            '--doUpdateKernel' => 'false',
355
            '--loaderBundleName' => $input->getOption('bundleBundleName'),
356
        );
357
358
        $this->runner->executeCommand(
359
            $arguments,
360
            $output,
361
            'Create bundle call failed, see above. Exiting.'
362
        );
363
    }
364
365
    /**
366
     * Generates our BundleBundle for dynamic bundles.
367
     * It basically replaces the Bundle main class that got generated
368
     * by the Sensio bundle task and it includes all of our bundles there.
369
     *
370
     * @return void
371
     */
372
    private function generateBundleBundleClass()
373
    {
374
        $dbbGenerator = new DynamicBundleBundleGenerator();
375
376
        // add optional bundles if defined by parameter.
377
        if ($this->bundleAdditions !== null) {
378
            $dbbGenerator->setAdditions($this->bundleAdditions);
379
        }
380
381
        $dbbGenerator->generate(
382
            $this->bundleBundleList,
383
            $this->bundleBundleNamespace,
384
            $this->bundleBundleClassname,
385
            $this->bundleBundleClassfile
386
        );
387
    }
388
389
    /**
390
     * Returns the path to the generated validation.xml
391
     *
392
     * @param string $namespace Namespace
393
     *
394
     * @return string path
395
     */
396
    private function getGeneratedValidationXmlPath($namespace)
397
    {
398
        return dirname(__FILE__) . '/../../../' . $namespace . '/Resources/config/validation.xml';
399
    }
400
401
    /**
402
     * Returns the field string as described in the json file
403
     *
404
     * @param JsonDefinition $jsonDef The json def
405
     *
406
     * @return string CommandLine string for the generator command
407
     */
408
    private function getFieldString(JsonDefinition $jsonDef)
409
    {
410
        $ret = array();
411
412
        foreach ($jsonDef->getFields() as $field) {
413
            // don't add 'id' field it seems..
414
            if ($field->getName() != 'id') {
415
                $ret[] = $field->getName() . ':' . $field->getTypeDoctrine();
416
            }
417
        }
418
419
        return implode(
420
            ' ',
421
            $ret
422
        );
423
    }
424
425
    /**
426
     * Checks an optional environment setting if this $routerBase is whitelisted there.
427
     * If something is 'not whitelisted' (return true) means that the controller should not be generated.
428
     * This serves as a lowlevel possibility to disable the generation of certain controllers.
429
     * If we have no whitelist defined, we consider that all services should be generated (default).
430
     *
431
     * @param string $routerBase router base
432
     *
433
     * @return bool true if yes, false if not
434
     */
435
    private function isNotWhitelistedController($routerBase)
436
    {
437
        if ($this->serviceWhitelist === null) {
438
            return false;
439
        }
440
441
        return !in_array($routerBase, $this->serviceWhitelist, true);
442
    }
443
444
    /**
445
     * renders and stores the validation.xml file of a bundle.
446
     *
447
     * what are we doing here?
448
     * well, when we started to generate our subclasses (hashes in our own service) as own
449
     * Document classes, i had the problem that the validation.xml always got overwritten by the
450
     * console task. sadly, validation.xml is one file for all classes in the bundle.
451
     * so here we merge the generated validation.xml we saved in the loop before back into the
452
     * final validation.xml again. the final result should be one validation.xml including all
453
     * the validation rules for all the documents in this bundle.
454
     *
455
     * @todo we might just make this an option to the resource generator, i need to grok why this was an issue
456
     *
457
     * @param XmlManipulator $xmlManipulator Helper to safe the validation xml file.
458
     * @param string         $location       Location where to store the file.
459
     *
460
     * @return void
461
     */
462
    private function generateValidationXml(XmlManipulator $xmlManipulator, $location)
463
    {
464
        if (file_exists($location)) {
465
            $xmlManipulator
466
                ->renderDocument(file_get_contents($location))
467
                ->saveDocument($location);
468
        }
469
    }
470
471
    /**
472
     * Returns an XMLElement from a generated validation.xml that was generated during Resources generation.
473
     *
474
     * @param string $namespace Namespace, ie GravitonDyn\ShowcaseBundle
475
     *
476
     * @return \SimpleXMLElement The element
477
     *
478
     * @deprecated is this really used?
479
     */
480
    public function getGeneratedValidationXml($namespace)
481
    {
482
        $validationXmlPath = $this->getGeneratedValidationXmlPath($namespace);
483
        if (file_exists($validationXmlPath)) {
484
            $validationXml = new \SimpleXMLElement(file_get_contents($validationXmlPath));
485
            $validationXml->registerXPathNamespace('sy', 'http://symfony.com/schema/dic/constraint-mapping');
486
        } else {
487
            throw new \LogicException('Could not find ' . $validationXmlPath . ' that should be generated.');
488
        }
489
490
        return $validationXml;
491
    }
492
}
493