• Время чтения ~2 мин
  • 11.02.2024

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

Итак, без лишних слов, давайте приступим !

Чрезмерная зависимость

от автоматического обнаружения Как разработчики Laravel, автоматическое обнаружение (AD), несомненно, сделало нашу жизнь гораздо, намного проще. Помните те времена, когда мы всегда должны были composer require и всегда забывали добавить пакет ServiceProvider в config/app.php? Несмотря на то, что это чрезвычайно полезная функция, облегчающая нашу повседневную жизнь, она не предоставляется бесплатно. Каждое решение при разработке программного обеспечения должно приниматься осознанно, взвешивая различные компромиссы.

Вот почему до сих пор есть люди, которые не выбирают автоматическое обнаружение пакетов из-за его вклада в более длительное время загрузки приложения. Проблема в том, что автоматическое обнаружение охотно загружает все пакеты ServiceProvider, даже если для текущего, активно обслуживаемого запроса вам может понадобиться только часть из них. Есть способы лениво загрузить нужный ServiceProviderвам s, и нет, Octane не является очевидным решением этой проблемы. Но это тема для другого поста в блоге. Если позволите, я могу представить преимущества отложенной загрузки в перспективе: мы смогли сократить время загрузки приложения до 80 мс, просто играя в игру с регистрацией зависимостей умным способом! Зачем регистрировать и загружать Livewire, Blade, Horizon, Telescope, Nova и т.д. (вы поняли суть), если вы просто собираетесь ответить простой полезной нагрузкой JSON?

Я хочу сказать, что есть разработчики Laravel, которые сознательно отказываются от этой функции и, к сожалению, остаются в неведении. «Но как?», — справедливо спросите вы. И я рад, что вы спросили! Поясню, пожалуйста.

Sleeper breaking changes

"Sleeper what?" - Теперь я должен признаться, что я только что придумал понятие "Sleeper Breaking Changes" (SBC), но я думаю, что оно очень хорошо описывает проблему. Это критические изменения, которые вносятся, несмотря на то, что вы уделяете пристальное внимание тому, чтобы не вводить их, в незначительных выпусках просто потому, что вы не знаете, что они происходят.

Пояснено на примере: Нить накала

Лично мне приходилось иметь дело с этой проблемой с незначительным обновлением нити накала . В какой-то момент кто-то решил модулизировать поведение уведомлений панели администрирования. Все было сделано так, чтобы быть обратно совместимым, так что нет ничего плохого в том, чтобы пометить его в минорном выпуске, верно? К сожалению, там был СБК, который ждал засаду. Библиотека полностью полагается на автообнаружение и делает ставку на то, что все также следуют одной и той же философии. Колодец... Это не всегда так.

Что случилось? Короче говоря, казалось бы, безобидное composer update , что было запущено локально и запущено в производство, сломало все приложение, потому что отсутствовала ключевая зависимость. В этом нет ничего нелогичного, потому что именно так люди, не использующие автоматическое обнаружение (NAD), в настоящее время должны обходить эту проблему (добавлять библиотеку и ее подзависимости одну за другой):

'providers' => [
    \BladeUI\Icons\BladeIconsServiceProvider::class,
    \BladeUI\Heroicons\BladeHeroiconsServiceProvider::class,
    \Kirschbaum\PowerJoins\PowerJoinsServiceProvider::class,
    \Filament\FilamentServiceProvider::class,
    \Filament\Actions\ActionsServiceProvider::class,
    \Filament\Forms\FormsServiceProvider::class,
    \Filament\Infolists\InfolistsServiceProvider::class,
    \Filament\Notifications\NotificationsServiceProvider::class,
    \Filament\Support\SupportServiceProvider::class,
    \Filament\Tables\TablesServiceProvider::class,
    \Filament\Widgets\WidgetsServiceProvider::class,
    \Livewire\LivewireServiceProvider::class,
];

Если библиотека добавляет новую зависимость ключа, она должна быть добавлена потребителями пакетов вручную. Потребители упаковки часто забывают об этом, потому что так быть не должно в первую очередь. В идеале нам нужно только зарегистрировать единственную зависимость и на этом закончить. Вот так:

'providers' => [
    \Filament\FilamentServiceProvider::class,
];

Все остальное является деталью реализации пакета Filament. Если что-то новое используется за кулисами, мы не должны знать об этом в виде безжалостных 500 взрывов на производстве. — Но решаема ли вообще эта проблема? Конечно! Это проще простого.

Введите AggregateServiceProvider...

Славный AggregateServiceProvider

Лекарство от этой боли лежит в AggregateServiceProvider. Я думаю, что этот класс является самым важным файлом во всей кодовой базе Laravel! У него простая цель, можете ли вы догадаться, что это такое? Я дам вам 5 секунд...

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

  • Зависимости пакетов проясняются сразу.
  • Мы позаботимся о каждом типе приложения (люди AD или NAD).
  • Позволяет построить мысленную модель взаимозависимостей пакета.
  • Контейнер IoC Laravel достаточно умен, чтобы пропускать уже зарегистрированные ServiceProviders, поэтому несколько регистраций не произойдут.

Если вы были на Laracon EU '24 и видели доклад Матеуса о модульных монолитах, то вам нужно знать об этом классе, потому что это самый острый инструмент в сарае в отношении модульности приложений Laravel!

Предлагаемое решение

Если вы создатель пакетов, то, пожалуйста, используйте славный AggregateServiceProvider. Я с радостью приведу пример из моего пакета Livewire Toaster:

final class ToasterServiceProvider extends AggregateServiceProvider
{
    protected $providers = [LivewireServiceProvider::class];
    
