DefinitionGenerator   D
last analyzed

Complexity

Total Complexity 49

Size/Duplication

Total Lines 412
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 19

Test Coverage

Coverage 76.83%

Importance

Changes 0
Metric Value
wmc 49
lcom 1
cbo 19
dl 0
loc 412
ccs 126
cts 164
cp 0.7683
rs 4.6787
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A getClassGenerator() 0 4 1
D generateClass() 0 112 11
A generateScopeList() 0 17 3
A generateParameterCondition() 0 12 2
A generateStartIfCondition() 0 15 3
A generateEndIfCondition() 0 4 1
A generateStaticFunctionCall() 0 5 1
A generateConstructor() 0 9 2
B generateArguments() 0 23 5
A generateSetters() 0 16 3
A generateProperty() 0 16 2
A generateReturnOperator() 0 9 2
C resolveDependency() 0 47 12

How to fix   Complexity   

Complex Class

Complex classes like DefinitionGenerator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DefinitionGenerator, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
0 ignored issues
show
Coding Style introduced by
End of line character is invalid; expected "\n" but found "\r\n"
Loading history...
2
/**
3
 * Created by Ruslan Molodyko.
4
 * Date: 09.09.2016
5
 * Time: 7:19
6
 */
7
namespace samsonframework\container\definition\builder;
8
9
use samsonframework\container\definition\ClassDefinition;
10
use samsonframework\container\definition\builder\exception\ReferenceNotImplementsException;
11
use samsonframework\container\definition\MethodDefinition;
12
use samsonframework\container\definition\PropertyDefinition;
13
use samsonframework\container\definition\reference\BoolReference;
14
use samsonframework\container\definition\reference\ClassReference;
15
use samsonframework\container\definition\reference\CollectionReference;
16
use samsonframework\container\definition\reference\ConstantReference;
17
use samsonframework\container\definition\reference\FloatReference;
18
use samsonframework\container\definition\reference\IntegerReference;
19
use samsonframework\container\definition\reference\NullReference;
20
use samsonframework\container\definition\reference\ParameterReference;
21
use samsonframework\container\definition\reference\ReferenceInterface;
22
use samsonframework\container\definition\reference\ServiceReference;
23
use samsonframework\container\definition\reference\StringReference;
24
use samsonframework\generator\ClassGenerator;
25
use samsonframework\generator\MethodGenerator;
26
27
/**
28
 * Class DefinitionCompiler
29
 *
30
 * @author Ruslan Molodyko <[email protected]>
31
 */
