Написать пост

Дыры в документации и ошибки laravel: в чём дело и как их исправить

Аватарка пользователя sudo >: )

Рассказали, как исправить ошибки в работе PHP-фреймворка laravel, которые возникают из-за дыр в документации.

Всем привет! Я очень двояко отношусь к фреймворку laravel. Там очень много неуместной магии, а некоторые решения я бы хотел сделать по-другому. Тем не менее, это хороший фреймворк для мелких или средних решений на PHP.

В нем довольно много готовых инструментов, неплохая реализация DI-контейнера и, что немаловажно, blade-шаблоны. А также хорошая конфигурация, фасады (В случае с laravel, я считаю такое решение скорее плюсом, чем минусом — очень помогает при рефакторинге) и хорошее комьюнити.

При этом в фреймворк встроен Eloquent, что в целом не обязывает к его использованию и все можно заменить фасадом DB. Глобальные хелперы, которые используют в обход явного внедрения зависимостей. Но на деле эти минусы не так влияют на общую картинку, и чистота кода зависит от того, кто этот фреймворк использует. Знаю немало хороших примеров проектов на laravel.

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

Отложенные сервисы

Недавно я заводил в один laravel сервис подробное логирование. Сервис занимается обращением к разным Api и получает определенную информацию. И вот достался мне Api одного сервиса, где ребята решили сделать авторизацию следующим образом: они требуют логин и пароль, а взамен отправляют куки (а не токен, как это часто бывает) и просят эту строку сохранить.

Я нашел стороннее решение на GitHub, которое этим всем занимается (куки хранит в текстовом файле), но тут меня ждали несколько подлянок. Я захотел, чтобы данный StrangeApiClient регистрировался в контейнере и запрашивал эти данные без моего участия.

Изначально я планировал, чтобы он делал каждый запрос при обращении к Api, а далее мы бы оптимизировали и сделали так, что это будет нужно только тогда, когда токен протух.

Решил не усложнять и прямо в провайдере написать следующую логику:

			namespace AppProviders;

use AppServiceStrangeApiClient;
use IlluminateContractsSupportDeferrableProvider;
use IlluminateSupportServiceProvider;

class StrangeClientServiceProvider extends ServiceProvider implements DeferrableProvider
{
   protected $defer = true;

   /**
    * Register any application services.
    *
    * @return void
    */
   public function register()
   {
       $this->app->singleton(StrangeApiClient::class, function ($app){
           $strangeClient = new StrangeApiClient();
           $userName = config('strange.strange_user');
           $password = config('strange.strange_password');

           if(!empty($userName) && !empty($password)) {
               $strangeClient->auth($userName, $password));
           }

           return $strangeClient;
       });
   }

   /**
    * Get the services provided by the provider.
    *
    * @return array
    */
   public function provides()
   {
       return [StrangeApiClient::class];
   }
}
		

Я подумал, возможно попахивает, но в целом авторизация идет верно. За счет того, что провайдер отложенный, он будет вызываться раз в 100 лет. Но логи laravel просто раздулись. Было куча запросов на авторизацию тогда, когда это было не нужно. Я не понимал, что происходит. Залез в доку, читаю:

"If your provider is only registering bindings in the service container, you may choose to defer its registration until one of the registered bindings is actually needed. Deferring the loading of such a provider will improve the performance of your application, since it is not loaded from the filesystem on every request.Laravel compiles and stores a list of all of the services supplied by deferred service providers, along with the name of its service provider class. Then, only when you attempt to resolve one of these services does Laravel load the service provider.To defer the loading of a provider, implement the IlluminateContractsSupportDeferrableProvider interface and define a provides method. The provides method should return the service container bindings registered by the provider."

Оказалось, что отложенные провайдеры не работают в artisan командах. Вот из-за этого.

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

Валидация

Есть сервис на laravel, в котором было какое-то чудо с валидацией краевых случаев.

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

Есть такая вещь как IlluminateFoundationHttpMiddlewareConvertEmptyStringsToNull:class. Казалось бы, отключи это и все заработает. Но я подумал, что это приведет к беде и не зря.

Поэтому, чтобы покончить с этим весельем, я попытался сделать следующий хак: вырубить эту мидлвару у отдельного роута с помощью withoutMiddleware, но это не сработало. Потому что это глобальная мидлвара, на нее это так не действует (в этот момент мне вспомнилась игра mtg, где мои заклинания буквально отменяют, хотя не было ни одной к этому предпосылки). Узнал я об этом вот отсюда. Решил сделать так:

			/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'api' => [
'throttle:api',
IlluminateRoutingMiddlewareSubstituteBindings::class,
IlluminateFoundationHttpMiddlewareConvertEmptyStringsToNull::class,
],
'web'=>[
IlluminateFoundationHttpMiddlewareConvertEmptyStringsToNull::class,
]
];
		

Ну вот теперь то сработало? Да, но упал тест, который никогда не падал. В тесте ожидалось 422 ошибка, прилетела 500. Передавалась вот такая штука:

			"nonEmptyParameter": ""
		

Это был PATCH запрос, то есть параметр необязательный (как все остальные), но, если передается, он должен быть не пустым. Из-за специфики этого параметра, у меня было кастомное правило.

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

			public function rules(): array
{
return [
'nonEmptyParameter' => [new nonEmptyParameterRule],
....
];
}
		

Очевидно, что тестировалась ошибка валидации. Тогда я полез глубже. 500 выползала из-за того, что в коде была доп. проверка на непустой параметр и выкидывалось уже подобие RuntimeException, что нам не подходило, так как сама ошибка 500 — это чудо и она могла и не быть. Тогда я решил копать исходники laravel.

В итоге выяснилось, что данный параметр я получаю с помощью $request->get(‘nonEmptyParameter’). Он возвращает в тесте пустую строку — все верно: ConvertEmptyStringsToNull я отключил.

Но в ядре laravel, валидатор это принудительно кастит независимо от мидлвара. Я уже хотел быть тревогу, кидать исходники в GitHub (и, возможно, я это все равно сделаю). Но как выяснилось это не баг, а фича. Чтобы это отключить, надо сделать поле обязательным либо писать кастомное правило имплементирующий интерфейс ImplictRule.

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

Следите за новыми постами
Следите за новыми постами по любимым темам
3К открытий3К показов