diff --git a/README.md b/README.md index 7ed0daa..6b0ed1d 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ A validation library for the Slim Framework. It internally uses [Respect/Validat - [Route parameters](#route-parameters) - [JSON requests](#json-requests) - [XML requests](#xml-requests) + - [Array](#array) - [Translate errors](#translate-errors) - [Testing](#testing) - [Contributing](#contributing) @@ -288,6 +289,42 @@ Array ``` +### Array + +If you want to validate a request that contains an array as the root level, you can directly use [Respect/Validation][respect-validation] as the first parameter of the Validation middleware. For example: + +```php +use Respect\Validation\Validator as v; + +$app = new \Slim\App(); + +//Create the validators +$validators = v::each( + v::keySet( + v::key("id", v::intVal()), + v::key("key", v::stringType()) + ) +); +``` + + +If you'll have an error, the result would be: + +```php +//In your route +$errors = $req->getAttribute('errors'); + +print_r($errors); +/* +Array +( + [0] => Must have keys { "id", "key" } + +) +*/ +``` + + ### Translate errors You can provide a callable function to translate the errors. diff --git a/src/Validation.php b/src/Validation.php index 3a31572..fdd8461 100644 --- a/src/Validation.php +++ b/src/Validation.php @@ -3,6 +3,7 @@ namespace DavidePastore\Slim\Validation; use Respect\Validation\Exceptions\NestedValidationException; +use Respect\Validation\Validator; /** * Validation for Slim. @@ -66,6 +67,13 @@ class Validation */ protected $translator_name = 'translator'; + /** + * Is the validators an instance of Validator? + * + * @var boolean + */ + protected $isValidator = false; + /** * Create new Validator service provider. * @@ -78,6 +86,9 @@ public function __construct($validators = null, $translator = null, $options = [ // Set the validators if (is_array($validators) || $validators instanceof \ArrayAccess) { $this->validators = $validators; + } elseif ($validators instanceof Validator) { + $this->validators = $validators; + $this->isValidator = true; } elseif (is_null($validators)) { $this->validators = []; } @@ -99,7 +110,11 @@ public function __invoke($request, $response, $next) $this->errors = []; $params = $request->getParams(); $params = array_merge((array) $request->getAttribute('routeInfo')[2], $params); - $this->validate($params, $this->validators); + if ($this->isValidator) { + $this->validateParam($params, $this->validators); + } else { + $this->validate($params, $this->validators); + } $request = $request->withAttribute($this->errors_name, $this->getErrors()); $request = $request->withAttribute($this->has_errors_name, $this->hasErrors()); @@ -126,14 +141,7 @@ private function validate($params = [], $validators = [], $actualKeys = []) if (is_array($validator)) { $this->validate($params, $validator, $actualKeys); } else { - try { - $validator->assert($param); - } catch (NestedValidationException $exception) { - if ($this->translator) { - $exception->setParam('translator', $this->translator); - } - $this->errors[implode('.', $actualKeys)] = $exception->getMessages(); - } + $this->validateParam($param, $validator, $actualKeys); } //Remove the key added in this foreach @@ -141,6 +149,30 @@ private function validate($params = [], $validators = [], $actualKeys = []) } } + /** + * Validate a param. + * @param any $param The parameter to validate. + * @param any $validator The validator to use to validate the given parameter. + * @param array $actualKeys An array with the position of the parameter. + */ + private function validateParam($param, $validator, $actualKeys = []) + { + try { + $validator->assert($param); + } catch (NestedValidationException $exception) { + if ($this->translator) { + $exception->setParam('translator', $this->translator); + } + + $messages = $exception->getMessages(); + if (empty($actualKeys)) { + $this->errors = $messages; + } else { + $this->errors[implode('.', $actualKeys)] = $messages; + } + } + } + /** * Get the nested parameter value. * diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php index cb3e7f3..6371ef9 100644 --- a/tests/ValidationTest.php +++ b/tests/ValidationTest.php @@ -31,7 +31,7 @@ class ValidationTest extends \PHPUnit_Framework_TestCase protected $response; /** - * Run before each test. + * Setup for the GET JSON requests. */ public function setupGet() { @@ -271,6 +271,208 @@ function ($message) { 'username' => 'jsonusername', ), ), + + //JSON validation with array without errors + array( + v::each( + v::keySet( + v::key("id", v::intVal()), + v::key("key", v::stringType()) + ) + ), + null, + false, + array(), + 'JSON', + array( + array( + 'id' => 1234, + 'key' => 'value' + ), + ), + ), + + //JSON validation with array with errors + array( + v::each( + v::keySet( + v::key("id", v::intVal()), + v::key("key", v::stringType()) + ) + ), + null, + true, + array( + 'Must have keys { "id", "key" }' + ), + 'JSON', + array( + array( + 'id' => 1234, + 'key' => 'value', + 'unwanted' => 'value' + ), + ), + ), + + //JSON validation with an empty array without errors + array( + v::each( + v::keySet( + v::key("nested", v::keySet( + v::key("id", v::intVal()) + )) + ) + ), + null, + false, + array(), + 'JSON', + array(), + ), + + //Complex JSON validation with array without errors + array( + v::each( + v::keySet( + v::key("nested", v::keySet( + v::key("id", v::intVal()) + )) + ) + ), + null, + false, + array(), + 'JSON', + array( + array( + 'nested' => array( + 'id' => 1234 + ), + ), + ), + ), + + //Complex JSON validation with array with errors + array( + v::each( + v::keySet( + v::key("nested", v::keySet( + v::key("key", v::stringType()) + )) + ) + ), + null, + true, + array( + 'key must be a string' + ), + 'JSON', + array( + array( + 'nested' => array( + 'key' => 1234 + ), + ), + ), + ), + + //Complex JSON validation with key with dependencies without errors (part 1) + array( + v::key('state', v::subdivisionCode('US')->notOptional()) + ->when( + v::key('state', v::equals('NY')), // if state = NY + v::key('email', v::notOptional()->email()), // then add validation to email + v::alwaysValid() // else email is always valid + ) + ->when( + v::key('state', v::equals('NY')), // if state = NY + v::key('license', v::notOptional()), // then make license required + v::alwaysValid() // else license is always valid + ), + null, + false, + array(), + 'JSON', + array( + 'state' => 'CA' + ), + ), + + //Complex JSON validation with key with dependencies without errors (part 2) + array( + v::key('state', v::subdivisionCode('US')->notOptional()) + ->when( + v::key('state', v::equals('NY')), // if state = NY + v::key('email', v::notOptional()->email()), // then add validation to email + v::alwaysValid() // else email is always valid + ) + ->when( + v::key('state', v::equals('NY')), // if state = NY + v::key('license', v::notOptional()), // then make license required + v::alwaysValid() // else license is always valid + ), + null, + false, + array(), + 'JSON', + array( + 'state' => 'NY', + 'email' => 'test@testfoo.com', + 'license' => 'GNU' + ), + ), + + //Complex JSON validation with key with dependencies with errors (part 1) + array( + v::key('state', v::subdivisionCode('US')->notOptional()) + ->when( + v::key('state', v::equals('NY')), // if state = NY + v::key('email', v::notOptional()->email()), // then add validation to email + v::alwaysValid() // else email is always valid + ) + ->when( + v::key('state', v::equals('NY')), // if state = NY + v::key('license', v::notOptional()), // then make license required + v::alwaysValid() // else license is always valid + ), + null, + true, + array( + 'state must be a subdivision code of United States' + ), + 'JSON', + array( + 'state' => 'SP' + ), + ), + + //Complex JSON validation with key with dependencies with errors (part 2) + array( + v::key('state', v::subdivisionCode('US')->notOptional()) + ->when( + v::key('state', v::equals('NY')), // if state = NY + v::key('email', v::notOptional()->email()), // then add validation to email + v::alwaysValid() // else email is always valid + ) + ->when( + v::key('state', v::equals('NY')), // if state = NY + v::key('license', v::notOptional()), // then make license required + v::alwaysValid() // else license is always valid + ), + null, + true, + array( + 'email must be valid email', + 'Key license must be present' + ), + 'JSON', + array( + 'state' => 'NY', + 'email' => '!not a valid email!' + ), + ), + //Complex JSON validation without errors array( array( @@ -747,4 +949,59 @@ public function routeParamValidationProvider() ), ); } + + public function testArrayParametersWithGet() + { + $uri = Uri::createFromString('https://example.com:443/foo/bar?test=1'); + $headers = new Headers(); + $headers->set('Content-Type', 'application/json;charset=utf8'); + $cookies = []; + $env = Environment::mock([ + 'SCRIPT_NAME' => '/index.php', + 'REQUEST_URI' => '/foo', + 'REQUEST_METHOD' => 'POST', + ]); + $serverParams = $env->all(); + $body = new RequestBody(); + $json = array( + array( + 'id' => 1234 + ), + ); + $body->write(json_encode($json)); + $this->request = new Request('POST', $uri, $headers, $cookies, $serverParams, $body); + $this->response = new Response(); + $expectedValidators = v::allOf( + v::each( + v::keySet( + v::key("id", v::intVal()) + ) + ), + v::key('test', v::stringType()) + ); + $expectedTranslator = null; + $expectedErrors = array(); + $mw = new Validation($expectedValidators, $expectedTranslator); + + $errors = null; + $hasErrors = null; + $validators = null; + $translator = null; + $next = function ($req, $res) use (&$errors, &$hasErrors, &$validators, &$translator) { + $errors = $req->getAttribute('errors'); + $hasErrors = $req->getAttribute('has_errors'); + $validators = $req->getAttribute('validators'); + $translator = $req->getAttribute('translator'); + + return $res; + }; + + $response = $mw($this->request, $this->response, $next); + + $this->assertEquals($expectedErrors, $errors); + $this->assertEquals(false, $hasErrors); + + $this->assertEquals($expectedValidators, $validators); + $this->assertEquals($expectedTranslator, $translator); + } }