CallableCollectionVoter   B
last analyzed

Complexity

Total Complexity 36

Size/Duplication

Total Lines 240
Duplicated Lines 0 %

Coupling/Cohesion

Dependencies 3

Test Coverage

Coverage 98.7%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 36
c 2
b 0
f 0
cbo 3
dl 0
loc 240
ccs 76
cts 77
cp 0.987
rs 8.8

5 Methods

Rating   Name   Duplication   Size   Complexity  
D supports() 0 38 9
A voteOnAttribute() 0 19 3
C normalizeCallable() 0 50 13
C gatherParameters() 0 67 10
A __construct() 0 6 1
1
<?php
2
3
namespace Yokai\SecurityExtraBundle\Voter;
4
5
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
6
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
7
use Yokai\SecurityExtraBundle\Exception\LogicException;
8
9
/**
10
 * @author Yann Eugoné <[email protected]>
11
 */
12
class CallableCollectionVoter extends Voter
13
{
14
    /**
15
     * Attribute list this is supporting.
16
     *
17
     * @var array<string>
18
     */
19
    private $supportedAttributes;
20
21
    /**
22
     * Subject types list this is supporting.
23
     *
24
     * @var array<string>
25
     */
26
    private $supportedSubjects;
27
28
    /**
29
     * Callable collection this must call.
30
     *
31
     * @var array<mixed>
32
     */
33
    private $callables;
34
35
    /**
36
     * @param array<string> $supportedAttributes Attribute list this is supporting
37
     * @param array<string> $supportedSubjects   Subject types list this is supporting
38
     * @param array<mixed>  $callables           Callable collection this must call
39
     */
40 9
    public function __construct($supportedAttributes, $supportedSubjects, $callables)
41
    {
42 9
        $this->supportedAttributes = $supportedAttributes;
43 9
        $this->supportedSubjects = $supportedSubjects;
44 9
        $this->callables = $callables;
45 9
    }
46
47
    /**
48
     * @inheritDoc
49
     */
50 9
    protected function supports($attribute, $subject)
51
    {
52
        // if at least one supported attribute is configured
53
        // check if provided attribute is in that list
54 9
        if (count($this->supportedAttributes) > 0 && !in_array($attribute, $this->supportedAttributes, true)) {
55 3
            return false;
56
        }
57
58
        // if there is no subject
59
        // of if there is not at least one supported subject
60
        // this is supporting
61 9
        if ($subject === null || count($this->supportedSubjects) === 0) {
62 6
            return true;
63
        }
64
65
        // iterate over supported subjects
66 4
        foreach ($this->supportedSubjects as $supportedSubject) {
67
            // if supported subject is a class (or interface)
68
            // this supports if subject is an instance of
69 4
            if (class_exists($supportedSubject)) {
70 4
                if ($subject instanceof $supportedSubject) {
71 4
                    return true;
72
                }
73
74 2
                continue;
75
            }
76
77
            // if supported subject is not a class, it must be an internal type
78
            // this support if subject type is the same
79 1
            if (gettype($subject) === $supportedSubject) {
80 1
                return true;
81
            }
82
        }
83
84
        // supported attribute but unsupported subject
85
86 1
        return false;
87
    }
88
89
    /**
90
     * @inheritDoc
91
     */
92 9
    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
93
    {
94
        // iterate over configured callables
95 9
        foreach ($this->callables as $callable) {
96 9
            $callable = $this->normalizeCallable($callable, $subject);
97 5
            $parameters = $this->gatherParameters($callable, $attribute, $subject, $token);
98
99
            // if one callable returns falsy result
100
            // this deny access
101 4
            if (!(bool) call_user_func_array($callable, $parameters)) {
102 4
                return false;
103
            }
104
        }
105
106
        // no callable returns falsy result
107
        // this grand access
108
109 4
        return true;
110
    }
111
112
    /**
113
     * Normalizes a callable.
114
     * Will return a callable array. See http://php.net/manual/en/language.types.callable.php .
115
     *
116
     * @param string|object|array $callable The callable to normalize
117
     * @param mixed               $subject  The subject being voting on
118
     *
119
     * @return array The normalized callable
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<object|string>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
120
     * @throws LogicException
121
     */
122 9
    private function normalizeCallable($callable, $subject)
0 ignored issues
show
Complexity introduced by
This operation has 1400 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

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

Loading history...
123
    {
124
        // callable is a string
125
        // it should be a method to call on subject
126 9
        if (is_string($callable)) {
127 3
            if (!is_object($subject) || !is_callable([$subject, $callable])) {
128 1
                throw new LogicException(
129
                    sprintf(
130 1
                        'Provided string callable "%s", but subject "%s" has no callable method with that name.',
131 1
                        (string) $callable,
132 1
                        is_object($subject) ? get_class($subject) : gettype($subject)
133
                    )
134
                );
135
            }
136
137 2
            return [$subject, $callable];
138
        }
139
140
        // callable is an object
141
        // it should have an __invoke method
142 8
        if (is_object($callable)) {
143 5
            if (!is_callable([$callable, '__invoke'])) {
144 1
                throw new LogicException(
145
                    sprintf(
146 1
                        'Provided object callable "%s", but it has no "__invoke" method.',
147 1
                        is_object($callable) ? get_class($callable) : gettype($callable)
148
                    )
149
                );
150
            }
151
152 4
            return [$callable, '__invoke'];
153
        }
154
155
        // callable is an array
156
        // it should be an array with [0] and [1]
157 5
        if (is_array($callable)) {
158 4
            if (!isset($callable[0]) || !isset($callable[1]) || !is_callable([$callable[0], $callable[1]])) {
159 1
                throw new LogicException('Provided array callable, but it is not callable.');
160
            }
161
162 3
            return [$callable[0], $callable[1]];
163
        }
164
165 1
        throw new LogicException(
166
            sprintf(
167 1
                'Unable to normalize callable "%". Please review your configuration.',
168 1
                is_object($callable) ? get_class($callable) : gettype($callable)
169
            )
170
        );
171
    }
172
173
    /**
174
     * Analyzes callable and determine the required parameters.
175
     *
176
     * @param array          $callable  The callable for which to gather parameters
177
     * @param string         $attribute The attribute being voting for
178
     * @param mixed          $subject   The subject being voting on
179
     * @param TokenInterface $token     The authentication being voting for
180
     *
181
     * @return array<mixed> The parameters list
182
     * @throws LogicException
183
     */
184 5
    private function gatherParameters($callable, $attribute, $subject, TokenInterface $token)
185
    {
186 5
        if ($callable[0] instanceof \Closure) {
187
            // don't know why but, it seems that ['\Closure', '__invoke'] is not ok with \ReflectionMethod
188 3
            $reflection = new \ReflectionFunction($callable[0]);
189
        } else {
190 3
            $reflection = new \ReflectionMethod(get_class($callable[0]), $callable[1]);
191
        }
192
193 5
        $parameters = [];
194
195
        // iterating over all parameters for method
196 5
        foreach ($reflection->getParameters() as $parameter) {
197 4
            $parameterType = $parameter->getType();
198 4
            if (method_exists($parameterType, 'getName')) {
199
                $parameterType = $parameterType->getName();
200
            } else {
201 4
                $parameterType = (string) $parameterType; // PHP < 7.1 supports
202
            }
203 4
            $parameterName = $parameter->getName();
204 4
            $parameterPosition = $parameter->getPosition();
205
            switch (true) {
206
                // attribute is a bit tricky, cannot use any type to determine whether or not it is required
207
                // if the parameter name is "attribute" this assume it should be provided
208
                // adding subject to required parameters
209 4
                case $parameterName === 'attribute':
210 2
                    $parameters[$parameterPosition] = $attribute;
211 2
                    break;
212
213
                // parameter looks like the subject being voting on
214
                // adding subject to required parameters
215 4
                case is_a($subject, $parameterType) || gettype($subject) === $parameterType:
216 3
                    $parameters[$parameterPosition] = $subject;
217 3
                    break;
218
219
                // parameter looks like a security token
220
                // adding token to required parameters
221 4
                case is_a($token, $parameterType):
222 3
                    $parameters[$parameterPosition] = $token;
223 3
                    break;
224
225
                // parameter looks like a security user
226
                // adding user to required parameters
227 4
                case is_a($token->getUser(), $parameterType):
228 3
                    $parameters[$parameterPosition] = $token->getUser();
229 4
                    break;
230
            }
231
        }
232
233
        // this gathered parameters, but the callable needs something more
234
        // calling with these parameters will probably results to an error
235
        // so throwing an exception is the only thing to do
236 5
        if ($reflection->getNumberOfRequiredParameters() !== count($parameters)) {
237 1
            throw new LogicException(
238
                sprintf(
239 1
                    'The callable method "%s"->"%s"() needs parameters that cannot be provided.',
240 1
                    get_class($callable[0]),
241 1
                    $callable[1]
242
                )
243
            );
244
        }
245
246
        // return sorted parameters
247 4
        ksort($parameters);
248
249 4
        return $parameters;
250
    }
251
}
252