Pull to refresh

Comments 11

Вы просто переписали часть документации апи платформы в статью на хабр?

Я просто изучил документацию, просто перевел часть статей документации на русский язык(wip), просто сделал туториал, и просто решил поделится им с друзьями.

А потом, подумал, а может этот туториал станет полезен не только мне и моим друзьям?
И просто поделился им с русскоговорящим сообществом Хабра.

В надежде, что кому-то это принесет пользу и упросит жизнь :)

По сути, мы написали одну строчку кода, добавив один атрибут, и уже имеем такой мощный функционал.

И бесполезный, подходит только для админки, и то только для самой простой.


В самой сущности, добавим поле plainPassword, которой добавим атрибут

Ух ты, прям в сущности хранить пароль в открытом виде? Да еще и в атрибуте, который сущности не принадлежит и используется только в одном сценарии.
Сущность начинает превращаться в God-object, в котором есть всё.


public function eraseCredentials()
// If you store any temporary, sensitive data on the user, clear it here

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


$data->setRoles($data->getRoles());

И роли по умолчанию для конкретного сценария. Данные по умолчанию для других сценариев тоже будем добавлять в сущность?


Сущность User
securityMessage: 'Пароль можно менять только себе'

Ага, и строки для представления сюда же добавим.


Далее нам необходимо реализовать процессор состояний

Во-во, God-object, в котором есть всё, и нужен специальный процессор, чтобы переходить из одного состояния в другое.


if (!$data->getPlainPassword())

Бизнес-сценарий определяем не явно, а через хаки с предположениями (если поле пустое, значит менять пароль не хотим).


if ($operation instanceof Post)

Добавили в этот же класс реализацию другого сценария.


назначим эти группы для полей в сущности User

Имитируем входные данные для бизнес-сценариев помощью групп.


Кстати, метод Patch тоже работает. И в нем так же можно изменить два поля

Что если надо изменить имя пользователя (поле name)? Будем проверять в процессоре, какие поля пришли?


$user->setToken(bin2hex(random_bytes(60)));
$this->repository->save($user, true);
...
throw new NotFoundHttpException();

Работа с HTTP в бизнес-логике.


security: "is_granted('ROLE_USER')"
Доступными переменными являются

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




Вот как это выглядит без использования API Platform.


60 строк кода
class User { ... }

class UserController {
  function registerAction(Request $request) {
    $validationResult = $this->validationService->validate($request, RegistrationForm::class);
    if ($validationResult->hasErrors()) {
      return $this->validationErrorResponse($validationResult->getErrors());
    }

    $form = $validationResult->getDto();
    $token = $this->userService->register($form);

    return $this->successResponse(['token' => $token]);
  }

  function loginAction(Request $request) {
    $validationResult = $this->validationService->validate($request, LoginForm::class);
    if ($validationResult->hasErrors()) {
      return $this->validationErrorResponse($validationResult->getErrors());
    }

    $form = $validationResult->getDto();
    $token = $this->userService->login($form);

    return $this->successResponse(['token' => $token]);
  }
}

class UserService {
  function register(RegistrationForm $form): string {
    $user = new User();

    $user->setEmail($form->getEmail());
    $passwordHash = $this->passwordHasher->hashPassword($form->getPassword());
    $user->setPasswordHash($passwordHash);

    $token = $this->generateToken();
    $user->setToken($token);

    $this->entityManager->save($user);

    return $token;
  }

  function login(LoginForm $form): string {
    $user = $this->userRepository->findOneByLogin($form->email);
    if ($user === null) {
      // HTTP response should be returned on validation step
      throw new RuntimeException('User not found');
    }

    $token = $this->generateToken();
    $user->setToken($token);

    return $token;
  }

  private function generateToken(): string {
    return bin2hex(random_bytes(60));
  }
}

Благодарю за ревью!

В туториале, я рассказываю как максимально быстро подступиться к изучению API-platform. Это не "серебряная пуля" для решения всех задач (в начале статьи, я подсветил, что можно делать API и на подходе с контроллерами).

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

По поводу Вашего замечания про God-object, только Вам решать как и куда раскладывать логику в проекте :)