32
class DefinitionGenerator
33
{
34
    /** Hot to get container from container */
35
    const CONTAINER_DEPENDENCY_NAME = 'container';
36
37
    /** @var  ClassGenerator */
38
    protected $generator;
39
40
    /**
41
     * DefinitionCompiler constructor.
42
     *
43
     * @param ClassGenerator $classGenerator
44
     */
45 4
    public function __construct(ClassGenerator $classGenerator)
46
    {
47 4
        $this->generator = $classGenerator;
48 4
    }
49
50
    /**
51
     * Get class generator
52
     *
53
     * @return ClassGenerator
54
     */
55 2
    public function getClassGenerator()
56
    {
57 2
        return $this->generator;
58
    }
59
60
    /**
61
     * Compile and get container
62
     *
63
     * @param DefinitionBuilder $definitionBuilder
64
     * @return string Get container code
65
     * @throws ReferenceNotImplementsException
66
     * @throws \InvalidArgumentException
67
     */
68 2
    public function generateClass(DefinitionBuilder $definitionBuilder): string
69
    {
70
        // Generate parameters if exists
71 2
        if (count($definitionBuilder->getParameterCollection())) {
72 1
            $parameterMethodGenerator = $this->generator
73 1
                ->defMethod('parameter')
74 1
                ->defProtected()
75 1
                ->defArgument('parameterName');
76
77 1
            $isFirstCondition = true;
78
            // Generate parameters
79 1
            foreach ($definitionBuilder->getParameterCollection() as $parameterName => $reference) {
80 1
                $parameterMethodGenerator->defLine(
81 1
                    $this->generateParameterCondition($parameterName, $reference, $isFirstCondition)
82
                );
83 1
                $isFirstCondition = false;
84
            }
85
86
            // Close method
87 1
            $parameterMethodGenerator->end();
88
        }
89
90 2
        $methodGenerator = $this->generator
91 2
            ->defMethod('logic')
92 2
            ->defProtected()
93 2
            ->defArgument('classNameOrServiceName')
94 2
            ->defLine('static $singletonCollection = [];')
95
        ;
96
97 2
        $scopes = [];
98 2
        $isFirstCondition = true;
99
        /** @var ClassDefinition $classDefinition */
100 2
        foreach ($definitionBuilder->getDefinitionCollection() as $classDefinition) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
101
102
            // Get scopes
103 2
            foreach ($classDefinition->getScopes() as $scope) {
104
                $id = $scope::getId();
105
                if (!array_key_exists($id, $scopes)) {
106
                    $scopes[$id] = [];
107
                }
108
                // Store definition id
109
                $scopes[$id][] = $classDefinition->getServiceName() ?: $classDefinition->getClassName();
110
            }
111
112 2
            $className = $classDefinition->getClassName();
113 2
            $serviceName = $classDefinition->getServiceName();
114
            // Name for static service collection
115 2
            $serviceId = $serviceName ?? $className;
116
117
            // Generate if condition by class name or value
118 2
            $methodGenerator->defLine($this->generateStartIfCondition($isFirstCondition, $className, $serviceName));
119
120
            // Generate static property service access if service is singleton
121
            // TODO Move this from if condition
122 2
            if ($classDefinition->isSingleton()) {
123 2
                $methodGenerator->defLine($this->generateStaticFunctionCall($serviceId));
124
            }
125
126
            // Generate constructor call if not
127 2
            $methodGenerator->defLine($this->generateConstructor($classDefinition));
128
129
            // Generate methods
130 2
            foreach ($classDefinition->getMethodsCollection() as $methodDefinition) {
131
                // Constructor is not a method skip it
132 2
                if ($methodDefinition->getMethodName() !== '__construct') {
133 2
                    $methodGenerator->defLine($this->generateSetters($classDefinition, $methodDefinition));
134
                }
135
            }
136
137
            // Generate properties
138 2
            foreach ($classDefinition->getPropertiesCollection() as $propertyDefinition) {
139
                // Generate properties
140 2
                $methodGenerator->defLine($this->generateProperty($classDefinition, $propertyDefinition));
141
            }
142
143
            // Generate return operator
144 2
            $methodGenerator->defLine($this->generateReturnOperator($classDefinition, $serviceId));
145
146
            // Close if
147 2
            $methodGenerator->defLine($this->generateEndIfCondition());
148
149 2
            $isFirstCondition = false;
150
        }
151
152
        // Generate if condition by class name or value
153 2
        $methodGenerator->defLine($this->generateStartIfCondition(
154 2
            false,
155 2
            '\\' . $this->generator->getNamespace() . '\\' . $this->generator->getClassName(),
0 ignored issues
show
Bug introduced by
The method getNamespace() does not seem to exist on object<samsonframework\generator\ClassGenerator>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getClassName() does not seem to exist on object<samsonframework\generator\ClassGenerator>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
156
            self::CONTAINER_DEPENDENCY_NAME
157
        ));
158
159
        // Return container
160
        $methodGenerator->defLine("\treturn \$this;");
161
162
        // Close if
163
        $methodGenerator->defLine($this->generateEndIfCondition());
164
165
        // Close method
166
        $methodGenerator->end();
167
168
        // Generate constructor
169
        $constructorGenerator = $this->generator
170
            ->defMethod('__construct');
171
172
        // Generate scope list
173
        $this->generateScopeList($constructorGenerator, $scopes);
174
175
        // Close constructor
176
        $constructorGenerator->end();
177
178
        return "<?php \n" . $this->generator->code();
179
    }
180
181
    /**
182
     * Generate scope list
183
     *
184
     * @param MethodGenerator $constructorGenerator
185
     * @param array $scopes
186
     * @throws \InvalidArgumentException
187
     */
188
    protected function generateScopeList(MethodGenerator $constructorGenerator, array $scopes)
189
    {
190
        $constructorGenerator->defLine('$this->scopes = [');
191
        /**
192
         * @var string $scopeName
193
         * @var array $ids
194
         */
195
        foreach ($scopes as $scopeName => $ids) {
196
            $constructorGenerator->defLine("\t'$scopeName' => [");
197
            // Iterate service ids
198
            foreach ($ids as $id) {
199
                $constructorGenerator->defLine("\t\t'$id',");
200
            }
201
            $constructorGenerator->defLine("\t]");
202
        }
203
        $constructorGenerator->defLine('];');
204
    }
205
206
    /**
207
     * Generate start if parameter condition with smart if/else creation
208
     *
209
     * @param string $parameterName
210
     * @param ReferenceInterface $reference
211
     * @param bool $isFirstCondition
212
     * @return string
213
     * @throws ReferenceNotImplementsException
214
     */
215 1
    protected function generateParameterCondition(
216
        string $parameterName,
217
        ReferenceInterface $reference,
218
        bool $isFirstCondition = false
219
    ): string
