Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Completed
Pull Request — annotations (#404)
by Vincent
19:31
created

AnnotationParser::getDescriptionConfiguration()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 17
c 0
b 0
f 0
ccs 10
cts 10
cp 1
rs 9.7
cc 4
nc 6
nop 2
crap 4
1
<?php
2
3
declare (strict_types = 1);
4
5
namespace Overblog\GraphQLBundle\Config\Parser;
6
7
use Composer\Autoload\ClassLoader;
8
use Doctrine\Common\Annotations\AnnotationReader;
9
use Doctrine\Common\Annotations\AnnotationRegistry;
10
use Symfony\Component\Config\Resource\FileResource;
11
use Symfony\Component\DependencyInjection\ContainerBuilder;
12
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
13
14
class AnnotationParser implements ParserInterface
15
{
16
    private static $annotationReader = null;
17
    private static $classesMap = [];
18
19
    /**
20
     * {@inheritdoc}
21
     *
22
     * @throws \ReflectionException
23
     * @throws UnexpectedValueException
24
     */
25 13
    public static function parse(\SplFileInfo $file, ContainerBuilder $container) : array
26
    {
27 13
        $container->addResource(new FileResource($file->getRealPath()));
28
        try {
29 13
            $fileContent = \file_get_contents($file->getRealPath());
30
31 13
            $shortClassName = \substr($file->getFilename(), 0, -4);
32 13
            if (\preg_match('#namespace (.+);#', $fileContent, $namespace)) {
33 13
                $className = $namespace[1] . '\\' . $shortClassName;
34
            } else {
35
                $className = $shortClassName;
36
            }
37
38 13
            $reflexionEntity = new \ReflectionClass($className);
39
40 13
            $classAnnotations = self::getAnnotationReader()->getClassAnnotations($reflexionEntity);
41
42 13
            $properties = $reflexionEntity->getProperties();
43
44 13
            $propertiesAnnotations = [];
45 13
            foreach ($properties as $property) {
46 8
                $propertiesAnnotations[$property->getName()] = self::getAnnotationReader()->getPropertyAnnotations($property);
47
            }
48
49 13
            $methods = $reflexionEntity->getMethods();
50 13
            $methodsAnnotations = [];
51 13
            foreach ($methods as $method) {
52 3
                $methodsAnnotations[$method->getName()] = self::getAnnotationReader()->getMethodAnnotations($method);
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
53
            }
54
55 13
            $gqlTypes = [];
56
57 13
            foreach ($classAnnotations as $classAnnotation) {
58 13
                $method = false;
59 13
                switch (get_class($classAnnotation)) {
60 13
                    case 'Overblog\GraphQLBundle\Annotation\Type':
61 7
                        $gqlTypes += self::getGraphqlType($shortClassName, $classAnnotation, $classAnnotations, $propertiesAnnotations, $methodsAnnotations);
0 ignored issues
show
Documentation introduced by
$classAnnotation is of type object<Overblog\GraphQLBundle\Annotation\Type>, but the function expects a object<Overblog\GraphQLB...nfig\Parser\Annotation>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
62 7
                        break;
63 9
                    case 'Overblog\GraphQLBundle\Annotation\InputType':
64 1
                        $gqlTypes += self::getGraphqlInputType($shortClassName, $classAnnotation, $classAnnotations, $propertiesAnnotations);
0 ignored issues
show
Documentation introduced by
$classAnnotation is of type object<Overblog\GraphQLB...e\Annotation\InputType>, but the function expects a object<Overblog\GraphQLB...nfig\Parser\Annotation>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
65 1
                        break;
66 9
                    case 'Overblog\GraphQLBundle\Annotation\Scalar':
67 2
                        $gqlTypes += self::getGraphqlScalar($shortClassName, $className, $classAnnotation, $classAnnotations);
0 ignored issues
show
Documentation introduced by
$classAnnotation is of type object<Overblog\GraphQLBundle\Annotation\Scalar>, but the function expects a object<Overblog\GraphQLB...nfig\Parser\Annotation>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
68 2
                        break;
69 8
                    case 'Overblog\GraphQLBundle\Annotation\Enum':
70 1
                        $gqlTypes += self::getGraphqlEnum($shortClassName, $classAnnotation, $classAnnotations, $reflexionEntity->getConstants());
0 ignored issues
show
Documentation introduced by
$classAnnotation is of type object<Overblog\GraphQLBundle\Annotation\Enum>, but the function expects a object<Overblog\GraphQLB...nfig\Parser\Annotation>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
71 1
                        break;
72 8
                    case 'Overblog\GraphQLBundle\Annotation\Union':
73 1
                        $gqlTypes += self::getGraphqlUnion($shortClassName, $classAnnotation, $classAnnotations);
0 ignored issues
show
Documentation introduced by
$classAnnotation is of type object<Overblog\GraphQLBundle\Annotation\Union>, but the function expects a object<Overblog\GraphQLB...nfig\Parser\Annotation>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
74 1
                        break;
75 8
                    case 'Overblog\GraphQLBundle\Annotation\TypeInterface':
76 1
                        $gqlTypes += self::getGraphqlInterface($shortClassName, $classAnnotation, $classAnnotations, $propertiesAnnotations, $methodsAnnotations);
0 ignored issues
show
Documentation introduced by
$classAnnotation is of type object<Overblog\GraphQLB...notation\TypeInterface>, but the function expects a object<Overblog\GraphQLB...nfig\Parser\Annotation>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
77 1
                        break;
78
                }
79
80 13
                if ($method) {
81 13
                    $gqlTypes += self::$method($shortClassName, $classAnnotation, $propertiesAnnotations);
82
                }
83
            }
84
85 13
            return $gqlTypes;
86
        } catch (\InvalidArgumentException $e) {
87
            throw new InvalidArgumentException(\sprintf('Unable to parse file "%s".', $file), $e->getCode(), $e);
88
        }
89
    }
90
91
    /**
92
     * Retrieve annotation reader
93
     * 
94
     * @return AnnotationReader
95
     */
96 13
    private static function getAnnotationReader()
97
    {
98 13
        if (null === self::$annotationReader) {
99 1
            if (!\class_exists('\\Doctrine\\Common\\Annotations\\AnnotationReader') ||
100 1
                !\class_exists('\\Doctrine\\Common\\Annotations\\AnnotationRegistry')) {
101
                throw new \Exception('In order to use graphql annotation, you need to require doctrine annotations');
102
            }
103
104 1
            AnnotationRegistry::registerLoader('class_exists');
105 1
            self::$annotationReader = new AnnotationReader();
106
        }
107
108 13
        return self::$annotationReader;
109
    }
110
111
    /**
112
     * Create a GraphQL Type configuration from annotations on class, properties and methods
113
     * 
114
     * @param string       $shortClassName
115
     * @param Annotation   $typeAnnotation
116
     * @param Annotation[] $classAnnotations
117
     * @param Annotation[] $propertiesAnnotations
118
     * @param Annotation[] $methodsAnnotations
119
     * 
120
     * @return array
121
     */
122 7
    private static function getGraphqlType(string $shortClassName, $typeAnnotation, $classAnnotations, $propertiesAnnotations, $methodsAnnotations)
123
    {
124 7
        $typeName = $typeAnnotation->name ? : $shortClassName;
125 7
        $typeConfiguration = [];
126
127 7
        $fields = self::getGraphqlFieldsFromAnnotations($propertiesAnnotations);
128 7
        $fields += self::getGraphqlFieldsFromAnnotations($methodsAnnotations, false, true);
129
130 7
        if (empty($fields)) {
131
            return [];
132
        }
133
134 7
        $typeConfiguration['fields'] = $fields;
135
136 7
        $publicAnnotation = self::getFirstAnnotationMatching($classAnnotations, 'Overblog\GraphQLBundle\Annotation\IsPublic');
137 7
        if ($publicAnnotation) {
138 1
            $typeConfiguration['fieldsDefaultPublic'] = self::formatExpression($publicAnnotation->value);
139
        }
140
141 7
        $accessAnnotation = self::getFirstAnnotationMatching($classAnnotations, 'Overblog\GraphQLBundle\Annotation\Access');
142 7
        if ($accessAnnotation) {
143 1
            $typeConfiguration['fieldsDefaultAccess'] = self::formatExpression($accessAnnotation->value);
144
        }
145
146 7
        $typeConfiguration += self::getDescriptionConfiguration($classAnnotations);
147 7
        if ($typeAnnotation->interfaces) {
148
            $typeConfiguration['interfaces'] = $typeAnnotation->interfaces;
149
        }
150
151 7
        return [$typeName => ['type' => $typeAnnotation->isRelay ? 'relay-mutation-payload' : 'object', 'config' => $typeConfiguration]];
152
    }
153
154
    /**
155
     * Create a GraphQL Interface type configuration from annotations on properties
156
     * 
157
     * @param string       $shortClassName
158
     * @param Annotation   $interfaceAnnotation
159
     * @param Annotation[] $propertiesAnnotations
160
     * 
161
     * @return array
162
     */
163 1
    private static function getGraphqlInterface(string $shortClassName, $interfaceAnnotation, $classAnnotations, $propertiesAnnotations, $methodsAnnotations)
164
    {
165 1
        $interfaceName = $interfaceAnnotation->name ? : $shortClassName;
166 1
        $interfaceConfiguration = [];
167
168 1
        $fields = self::getGraphqlFieldsFromAnnotations($propertiesAnnotations);
169 1
        $fields += self::getGraphqlFieldsFromAnnotations($methodsAnnotations, false, true);
170
171 1
        if (empty($fields)) {
172
            return [];
173
        }
174
175 1
        $interfaceConfiguration['fields'] = $fields;
176 1
        $interfaceConfiguration += self::getDescriptionConfiguration($classAnnotations);
177
178 1
        return [$interfaceName => ['type' => 'interface', 'config' => $interfaceConfiguration]];
179
    }
180
181
    /**
182
     * Create a GraphQL Input type configuration from annotations on properties
183
     * 
184
     * @param string       $shortClassName
185
     * @param Annotation   $inputAnnotation
186
     * @param Annotation[] $propertiesAnnotations
187
     * 
188
     * @return array
189
     */
190 1
    private static function getGraphqlInputType(string $shortClassName, $inputAnnotation, $classAnnotations, $propertiesAnnotations)
191
    {
192 1
        $inputName = $inputAnnotation->name ? : self::suffixName($shortClassName, 'Input');
193 1
        $inputConfiguration = [];
194 1
        $fields = self::getGraphqlFieldsFromAnnotations($propertiesAnnotations, true);
195
196 1
        if (empty($fields)) {
197
            return [];
198
        }
199
200 1
        $inputConfiguration['fields'] = $fields;
201 1
        $inputConfiguration += self::getDescriptionConfiguration($classAnnotations);
202
203 1
        return [$inputName => ['type' => $inputAnnotation->isRelay ? 'relay-mutation-input' : 'input-object', 'config' => $inputConfiguration]];
204
    }
205
206
    /**
207
     * Get a Graphql scalar configuration from given scalar annotation 
208
     * 
209
     * @param string     $shortClassName
210
     * @param string     $className
211
     * @param Annotation $scalarAnnotation
212
     * 
213
     * @return array
214
     */
215 2
    private static function getGraphqlScalar(string $shortClassName, string $className, $scalarAnnotation, $classAnnotations)
216
    {
217 2
        $scalarName = $scalarAnnotation->name ? : $shortClassName;
218 2
        $scalarConfiguration = [];
219
220 2
        if ($scalarAnnotation->scalarType) {
221 1
            $scalarConfiguration['scalarType'] = self::formatExpression($scalarAnnotation->scalarType);
222
        } else {
223
            $scalarConfiguration = [
224 1
                'serialize' => [$className, 'serialize'],
225 1
                'parseValue' => [$className, 'parseValue'],
226 1
                'parseLiteral' => [$className, 'parseLiteral']
227
            ];
228
        }
229
230 2
        $scalarConfiguration += self::getDescriptionConfiguration($classAnnotations);
231
232 2
        return [$scalarName => ['type' => 'custom-scalar', 'config' => $scalarConfiguration]];
233
    }
234
235
    /**
236
     * Get a Graphql Enum configuration from given enum annotation 
237
     * 
238
     * @param string     $shortClassName
239
     * @param Annotation $enumAnnotation
240
     * 
241
     * @return array
242
     */
243 1
    private static function getGraphqlEnum($shortClassName, $enumAnnotation, $classAnnotations, $constants)
244
    {
245 1
        $enumName = $enumAnnotation->name ? : self::suffixName($shortClassName, 'Enum');
246 1
        $enumValues = $enumAnnotation->values ? $enumAnnotation->values : [];
247
248 1
        $values = [];
249
250 1
        foreach ($constants as $name => $value) {
251
            $valueAnnotation = current(array_filter($enumValues, function ($enumValueAnnotation) use ($name) {
252 1
                return $enumValueAnnotation->name == $name;
253 1
            }));
254 1
            $valueConfig = [];
255 1
            $valueConfig['value'] = $value;
256
257 1
            if ($valueAnnotation && $valueAnnotation->description) {
258 1
                $valueConfig['description'] = $valueAnnotation->description;
259
            }
260
261 1
            if ($valueAnnotation && $valueAnnotation->deprecationReason) {
262
                $valueConfig['deprecationReason'] = $valueAnnotation->deprecationReason;
263
            }
264
265 1
            $values[$name] = $valueConfig;
266
        }
267
268 1
        $enumConfiguration = ['values' => $values];
269 1
        $enumConfiguration += self::getDescriptionConfiguration($classAnnotations);
270
271 1
        return [$enumName => ['type' => 'enum', 'config' => $enumConfiguration]];
272
    }
273
274
    /**
275
     * Get a Graphql Union configuration from given union annotation 
276
     * 
277
     * @param string     $shortClassName
278
     * @param Annotation $unionAnnotation
279
     * 
280
     * @return array
281
     */
282 1
    private static function getGraphqlUnion($shortClassName, $unionAnnotation, $classAnnotations)
283
    {
284 1
        $unionName = $unionAnnotation->name ? : $shortClassName;
285 1
        $unionConfiguration = ['types' => $unionAnnotation->types];
286 1
        $unionConfiguration += self::getDescriptionConfiguration($classAnnotations);
287
288 1
        return [$unionName => ['type' => 'union', 'config' => $unionConfiguration]];
289
    }
290
291
    /**
292
     * Create Graphql fields configuration based on annotation
293
     * 
294
     * @param Annotation[] $annotations
295
     * @param boolean      $isInput
296
     * @param boolean      $isMethod
297
     * 
298
     * @return array
299
     */
300 9
    private static function getGraphqlFieldsFromAnnotations($annotations, $isInput = false, $isMethod = false) : array
301
    {
302 9
        $fields = [];
303 9
        foreach ($annotations as $target => $annotations) {
304 9
            $fieldAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Field');
305 9
            $accessAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Access');
306 9
            $publicAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\IsPublic');
307
308 9
            if (!$fieldAnnotation) {
309
                if ($accessAnnotation || $publicAnnotation) {
310
                    throw new InvalidArgumentException(\sprintf('The annotations "@Access" and/or "@Visible" defined on "%s" are only usable in addition of annotation @Field', $target));
311
                }
312
                continue;
313
            }
314
315
            // Ignore field with resolver when the type is an Input
316 9
            if ($fieldAnnotation->resolve && $isInput) {
317
                continue;
318
            }
319
320 9
            $propertyName = $target;
321 9
            $fieldType = $fieldAnnotation->type;
322 9
            $fieldConfiguration = [];
323 9
            if ($fieldType) {
324 7
                $fieldConfiguration['type'] = $fieldType;
325
            }
326
327 9
            $fieldConfiguration += self::getDescriptionConfiguration($annotations, true);
328
329 9
            if (!$isInput) {
330 8
                $args = [];
331 8
                if ($fieldAnnotation->args) {
332 1
                    foreach ($fieldAnnotation->args as $annotationArg) {
333 1
                        $args[$annotationArg->name] = ['type' => $annotationArg->type] + ($annotationArg->description ? ['description' => $annotationArg->description] : []);
334
                    }
335
336 1
                    if (!empty($args)) {
337 1
                        $fieldConfiguration['args'] = $args;
338
                    }
339
340
                    $args = array_map(function ($a) {
341 1
                        return sprintf("args['%s']", $a);
342 1
                    }, array_keys($args));
343
                }
344
345 8
                $propertyName = $fieldAnnotation->name ? : $propertyName;
346
347 8
                if ($fieldAnnotation->resolve) {
348 1
                    $fieldConfiguration['resolve'] = self::formatExpression($fieldAnnotation->resolve);
349
                } else {
350 8
                    if ($isMethod) {
351 2
                        $fieldConfiguration['resolve'] = self::formatExpression(sprintf("value_resolver([%s], '%s')", implode(", ", $args), $target));
352 7
                    } else if ($fieldAnnotation->name) {
353
                        $fieldConfiguration['resolve'] = self::formatExpression(sprintf("value.%s", $target));
354
                    }
355
                }
356
357 8 View Code Duplication
                if ($fieldAnnotation->argsBuilder) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
358 1
                    if (is_string($fieldAnnotation->argsBuilder)) {
359 1
                        $fieldConfiguration['argsBuilder'] = $fieldAnnotation->argsBuilder;
360 1
                    } elseif (is_array($fieldAnnotation->argsBuilder)) {
361 1
                        list($builder, $builderConfig) = $fieldAnnotation->argsBuilder;
362 1
                        $fieldConfiguration['argsBuilder'] = ['builder' => $builder, 'config' => $builderConfig];
363
                    } else {
364
                        throw new \UnexpectValueException(\sprintf('The attribute "argsBuilder" on Graphql annotation "@Field" defined on %s must be a string or an array where first index is the builder name and the second is the config.', $target));
365
                    }
366
                }
367
368 8 View Code Duplication
                if ($fieldAnnotation->fieldBuilder) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
369 2
                    if (is_string($fieldAnnotation->fieldBuilder)) {
370 2
                        $fieldConfiguration['builder'] = $fieldAnnotation->fieldBuilder;
371 2
                    } elseif (is_array($fieldAnnotation->fieldBuilder)) {
372 2
                        list($builder, $builderConfig) = $fieldAnnotation->fieldBuilder;
373 2
                        $fieldConfiguration['builder'] = $builder;
374 2
                        $fieldConfiguration['builderConfig'] = $builderConfig ? : [];
375
                    } else {
376
                        throw new \UnexpectValueException(\sprintf('The attribute "argsBuilder" on Graphql annotation "@Field" defined on %s must be a string or an array where first index is the builder name and the second is the config.', $target));
377
                    }
378
                }
379
380 8
                if ($accessAnnotation) {
381 1
                    $fieldConfiguration['access'] = self::formatExpression($accessAnnotation->value);
382
                }
383
384 8
                if ($publicAnnotation) {
385 1
                    $fieldConfiguration['public'] = self::formatExpression($publicAnnotation->value);
386
                }
387
            }
388
389 9
            $fields[$propertyName] = $fieldConfiguration;
390
        }
391
392 9
        return $fields;
393
    }
394
395
    /**
396
     * Get the first annotation matching given class
397
     * 
398
     * @param Annotation[] $annotations
399
     * @param string       $annotationClass
400
     * 
401
     * @return Annotation|false
402
     */
403 13
    private static function getFirstAnnotationMatching($annotations, $annotationClass)
404
    {
405 13
        foreach ($annotations as $annotation) {
406 13
            if ($annotation instanceof $annotationClass) {
407 13
                return $annotation;
408
            }
409
        }
410
411 10
        return false;
412
    }
413
414
    /**
415
     * Get the config for description & deprecation reason
416
     * 
417
     * @param Annotation $descriptionAnnotation
0 ignored issues
show
Bug introduced by
There is no parameter named $descriptionAnnotation. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
418
     * 
419
     * @return array
420
     */
421 13
    private static function getDescriptionConfiguration($annotations, $withDeprecation = false)
422
    {
423 13
        $config = [];
424 13
        $descriptionAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Description');
425 13
        if ($descriptionAnnotation) {
426 6
            $config['description'] = $descriptionAnnotation->value;
427
        }
428
429 13
        if ($withDeprecation) {
430 9
            $deprecatedAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Deprecated');
431 9
            if ($deprecatedAnnotation) {
432 1
                $config['deprecationReason'] = $deprecatedAnnotation->value;
433
            }
434
        }
435
436 13
        return $config;
437
    }
438
439
    /**
440
     * Format an expression (ie. add "@=" if not set)
441
     * 
442
     * @param string $expression
443
     * 
444
     * @return string
445
     */
446 6
    private static function formatExpression($expression)
447
    {
448 6
        return substr($expression, 0, 2) === '@=' ? $expression : sprintf('@=%s', $expression);
449
    }
450
451
    /**
452
     * Suffix a name if it is not already
453
     * 
454
     * @param string $name
455
     * @param string $suffix
456
     * 
457
     * @return string
458
     */
459 2
    private static function suffixName(string $name, string $suffix)
460
    {
461 2
        return substr($name, strlen($suffix)) === $suffix ? $name : sprintf("%s%s", $name, $suffix);
462
    }
463
}
464