Сегодня мы более подробно остановимся на теме, которую я затронул в предыдущей статье — «Создание и проверка форм». Надеюсь, у вас появится пару светлых идей, которые вы сможете разработать и использовать самостоятельно.
Если вы ещё не прочитали предыдущую статье я советую вам сделать это перед продолжением, хотя, думаю, вы справитесь и без неё.
«Как это нужно делать»
Вы часто слышите эту фразу, верно? Я — да. Бесконечное число людей обсуждали со мной свои сумасбродные идеи о том, как нужно решать определённую проблему правильно. Это всё замечательно и хорошо, но в конечном счёте как это реализовать — целиком ваша забота. И когда я думаю о том, «как это нужно делать», то на самом деле я думаю о лучшем решении для меня самого — так что помните об этом.
К слову, какие существуют общепринятые механизмы проверки пользовательского ввода? Часто говорят, что проверку не нужно делать в контроллере (или в маршруте), но нужно это делать в модели. Мне близка эта идея, я в любом случае люблю поддерживать хорошее ответственности. Работая над проверкой я предпочитаю использовать исключения и блоки try..catch.
Сервис проверок
Для поддержания чистоты мы создадим класс-сервис для наших проверок. Он будет ответственным за создание правил, сообщений ошибок и собственно проверку. Он будет очень простой:
<?php
namespace Services;
use Validator, Exception, ValidateException;
abstract class Validation {
protected $validator;
protected $data;
public $rules = array();
public $messages = array();
public function __construct($input) {
$this->input = $input;
}
protected function validate() {
$this->validator = Validator::make($this->input, $this->rules, $this->messages);
if ($this->validator->invalid()) {
throw new ValidateException($this->validator);
}
}
public function __set($key, $value) {
$this->data[$key] = $value;
}
public function __get($key) {
if (!array_key_exists($key, $this->data)) {
throw new Exception('Значение [' . $key . '] не найдено в массиве данных Services\\Validation.');
}
return $this->data[$key];
}
}
Довольно просто. Это абстрактный класс, поэтому мы не можем создавать его экземпляры и должны расширить его («extend»). Вы, наверное, заметили, что в нём используются сразу два класса исключений — Exception и ValidateException. Второй из них нам нужно создать:
<?php
class ValidateException extends Exception {
private $errors;
public function __construct($container) {
$this->errors = ($container instanceof Validator) ? $container->errors : $container;
parent::__construct(null);
}
public function get() {
return $this->errors;
}
}
Сохраним этот код в application/libraries/exceptions.php для того, чтобы позже можно было легко добавлять новые исключения. Из-за названия класса и его нестандартного расположения нам нужно зарегистрировать его в автозагрузчике (файл application/start.php):
Autoloader::map(array(
'ValidateException' => path('app') . 'libraries/exceptions.php'
));
С подготовкой обработки ошибок закончено — надеюсь, если что-то вам пока не понятно оно вскоре прояснится. В двух словах, мы хотим, чтобы когда вызывается метод проверки и он возбуждает исключение PHPValidateException
мы можем его поймать и переадресовать клиента обратно к форме, но уже с сообщениями об ошибках. Конечная цель — та же, что и в предыдущей статье, но в этот раз мы следуем принципам DRY, оптимально структурируя наш код.
Всё целиком
Раз мы всё настроили нам требуется что-то для проверки. Я буду использовать выдуманные значения для примера, но в вашем случае они будут поступать из формы или откуда-то из другого источника. Давайте предположим, что мы проверяем форму комментария перед его публикацией. Создадим сервис проверки комментария для поддержания высокой организации нашего кода:
<?php
namespace Services\Comments;
use Services\Validation as Validation_Service;
class Validation extends Validation_Service {
public function publish() {
$this->rules = array(
'name' => array('required'),
'email' => array('required', 'email'),
'comment' => array('required', 'max:200')
);
$this->validate();
}
}
Если нам потребуется проверить другие типы данных мы можем создать метод для каждого из них — например, для проверки редактирования комментария. Хотя всё пока просто, вы можете пойти дальше и определить общие правила для проверок в конструкторе нового подкласса, а затем вызвать родительский конструктор. В общем, вы ограничены только своей фантазией.
Теперь когда мы смотрим на маршрут комментирования мы видим аккуратный блок try..catch:
Route::post('posts/(:num)/comment', array('before' => 'csrf', function ($id) {
try {
$validation = new Services\Comments\Validation(Input::all());
$validation->publish();
} catch (ValidateException $errors) {
return Redirect::to('posts/' . $id)->with_errors($errors->get());
}
// Теперь мы можем добавить комментарий. Мы могли бы создать для этого новый сервис комментариев.
Services\Comments\Creator::create(Input::all(), $id);
});
Если проверка не прошла мы ловим исключение (catch) и получаем ошибки («get»). Весьма просто.
Вы можете пойти дальше и установить try..catch в методе PHPServices\Comments\Creator::create()
— и если он сработает, то просто повторно возбудите пойманное исключение:
<?php
namespace Services\Comments;
class Creator {
public static function create($input, $post_id) {
try {
$validation = new Validation($input);
$validation->publish();
} catch (ValidateException $errors) {
throw $errors;
}
// код добавления комментария.
}
}
Route::post('posts/(:num)/comment', array('before' => 'csrf', function ($id) {
try {
Services\Comments\Creator::create(Input::all(), $id);
// Комментарий был успешно добавлен.
return Redirect::to('posts/' . $id);
} catch (ValidateException $errors) {
return Redirect::to('posts/' . $id)->with_errors($errors->get());
}
});
Вот и всё. Теперь вы знакомы с несколькоими вариантами решения этой задачи. Я не утверждаю, что способ, продемонстрированный мной здесь — лучший или единственный; вполне возможно, что вы можете найти более подходящее под ваши условия решение. Это просто ещё один из методов проверки данных без лишних сложностей и с минимумом кода.