Исчерпывающее руководство по разработке перспективных контроллеров: Часть 2

Введение

Эта статья является второй частью серии статей, в которых мы рассказываем о том, как разделение кода на основе функциональности может улучшить качество вашей кодовой базы. Если вы еще не читали первую часть, ознакомьтесь с ней здесь.

Эта статья будет посвящена отделению проверки запроса от остального кода в контроллере. В этом руководстве я буду использовать 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. Я искренне надеюсь, что после прочтения этой статьи вы чему-то научились, неважно, насколько малому. Если это так, пожалуйста, поставьте большой палец вверх.
Спасибо, что оставались со мной до конца. Если у вас есть какие-либо предложения или отзывы, пожалуйста, напишите их в разделе комментариев. Приятного вам отдыха. Пока 😊.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *