Passed
Push — main ( 1d56c7...2e11aa )
by Alexandre
21:21 queued 15:20
created

LoopInfoCommand::getAllLoopsInfo()   C

Complexity

Conditions 11
Paths 168

Size

Total Lines 78
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 44
c 1
b 0
f 1
dl 0
loc 78
rs 6.75
cc 11
nc 168
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the Thelia package.
5
 * http://www.thelia.net
6
 *
7
 * (c) OpenStudio <[email protected]>
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12
13
namespace Thelia\Command;
14
15
use Symfony\Component\Console\Helper\Table;
16
use Symfony\Component\Console\Input\InputArgument;
17
use Symfony\Component\Console\Input\InputInterface;
18
use Symfony\Component\Console\Input\InputOption;
19
use Symfony\Component\Console\Output\OutputInterface;
20
use Thelia\Model\Currency;
21
use Thelia\Model\Lang;
22
use Thelia\Type\BaseType;
23
use TheliaSmarty\Template\Plugins\TheliaLoop;
0 ignored issues
show
Bug introduced by
The type TheliaSmarty\Template\Plugins\TheliaLoop was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
24
25
/**
26
 * Give information about the given loop.
27
 * This information is the arguments names, types, default value, mandatory status, examples of use and enum possible value.
28
 *
29
 * Class LoopInfoCommand
30
 *
31
 * @author Matthias Nordest
32
 */