В данном примере, я показал подход, как обработку каждого метода(POST, PATCH, GET) можно увести в процессор или провайдер (используя принцип CQRS). А дальше, естественно, реализовывать бизнес логику в сервисах или других компонентах системы.

Так же, если Вас не устраивает такой подход, API-platfom вполне поддерживает подход с контроллерами.

То что, произошла "концентрация" атрибутов в сущности пользователя, для описании поведения API, не вижу в этом ничего криминального, для формата туториала. И в сущности пароли не храниться в открытом виде, поле plainPassword необходимо, только для чтения пароля чтобы сразу закэшировать его.

По поводу высказывания "Программирование на другом языке" - если вы про атрибуты, то это все тот же php.

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

я рассказываю как максимально быстро подступиться к изучению API-platform

Ну а я даю совет, что не надо ее изучать)


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

Ну я вот поместил в 60 строк кода оба ваши процессора. При этом не надо читать документацию, не используются исключения, используется типизация (а не mixed $data) и не нужны проверки instanceof. И мы даже не дошли до сложной валидации с полями из связанных сущностей, типа указывать у товара только активную категорию. И не рассмотрели проблему появления N+1 в коде вида public function __invoke(Book $book), куда API Platform будет сама передавать Book, загруженный по id без связей.


По поводу Вашего замечания про God-object, только Вам решать как и куда раскладывать логику в проекте

Нет, это решать еще и API Platform. У нее есть требования где какие аннотации писать, куда что положить и как это должно называться.


я показал подход, как обработку каждого метода(POST, PATCH, GET) можно увести в процессор или провайдер (используя принцип CQRS).

А я хочу показать, что нескольких методов HTTP недостаточно для выражения всех возможных действий с сущностью, поэтому такие попытки приводят к плохо поддерживаемому коду. Например, в какой метод HTTP поместить активацию пользователя при переходе по ссылке из письма? Это GET-запрос, но GET-обработчик уже занят, поэтому придется делать отдельный endpoint /activation. То есть в результате все равно получится RPC, а не REST.


У вас нет разделения на Command и Query. Это должны быть выделенные структуры данных с теми полями, которые вы задаете группами.


А дальше, естественно, реализовывать бизнес логику в сервисах или других компонентах системы

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


Так же, если Вас не устраивает такой подход, API-platfom вполне поддерживает подход с контроллерами.

Зачем же она тогда нужна?) Контроллеры можно писать и без нее.


не вижу в этом ничего криминального, для формата туториала

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


необходимо только для чтения пароля

Вот я и говорю, всякие хаки для работы API Platform. А в моем коде этого не нужно, ни plainPassword, ни eraseCredentials.


если вы про атрибуты, то это все тот же php

Я про код внутри строковых литералов — "is_granted('ROLE_USER')", "object == user".


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

А с обычным кодом всё уместилось бы в одну статью.

Спасибо за совет!

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

Сам грешен, вел себя так же. Так как на изучение нового, уходит время и не всегда это новое окажется годным.

Со своей стороны, вижу свою задачу, рассказывать про инструменты такие как API platform, так как верю, что кому-то это сможет сэкономить много времени и ресурса.

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

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


так как верю, что кому-то это сможет сэкономить много времени и ресурса

Ну а я уже проверил, она не экономит время на реальных сложных задачах. На любое нестандартное действие надо будет лазить в документацию и исходный код библиотеки, проверять в отладчике что и как она делает. Я вот сходу нашел в вашем коде десяток примеров, которые считаются костылями и плохо поддерживаемым кодом. А главное, стремиться к REST бессмысленно, привязкой к 4 действиям с сущностью нельзя покрыть все бизнес-сценарии. Иначе это будет или простой CRUD без логики, или God-object, где в одном процессоре определяется нужное действие по набору входных полей и их значениям.


