CallableCollectionVoter::gatherParameters()   C
last analyzed

Complexity

Conditions 10
Paths 44

Size

Total Lines 67
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 10.0033

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 67
ccs 30
cts 31
cp 0.9677
rs 6.1506
c 1
b 0
f 0
cc 10
eloc 35
nc 44
nop 4
crap 10.0033

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
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