I have never liked working with bugs in PHP frameworks. And even the use of this word did not like. To clarify right away, I'm not talking about fatal errors, not about error_reporting, I'm talking about what they call validation errors. That in models, in forms - it depends on the framework.
Just look. For example, Yii and Yii2, getting model validation errors:
$errors = $model->getErrors();
Symfony form errors:
$errors = $form->getErrors();
Actively advertised Pixie (there was nothing about him for quite a while):
$result = $validator->validate($data); $errors = $result->errors();
What is wrong here?
Yes all. All wrong. All this code smells really bad, it smells like PHP4 at times, spaghetti architecture and wild mix of concepts.
')
What to do?
Start to figure it out. From the very beginning.
We define important concepts.
1.
Validity is the answer to the question "is the value valid, in other words
valid , in this context." The context can be different, it is a field in the form, and a property of the object. Interestingly, the answer “yes” to the question of validity does not imply any additional information, but the answer “no” needs clarification. For example: the password is invalid BECAUSE its length is less than 6 characters.
2.
Validation -
validation process. We have with you some meaning and there is a context.
The validator (the process performing the validation) must unambiguously answer whether the value is valid in this context, and if not, then why.
3. “Why” from the previous paragraph is called the “
validation error ”.
Validation errors - detailed information about what exactly caused the false response to the question about the validity of the data, that is, the reason for the failure to validate. In fact, these are not errors in the sense of “boss, everything is lost!”, But simply some kind of validator report, but the word “error” has already taken root among the developers.
4.
Validation rules are functions that take a context and a value as input and return a validity response. The answer must include both true / false and a validation report, that is, a set of errors, if any.
With validation, quite often (especially in some frameworks that still support PHP 5.2, we won’t point a finger at them) to confuse sanitize (or “cleaning” in Russian) values. Do not confuse the concept of "validation" and "cleaning" (or leading to the canonical form), these are two completely different processes.
A good example that I like: entering a Russian phone number. For validation, it is sufficient (in general) to have 11 digits in the entered string, the first of which is 7, with an arbitrary number and positions of other characters. If not, validation fails. The task of the sanitizer is to remove everything from this value except digits, so that we can save the standardized msisdn in the database.
Read to finally understand the difference: php.net/manual/ru/filter.filters.php
Well, but what's wrong?
That the collection of validation errors is no exception.
All these are wonderful
->getErrors()
no exceptions. Therefore, we lack many advantages:
- Exceptions are typed. In frameworks similar to the above, I cannot create a FormException -> FormFieldException -> FormPasswordFieldException -> FormPasswordFieldNotSameException hierarchy. This is very important, especially with the release of PHP 7, which makes tip-hintings finally the norm and standard
- Exceptions encapsulate a lot of necessary. This is the PLO! For example: on which page (URL) did a validation error occur? Who is the user? What is the specific form field? What validation rule worked? Finally, "but give me the translation of this message into Estonian." Can this all make a simple array of error messages? Of course not. (By the way, it suffices to implement the __toString () method and the exception in the template will continue to behave like a simple error message)
- Exceptions control the flow. I can quit it. It pops up. I can catch him, but I can catch him and throw further. The $ errors array is deprived of the right to control the flow of code, so it is very inconvenient. How can I use $ errors to escalate validation error handling from the model above, for example, to the controller or application component?
And what to do?
Let's try to set a task. What would you like to see in the code? Well, let's say something like this:
Somewhere in the active code:
try { $user = new User; $user->fill($_POST); $user->save(); redirect('hello.php'); catch (ValidationErrors $e) { $this->view->assign('errors', $e); }
Somewhere in the template:
<?php foreach ($errors as $error): ?> <div class="alert alert-danger"><?php echo $error->getMessage(); ?></div> <?php endforeach; ?>
The essence of the proposed architectural template can be expressed very briefly:
Multi-exclusion. An exception that is a collection of other exceptions.
How to achieve this? Fortunately, modern PHP allows us to not such tricks.
We turn an exception into a collection
All the fun is here!An interface that inherits all the useful interfaces for turning an object into an array:
interface IArrayAccess extends \ArrayAccess, \Countable, \IteratorAggregate, \Serializable { }
Trait that implements this interface:
trait TArrayAccess { protected $storage = []; protected function innerIsset($offset) { return array_key_exists($offset, $this->storage); } protected function innerGet($offset) { return isset($this->storage[$offset]) ? $this->storage[$offset] : null; } protected function innerSet($offset, $value) { if ('' == $offset) { if (empty($this->storage)) { $offset = 0; } else { $offset = max(array_keys($this->storage))+1; } } $this->storage[$offset] = $value; } protected function innerUnset($offset) { unset($this->storage[$offset]); } public function offsetExists($offset) { return $this->innerIsset($offset); } public function offsetGet($offset) { return $this->innerGet($offset); } public function offsetSet($offset, $value) { $this->innerSet($offset, $value); } public function offsetUnset($offset) { $this->innerUnset($offset); } public function count() { return count($this->storage); } public function isEmpty() { return empty($this->storage); } }
I personally add one more useful interface and its implementation by treyt, but it, of course, is completely optional:
interface ICollection { public function add($value); public function prepend($value); public function append($value); public function slice($offset, $length=null); public function existsElement(array $attributes); public function findAllByAttributes(array $attributes); public function findByAttributes(array $attributes); public function asort(); public function ksort(); public function uasort(callable $callback); public function uksort(callable $callback); public function natsort(); public function natcasesort(); public function sort(callable $callback); public function map(callable $callback); public function filter(callable $callback); public function reduce($start, callable $callback); public function collect($what); public function group($by); public function __call($method, array $params = []); }
and finally, we put everything together:
class MultiException extends \Exception implements IArrayAccess { use TArrayAccess; }
Simple application example
The method of filling the model with data.
The model creates validation rules. They throw exceptions every time a value fails validation when assigned to a model field. For example:
protected function validatePassword($value) { if (strlen($value) < 3) { throw new Exception(' '); } ... return true; }
Create a magic setter that will automatically call the validator for the field. And at the same time convert the thrown exception to another type, which contains not only the validation error message, but also the field name:
public function __set($key, $val) { $validator = 'validate' . ucfirst($key); if (method_exists($this, $validator)) { try { if ($this->$validator($value)) { parent::__set($key, $val); } } catch (Exception $e) { throw new ModelColumnException($key, $e->getMessage()); } } }
Create a method fill ($ data), which will try to fill the model with data and accurately collect all validation errors for individual fields into one:
public function fill($data) { $errors = new Multiexception; foreach ($data as $key => $val) { try { $this->$key = $val; } catch (ModelColumnException $e) { $errors[] = $e; } } if (!$errors->isEmpty()) { throw $errors; } }
Actually, everything. You can apply. A bunch of pluses:
- This exception means you can catch it in the right place.
- This is an array of exceptions, so that we can at any time add a new exception to it or delete an already processed one.
- This is an exception, so after some processing phase it can be thrown further.
- This is an object, so we can easily pass it anywhere
- This is a class, so we build our own class hierarchy.
- And finally, this is still an exception, which means that all its standard properties and methods are available to us. Yes, yes, and even getTrace ()!
Instead of conclusion
This is all, with a few nuances, quite a combat code, which I have been using for a long time, and even teach my students to use. I am surprised that I have never seen such a simple concept before. If, all of a sudden, I’m the first, I’m transferring the idea and code in this article to the public domain. If not the first - forgive the author of his error (s)
UPD based on comments
I thank all the commentators for valuable thoughts and opinions.
The essence of the article is not validation. At all. Validation is just an unfortunate holivorn example, I just could not think of a better one.
The point is very simple. In PHP, there may be an object that is both an exception and a collection of other exceptions at the same time. And it is convenient.