33
class LoopInfoCommand extends ContainerAwareCommand
34
{
35
    private TheliaLoop $theliaLoop;
36
37
    public function __construct(TheliaLoop $theliaLoop)
38
    {
39
        parent::__construct();
40
        $this->theliaLoop = $theliaLoop;
41
    }
42
43
    protected function configure(): void
44
    {
45
        $this
46
            ->setName('loop:info')
47
            ->setDescription('Displays arguments and possible enumeration values for the given loop')
48
            ->addArgument(
49
                'loop-name',
50
                InputArgument::OPTIONAL,
51
                'Name of the wanted loop'
52
            )
53
            ->addOption(
54
                'all',
55
                null,
56
                InputOption::VALUE_NONE,
57
                'Display all loop arguments and possible enumeration values in json format (mainly for parsing)'
58
            )
59
        ;
60
    }
61
62
    /**
63
     * @throws \ReflectionException
64
     */
65
    protected function execute(InputInterface $input, OutputInterface $output): int
66
    {
67
        if ($input->getOption('all')) {
68
            $this->getAllLoopsInfo();
69
        } else {
70
            $loopName = $input->getArgument('loop-name');
71
            if ($loopName === null) {
72
                trigger_error("'loop-name' argument or 'all' option missing", \E_USER_ERROR);
73
            }
74
            $this->getOneLoopInfo($output, $loopName);
75
        }
76
77
        return 0;
78
    }
79
80
    /**
81
     * Manage the display of all loops info in json format.
82
     *
83
     * @throws \ReflectionException
84
     */
85
    protected function getAllLoopsInfo(): void
86
    {
87
        // We get the list of all loops
88
        $loops = $this->theliaLoop->getLoopList();
89
        $classes = [];
90
        $allArgs = [];
91
        $result = [];
92
93
        // Put all the classes in the array $classes and associate them with the loop names associated
94
        // (A same loop can have several names)
95
        foreach ($loops as $name => $class) {
96
            if (!isset($classes[$class])) {
97
                $classes[$class] = [$name];
98
            } else {
99
                $classes[$class][] = $name;
100
            }
101
        }
102
103
        // Put all the arguments of the loops in an array $parentClass
104
        // Put the parent class name in the $result array
105
        foreach ($classes as $class => $names) {
106
            $dynamicDefinitionWarning = '';
107
            $arguments = $this->getLoopArgs($class, $dynamicDefinitionWarning);
108
            $allArgs[$names[0]] = $arguments;
109
            $result[$names[0]]['warning'] = $dynamicDefinitionWarning;
110
            $parentClass = $this->getParentLoop($class);
111
            $result[$names[0]]['extends'] = $parentClass;
112
        }
113
114
        // For each loop
115
        foreach ($allArgs as $loop => $args) {
116
            // The array of all arrays of the enum possible value by arguments
117
            // This array of array is useful in case an argument is an enum type AND an enum list type
118
            $additionalArrays = [];
119
            // The array of all titles for each enum possible value
120
            $additionalTitle = [];
121
            $positionInAdditionalArrays = 0;
122
123
            // Each loop can have several arguments
124
            foreach ($args as $arg) {
125
                // Get all the argument types
126
                $reflectionClass = new \ReflectionClass($arg->type);
127
                $property = $reflectionClass->getProperty('types');
128
                $property->setAccessible(true);
129
                $types = $property->getValue($arg->type);
130
131
                if ($arg->mandatory) {
132
                    $mandatory = 'yes';
133
                } else {
134
                    $mandatory = '';
135
                }
136
137
                $formatedTypes = '';
138
                $nbTypes = 0;
139
                $argumentExample = '';
140
                $nbEnumExample = 0;
141
142
                // Each argument can hava several types
143
                foreach ($types as $type) {
144
                    $this->parseType($nbTypes, $formatedTypes, $type, $nbEnumExample, $argumentExample, $arg->name, $additionalArrays, $additionalTitle, $positionInAdditionalArrays);
145
                }
146
147
                if ($arg->default === false) {
148
                    $default = 'false';
149
                } else {
150
                    $default = $arg->default;
151
                }
152
                $result[$loop]['args'][$arg->name] = [$formatedTypes, $default, $mandatory, $argumentExample];
153
            }
154
            // Each loop can have several enumerable arguments
155
            foreach ($additionalArrays as $index => $additionalArray) {
156
                // Each enumerable argument can have several enum possible values
157
                foreach ($additionalArray as $enumPossibility) {
158
                    $result[$loop]['enums'][$additionalTitle[$index]][] = $enumPossibility;
159
                }
160
            }
161
        }
162
        print_r(json_encode($result));
163
    }
164
165
    /**
166
     * Return the detailed loop arguments.
167
     *
168
     * @param string $loopClass                The loop class file path
169
     * @param string $dynamicDefinitionWarning To report any warnings
170
     *
171
     * @throws \ReflectionException
172
     *
173
     * @return mixed All detailed loop arguments
174
     */
175
    protected function getLoopArgs(string $loopClass, string &$dynamicDefinitionWarning): mixed
176
    {
177
        $loop = new $loopClass();
178
179
        // Need to use a protected method to get the arguments details
180
        // Reflecting the class to modify the accessibility.
181
        $reflectionClass = new \ReflectionClass($loop);
182
        $method = $reflectionClass->getMethod('getArgDefinitions');
183
        $method->setAccessible(true);
184
185
        try {
186
            // Try to execute the method to get the arguments details
187
            $result = $method->invoke($loop);
188
        } catch (\Throwable $error1) {
189
            // If execution failed, there is a dynamically defined argument detail
190
            try {
191
                // A known case of failure is the not defined request stack
192
                // Try to define one
193
                $this->initRequest();
194
                $session = $this->getContainer()->get('request_stack')->getCurrentRequest()->getSession();
195
                $session
196
                    ->setLang(Lang::getDefaultLanguage())
197
                    ->setCurrency(Currency::getDefaultCurrency())
198
                ;
199
                $this->getContainer()->get('request')->setSession($session);
200
201
                // To access the request stack's private attribute
202
                $property = $reflectionClass->getProperty('requestStack');
203
                $property->setAccessible(true);
204
                $property->setValue($loop, $this->getContainer()->get('request_stack'));
205
206
                $result = $method->invoke($loop);
207
208
                error_log("[Info] One or more \033[4;33m".$loopClass."\033[0m arguments or enum possible values are dynamically defined, you need to be careful");
209
                $dynamicDefinitionWarning = 'One or more loop argument values are defined dynamically, you need to be careful.';
210
            } catch (\Throwable $error2) {
211
                // If execution fails again, the problem will not be repaired and the result will be an empty loop.
212
                $result = $loop;
213
                $dynamicDefinitionWarning = 'An error occur when trying to get the loop arguments.';
214
215
                // The program will display errors, but will not crash
216
                error_log("[\033[31mWarning\033[0m] Error while trying to get the \033[4;33m".$loopClass."\033[0m arguments.");
217
                error_log('[Hint] This is probably due to the lack of an element instantiation in the getArgDefinitions() function, which creates and retrieves the loop arguments.');
218
                error_log($error2);
219
            }
220
        }
221
222
        return $result;
223
    }
224
225
    /**
226
     * Return the parent loop name of the loop given in parameter.
227
     *
228
     * @param string $loopClass The loop class file path
229
     *
230
     * @return string The parent loop class file path
231
     */
232
    protected function getParentLoop(string $loopClass): string
233
    {
234
        $parentClass = get_parent_class(new $loopClass());
235
236
        return $parentClass !== false ? $parentClass : '';
237
    }
238
239
    /**
240
     * Manage the display of the loop given by parameter.
241
     *
242
     * @throws \ReflectionException
243
     */
244
    protected function getOneLoopInfo(OutputInterface $output, string $loopName): void
245
    {
246
        // Get the list of all loop in the project
247
        $loops = $this->theliaLoop->getLoopList();
248
249
        // Search for the loop name given in parameter to get the loop class file path
250
        foreach ($loops as $name => $class) {
251
            if ($loopName === $name) {
252
                $loopClass = $class;
253
                break;
254
            }
255
        }
256
257
        // If the loop doesn't exist in the project
258
        if (!isset($loopClass)) {
259
            trigger_error("The loop name '".$loopName."' doesn't correspond to any loop in the project.\n".
260
                "'php Thelia loop:list' can help you find the loop name you're looking for.", \E_USER_ERROR);
261
        }
262
263
        $dynamicDefinitionWarning = '';
264
        $arguments = $this->getLoopArgs($loopClass, $dynamicDefinitionWarning);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $loopClass does not seem to be defined for all execution paths leading up to this point.
Loading history...
265
266
        // Output display
267
        echo "\033[1m\033[32m".$loopName." loop\033[0m\n\n";
268
        echo "\033[1m\033[4mArguments\033[0m\n";
269
        echo $dynamicDefinitionWarning."\n" ?? '';
270
271
        // An array to save all arguments and associated details
272
        $tableToSort = [];
273
274
        // An array to save the enum possible values
275
        $additionalArrays = [];
276
        // Another one to save the arguments relative to the previous enum possible values
277
        $additionalTitle = [];
278
        $positionInAdditionalArrays = 0;
279
280
        foreach ($arguments as $argument) {
281
            if ($argument->mandatory) {
282
                $mandatory = 'yes';
283
            } else {
284
                $mandatory = '';
285
            }
286
287
            // To access the protected attribute of the argument types
288
            $reflectionClass = new \ReflectionClass($argument->type);
289
            $property = $reflectionClass->getProperty('types');
290
            $property->setAccessible(true);
291
            $types = $property->getValue($argument->type);
292
            // To get the types list in string format
293
            $formatedTypes = '';
294
            $nbTypes = 0;
295
            $argumentExample = '';
296
            $nbEnumExample = 0;
297
298
            // There are one or many type foreach argument
299
            foreach ($types as $type) {
300
                $this->parseType($nbTypes, $formatedTypes, $type, $nbEnumExample, $argumentExample, $argument->name, $additionalArrays, $additionalTitle, $positionInAdditionalArrays);
301
            }
302
303
            if ($argument->default === false) {
304
                // If the default argument value is a Boolean false, no value is displayed
305
                // Conversion in string format
306
                $default = 'false';
307
            } else {
308
                $default = $argument->default;
309
            }
310
311
            $tableToSort[] = [$argument->name, $formatedTypes, $default, $mandatory, $argumentExample];
312
        }
313
314
        sort($tableToSort);
315
316
        // To get a table display of the arguments
317
        $table = new Table($output);
318
        foreach ($tableToSort as $line) {
319
            $table->addRow([$line[0], $line[1], $line[2], $line[3], $line[4]]);
320
        }
321
        $table
322
            ->setHeaders(['Name', 'Type', 'Default', 'Mandatory', 'Examples'])
323
            ->render()
324
        ;
325
326
        if ($additionalArrays != []) {
327
            echo "\n\033[1m\033[4mEnum possible values\033[0m\n";
328
        }
329
330
        $additionalTitleIterator = 0;
331
        // To get a table display of all the enum possible values
332
        // There may be several enumerable arguments for the loop
333
        foreach ($additionalArrays as $additionalArray) {
334
            $newTable = new Table($output);
335
            $newTable->setHeaders([$additionalTitle[$additionalTitleIterator]]);
336
            // There may also be several possible value foreach enumerable argument
337
            foreach ($additionalArray as $enumPossibility) {
338
                $newTable->addRow($enumPossibility);
339
            }
340
            ++$additionalTitleIterator;
341
            $newTable->render();
342
        }
343
    }
344
345
    /**
346
     * Manage the loop argument type analysis.
347
     *
348
     * @param int      $nbTypes                    Counts the number of types an argument has
349
     * @param string   $formatedTypes              The types list in string format
350
     * @param BaseType $type                       The argument type studied
351
     * @param int      $nbEnumExample              Counts the number of enumerable types an argument has
352
     * @param string   $argumentExample            The example of use deduced for each types of the argument
353
     * @param string   $argumentName               The argument name
354
     * @param array    $additionalArrays           An array to save the arrays of enum possible values
355
     * @param array    $additionalTitle            An array to save the arguments relative to the enum possible values
356
     * @param int      $positionInAdditionalArrays A way to know where the type is in the $additionalArrays to get an enum possible value for the $argumentExample
357
     *
358
     * @throws \ReflectionException
359
     */
360
    protected function parseType(int &$nbTypes, string &$formatedTypes, BaseType $type, int &$nbEnumExample, string &$argumentExample, string $argumentName, array &$additionalArrays, array &$additionalTitle, int &$positionInAdditionalArrays): void
361
    {
362
        ++$nbTypes;
363
        if ($nbTypes > 1) {
364
            // If there are several types for a common argument, we need to separate the elements them with 'or' or ','
365
            $formatedTypes .= ' or ';
366
            if (($type->getType() != 'Enum list type' && $type->getType() != 'Enum type') || $nbEnumExample <= 0) {
367
                $argumentExample .= ', ';
368
            }
369
        }
370
        $formatedTypes .= $type->getType();
371
        if (($type->getType() != 'Enum list type' && $type->getType() != 'Enum type') || $nbEnumExample <= 0) {
372
            $argumentExample .= $argumentName.'=';
373
        }
374
375
        if ($type->getType() == 'Enum list type' || $type->getType() == 'Enum type') {
376
            // If this is an enum type, we must generate a specific array to list all the enum possible values
377
            $this->enumArrayGenerator($additionalArrays, $additionalTitle, $type, $argumentName);
378
            if ($nbEnumExample <= 0) {
379
                // If there isn't any example for an enumerable type already
380
                // Add example with a possible enum value
381
                $argumentExample .= '"'.$additionalArrays[$positionInAdditionalArrays][0][0].'"';
382
                ++$positionInAdditionalArrays;
383
            }
384
            ++$nbEnumExample;
385
        } elseif ($type->getType() == 'Int type') {
386
            // Case-by-case treatment for each type to deduce an example
387
            $argumentExample .= '"2"';
388
        } elseif ($type->getType() == 'Int list type') {
389
            $argumentExample .= '"2", '.$argumentName.'="1,4,7"';
390
        } elseif ($type->getType() == 'Boolean type') {
391
            $argumentExample .= '"true"';
392
        } elseif ($type->getType() == 'Boolean or both type') {
393
            $argumentExample .= '"true", '.$argumentName.'="*"';
394
        } elseif ($type->getType() == 'Float type') {
395
            $argumentExample .= '"30.1"';
396
        } elseif ($type->getType() == 'Any type') {
397
            $argumentExample .= '"foo"';
398
        } elseif ($type->getType() == 'Any list type') {
399
            $argumentExample .= '"foo", '.$argumentName.'="foo,bar,baz"';
400
        } elseif ($type->getType() == 'Alphanumeric string type') {
401
            $argumentExample .= '"foo"';
402
        } elseif ($type->getType() == 'Alphanumeric string list type') {
403
            $argumentExample .= '"foo", '.$argumentName.'="foo,bar,baz"';
404
        } else {
405
            // If we don't recognize the type, there is no need to display the argument name in the examples section
406
            $argumentExample = preg_replace('/'.$argumentName.'=$/', ' ', $argumentExample);
407
            $argumentExample = preg_replace('/, /', ' ', $argumentExample);
408
        }
409
    }
410
411
    /**
412
     * Manage the generation of the enum possibles values arrays.
413
     *
414
     * @param array    $additionalArrays An array to save the arrays of enum possible values
415
     * @param array    $additionalTitle  An array to save the arguments relative to the enum possible values
416
     * @param BaseType $type             The argument type studied
417
     * @param string   $argumentName     The argument name
418
     *
419
     * @throws \ReflectionException
420
     */
421
    protected function enumArrayGenerator(array &$additionalArrays, array &$additionalTitle, BaseType $type, string $argumentName): void
422
    {
423
        // To access the type values protected attribute, where all the enum possible values are defined
424
        $reflectionClass = new \ReflectionClass($type);
425
        $property = $reflectionClass->getProperty('values');
426
        $property->setAccessible(true);
427
        $values = $property->getValue($type);
428
429
        // The new array to save the enum possible values of the studied enum type
430
        $newArray = [];
431
432
        // Specific treatment if the argument is an order enumerable to separate normal and reverse values
433
        if ($argumentName == 'order') {
434
            // An array to distinguish the normal order values
435
            $arrayNormal = [];
436
            // An array to distinguish the reverse order values
437
            $arrayReverse = [];
438
439
            // Sorting of each enum possible value according to whether they are normal or reverse
440
            foreach ($values as $value) {
441
                if (str_contains($value, '_reverse') || str_contains($value, '-reverse')) {
442
                    $arrayReverse[] = $value;
443
                } else {
444
                    $arrayNormal[] = $value;
445
                }
446
            }
447
448
            // Association of normal and reverse values
449
            // The $newArray which save the enum possible values will save pairs of normal-reverse values
450
            foreach ($arrayNormal as $normalValue) {
451
                $isReverseFind = false;
452
                foreach ($arrayReverse as $reverseValue) {
453
                    if ($normalValue.'_reverse' == $reverseValue || $normalValue.'-reverse' == $reverseValue) {
454
                        // If normal and reverse values correspond
455
                        $newArray[] = [$normalValue, $reverseValue];
456
457
                        // Search and delete the reverse value matched
458
                        $index = array_search($reverseValue, $arrayReverse);
459
                        if ($index !== false) {
460
                            unset($arrayReverse[$index]);
461
                        }
462
                        $isReverseFind = true;
463
                        break;
464
                    }
465
                }
466
                if (!$isReverseFind) {
467
                    // If no match were found, save of the normal value without any reverse one
468
                    $newArray[] = [$normalValue, ''];
469
                }
470
            }
471
472
            // Save of all the no matched reverse values without any normal one
473
            foreach ($arrayReverse as $reverseValue) {
474
                $newArray[] = ['', $reverseValue];
475
            }
476
        } else {
477
            foreach ($values as $value) {
478
                // Use of an array [$value] to enable the same treatment to be used for classic enums
479
                // and order enums with an array of normal-reverse pairs
480
                $newArray[] = [$value];
481
            }
482
        }
483
        sort($newArray);
484
485
        $additionalArrays[] = $newArray;
486
        $additionalTitle[] = $argumentName;
487
    }
488
}
489