Completed
Pull Request — master (#40)
by Davide
03:14
created

Validation::validateParam()   A

Complexity

Conditions 4
Paths 5

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
ccs 10
cts 10
cp 1
rs 9.7
c 0
b 0
f 0
cc 4
nc 5
nop 3
crap 4
1
<?php
2
3
namespace DavidePastore\Slim\Validation;
4
5
use Respect\Validation\Exceptions\NestedValidationException;
6
use Respect\Validation\Validator;
7
8
/**
9
 * Validation for Slim.
10
 */
11
class Validation
12
{
13
    /**
14
     * Validators.
15
     *
16
     * @var array
17
     */
18
    protected $validators = [];
19
20
    /**
21
     * Options.
22
     *
23
     * @var array
24
     */
25
    protected $options = [
26
    ];
27
28
    /**
29
     * The translator to use fro the exception message.
30
     *
31
     * @var callable
32
     */
33
    protected $translator = null;
34
35
    /**
36
     * Errors from the validation.
37
     *
38
     * @var array
39
     */
40
    protected $errors = [];
41
42
    /**
43
     * The 'errors' attribute name.
44
     *
45
     * @var string
46
     */
47
    protected $errors_name = 'errors';
48
49
    /**
50
     * The 'has_error' attribute name.
51
     *
52
     * @var string
53
     */
54
    protected $has_errors_name = 'has_errors';
55
56
    /**
57
     * The 'validators' attribute name.
58
     *
59
     * @var string
60
     */
61
    protected $validators_name = 'validators';
62
63
    /**
64
     * The 'translator' attribute name.
65
     *
66
     * @var string
67
     */
68
    protected $translator_name = 'translator';
69
70
    /**
71
     * Is the validators an instance of Validator?
72
     *
73
     * @var boolean
74
     */
75
    protected $isValidator = false;
76
77
    /**
78
     * Create new Validator service provider.
79
     *
80
     * @param null|array|ArrayAccess $validators
81
     * @param null|callable          $translator
82
     * @param []|array               $options
0 ignored issues
show
Documentation introduced by
The doc-type []|array could not be parsed: Unknown type name "" at position 0. [(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...
83
     */
84 35
    public function __construct($validators = null, $translator = null, $options = [])
85
    {
86
        // Set the validators
87 35
        if (is_array($validators) || $validators instanceof \ArrayAccess) {
88 24
            $this->validators = $validators;
89 11
        } elseif ($validators instanceof Validator) {
90 9
            $this->validators = $validators;
91 9
            $this->isValidator = true;
92 2
        } elseif (is_null($validators)) {
93 1
            $this->validators = [];
94
        }
95 35
        $this->translator = $translator;
96 35
        $this->options = array_merge($this->options, $options);
97 35
    }
98
99
    /**
100
     * Validation middleware invokable class.
101
     *
102
     * @param \Psr\Http\Message\ServerRequestInterface $request  PSR7 request
103
     * @param \Psr\Http\Message\ResponseInterface      $response PSR7 response
104
     * @param callable                                 $next     Next middleware
105
     *
106
     * @return \Psr\Http\Message\ResponseInterface
107
     */
108 35
    public function __invoke($request, $response, $next)
109
    {
110 35
        $this->errors = [];
111 35
        $params = $request->getParams();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Psr\Http\Message\ServerRequestInterface as the method getParams() does only exist in the following implementations of said interface: Slim\Http\Request.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
112 35
        $params = array_merge((array) $request->getAttribute('routeInfo')[2], $params);
113 35
        if ($this->isValidator) {
114 9
            $this->validateParam($params, $this->validators);
0 ignored issues
show
Documentation introduced by
$params is of type array, but the function expects a object<DavidePastore\Slim\Validation\any>.

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...
Documentation introduced by
$this->validators is of type array, but the function expects a object<DavidePastore\Slim\Validation\any>.

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...
115
        } else {
116 26
            $this->validate($params, $this->validators);
117
        }
118
119 35
        $request = $request->withAttribute($this->errors_name, $this->getErrors());
120 35
        $request = $request->withAttribute($this->has_errors_name, $this->hasErrors());
121 35
        $request = $request->withAttribute($this->validators_name, $this->getValidators());
122 35
        $request = $request->withAttribute($this->translator_name, $this->getTranslator());
123
124 35
        return $next($request, $response);
125
    }
126
127
    /**
128
     * Validate the parameters by the given params, validators and actual keys.
129
     * This method populates the $errors attribute.
130
     *
131
     * @param array $params     The array of parameters.
132
     * @param array $validators The array of validators.
133
     * @param array $actualKeys An array that will save all the keys of the tree to retrieve the correct value.
134
     */
135 26
    private function validate($params = [], $validators = [], $actualKeys = [])
136
    {
137
        //Validate every parameters in the validators array
138 26
        foreach ($validators as $key => $validator) {
139 24
            $actualKeys[] = $key;
140 24
            $param = $this->getNestedParam($params, $actualKeys);
141 24
            if (is_array($validator)) {
142 9
                $this->validate($params, $validator, $actualKeys);
143
            } else {
144 24
                $this->validateParam($param, $validator, $actualKeys);
145
            }
146
147
            //Remove the key added in this foreach
148 24
            array_pop($actualKeys);
149
        }
150 26
    }
151
152
    /**
153
     * Validate a param.
154
     * @param any $param The parameter to validate.
155
     * @param any $validator The validator to use to validate the given parameter.
156
     * @param array $actualKeys An array with the position of the parameter.
157
     */
158 33
    private function validateParam($param, $validator, $actualKeys = [])
159
    {
160
        try {
161 33
            $validator->assert($param);
162 18
        } catch (NestedValidationException $exception) {
163 18
            if ($this->translator) {
164 2
                $exception->setParam('translator', $this->translator);
165
            }
166
167 18
            $messages = $exception->getMessages();
168 18
            if (empty($actualKeys)) {
169 4
                $this->errors = $messages;
170
            } else {
171 14
                $this->errors[implode('.', $actualKeys)] = $messages;
172
            }
173
        }
174 33
    }
175
176
    /**
177
     * Get the nested parameter value.
178
     *
179
     * @param array $params An array that represents the values of the parameters.
180
     * @param array $keys   An array that represents the tree of keys to use.
181
     *
182
     * @return mixed The nested parameter value by the given params and tree of keys.
183
     */
184 24
    private function getNestedParam($params = [], $keys = [])
185
    {
186 24
        if (empty($keys)) {
187 22
            return $params;
188
        } else {
189 24
            $firstKey = array_shift($keys);
190 24
            if ($this->isArrayLike($params) && array_key_exists($firstKey, $params)) {
191 22
                $params = (array) $params;
192 22
                $paramValue = $params[$firstKey];
193
194 22
                return $this->getNestedParam($paramValue, $keys);
195
            } else {
196 3
                return;
197
            }
198
        }
199
    }
200
201
    /**
202
     * Check if the given $params is an array like variable.
203
     *
204
     * @param array $params The variable to check.
205
     *
206
     * @return boolean Returns true if the given $params parameter is array like.
207
     */
208 24
    private function isArrayLike($params)
209
    {
210 24
        return is_array($params) || $params instanceof \SimpleXMLElement;
211
    }
212
213
    /**
214
     * Check if there are any errors.
215
     *
216
     * @return bool
217
     */
218 35
    public function hasErrors()
219
    {
220 35
        return !empty($this->errors);
221
    }
222
223
    /**
224
     * Get errors.
225
     *
226
     * @return array The errors array.
227
     */
228 35
    public function getErrors()
229
    {
230 35
        return $this->errors;
231
    }
232
233
    /**
234
     * Get validators.
235
     *
236
     * @return array The validators array.
237
     */
238 35
    public function getValidators()
239
    {
240 35
        return $this->validators;
241
    }
242
243
    /**
244
     * Set validators.
245
     *
246
     * @param array $validators The validators array.
247
     */
248 1
    public function setValidators($validators)
249
    {
250 1
        $this->validators = $validators;
251 1
    }
252
253
    /**
254
     * Get translator.
255
     *
256
     * @return callable The translator.
257
     */
258 35
    public function getTranslator()
259
    {
260 35
        return $this->translator;
261
    }
262
263
    /**
264
     * Set translator.
265
     *
266
     * @param callable $translator The translator.
267
     */
268 1
    public function setTranslator($translator)
269
    {
270 1
        $this->translator = $translator;
271 1
    }
272
}
273