    // omitted for brevity
}

Как видите, пакет имеет зависимость от Livewire. Об этом нетрудно догадаться, но мы должны сделать эту зависимость явной, как показано выше. Будет ли этот пакет затем установлен в приложении AD или NAD, теперь не имеет значения. Ребятам из NAD придется зарегистрироваться вручную ToasterServiceProvider один раз и с этого момента забыть об остальных. Если я вдруг решу использовать другую зависимость, это не взорвет производство – ура!

Недостаточное использование контейнерных событий

Вы когда-нибудь слышали о "Container Events"? Нет? Что ж, я не виню вас, потому что это такая крошечная тема в официальной документации Laravel, хотя это может помочь ускорить время загрузки нашего приложения на тонну. Проще говоря, это способ подключиться к жизненному циклу разрешения объектов контейнера для различных служб, которые могут использоваться на протяжении всего жизненного цикла входящего веб-запроса. Почему это важно знать? Ну, одно слово: производительность.

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

Поясняю на примере: Инерция

Позвольте мне привести пример из PR, который я отправил в Inertia пару лет назад, который прояснит эту мысль.

protected function registerBladeDirectives(): void
{
    Blade::directive('inertia', [Directive::class, 'compile']);
    Blade::directive('inertiaHead', [Directive::class, 'compileHead']);
}

Этот фрагмент кода может показаться знакомым. В конце концов, мы используем изысканные фасады, чтобы зарегистрировать пару директив Blade. Единственное, с чем нам нужно быть осторожным, это зарегистрировать егоboot, а не register потому, что в противном случае сервис может быть еще недоступен на данный момент времени. Пока все хорошо, так в чем же проблема? Что ж, для этого нам сначала нужно понять, как протокол Inertia работает за кулисами.

Если клиент делает первый запрос, то есть он вводит URL-адрес в окне браузера и нажимает Enter, то веб-страница будет обслуживаться как есть. При поступлении последующего запроса, означающего, что пользователь взаимодействовал с пользовательским интерфейсом JavaScript, Inertia обходит всю логику рендеринга HTML и выдает полезную нагрузку JSON в качестве ответа. Именно здесь в игру вступила «пустая трата цикла процессора»: несмотря на то, что компонент Laravel Blade был необходим для самого первого клиентского запроса, он все равно решался для каждого последующего запроса; Все эти дополнительные накладные расходы, в то время как заранее известно, что они не понадобятся! Это означает, что Blade разрешается без причины в течение 99% всех всех запросов!

Предлагаемое решение

Если вы создатель пакетов, то, пожалуйста, используйте события контейнера:

$this->callAfterResolving('blade.compiler', function ($blade) {
    $blade->directive('inertia', [Directive::class, 'compile']);
    $blade->directive('inertiaHead', [Directive::class, 'compileHead']);
});

Об инерции уже позаботились, так что теперь ваша очередь!

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

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

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

Похоже, что мы, как правило, избегаем шаблона дизайна декоратора. Но почему? Это очень мощный инструмент в нашем арсенале. Может быть, потому, что он взаимоисключающий с использованием фасадов Laravel? Это очень далеко от истины. Если вы посмотрите вниз в приведенном ниже примере, вы можете заметить крошечный, маленький интерфейс под названием Collector:

final class Toaster extends Facade
{
    // omitted for brevity
    public static function error(string $message, array $replace = []): PendingToast
    {
        return self::toast()->message($message, $replace)->error();
    }
    #[Override]
    protected static function getFacadeAccessor(): string
    {
        return ToasterServiceProvider::NAME;
    }
    #[Override]
    protected static function getMockableClass(): string
    {
        return Collector::class;
    }
}

Знаете ли вы, как он используется в соответствующем ServiceProvider? Нет? Что ж, это означает, что вы не читали мою предыдущую статью в блоге, поэтому, пожалуйста, сделайте это!

if ($config->wantsAccessibility) {
    $this->app->extend(Collector::class, static fn (Collector $next) => new AccessibleCollector($next));
}
if ($config->wantsTranslation) {
    $this->app->extend(Collector::class, fn (Collector $next) => new TranslatingCollector($next, $this->app['translator']));
}

Благодаря силе и мощи декораторов мы можем добавлять поведение во время выполнения! Что это значит для вас, как для потребителя упаковки?

  • Вы можете продолжать использовать фасад, как Toaster::error('Uh oh!')
  • Вы можете внедрить сервис, если предпочитаете это (внедрение конструктора / метода)
  • Вы можете смешивать и сопоставлять пункты, указанные выше
  • Можно заняться чем-то другим

Возможности безграничны, а пакет позволяет использовать любой подход, который вы хотите! Но что, если вы хотите расширить какое-то поведение лично?

Объяснение на примере: расширение поведения То, что декораторы предоставляют нам бесплатно, — это тот факт, что расширение существующего поведения

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

final readonly class DailyCountingCollector implements Collector
{
    public function __construct(private Collector $next) {}
    public function collect(Toast $toast): void
    {
        // increment the counter on durable storage
        $this->next->collect($toast);
    }
    public function release(): array
    {
        return $this->next->release();
    }
}

AppServiceProvider

public function register(): void
{
    $this->app->extend(Collector::class, 
        static fn (Collector $next) => new DailyCountingCollector($next)
    );
}

That's y! Больше ничего не нужно модифицировать, чтобы заставить эту штуку работать! Все очень просто. Я надеюсь, что это достаточное доказательство того, что нестандартное мышление может быть полезно для всех нас.

Заключение

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

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Об авторе CrazyBoy49z
WORK EXPERIENCE
Контакты
Ukraine, Lutsk
+380979856297