220
    {
0 ignored issues
show
Coding Style introduced by
The closing parenthesis and the opening brace of a multi-line function declaration must be on the same line
Loading history...
221
        // If call this method first time then generate simple if or elseif construction
222 1
        $ifCondition = $isFirstCondition ? 'if' : 'elseif';
223 1
        return "$ifCondition (\$parameterName === '$parameterName') {\n" .
224 1
        "\t\t\treturn " . $this->resolveDependency($reference) . ";\n" .
225 1
        "\t\t}";
226
    }
227
228
    /**
229
     * Generate start if condition with smart if/else creation
230
     *
231
     * @param bool $isFirstCondition
232
     * @param string $className
233
     * @param string|null $serviceName
234
     * @return string
235
     */
236 2
    protected function generateStartIfCondition(
237
        bool $isFirstCondition,
238
        string $className,
239
        string $serviceName = null
240
    ): string
241
    {
0 ignored issues
show
Coding Style introduced by
The closing parenthesis and the opening brace of a multi-line function declaration must be on the same line
Loading history...
242
        // If call this method first time then generate simple if or elseif construction
243 2
        $ifCondition = $isFirstCondition ? 'if' : 'elseif';
244
        // If service name exists then add it to condition
245 2
        $serviceCondition = $serviceName ? "\$classNameOrServiceName === '$serviceName' || " : '';
246
        // Get class name without slash
247 2
        $classNameWithoutSlash = ltrim($className, '\\');
248 2
        return "$ifCondition ($serviceCondition\$classNameOrServiceName === '$className'" .
249 2
            " || \$classNameOrServiceName === '$classNameWithoutSlash') {";
250
    }
251
252
    /**
253
     * Close if condition
254
     *
255
     * @return string
256
     */
257 2
    protected function generateEndIfCondition(): string
258
    {
259 2
        return '}';
260
    }
261
262
    /**
263
     * When this service is singleton then create if with call already defined instance
264
     *
265
     * @param string $id Class or service name
266
     * @return string
267
     */
268 2
    protected function generateStaticFunctionCall(string $id): string
269
    {
270 2
        return "\tif (array_key_exists('$id', \$singletonCollection)) " .
271 2
        "{\n\t\t\t\treturn  \$singletonCollection['$id'];\n\t\t\t}";
272
    }
273
274
    /**
275
     * Generate constructor for service
276
     *
277
     * @param ClassDefinition $classDefinition
278
     * @return string
279
     * @throws ReferenceNotImplementsException
280
     */
281 2
    protected function generateConstructor(ClassDefinition $classDefinition): string
282
    {
283 2
        $className = $classDefinition->getClassName();
284 2
        $arguments = '';
285 2
        if (array_key_exists('__construct', $classDefinition->getMethodsCollection())) {
286 2
            $arguments .= $this->generateArguments($classDefinition->getMethodsCollection()['__construct']);
287
        }
288 2
        return "\t\$temp = new $className($arguments);";
289
    }
290
291
    /**
292
     * Generate arguments for method
293
     *
294
     * @param MethodDefinition $methodDefinition
295
     * @return string
296
     * @throws ReferenceNotImplementsException
297
     */
298 2
    protected function generateArguments(MethodDefinition $methodDefinition): string
299
    {
300 2
        $parameterCollection = $methodDefinition->getParametersCollection();
301
302
        // If arguments more than one then generate this ones on new line
303 2
        $newLinePrefix = count($parameterCollection) > 1 ? "\n\t\t\t\t" : '';
304
305 2
        $arguments = '';
306
        // Iterate all parameters and generate arguments
307 2
        foreach ($parameterCollection as $parameterDefinition) {
308 2
            $dependencyValue = $this->resolveDependency($parameterDefinition->getDependency());
309 2
            $arguments .= $newLinePrefix . "$dependencyValue,";
310
        }
311
        // Remove comma
312 2
        if (count($parameterCollection)) {
313 2
            $arguments = rtrim($arguments, ',');
314
        }
315
        // Add tabs
316 2
        if (count($parameterCollection) > 1) {
317 2
            $arguments .= "\n\t\t\t";
318
        }
319 2
        return $arguments;
320
    }
321
322
    /**
323
     * Generate setters for class
324
     *
325
     * @param ClassDefinition $classDefinition
326
     * @param MethodDefinition $methodDefinition
327
     * @return string
328
     * @throws ReferenceNotImplementsException
329
     */
330 2
    protected function generateSetters(ClassDefinition $classDefinition, MethodDefinition $methodDefinition): string
