Введение
Эта статья является второй частью серии статей, в которых мы рассказываем о том, как разделение кода на основе функциональности может улучшить качество вашей кодовой базы. Если вы еще не читали первую часть, ознакомьтесь с ней здесь.
Эта статья будет посвящена отделению проверки запроса от остального кода в контроллере. В этом руководстве я буду использовать Laravel. Тем не менее, вы можете применить идеи, которые мы будем здесь обсуждать, к любому языку программирования, который поддерживает любую форму разделения кода на пакеты, модули и т.д., таким образом, чтобы вы могли использовать код, написанный в file A
в file B
.
Предварительные условия
- Чтение первой части этой статьи здесь для понимания некоторых концепций и терминологии
- Требуется знание объектно-ориентированного программирования (ООП)
- Базовые знания о Laravel
- Если вы хотите протестировать код на своей системе, то вы ** ДОЛЖНЫ ** иметь установленный PHP на вашей системе
Давайте погрузимся 🚀.
Весь код из этой статьи вы можете найти здесь.
Что мы имеем на данный момент
Давайте рассмотрим простую функцию контроллера, которая добавляет нового пользователя в базу данных во время процесса регистрации. Я добавил комментарии, чтобы проиллюстрировать, как мы можем определить три проблемы, задействованные в функции register.
/**
* create a new account
* @param IlluminateHttpRequest $request
* @return IlluminateHttpResponse
*/
public function register(Request $request)
{
/*----Concern (Request validation)----*/
$validator = Validator::make($request->all(), [
'email' => 'required|email|unique:users',
'name' => 'required|string',
'password' => 'required|min:6'
]);
if ($validator->fails()) {
/*----Concern (Sending response to the client)----*/
return response()->json(
[
"status" => false,
"message" => $validator->errors()
],
422
);
}
/*----Concern (Handling business logic (In this case, hashing the user's password and interaction with database)----*/
$password = Hash::make($request->password);
$user = User::create([
'name' => $request->name,
'password' => $password,
'email' => $request->email
]);
/*----Concern (Sending response to the client)----*/
return response()->json(
[
"status" => true,
"message" => 'User account created successfully',
"data" => $user
],
201
);
}
Разделение проблемы проверки запроса
Мы обсудим два подхода к отделению проблемы проверки запроса от контроллера.
- Первый подход предполагает создание класса проверки запроса и запись всей логики проверки в класс проверки. Класс должен содержать некоторые
public
методы, чтобы Контроллер мог выполнить логику проверки. - Второй подход называется валидацией запроса формы. Этот подход специфичен для Laravel, и класс валидации должен следовать определенному шаблону. Это мощная функция.
Подход 1: Создание класса валидации запроса
- Создайте папку
Validators
внутри каталогаapp/Http
. - Создайте новый файл
AuthValidator.php
во вновь созданной папкеValidators
. - Содержимым файла
AuthValidator.php
должен быть приведенный ниже код
<?php
namespace AppHttpValidators;
use IlluminateSupportFacadesValidator;
class AuthValidator
{
protected $errors = [];
/**
* validate user registration data
*
* @param array $requestData
* @return AppHttpValidatorsAuthValidator
*/
public function validateRegistrationData(array $requestData)
{
$validator = Validator::make($requestData, [
'email' => 'required|email|unique:users',
'name' => 'required|string',
'password' => 'required|min:6'
]);
$this->errors = $validator->errors();
return $this;
}
/**
* get the errors that occurred during
* validation
*
* @return array
*/
public function getErrors()
{
return $this->errors;
}
}
Что именно делает этот код? 😕
- Во-первых, мы создаем свойство класса
$errors
и устанавливаем его значение как пустой массив. - Затем мы создаем метод
validateRegisterationData(),
который будет отвечать за выполнение логики проверки. - Результат проверки валидности (
$validator->errors()
) хранится в свойстве$errors
текущего объекта. Если все данные в запросе валидны и ошибок валидации не возникло, валидатор возвращает пустой массив. - Наконец, мы возвращаем текущий объект (
$this
). - Второй метод,
getErrors()
просто возвращает свойство$errors
для текущего объекта. Это свойство будет содержать фактические ошибки, которые произошли во время валидации, или пустой массив, если ошибок не было.
Как мы используем AuthValidator в контроллере? 🤔
Создайте метод конструктора (__construct()
) в классе AuthController и передайте класс Authvalidator в качестве параметра в конструктор. Затем значение параметра хранится в свойстве класса.
class AuthController extends Controller
{
protected $authValidator;
/**
* __construct
*
* @param AppHttpValidatorsAuthValidator $authValidator
* @return void
*/
public function __construct(AuthValidator $authValidator)
{
$this->authValidator = $authValidator;
}
Теперь мы можем удалить существующую логику проверки в методе register()
контроллера и использовать логику в классе AuthValidator, который мы создали ☺️.
/**
* create a new account
* @param IlluminateHttpRequest $request
* @return IlluminateHttpResponse
*/
public function register(Request $request)
{
$errors = $this->authValidator
->validateRegistrationData($request->all())
->getErrors();
if (count($errors)) {
return response()->json([ "status" => false,"message" => $errors],
422
);
}
Поскольку метод validateRegistrationData()
возвращает объект, мы можем немедленно выполнить метод getErrors()
на возвращенном объекте, не присваивая его сначала переменной. Этот паттерн называется Fluent interface. count($errors)
проверяет, есть ли хотя бы один элемент в массиве ошибок, возвращенном из валидатора, и возвращает ответ клиенту, если ошибка существует.
При таком построении кода контроллеру не важно, как работает логика валидации. Контроллер знает, что валидатор выполнит свою работу и вернет ответ после завершения процесса валидации. Затем окончательный ответ отправляется из контроллера. Поэтому мы можем добавлять, удалять или изменять логику валидации, и Контроллер по-прежнему будет работать безупречно без каких-либо проблем. Отлично. Если вы считаете, что это здорово, давайте рассмотрим второй метод, который заключается в использовании Laravel Form Requests.
Подход 2: Использование Laravel Form Requests
Запросы формы — это пользовательские классы запросов, содержащие свою логику валидации, как мы делали в первом подходе. Запросы форм Laravel также замечательны тем, что по умолчанию содержат логику авторизации. Чтобы создать класс запроса формы, вы можете использовать команду make:request
artisan, предоставляемую Laravel.
php artisan make:request CreateNewUserRequest
Laravel создаст класс запроса формы в каталоге app/Http/Requests
с именем, переданным в качестве имени файла. В нашем случае будет создан новый файл CreateNewUserRequest.php
.
По умолчанию классы запросов формы, создаваемые Laravel, содержат два метода, authorize()
и rules()
. Метод authorize()
содержит логику, которая определяет, имеет ли пользователь, делающий запрос, разрешение на это. Конечно, вы можете в любой момент изменить эту логику в соответствии с вашим конкретным случаем использования. В нашем случае нам не требуется никакого специального разрешения или авторизации для класса CreateNewUserRequest
, поэтому мы возвращаем true.
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
С другой стороны, метод rules()
возвращает правила валидации, которые должны применяться к данным запроса.
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'email' => 'required|email|unique:users',
'name' => 'required|string',
'password' => 'required|min:6'
];
}
Как использовать правила в классе CreateNewUserRequest в контроллере? 🤔
- Измените тип параметра в функции register с
IlluminateHttpRequest
наAppHttpRequestsCreateNewUserRequestwith
(это путь к классу запроса формы Laravel, который мы только что создали). - Удалите логику валидации в функции
register()
. - Поскольку Laravel внутренне проверяет ошибки и возвращает соответствующий ответ при использовании запросов формы, нам не нужен оператор
if
, чтобы проверить, не прошла ли валидация
/**
* create a new account
* @param AppHttpRequestsCreateNewUserRequest $request
* @return IlluminateHttpResponse
*/
public function register(CreateNewUserRequest $request)
{
/*--------Concern 2 (Business logic execution)------------------*/
$password = Hash::make($request->password);
$user = User::create([
'name' => $request->name,
'password' => $password,
'email' => $request->email
]);
/*--------Concern 3(Response formatting and return)------------------*/
return response()->json(
[
"status" => true,
"message" => 'User account created',
"data" => $user
],
201
);
}
И вуаля! Наш Контроллер не имеет абсолютно никакого представления о логике валидации, и изменения правил валидации не влияют на Контроллер. Таким образом, мы снова можем менять логику валидации как угодно, а Контроллер останется прежним и будет прекрасно работать.
Давайте рассмотрим запросы формы немного подробнее 💪🏿.
Вот пример ответа на ошибку класса CreateNewUserRequest
после отправки запроса с пустым значением в качестве имени
нового пользователя
{
"message": "The given data was invalid.",
"errors": {
"name": [
"The name field is required."
]
}
}
Теперь предположим, что в соответствии с правилами нашей команды нам необходимо изменить структуру ответа на ошибку, отправляемого клиенту после неудачной проверки. Мы можем добиться этого, переопределив метод по умолчанию failedValidation()
во всех классах запросов формы Laravel, которые расширяют FormRequest
, как и наш класс CreateNewUserRequest
.
Теперь у нас есть дополнительный метод failedValidation()
, который выглядит следующим образом.
/**
* Handle a failed validation attempt.
*
* @param IlluminateContractsValidationValidator $validator
* @return void
*
* @throws IlluminateValidationValidationException
*/
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(
response()->json(
[
'status' => false,
'message' => 'Validation errors occurred',
'errors' => $validator->errors(),
],
422
)
);
}
После внесения изменений в структуру сообщения об ошибке наш ответ выглядит следующим образом.
{
"status": false,
"message": "Validation errors occurred",
"errors": {
"name": [
"The name field is required."
]
}
}
Что дальше? 💭
В следующей части этой серии мы рассмотрим, как использовать Actions
для отделения бизнес-логики от контроллера. Поверьте, это будет потрясающе, и вы не захотите это пропустить.
Краткое резюме 🔄
- Контроллеры в вашей кодовой базе должны знать, какой класс валидации нужно вызвать для конкретного запроса, но не иметь никаких знаний о правилах валидации, которые применяются к данным запроса. За это отвечают валидаторы.
- Вы можете создать свой класс валидатора или использовать запросы формы Laravel. Я не думаю, что какой-то подход лучше другого. Все зависит от того, какой подход вы предпочитаете.
- Класс запросов формы Laravel также может обрабатывать авторизацию пользователя еще до проверки того, что данные от клиента проходят все правила валидации, и это делается с помощью метода
authorize()
. - Преимущество создания собственных валидаторов заключается в том, что ваши методы могут следовать любому соглашению об именовании, которое вы захотите. Однако при использовании запросов Laravel Form метод, обрабатывающий проверку данных, должен иметь имя
rules
, а метод, обрабатывающий авторизацию, должен иметь имяauthorize
.
Вот и все 🎉.
Вы можете проверить весь код в этой статье на Github. Я искренне надеюсь, что после прочтения этой статьи вы чему-то научились, неважно, насколько малому. Если это так, пожалуйста, поставьте большой палец вверх.
Спасибо, что оставались со мной до конца. Если у вас есть какие-либо предложения или отзывы, пожалуйста, напишите их в разделе комментариев. Приятного вам отдыха. Пока 😊.