├── .gitignore
├── .travis.yml
├── phpunit.xml
├── src
└── Sudzy
│ ├── ValidationException.php
│ └── ValidModel.php
├── composer.json
├── test
├── bootstrap.php
└── SudzyTest.php
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | composer.lock
2 | vendor/*
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | php:
3 | - '5.6'
4 | - '7.0'
5 | - '7.1'
6 | - hhvm
7 |
8 | before_script:
9 | - export PATH="$PATH:$HOME/.composer/vendor/bin"
10 | - composer global require "squizlabs/php_codesniffer=*"
11 |
12 | install: composer install
13 |
14 | script:
15 | - phpcs --config-set ignore_warnings_on_exit 1
16 | - phpcs --standard=PSR2 ./src --colors
17 | - "phpunit --colors --coverage-text"
18 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 | test
9 |
10 |
11 |
12 |
13 |
14 | ./src
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/Sudzy/ValidationException.php:
--------------------------------------------------------------------------------
1 | _validationExceptions = $exceptions;
12 | $this->_validationErrors = $errs;
13 |
14 | $errs = array_map(
15 | function ($val) {
16 | return implode("\n", $val);
17 | },
18 | $errs
19 | );
20 | $errStr = implode("\n", $errs);
21 | parent::__construct($errStr);
22 | }
23 |
24 | public function getValidationErrors()
25 | {
26 | return $this->_validationErrors;
27 | }
28 |
29 | public function getValidationExceptions()
30 | {
31 | return $this->_validationExceptions;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tag/sudzy",
3 | "description": "Model validator for use with Paris (and Idiorm).",
4 | "keywords": ["validator", "model","idiorm", "paris"],
5 | "homepage": "https://github.com/tag/sudzy",
6 | "license": "BSD",
7 | "authors": [
8 | {
9 | "name": "Tom Gregory",
10 | "email": "tom@alt-tag.com",
11 | "homepage": "http://alt-tag.com/",
12 | "role": "Developer"
13 | }
14 | ],
15 | "repository": {
16 | "type": "vcs",
17 | "url": "https://github.com/tag/sudzy"
18 | },
19 | "require": {
20 | "php": ">=5.6.0",
21 | "j4mie/paris": ">=1.4",
22 | "respect/validation": "^1.1"
23 | },
24 | "require-dev": {
25 | "phpunit/phpunit": "^5.7"
26 | },
27 | "autoload": {
28 | "psr-0": {
29 | "Sudzy": "src",
30 | "ValidationException": "src"
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/test/bootstrap.php:
--------------------------------------------------------------------------------
1 | current_row == 5) {
23 | return false;
24 | } else {
25 | return array('name' => 'Fred', 'email' => 'jim@example.com', 'age'=>34, 'id' => ++$this->current_row);
26 | }
27 | }
28 | }
29 |
30 | /**
31 | * Mock database class implementing a subset
32 | * of the PDO API.
33 | */
34 | class MockPDO extends PDO {
35 |
36 | /**
37 | * Return a dummy PDO statement
38 | */
39 | public function prepare($statement, $driver_options=array()) {
40 | return new MockPDOStatement($statement);
41 | }
42 | }
43 |
44 | /**
45 | * Models for use during testing
46 | */
47 | class Simple extends \Sudzy\ValidModel {
48 | public function prepareValidations()
49 | {
50 | $this->setValidation('age', \Respect\Validation\Validator::intVal());
51 |
52 | }
53 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sudzy [](https://travis-ci.org/tag/sudzy)
2 | **Breaking change on v0.3: Validations now use `respect/validation` validation library.**
3 |
4 | Sudzy implements validarion against model classes using
5 | [Paris][paris]/[Idiorm][idiorm] (an active record ORM, often used with Slim),
6 | although it could be adapted easily.
7 |
8 | Sudzy's `ValidModel` class decorates Paris' `Model` class. By extending
9 | `ValidModel`, your model classes gain immediate access to validations.
10 |
11 | By default the `ValidModel` will store validation errors when model properties
12 | are set (for an exising model) or a new model is saved, and throw a
13 | `ValidationException` on save if errors were encountered.
14 |
15 | Sudzy's `ValidModel` class uses [`Respect/Validation`][respect] as its validation
16 | engine. See that project for details.
17 |
18 | [paris]: https://github.com/j4mie/paris
19 | [idiorm]: https://github.com/j4mie/idiorm
20 | [respect]: https://github.com/Respect/Validation
21 |
22 | ### Installation
23 | The easiest way to install Sudzy is via [Composer][composer]. Start by creating or adding to your project's `composer.json` file:
24 |
25 | ```js
26 | {
27 | "require": {
28 | "tag/sudzy" : "dev-master" // Grab the most recent version from github
29 | }
30 | }
31 | ```
32 |
33 | [composer]: http://getcomposer.org
34 |
35 | ## ValidModel Example
36 | The `ValidModel` class requires you to implement the abstract method
37 | `#prepareValidations()`, in order to lazily load the validations. Thus,
38 | constructors will not have the overhead of creating unused validation objects.
39 |
40 | Validations can also be added at any time with the `#addValidation()` method.
41 |
42 | The `#setValidation()` method is passed the model property to watch, and a
43 | Respect validation object to be checked against. Multiple calls on the same
44 | property overwrite previous validations.
45 |
46 | `Respect\Validation` is namespaced, but you can make your life easier by importing a single class into your context:
47 |
48 | ```php
49 | use Respect\Validation\Validator as v;
50 | ```
51 |
52 | ```php
53 | // Within a `ValidModel` class declaration:
54 |
55 | public function prepareValidation()
56 | {
57 | $this->setValidation('username', v::alnum()->noWhitespace()->length(1, 15) );
58 | $this->setValidation('email', v::email() );
59 | $this->setValidation('password', v::stringType()->length(6, null)->length(1, 15) );
60 | $this->setValidation('birthdate', v::date()->age(18));
61 |
62 | }
63 | ```
64 | When using `Respect\Validation`, create different validations for each field, instead of a single validator for the entire object.
65 |
66 | ### Full Example
67 | Example model class:
68 |
69 | ```php
70 | namespace Models;
71 |
72 | use Respect\Validation\Validator as v;
73 |
74 | class User extends \Sudzy\ValidModel
75 | {
76 | public function prepareValidation()
77 | {
78 | $this->setValidation('username', v::alnum()->noWhitespace()->length(1, 15) );
79 | $this->setValidation('email', v::email() );
80 | $this->setValidation('password', v::stringType()->length(6, null)->length(1, 15) );
81 | $this->setValidation('birthdate', v::date()->age(18));
82 | }
83 | }
84 | ```
85 |
86 | Example controller snip:
87 |
88 | ```php
89 | // This example assumes Slim context and access to flash messages
90 | // ... ...
91 |
92 | $newUser = Model::factory('\Models\User')->create();
93 |
94 | try {
95 | $newUser->email = $_POST['email'];
96 | $newUser->password = $_POST['password'];
97 |
98 | $newWard->save();
99 |
100 | $this->flash->addMessage('success', 'New User created.');
101 | } catch (Sudzy\ValidationException $sve) {
102 | foreach ($sve->getMessages() as $msg) {
103 | $this->flash->addMessage('error', $msg);
104 | }
105 | }
106 | ```
107 |
108 | ### Validation Exceptions and Errors
109 | By default, Sudzy's `ValidModel` does validation checks whenever objects are
110 | committed to the database via `#save()`, but can be configured to throw an
111 | exception when properties are set, or not at all.
112 |
113 | Because an object can have multiple fields fail, it is necessary to catch and
114 | wrap Respect's exceptions.
115 |
116 | :TODO:
117 |
118 | Validation failures are stored, and available through `getValidationErrors()`,
119 | a method of both the `ValidModel` object and the thrown `ValidationException`.
120 | An object that fails validation throws a `ValidationException` when `save()` is
121 | attempted (default behavior). This can be changed to `::ON_SET` or `::NEVER` by
122 | setting the `throw` option:
123 |
124 | ```php
125 | $model->setValidationOptions(
126 | array('throw' => self::ON_SET)
127 | );
128 | ```
129 |
130 | Be careful of using `::ON_SET`, as Paris' internal `set()` method is not called
131 | when a model is built via Paris' `hydrate()` or `create()` methods. Also,
132 | `::ON_SET` tiggers the validation exception immediately, whereas `::ON_SAVE`
133 | permits validating all fields before throwing an exception.
134 |
135 | Regardless of the value of the `throw` option, validations are checked when
136 | properties are set. In the case of new models (such as one built with Paris
137 | methods `create()` or `hydrate()`), validations are also checked on save.
138 | Regardless of when exceptions are thrown (or not), errors are immediately
139 | available through `getValidationErrors()`.
140 |
--------------------------------------------------------------------------------
/src/Sudzy/ValidModel.php:
--------------------------------------------------------------------------------
1 | false, // If True getValidationErrors will return an array with the index
15 | // being the field name and the value an *array* of errors.
16 |
17 | 'throw' => self::VALIDATE_ON_SAVE // One of self::ON_SET|ON_SAVE|NEVER.
18 | // + ON_SET throws immediately when field is set()
19 | // + ON_SAVE throws on save()
20 | // + NEVER means an exception is never thrown; check for ->getValidationErrors()
21 | ];
22 |
23 | const VALIDATE_ON_SET = 'set';
24 | const VALIDATE_ON_SAVE = 'save';
25 | const VALIDATE_NEVER = null;
26 |
27 | public function setValidationOptions($options)
28 | {
29 | $this->_validationOptions = array_merge($this->_validationOptions, $options);
30 | }
31 |
32 | public function getValidationOptions()
33 | {
34 | return $this->_validationOptions;
35 | }
36 |
37 | abstract public function prepareValidations();
38 |
39 | /**
40 | * @param string $prop Property name to be validated
41 | * @param object $validator An instance of Respect\Validation\Validator
42 | */
43 | public function setValidation($prop, $validator)
44 | {
45 | $this->lazyLoadValidations();
46 | if ($validator === null) {
47 | unset($this->_validators[$prop]);
48 | } else {
49 | $this->_validators[$prop] = $validator;
50 | }
51 | }
52 |
53 | /**
54 | * @param string $prop Property name to be validated
55 | * @return object $validator An instance of Respect\Validation\Validator
56 | */
57 | public function getValidation($prop)
58 | {
59 | $this->lazyLoadValidations();
60 | return isset($this->_validators[$prop]) ? $this->_validators[$prop] : null;
61 | }
62 |
63 | /**
64 | * @return array Arrany of Respect\Validation\Validator instances
65 | */
66 | public function getAllValidations()
67 | {
68 | $this->lazyLoadValidations();
69 | return $this->_validators;
70 | }
71 |
72 | /**
73 | * Manually trigger validation checking
74 | *
75 | * @return bool `true` if passes validation, otherwise,
76 | * throws `Respect\Validation\Exceptions\NestedValidationException`
77 | */
78 | public function validate()
79 | {
80 | $this->lazyLoadValidations();
81 | foreach ($this->_validators as $key => $val) {
82 | $this->validateProperty($key, $this->$key);
83 | }
84 | }
85 |
86 | /**
87 | * @param string $prop Property name to be validated
88 | * @param mixed $value Property value to be validated
89 | * @param bool $throw Whether (true) to throw or a consequent validation exception or (false) return false
90 | * @return bool Will set a message if returning false
91 | * @throws Respect\Validation\Exceptions\NestedValidationException If validations fail and options permit throwing
92 | **/
93 | public function validateProperty($prop, $value, $throw = false)
94 | {
95 | $this->lazyLoadValidations();
96 |
97 | unset($this->_validationErrors[$prop]);
98 | unset($this->_validationExceptions[$prop]);
99 |
100 | if (!isset($this->_validators[$prop])) {
101 | return true; // No validations, return true by default
102 | }
103 |
104 | try {
105 | $this->_validators[$prop]->assert($value);
106 | } catch (NestedValidationException $validationException) {
107 | $this->_validationErrors[$prop] = $validationException->getMessages();
108 | $this->_validationExceptions[] = $validationException;
109 |
110 | if (!$throw) {
111 | return false;
112 | }
113 |
114 | throw $validationException;
115 | }
116 |
117 | return true;
118 | }
119 |
120 | public function getValidationErrors()
121 | {
122 | return $this->_validationErrors;
123 | }
124 |
125 | public function getValidationExceptions()
126 | {
127 | return $this->_validationExceptions;
128 | }
129 |
130 | public function resetValidationErrors()
131 | {
132 | $this->_validationErrors = [];
133 | $this->_validationExceptions = [];
134 | }
135 |
136 | ///////////////////
137 | // Overloaded methods
138 |
139 | /**
140 | * Overload __set to call validateAndSet
141 | */
142 | public function __set($name, $value)
143 | {
144 | return $this->validateAndSet($name, $value);
145 | }
146 |
147 | /**
148 | * Overload save; checks if errors exist before saving
149 | */
150 | public function save()
151 | {
152 | if ($this->isNew()) { //Properties populated by create() or hydrate() don't pass through set()
153 | $this->validate(); // Check the valide of $this->getValidationErrors rather than the return value here
154 | }
155 |
156 | if (!empty($this->getValidationErrors())) {
157 | $this->doValidationError(self::VALIDATE_ON_SAVE);
158 | }
159 |
160 | return parent::save();
161 | }
162 |
163 | /**
164 | * Overload set; to call validateAndSet
165 | * // TODO: handle multiple sets if $name is a property=>val array
166 | */
167 | public function set($name, $value = null)
168 | {
169 | return $this->validateAndSet($name, $value);
170 | }
171 |
172 | ////////////////////
173 | // Protected methods
174 |
175 | protected function lazyLoadValidations()
176 | {
177 | if (!$this->_validatorsLoaded) {
178 | $this->_validatorsLoaded = true;
179 | $this->prepareValidations();
180 | }
181 | }
182 |
183 | protected function doValidationError($context)
184 | {
185 | if ($context === $this->_validationOptions['throw']) {
186 | throw new \Sudzy\ValidationException($this->_validationErrors, $this->_validationExceptions);
187 | }
188 | }
189 |
190 | /**
191 | * Overload set; to call validateAndSet
192 | * @param string $name Property name
193 | * @param mixed $value Property value
194 | */
195 | protected function validateAndSet($name, $value)
196 | {
197 | if (!$this->validateProperty($name, $value)) {
198 | $this->doValidationError(self::VALIDATE_ON_SET);
199 | }
200 |
201 | return parent::set($name, $value);
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/test/SudzyTest.php:
--------------------------------------------------------------------------------
1 | find_one(1);
21 | $this->assertNotEmpty($simple->email);
22 |
23 | $simple->setValidation('email', v::notEmpty() );
24 | $simple->validateProperty('email', $simple->email); //Initially populated with email address
25 | $this->assertEmpty($simple->getValidationErrors());
26 |
27 | $simple->email = '';
28 | $this->assertNotEmpty($simple->getValidationErrors());
29 |
30 | try {
31 | $simple->save();
32 | } catch (\Sudzy\ValidationException $e) {
33 | $this->assertNotEmpty($e->getValidationErrors());
34 | $this->assertNotEmpty($e->getValidationExceptions());
35 | $this->assertInstanceOf(
36 | Respect\Validation\Exceptions\NestedValidationException::class,
37 | $e->getValidationExceptions()[0]
38 | );
39 | return;
40 | }
41 | $this->fail('ValidationException expected, but not raised.');
42 | }
43 |
44 | public function testValidationIsInt() {
45 | $simple = Model::factory('Simple')->find_one(1);
46 |
47 | // This validation set in the Simple model
48 | //$simple->setValidation('age', v::intVal());
49 |
50 | $simple->age = 23;
51 | $this->assertEmpty($simple->getValidationErrors());
52 |
53 | $simple->age = '24';
54 | $this->assertEmpty($simple->getValidationErrors());
55 |
56 | // Test #set(), not just #__set()
57 | $simple->set('age', '3.14159');
58 | $this->assertNotEmpty($simple->getValidationErrors());
59 | $this->assertNotEmpty($simple->getValidationExceptions());
60 |
61 | $simple->resetValidationErrors();
62 | $this->assertEmpty($simple->getValidationErrors());
63 |
64 | $simple->age = false;
65 | $this->assertNotEmpty($simple->getValidationErrors());
66 |
67 | $simple->resetValidationErrors();
68 | $this->assertEmpty($simple->getValidationErrors());
69 |
70 | $simple->age = 'orange';
71 | $this->assertNotEmpty($simple->getValidationErrors());
72 | }
73 |
74 | //superfluous test, but useful to see an example of email validation
75 | public function testValidationEmail() {
76 | $simple = Model::factory('Simple')->find_one(1);
77 | $simple->setValidation('email', v::notEmpty()->email() );
78 |
79 | $simple->email = 'valid@example.com';
80 | $this->assertEmpty($simple->getValidationErrors());
81 |
82 | $simple->email = 'invalid@@example.com'; // Incorrect email
83 | $this->assertNotEmpty($simple->getValidationErrors());
84 | }
85 |
86 | public function testSuccessfulValidation() {
87 | $simple = Model::factory('Simple')->create(
88 | array('name'=>'Steve', 'age'=>'16')
89 | );
90 |
91 | $this->assertTrue(
92 | $simple->validateProperty('name', $simple->name)
93 | ); // Success, because no validation assigned to 'name'
94 |
95 | // Using the default validation on age
96 |
97 | try {
98 | $simple->save();
99 | } catch (\Sudzy\ValidationException $e) {
100 | $this->fail('ValidationException raised.');
101 | }
102 | // Success!
103 | }
104 |
105 | public function testValidationOfNewModel() {
106 | $simple = Model::factory('Simple')->create(
107 | ['name'=>'Steve', 'age'=>'unknown']
108 | );
109 |
110 | $options = $simple->getValidationOptions();
111 | $this->assertEquals($options['throw'], Sudzy\ValidModel::VALIDATE_ON_SAVE);
112 |
113 | // Use the default validation on age
114 |
115 | $this->expectException(Sudzy\ValidationException::class);
116 | $simple->save();
117 |
118 | }
119 |
120 | public function testValidationMessageResetOnSet() {
121 | $simple = Model::factory('Simple')->create(
122 | array('name'=>'Steve', 'age'=>'0')
123 | );
124 | $simple->setValidation('age', v::intVal()->positive());
125 |
126 | $this->assertEmpty($simple->getValidationErrors());
127 |
128 | $simple->age = null;
129 | $this->assertNotEmpty($simple->getValidationErrors());
130 |
131 | $simple->age = 25;
132 | $this->assertEmpty($simple->getValidationErrors());
133 | }
134 |
135 | public function testRemoveValidation() {
136 | $simple = Model::factory('Simple')->create(
137 | array('name'=>'Steve', 'age'=>'16')
138 | );
139 | $this->assertEmpty($simple->getValidationErrors());
140 |
141 | $this->assertNotEmpty($simple->getAllValidations());
142 |
143 | //remove the default validation on age
144 | $simple->setValidation('age', null);
145 |
146 | $this->assertNull(
147 | $simple->getValidation('age')
148 | );
149 |
150 | $this->assertEmpty($simple->getAllValidations());
151 |
152 | $simple->age = 'not a valid age';
153 |
154 | $this->assertEmpty($simple->getValidationErrors());
155 |
156 | }
157 |
158 | public function testGetAndOverwriteValidations() {
159 | $simple = Model::factory('Simple')->find_one(1);
160 |
161 | $validation = $simple->getValidation('email');
162 | $this->assertNull($validation);
163 |
164 | $simple->setValidation('email', v::notEmpty()->email() );
165 |
166 | $this->assertNotNull($simple->getValidation('email'));
167 | }
168 |
169 | public function testThrowNativeRespectException() {
170 | $simple = Model::factory('Simple')->create(
171 | array('name'=>'Steve', 'age'=>'16')
172 | );
173 |
174 | $this->expectException(Respect\Validation\Exceptions\NestedValidationException::class);
175 |
176 | $simple->validateProperty('age', 'not a valid age', true);
177 | }
178 |
179 | public function testNoExceptions() {
180 | $simple = Model::factory('Simple')->create(
181 | array('name'=>'Steve', 'age'=>'16')
182 | );
183 |
184 | $simple->setValidationOptions([
185 | 'throw' => $simple::VALIDATE_NEVER
186 | ]);
187 |
188 | $simple->age = 'not a valid age';
189 | try {
190 | $simple->save();
191 | } catch (\Sudzy\ValidationException $e) {
192 | $this->fail('ValidationException raised incorrectly.');
193 | }
194 | $this->assertNotEmpty($simple->getValidationErrors());
195 | $this->assertNotEmpty($simple->getValidationExceptions());
196 | }
197 | }
--------------------------------------------------------------------------------