Если хотите, можем проверить. Добавим такую функциональность:
— Действие activate для активации из email
— Действие deactivate если пользователь хочет деактивировать свой аккаунт (не метод DELETE, DELETE будет полностью удалять)
— Фильтр списка пользователей по id/name/email (одно входное поле, SQL-запрос id = 'text' OR name LIKE '%text%' OR email LIKE '%text%')
— Фильтр по наличию статей has_articles (добавим таблицу статей, API для нее не нужно)


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

60 строк кода, ага. А как насчёт кода внутри RegistrationForm? А вот это $validationResult->getDto()? `successResponse()`? И это только для всего двух роутов регистрации/логина. Дорисуйте остальную сову?

---

Теперь представим что у вас 30 сущностей и для всех из них нужен типичный CRUD с разделением доступа по 3-4 ролям.

С подходом от APiPlatform я добавлю 30 аннотаций с security параметром и всё (1-4 строки для каждой сущности). И может накину два кастомных контроллера для очень специфических роутов.

Вы же сделаете 30 сервисов и 30*4 контроллеров (ну или 30 контроллеров с 4 методами в каждом по вкусу), в котором будет примерно один и тот же код? Да ещё и самописный, а не оттестированный тысячами пользователей?

Теперь добавим фильтрацию по любым параметрам, например, которая должна работать везде – сколько кода займёт это в вашем подходе? Явно не по одной строке для каждой сущности (в ApiPlatform именно так).

---

Я это к тому что для каждой задачи свой инструмент. И если в ваших проектах используется 5-6 сущностей со сложной логикой в каждом роуте – ApiPlatform ничего не упростит. Но если у вас большой проект с кучей сущностей и связей между ними и нужен разнообразный CRUD, то оно сэкономит очень много времени разработки и тестирования.

А как насчёт кода внутри RegistrationForm?

Так и автор в статье не весь код привел.


Теперь представим что у вас 30 сущностей и для всех из них нужен типичный CRUD

Я уже представил и написал про это, типичный CRUD без логики это бессмысленно. Отдельный сервис с API делают для того, чтобы сконцентрировать там некоторую бизнес-логику, какие-то знания о предметной области, чтобы не помещать их в другие системы. А если там простой CRUD, значит вся бизнес-логика находится в клиенте этого API, то есть мы не достигли этой цели. А если не простой, значит там God-object в обработчике PATCH с кучей проверок вида if ($srcArticleState->is_published === false && $newArticleState->is_published === true) $this->publishArticle(); else $this->hideArticle();.


Вы же сделаете 30 сервисов и 30*4 контроллеров (ну или 30 контроллеров с 4 методами в каждом по вкусу), в котором будет примерно один и тот же код?

У меня не будет 4 метода CRUD, у меня будут методы по числу бизнес-сценариев. Для пользователя например это register, login, activate, saveProfile и т.д.
В сервисах не будет один и тот же код. В контроллерах да, он похож, но если хочется, можно это вынести в какую-нибудь абстракцию. Это все равно будет проще и гибче, чем API Platform.
Проблема API Platform не в том, что это абстракция, а в том, что она заточена на REST и CRUD-действия, что для большинства реальных приложений не подходит.


Теперь добавим фильтрацию по любым параметрам, например, которая должна работать везде

Вот как раз для фильтрации в реальных приложениях мой подход более удобен. Набор полей в фильтрах обычно не соответствует набору полей сущности. В сущности есть поле created_at, а в фильтре 2 поля "от" и "до" (я знаю, что в API Platform есть специальный фильтр для этого). В фильтре галочка "has_images", а в сущности такого поля нет, нужен специальный подзапрос. В фильтре есть поле "search_text", и оно должно искать по нескольким текстовым полям сущности (название товара, guid, артикул).
Вот кстати фильтрация по любым полям обычно нужна когда бизнес-логика находится на клиенте.


сколько кода займёт это в вашем подходе?

Я ж выше предложил написать и сравнить. Пишите, сравним.

типичный CRUD без логики это бессмысленно. 

Где "круд без логики"? Я писал про задачи рода "типичный круд плюс много логики", – и здесь нет проблем раскидать логику в те же сущности и/или сервисы. Но необходимость круда при этом никто не отменял (по крайней мере, в тех случаях о которых я пишу и которые решает платформа).

