Как правильно организовать обработку данных в экшене контроллера?

Задался вопросом простых и гибких экшинов в контроллерах. Собственно используя свой подход к проектированию столкнулся с проблемами, для решения которых хочу воспользоваться вашими советами.
Примеры на laravel5

Контроллер:
<?php

namespace App\Http\Controllers;

use App\Http\Requests\TestRequest;
use App\Services\TestService;
use Exception;
use Session;
use DB;

class TestController extends Controller
{
    protected $testService;


    public function __construct(TestService $testService)
    {
        $this->testService = $testService;
    }


    public function index(TestRequest $request)
    {
        DB::beginTransaction();

        try {

            $result = $this->testService->process($request->all());

            if ($result) {
                DB::commit();

                // Данные сохранены успешно

            } else {
                DB::rollBack();

                // Ошибка сохранения данных

            }
        } catch (Exception $e) {
            DB::rollBack();

            // Ошибка обработки данных
        }

        //return redirect || view;
    }

}


Сервис
<?php

namespace App\Services;

use ErrorException;

class TestService
{
    public function process(array $data)
    {
        // Логика и аналитика для получания переменной $var
        $var = 7;

        if ($var > $data['count']) {
            throw new ErrorException('Возникла критическая ошибка, ...');
        }

        return true;
    }
}


Реквест
<?php

namespace App\Http\Requests;

use HttpResponseException;
use Validator;
use Response;

class TestRequest extends Request
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        $this->sanitize();

        return [
            // rules
        ];
    }

    public function messages()
    {
        return [];
    }

    public function sanitize()
    {
        $input = $this->all();
        $input['var1'] = filter_var(isset($input['var1']) ? $input['var1'] : null, FILTER_SANITIZE_STRING);
        $input['var2'] = filter_var(isset($input['var2']) ? $input['var2'] : null, FILTER_SANITIZE_STRING);
        $input['var3'] = filter_var(isset($input['var3']) ? $input['var3'] : null, FILTER_SANITIZE_STRING);
        $this->replace($input);
    }

}


Возник вопрос/проблема с валидацией данных

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

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

Соответственно получается неудобная ситуация, когда часть валидации нужно разместить в реквесте, а часть в сервисе. Еще остается проблема рендеринга ошибок, когда у нас получается два типа валидации, реквест и валидация по средствам эксепшинов из сервисов.

Подскажите пожалуйста (желательно на примерах псевдокода), как это красиво оформить ?
Буду очень признателен за развернуты ответы по этому вопросу.
  • Вопрос задан
  • 1157 просмотров
Решения вопроса 1
index0h
@index0h
PHP, Golang. https://github.com/index0h
По хорошему валидацию должен делать реквест

Неа. Request - это просто набор данных.

Тоесть выходит 2 запроса, 1 - через валидаторы фреймворка, а 2 - когда в контроллере или сервисе достаем эту-же запись для дальнейшей работы.

Вы пытаетесь найти единственное правильно место для проверок. По хорошему проверки должны быть во всех методах, даже в приватных. Что-то не так - бросайте исключение. Эта практика на первый взгляд жуткая, но она вас обезопасит от огромнейшего количества ошибок.

Опережая ваш вопрос: "это ж сколько дублировать проверки придется?". Да, много, но поверьте, оно стоит того.

В добавок к этому иногда логика может быть достаточно замысловатой

Если вы пытаетесь в одной точке захватить множество контекстов - безусловно. Если же будете делать необходимые проверки всюду - эта сложность не возникнет.

Мне не очень нравится идея выносить куски логики в реквест

Мысль здравая.

Соответственно получается неудобная ситуация, когда часть валидации нужно разместить в реквесте, а часть в сервисе.

Не нарушайте SRP.

<?php

namespace Vendor\Project\AppBundle\Controller;

use KoKoKo\assert\Assert;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Vendor\Project\Path\Authorization\UserNotFoundException;
use Vendor\Project\Path\Authorization\InvalidCredentialsException;

class LoginController extends Controller
{
    /**
     * @Route("/login", name="login")
     * @Method({"POST"})
     * @param Request $request
     * @return JsonResponse
     */
    public function loginAction(Request $request) : JsonResponse
    {
        try {
            $login = $request->request->get('login');
            $pass  = $request->request->get('pass');

            Assert::assert($login, 'login')->string()->notEmpty()->match('/^[a-z\d]{3,32}$/i');
            Assert::assert($pass, 'pass')->string()->notEmpty()->lengthBetween(6, 32);
        } catch (\Throwable $exception) {
            return new JsonResponse($exception->getMessage(), JsonResponse::HTTP_BAD_REQUEST);
        }

        try {
            $user = $this->get('UserAuthorizator')->authorize($login, $pass);

            return new JsonResponse($user->getId());
        } catch (UserNotFoundException $exception) {
            return new JsonResponse('User not found', JsonResponse::HTTP_BAD_REQUEST);
        } catch (InvalidCredentialsException $exception) {
            return new JsonResponse('Invalid login or path', JsonResponse::HTTP_BAD_REQUEST);
        } catch (\Throwable $exception) {
            $this->get('logger')->error($exception->getMessage(), ['exception' => $exception]);

            return new JsonResponse('Something went wrnog :((', JsonResponse::HTTP_INTERNAL_SERVER_ERROR);
        }
    }
}
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
Alex_Wells
@Alex_Wells
PHP/TS/Kotlin developer
К сожалению, никак. Чекни это: https://github.com/illuminate/validation/blob/mast...
Запросы к базе идут напрямую, по-этому передать готовую модель валидатору невозможно. Так-же посмотри на это: https://github.com/illuminate/validation/blob/mast...
Тоже хардкодинг. Laravel - не волшебная палочка, и очеень далек от совершенства. С ним стает очень тяжело при выполнении задач сложнее CRUD. Но всегда есть варианты:

1) Можно создать свой класс валидатора, который будет наследовать обычный, там переписать методы проверки из базы, а так-же переписать DatabasePresenceVerifier, дабы тот юзал переданную модель. Но все это явно не будет элегантным и привальным решением, чисто как вариант.
2) Использовать сторонние валидаторы.
3) И самый простой - просто доставать модель, а потом уже $model->toArray() передавать валидатору. Таким образом можно полностью контролировать что и откуда достается, а так-же делать разные типы валидаций, выходящие за рамки обычного валидатора. Но в таком подходе тоже есть минусы - часть обычных методов валидатора можно выкинуть, таких как unique и тд.

Если 3 вариант не подходит, что скорее всего именно так, то нужно искать абсолютно кастомные верификаторы.

Других вариантов нету. Использовать встроенные средства Ларьки - ад. Я вот UUID в виде binary храню, так мне тут половину фреймворка раскопать надо, что-бы реализовать это нормально. Symfony в этом плане явно выигрывает.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы