Completed
Push — 2.x ( 6946d0...9e5583 )
by Akihito
29s queued 15s
created

Name::withAttributes()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 9.6666
c 0
b 0
f 0
cc 4
nc 6
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ray\Di;
6
7
use Ray\Di\Di\Named;
8
use Ray\Di\Di\Qualifier;
9
use ReflectionAttribute;
10
use ReflectionClass;
11
use ReflectionMethod;
12
use ReflectionParameter;
13
14
use function assert;
15
use function class_exists;
16
use function explode;
17
use function get_class;
18
use function is_object;
19
use function is_string;
20
use function preg_match;
21
use function substr;
22
use function trim;
23
24
final class Name
25
{
26
    /**
27
     * 'Unnamed' name
28
     */
29
    public const ANY = '';
30
31
    /** @var string */
32
    private $name = '';
33
34
    /**
35
     * Named database
36
     *
37
     * format: array<varName, NamedName>
38
     *
39
     * @var array<string, string>
40
     */
41
    private $names = [];
42
43
    /**
44
     * @param string|array<string, string>|null $name
45
     */
46
    public function __construct($name = null)
47
    {
48
        if ($name === null) {
49
            return;
50
        }
51
52
        if (is_string($name)) {
53
            $this->setName($name);
54
55
            return;
56
        }
57
58
        $this->names = $name;
59
    }
60
61
    /**
62
     * Create instance from PHP8 attributes
63
     *
64
     * @psalm-suppress MixedAssignment
65
     * @psalm-suppress UndefinedMethod
66
     * @psalm-suppress MixedMethodCall
67
     * @psalm-suppress MixedArrayAccess
68
     *
69
     * psalm does not know ReflectionAttribute?? PHPStan produces no type error here.
70
     */
71
    public static function withAttributes(ReflectionMethod $method): ?self
72
    {
73
        $params = $method->getParameters();
74
        $names = [];
75
        foreach ($params as $param) {
76
            /** @var array{0: ReflectionAttribute}|null $attributes */
77
            $attributes = $param->getAttributes();
78
            if ($attributes) {
79
                $names[$param->name] = self::getName($attributes);
80
            }
81
        }
82
83
        if ($names) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $names of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
84
            return new self($names);
85
        }
86
87
        return null;
88
    }
89
90
    /**
91
     * @param array{0: ReflectionAttribute} $attributes
92
     */
93
    private static function getName(array $attributes): string
94
    {
95
        $refAttribute = $attributes[0];
96
        $attribute = $refAttribute->newInstance();
97
        assert(is_object($attribute));
98
        if ($attribute instanceof Named) {
99
            return $attribute->value;
100
        }
101
102
        $isQualifer = (bool) (new ReflectionClass($attribute))->getAttributes(Qualifier::class);
103
        if ($isQualifer) {
104
            return get_class($attribute);
105
        }
106
107
        return '';
108
    }
109
110
    public function __invoke(ReflectionParameter $parameter): string
111
    {
112
        // single variable named binding
113
        if ($this->name) {
114
            return $this->name;
115
        }
116
117
        // multiple variable named binding
118
        if (isset($this->names[$parameter->name])) {
119
            return $this->names[$parameter->name];
120
        }
121
122
        // ANY match
123
        if (isset($this->names[self::ANY])) {
124
            return $this->names[self::ANY];
125
        }
126
127
        // not matched
128
        return self::ANY;
129
    }
130
131
    private function setName(string $name): void
132
    {
133
        // annotation
134
        if (class_exists($name, false)) {
135
            $this->name = $name;
136
137
            return;
138
        }
139
140
        // single name
141
        // @Named(name)
142
        if ($name === self::ANY || preg_match('/^\w+$/', $name)) {
143
            $this->name = $name;
144
145
            return;
146
        }
147
148
        // name list
149
        // @Named(varName1=name1, varName2=name2)]
150
        $this->names = $this->parseName($name);
151
    }
152
153
    /**
154
     * @return array<string, string>
0 ignored issues
show
Documentation introduced by
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
155
     */
156
    private function parseName(string $name): array
157
    {
158
        $names = [];
159
        $keyValues = explode(',', $name);
160
        foreach ($keyValues as $keyValue) {
161
            $exploded = explode('=', $keyValue);
162
            if (isset($exploded[1])) {
163
                [$key, $value] = $exploded;
0 ignored issues
show
Bug introduced by
The variable $key does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $value does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
164
                assert(is_string($key));
165
                if (isset($key[0]) && $key[0] === '$') {
166
                    $key = substr($key, 1);
167
                }
168
169
                $names[trim($key)] = trim($value);
170
            }
171
        }
172
173
        return $names;
174
    }
175
}
176