Статьи

Проверка данных в Laravel: правильный путь

Если бы приложение было миром, данные были бы его валютой. Каждое приложение, независимо от его цели, имеет дело с данными. И почти каждый тип приложения работает с пользовательским вводом, что означает, что он ожидает некоторых данных от пользователей и действует на них соответственно. Но эти данные должны быть проверены, чтобы убедиться, что они имеют правильный тип, и пользователь (с гнусными намерениями) не пытается взломать или взломать ваше приложение. Именно поэтому, если вы создаете приложение, требующее ввода данных пользователем, вам необходимо написать код для проверки этих данных, прежде чем что-то делать с ними.

Давным-давно

В какой-то момент вы, вероятно, провели проверку данных следующим образом:

<?php $errors = array(); if ( empty( $_POST['name'] ) || ! is_string( $_POST['name'] ) ) { $errors[] = 'Name is required'; } elseif ( ! preg_match( "/^[A-Za-z\s-_]+$/", $_POST['name'] ) ) { $errors[] = 'Name can have only alphabets, spaces and dashes'; } if ( empty( $_POST['email'] ) || ! is_string( $_POST['email'] ) ) { $errors[] = 'Email is required'; } //........... //.... some more code here //........... //display errors if ( ! empty( $errors ) ) { for ( $i = 0; $i < count( $errors ); $i++ ) { echo '<div class="error">' . $errors[ $i ] . '</div>'; } } 

Ну, это был каменный век для вас. К счастью, в наши дни у нас есть намного лучшие и более сложные пакеты валидации (и они были у нас довольно давно в PHP).

Если вы используете какую-либо инфраструктуру приложения в качестве основы своего приложения, скорее всего, у нее будет свой собственный или рекомендуемый сторонний пакет для проверки данных, особенно если это среда с полным стеком. Поскольку Laravel представляет собой интегрированную среду стека, он поставляется с собственным пакетом проверки. Как всегда, вы не обязаны использовать это и можете использовать любой другой пакет проверки данных, который вы хотите. Однако для целей данного руководства мы будем придерживаться того, что поставляется с Laravel по умолчанию.

Проверка данных: путь Laravel

Исходный код этого урока доступен здесь . Вам нужно просто запустить composer install чтобы установить среду Laravel внутри директории проекта, прежде чем вы сможете запустить этот код.

Теперь, продолжая предыдущий пример, давайте предположим, что у нас есть форма с полями Name и Email, и мы хотели бы проверить эти данные перед их сохранением. Вот как предыдущая рудиментарная попытка валидации могла бы перевести Laravel.

 <?php $validation = Validator::make( array( 'name' => Input::get( 'name' ), 'email' => Input::get( 'email' ), ), array( 'name' => array( 'required', 'alpha_dash' ), 'email' => array( 'required', 'email' ), ) ); if ( $validation->fails() ) { $errors = $validation->messages(); } //........... //.... some more code here //........... //display errors if ( ! empty( $errors ) ) { foreach ( $errors->all() as $error ) { echo '<div class="error">' . $error . '</div>'; } } 

В приведенном выше коде мы проверяли данные с помощью Laravel, используя фасад Validator. Мы передали массив данных, которые мы хотим проверить, и массив правил проверки, в соответствии с которыми мы хотим, чтобы данные были проверены. Мы получаем объект, возвращенный нам, и затем проверяем, не прошла ли проверка данных или нет. Если это не удалось, то мы берем объект сообщений об ошибках (объект класса MessageBag Laravel) и затем зацикливаем его, чтобы распечатать все сообщения об ошибках. Правила валидации, которые мы использовали, встроены в пакет валидации, и их доступно много , некоторые даже проверят базу данных.

Теперь нередко встречается код, в котором валидация данных размещается в нечетных местах, например, в методах контроллера или в моделях данных. Код проверки данных не относится ни к одному из этих мест, поскольку такое размещение не соответствует понятиям « Единственная ответственность» и « СУХОЙ» (не повторяйте себя).

Одиночная ответственность: один класс должен иметь одну и только одну работу для выполнения. Работа Контроллера заключается в том, чтобы действовать как связующее звено между бизнес-логикой и клиентом. Он должен захватить запрос и передать его кому-то, кто может обработать запрос, он не должен начинать обрабатывать сам запрос.