она заточена на REST и CRUD-действия, что для большинства реальных приложений не подходит.

Кажется, об этом и речь. Может быть, у вас основной пул задач в контексте приложения, где не нужен круд, а кол-во роутов = кол-во бизнес-сценариев. В таком случае ценность платформы уменьшается, конечно. Я пишу про то что вокруг очень много задач, где нужно всё вместе: и почти 100% круд на всё, и ещё логика в добавок. "Показать список всех заказов", "добавить нового курьера", "удалить тариф", "показать все мои транзакции и баланс", – это не бизнес-процесс, а обычный круд. И он всегда будет там, где нужно что-то отображать конечному пользователю (не говоря уже про "админов" с ещё десятком разных ролей).

Я пока не видел исследований большинства пхп-проектов где бы сравнивали "покрытие сущностей крудом", может быть у вас есть ссылка на такое если утверждаете что "для большинства"?

В сущности есть поле created_at, а в фильтре 2 поля "от" и "до"

В фильтре галочка "has_images", а в сущности такого поля нет

В фильтре есть поле "search_text", и оно должно искать по нескольким текстовым полям сущности

Все эти фильтры уже есть из коробки в apiplatform, которые можно настроить одной строкой там же.

Вот кстати фильтрация по любым полям обычно нужна когда бизнес-логика находится на клиенте.

В таблице заказов будете делать десяток полей ввода для поиска по маске карты/фио/id/трекномеру на половину страницы? Или всё-таки один, который будет искать по всем полям одновременно? Можно и на клиенте решать по каким точно параметрам искать, можно и решить это на стороне сервера, – уже зависит от фантазии клиента, удобство в том что на бэке не надо делать ровно ничего для этого.

Я писал про задачи рода "типичный круд плюс много логики", – и здесь нет проблем раскидать логику в те же сущности и/или сервисы.

Проблемы я указал в первом комментарии. Код получается громоздкий и сложный для понимания, с кучей неочевидных действий.


"Показать список всех заказов", "добавить нового курьера", "удалить тариф", "показать все мои транзакции и баланс", – это не бизнес-процесс, а обычный круд.

Нет, это как раз бизнес-процессы. В "показать список всех заказов" есть фильтр, причем для админки один, а для пользователя другой, хотя сущность одна.
Для тарифа есть удаление, а есть деактивация. Это всё бизнес-действия, требования к ним обсуждаются на бизнес-уровне.


И он всегда будет там

Любые записи в базе данных имеют CRUD-операции, это не значит, что нужно предоставлять к ним веб-интерфейс. API это не интерфейс к БД, а если есть какая-то логика, то на CRUD-действия ее замапить сложно.


может быть у вас есть ссылка на такое если утверждаете что "для большинства"?

Я говорю по моему опыту и привожу конкретные примеры. Вы пока что ни один не привели. Можете описать конкретное полезное в реальных задачах API, где для действий с сущностями достаточно 4 HTTP-методов?


Все эти фильтры уже есть из коробки в apiplatform, которые можно настроить одной строкой там же.

Ага, а теперь надо сделать для админов один набор фильтров, а для пользователя другой, и для пользователя всегда показывать только его заказы.
То что все 3 есть из коробки, согласен, это я пропустил, был в курсе только про DateFilter, на запрос по остальным Google мне выдал ссылки на StackOverflow.


В таблице заказов будете делать десяток полей ввода для поиска по маске карты/фио/id/трекномеру на половину страницы? Или всё-таки один, который будет искать по всем полям одновременно?

Для админа большой фильтр, для пользователя поменьше, хотя бывает и наоборот. Можете представить сервис не для пользователя, а для поставщика товаров в интернет-магазине. Поставщику нужны разные фильтры по своим товарам.


Можно и на клиенте решать по каким точно параметрам искать
на бэке не надо делать ровно ничего для этого

Ага, а потом пользователь может просматривать чужие заказы, подставил user_id в фильтр и все. Корректность запросов, которые идут в базу, должен контролировать сервер, они зависят не только от GET-параметров, а еще и от разрешений пользователя.

Sign up to leave a comment.

Articles