331
    {
332 2
        $className = $classDefinition->getClassName();
333 2
        $methodName = $methodDefinition->getMethodName();
334 2
        $arguments = $this->generateArguments($methodDefinition);
335
        // Call method by reflection
336 2
        if (!$methodDefinition->isPublic()) {
337 2
            $isEmptyArguments = count($methodDefinition->getParametersCollection()) === 0;
338 2
            return "\t\$method = (new \\ReflectionClass('$className'))->getMethod('$methodName');" .
339 2
            "\n\t\t\t\$method->setAccessible(true);" .
340 2
            "\n\t\t\t\$method->invoke(\$temp" . ($isEmptyArguments ? '' : ", $arguments") . ');' .
341 2
            "\n\t\t\t\$method->setAccessible(false);";
342
        } else {
343
            return "\t\$temp->$methodName($arguments);";
344
        }
345
    }
346
347
    /**
348
     * Generate property for class
349
     *
350
     * @param PropertyDefinition $propertyDefinition
351
     * @param ClassDefinition $classDefinition
352
     * @return string
353
     * @throws ReferenceNotImplementsException
354
     */
355 2
    protected function generateProperty(ClassDefinition $classDefinition, PropertyDefinition $propertyDefinition): string
356
    {
357 2
        $dependencyValue = $this->resolveDependency($propertyDefinition->getDependency());
358 2
        $propertyName = $propertyDefinition->getPropertyName();
359 2
        $className = $classDefinition->getClassName();
360
361 2
        if ($propertyDefinition->isPublic()) {
362
            return "\t\$temp->$propertyName = $dependencyValue;";
363
            // Use reflection to setting the value
364
        } else {
365 2
            return "\t\$property = (new \\ReflectionClass('$className'))->getProperty('$propertyName');" .
366 2
                "\n\t\t\t\$property->setAccessible(true);" .
367 2
                "\n\t\t\t\$property->setValue(\$temp, $dependencyValue);" .
368 2
                "\n\t\t\t\$property->setAccessible(false);";
369
        }
370
    }
371
372
    /**
373
     * Generate return operator and save instance to static properties it its singleton
374
     *
375
     * @param ClassDefinition $classDefinition
376
     * @param string $id
377
     * @return string
378
     */
379 2
    protected function generateReturnOperator(ClassDefinition $classDefinition, string $id = null): string
380
    {
381 2
        $code = "\treturn \$temp;";
382
        // If there is a singleton then save it to static service collection
383 2
        if ($classDefinition->isSingleton()) {
384 2
            $code = "\t\$singletonCollection['$id'] = \$temp;\n\t\t" . $code;
385
        }
386 2
        return $code;
387
    }
388
389
    /**
390
     * Resolve ReferenceInterface argument into value
391
     *
392
     * @param ReferenceInterface $reference
393
     * @return string
394
     * @throws ReferenceNotImplementsException
395
     */
396 2
    protected function resolveDependency(ReferenceInterface $reference): string
397
    {
398 2
        if ($reference instanceof ClassReference) {
399 2
            $value = $reference->getClassName();
400 2
            return "\$this->logic('$value')";
401
        } elseif ($reference instanceof NullReference) {
402
            return 'null';
403 2
        } elseif ($reference instanceof ServiceReference) {
404
            $value = $reference->getName();
405
            return "\$this->logic('$value')";
406
        } elseif ($reference instanceof StringReference) {
407 2
            $value = $reference->getValue();
408 2
            return "'$value'";
409
        } elseif ($reference instanceof FloatReference) {
410
            $value = $reference->getValue();
411
            return "$value";
412
        } elseif ($reference instanceof BoolReference) {
413 1
            $value = $reference->getValue();
414 1
            return "$value";
415
        } elseif ($reference instanceof IntegerReference) {
416 2
            $value = $reference->getValue();
417 2
            return "$value";
418 1
        } elseif ($reference instanceof ParameterReference) {
419 1
            $value = $reference->getParameterName();
420 1
            return "\$this->parameter('$value')";
421
        } elseif ($reference instanceof ConstantReference) {
422 2
            $value = $reference->getValue();
423 2
            return "$value";
424
        } elseif ($reference instanceof CollectionReference) {
425
            /** @var array $value */
426 2
            $value = $reference->getCollection();
427 2
            $string = '[';
428
            // Iterate items
429 2
            foreach ($value as $item) {
0 ignored issues
show
Bug introduced by
The expression $value of type array|string 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...
430 2
                $value = $this->resolveDependency($item->getValue());
431 2
                $key = $this->resolveDependency($item->getKey());
432 2
                $string .= "$key => $value, ";
433
            }
434
            // Remove comma
435 2
            $string = rtrim($string, ', ');
436 2
            return $string . ']';
437
        } else {
438
            throw new ReferenceNotImplementsException(sprintf(
439
                'Class "%s" does not have correct implementation in generator', get_class($reference)
440
            ));
441
        }
442
    }
443
}
444