Точно так же работа модели заключается в том, чтобы выступать в качестве посредника между источником данных и остальной частью приложения. Он должен принимать данные только для сохранения и передавать их по запросу.

В этом есть более чем одна школа мысли; некоторые добавили бы валидацию данных в Модели, но даже те не поместили бы действительный код валидации данных в класс Модели. Скорее всего, он будет передан другому классу, который будет принимать данные и сообщать, являются ли данные действительными или нет.

Итак, куда мы помещаем код, который выполняет проверку данных и который можно использовать в любом месте приложения?

Валидация как услуга

Идеальный выбор — переместить проверочный код в отдельный класс (ы), который может использоваться по мере необходимости.

Продолжая наш предыдущий пример кода, давайте переместим его в свой собственный класс. Создайте каталог с именем RocketCandy внутри каталога app . Это наш основной каталог (или каталог домена), в который мы поместим все наши пользовательские элементы (службы, исключения, служебные библиотеки и т. Д.). Теперь создайте структуру каталогов Services/Validation внутри RocketCandy . Внутри каталога Validation создайте Validator.php .

Теперь, прежде чем мы продолжим, откройте ваш composer.json и после classmap в узле autoload добавьте пространство имен RocketCandy для автозагрузки PSR-4. Это будет выглядеть примерно так:

 "autoload": { "classmap": [ "app/commands", "app/controllers", "app/models", "app/database/migrations", "app/database/seeds", "app/tests/TestCase.php" ], "psr-4": { "RocketCandy\\": "app/RocketCandy" } }, 

Затем в своем терминале запустите composer dump-autoload -o чтобы composer мог сгенерировать автозагрузчик для нашего пространства имен RocketCandy .

Теперь откройте RocketCandy/Services/Validation/Validator.php . После того, как мы переместим код проверки сверху, он будет выглядеть примерно так:

 <?php namespace RocketCandy\Services\Validation; class Validator { public function validate() { $validation = \Validator::make( array( 'name' => \Input::get( 'name' ), 'email' => \Input::get( 'email' ), ), array( 'name' => array( 'required', 'alpha_dash' ), 'email' => array( 'required', 'email' ), ) ); if ( $validation->fails() ) { return $validation->messages(); } return true; } } //end of class //EOF 

Теперь мы можем использовать это как:

 <?php $validator = new \RocketCandy\Services\Validation\Validator; $validation = $validator->validate(); if ( $validation !== true ) { //show errors } 

Это несколько лучше, чем то, что мы делали ранее, но все еще не идеально по следующим причинам:

  1. Наш класс Валидации все еще недостаточно СУХОЙ. Нам потребуется скопировать весь этот код проверки в другой класс, чтобы проверить данные другого объекта.
  2. Почему мы выбираем входные данные внутри класса проверки? Там нет причин делать это там, потому что это ограничило бы нас в отношении того, какие данные мы можем проверить. Здесь мы сможем проверить эти данные, только если они поступят из формы ввода.
  3. Нет способа переопределить правила валидации, они установлены в камне.
  4. Механизм, с помощью которого мы узнаем, являются ли данные проверенными или нет, не является чистым. Конечно, это служит цели, но это определенно можно улучшить.
  5. Мы используем псевдостатический вызов пакета проверки Laravel. Это также может быть улучшено.

Решение?

Мы немного дальше абстрагируем проверочный код и используем исключения.

Во-первых, давайте сделаем наши собственные исключения. Создайте каталог Exceptions в RocketCandy и создайте BaseException.php . Откройте его и вставьте в него следующий код.

 <?php namespace RocketCandy\Exceptions; use Exception; use Illuminate\Support\MessageBag; abstract class BaseException extends Exception { protected $_errors; public function __construct( $errors = null, $message = null, $code = 0, Exception $previous = null ) { $this->_set_errors( $errors ); parent::__construct( $message, $code, $previous ); } protected function _set_errors( $errors ) { if ( is_string( $errors ) ) { $errors = array( 'error' => $errors, ); } if ( is_array( $errors ) ) { $errors = new MessageBag( $errors ); } $this->_errors = $errors; } public function get_errors() { return $this->_errors; } } //end of class //EOF 

Здесь мы создали абстрактный класс, и все наши пользовательские исключения наследуют этот класс. Первый параметр для конструктора — это то, что нас интересует, поэтому давайте посмотрим на это. Мы используем MessageBag Laravel для хранения наших ошибок (если они еще не в нем), чтобы у нас был единый способ циклического прохождения и отображения этих ошибок независимо от того, было ли исключение выдано службой проверки или любым другим. Таким _set_errors() метод _set_errors() проверяет, было ли _set_errors() одно сообщение об ошибке в виде строки или был передан массив сообщений об ошибках. Соответственно, он сохраняет их в объекте MessageBag (если только он не находится внутри, в этом случае он будет сохранен как есть). И у нас есть метод get_errors() который просто возвращает содержимое нашей переменной класса как есть.

Теперь в том же каталоге создайте ValidationException.php и его код будет:

 <?php namespace RocketCandy\Exceptions; class ValidationException extends BaseException { } //end of class //EOF 

Вот и все, здесь нам больше ничего не нужно, это будет пустая оболочка, потому что все, что нам нужно сделать, будет обрабатываться BaseException .

Теперь мы продолжаем переоснащение нашего класса Validator . Нам нужно абстрагировать код валидации, ValidationException исключение ValidationException при ошибках и разрешить переопределение правил валидации. Так это будет выглядеть так:

 <?php namespace RocketCandy\Services\Validation; use Illuminate\Validation\Factory as IlluminateValidator; use RocketCandy\Exceptions\ValidationException; /** * Base Validation class. All entity specific validation classes inherit * this class and can override any function for respective specific needs */ abstract class Validator { /** * @var Illuminate\Validation\Factory */ protected $_validator; public function __construct( IlluminateValidator $validator ) { $this->_validator = $validator; } public function validate( array $data, array $rules = array(), array $custom_errors = array() ) { if ( empty( $rules ) && ! empty( $this->rules ) && is_array( $this->rules ) ) { //no rules passed to function, use the default rules defined in sub-class $rules = $this->rules; } //use Laravel's Validator and validate the data $validation = $this->_validator->make( $data, $rules, $custom_errors ); if ( $validation->fails() ) { //validation failed, throw an exception throw new ValidationException( $validation->messages() ); } //all good and shiny return true; } } //end of class //EOF 

Вот в этом абстрактном классе мы имеем:

  1. Извлечен код проверки. Он может использоваться как есть для проверки данных любого объекта.
  2. Удалена выборка данных из класса. Класс валидации не должен знать, откуда поступают данные. Он принимает массив данных для проверки в качестве параметра.
  3. Удалены правила проверки из этого класса. Каждая сущность может иметь свой собственный набор правил проверки, определенных либо в классе, либо они могут быть переданы в виде массива для validate() . Если вы хотите определить правила в дочерних классах и убедиться, что они присутствуют, я писал об эмуляции абстрактных свойств в PHP некоторое время назад.
  4. Улучшен механизм определения ошибки проверки. Если проверка данных завершится неудачно, служба проверки выдаст исключение ValidationException и мы можем получить из этого ошибки вместо проверки возвращаемого типа данных или значений и т. Д. Это также означает, что мы можем выдать другое исключение, если правила проверки не определены. Это будет другое исключение, и мы сразу узнаем, что мы где-то напутали.
  5. Удалено использование статического вызова для проверки данных. Здесь мы теперь внедряем класс проверки Laravel в конструктор нашего класса. Если мы разрешим наш сервис проверки из контейнера IoC Laravel (что мы и сделали бы), нам не пришлось бы беспокоиться о внедрении зависимостей в конструктор.

Теперь мы создадим класс проверки для нашей формы, который расширит этот абстрактный класс. В этом же каталоге создайте TestFormValidator.php и добавьте в него следующий код:

 <?php namespace RocketCandy\Services\Validation; class TestFormValidator extends Validator { /** * @var array Validation rules for the test form, they can contain in-built Laravel rules or our custom rules */ public $rules = array( 'name' => array( 'required', 'alpha_dash', 'max:200' ), 'email' => array( 'required', 'email', 'min:6', 'max:200' ), 'phone' => array( 'required', 'numeric', 'digits_between:8,25' ), 'pin_code' => array( 'required', 'alpha_num', 'max:25' ), ); } //end of class //EOF 

Это класс, который мы создадим для проверки данных нашей формы. Мы установили правила валидации в этом классе, поэтому нам просто нужно вызвать метод validate() для его объекта и передать ему данные нашей формы.

Примечание. Если вы не сделали инструмент artisan в каталоге вашего проекта исполняемым, вам нужно заменить ./artisan из команд ./artisan в этом руководстве и заменить на /path/to/php artisan .

Рекомендуется сделать artisan исполняемым, так как это избавляет от лишних хлопот с префиксом php в каждой команде.

Давайте сделаем Контроллер и форму правильно, чтобы принять это за спин. В своем терминале перейдите в каталог вашего проекта и запустите

 ./artisan controller:make DummyController --only=create,store 

Это создаст app/controllers/DummyController.php с двумя методами — create() и store() . Откройте app/routes.php и добавьте следующую директиву маршрута.

 Route::resource( 'dummy', 'DummyController', array( 'only' => array( 'create', 'store' ), ) ); 

Это настроит наш контроллер для работы в режиме RESTful; /dummy/create/ принимает запросы GET и /dummy/store/ принимает запросы POST . Мы можем удалить only директиву как из команды route, так и из кустарной команды, и контроллер также будет принимать запросы PUT и DELETE но для текущего упражнения они нам не нужны.

Теперь нам нужно добавить код в наш контроллер, поэтому откройте app/controllers/DummyController.php . Это была бы пустая оболочка с методами create() и store() . Нам нужно, чтобы create() визуализировал представление, поэтому сделайте так:

 /** * Show the form for creating a new resource. * * @return Response */ public function create() { return View::make( 'dummy/create' ); } 

Теперь нам нужно представление, которое мы отображаем здесь и которое будет отображать нашу форму.

Сначала давайте создадим файл макета, в который мы можем поместить шаблон HTML-кода. Создайте каталог layouts в app/views и создайте в нем default.blade.php . Обратите внимание на суффикс .blade имени представления здесь. Он сообщает Laravel, что это представление использует синтаксис шаблонов Blade Laravel и должно быть проанализировано как таковое. Откройте его и добавьте следующий шаблонный код:

 <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Advanced Data Validations Demo</title> <!-- Bootstrap core CSS --> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css"> <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> <!--[if lt IE 9]> <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script> <![endif]--> </head> <body> <div class="row"> <h1 class="col-md-6 col-md-offset-3">Advanced Data Validations Demo</h1> </div> <p>&nbsp;</p> <div class="container"> @yield( "content" ) </div><!-- /.container --> <!-- Bootstrap core JavaScript --> <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script> <script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script> </body> </html> 

Это простой шаблонный код для HTML-страницы, который использует Bootstrap . Единственное, на что следует обратить внимание — это заполнитель, в который мы вставляем код нашего представления; @yield( "content" ) сообщит парсеру Laravel’s Blade, что код раздела content нашего представления должен быть введен в этом месте.

Теперь создайте app/views/dummy/create.blade.php и добавьте в него следующий код:

 @extends( "layouts/default" ) @section( "content" ) <div class="row"> <h3 class="col-md-6 col-md-offset-2">Test Form</h3> </div> <p>&nbsp;</p> @if ( ! $errors->isEmpty() ) <div class="row"> @foreach ( $errors->all() as $error ) <div class="col-md-6 col-md-offset-2 alert alert-danger">{{ $error }}</div> @endforeach </div> @elseif ( Session::has( 'message' ) ) <div class="row"> <div class="col-md-6 col-md-offset-2 alert alert-success">{{ Session::get( 'message' ) }}</div> </div> @else <p>&nbsp;</p> @endif <div class="row"> <div class="col-md-offset-2 col-md-6"> {{ Form::open( array( 'route' => 'dummy.store', 'method' => 'post', 'id' => 'test-form', ) ) }} <div class="form-group"> {{ Form::label( 'name', 'Name:' ) }} {{ Form::text( 'name', '', array( 'id' => 'name', 'placeholder' => 'Enter Your Full Name', 'class' => 'form-control', 'maxlength' => 200, ) ) }} </div> <div class="form-group"> {{ Form::label( 'email', 'Email:' ) }} {{ Form::text( 'email', '', array( 'id' => 'email', 'placeholder' => 'Enter Your Email', 'class' => 'form-control', 'maxlength' => 200, ) ) }} </div> <div class="form-group"> {{ Form::label( 'phone', 'Phone:' ) }} {{ Form::text( 'phone', '', array( 'id' => 'phone', 'placeholder' => 'Enter Your Phone Number', 'class' => 'form-control', 'maxlength' => 25, ) ) }} </div> <div class="form-group"> {{ Form::label( 'pin_code', 'Pin Code:' ) }} {{ Form::text( 'pin_code', '', array( 'id' => 'pin_code', 'placeholder' => 'Enter Your Pin Code', 'class' => 'form-control', 'maxlength' => 25, ) ) }} </div> <div class="form-group"> {{ Form::submit( '&nbsp; Submit &nbsp;', array( 'id' => 'btn-submit', 'class' => 'btn btn-primary', ) ) }} </div> {{ Form::close() }} </div> </div> @stop 

В этом представлении мы сначала сообщаем Laravel, что хотим использовать макет default.blade.php используя директиву @extends( "layouts/default" ) . Затем мы создаем раздел content как тот, который мы установили для добавления в макет. Представление отображает форму с полями «Имя», «Электронная почта», «Телефон» и «PIN-код», используя конструктор форм Laravel ( мы могли бы использовать поля HTML5 здесь с включенной базовой проверкой браузера, как для электронной почты, мы могли бы использовать Form::email() , но так как мы хотим чтобы проверить нашу проверку на стороне сервера, мы используем обычные текстовые поля для ввода ). Также над формой мы проверяем, есть ли у нас что-либо в $errors и показываем ее, если есть какие-либо ошибки. Также мы проверяем наличие любых флеш-сообщений.

Если теперь мы перейдем по http://<your-project-domain>/dummy/create ( здесь предполагается, что вы уже настроили домен в стеке разработки для этого проекта ), то мы получим для нас визуализованную форму. Теперь нам нужно принимать данные из этой формы и проверять их. Итак, вернувшись в наш DummyController мы DummyController бы наш TestFormValidator в конструктор и приняли бы данные в store() и проверили их. Таким образом, контроллер будет выглядеть так:

 <?php use RocketCandy\Exceptions\ValidationException; use RocketCandy\Services\Validation\TestFormValidator; class DummyController extends BaseController { /** * @var RocketCandy\Services\Validation\TestFormValidator */ protected $_validator; public function __construct( TestFormValidator $validator ) { $this->_validator = $validator; } /** * Show the form for creating a new resource. * * @return Response */ public function create() { return View::make( 'dummy/create' ); } /** * Store a newly created resource in storage. * * @return Response */ public function store() { $input = Input::all(); try { $validate_data = $this->_validator->validate( $input ); return Redirect::route( 'dummy.create' )->withMessage( 'Data passed validation checks' ); } catch ( ValidationException $e ) { return Redirect::route( 'dummy.create' )->withInput()->withErrors( $e->get_errors() ); } } } //end of class //EOF 

Laravel позаботится о внедрении зависимостей в конструктор, так как все контроллеры по умолчанию разрешаются из его контейнера IoC, поэтому нам не нужно об этом беспокоиться. В методе store() мы берем все входные данные формы в var, а внутри try/catch мы передаем данные нашей службе проверки. Если данные проверяются, то они перенаправляют нас обратно на страницу формы с сообщением об успешном завершении, в противном случае генерируется ValidationException которое мы обнаружим, фиксируем ошибки и возвращаемся обратно в форму, чтобы отобразить, что пошло не так.

Резюме

В этой первой части мы узнали, как проводить валидацию данных способом Laravel и как абстрагировать валидацию от сервиса, который можно использовать для валидации данных для каждого объекта в любом месте приложения. В следующей и последней части мы узнаем, как расширить пакет проверки Laravel, чтобы иметь собственные правила.


Есть мысли? Вопросов? Огонь в комментариях.