• Час читання ~2 хв
  • 11.02.2024

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

Тож без зайвих слів, давайте зануримося!

Надмірна залежність

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

Ось чому все ще є люди, які не ввімкнули автоматичне виявлення пакетів через його внесок у довший час завантаження програми. Проблема полягає в тому, що автоматичне виявлення жадібно завантажує всі пакункиServiceProvider, навіть якщо вам може знадобитися лише частина з них для поточного запиту, що активно обслуговується. Є способи ліниво завантажувати потрібні ServiceProviders, і ні, Octane не є очевидною відповіддю на цю проблему. Але це тема для іншої публікації в блозі. Якщо можна поглянути на переваги лінивого завантаження в перспективі: ми змогли скоротити до 80 мс завантаження програми, просто розумно зігравши в гру реєстрації залежностей! Навіщо реєструвати та завантажувати Livewire, Blade, Horizon, Telescope, Nova тощо (ви зрозуміли суть), якщо ви просто збираєтеся відповісти простим корисним навантаженням JSON?

Я хочу сказати, що є розробники Laravel, які свідомо відмовляються від цієї функції і, на жаль, залишаються в невіданні. «Але як?» – справедливо запитаєте ви. І я рада, що ви запитали! Дозвольте, будь ласка, пояснити.

Зміни

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

Пояснення на прикладі: Filament

Особисто мені доводилося мати справу з цією проблемою з незначним оновленням Filament. У якийсь момент хтось вирішив модульувати поведінку сповіщень панелі адміністрування. Все було зроблено для зворотної сумісності, тому немає нічого поганого в тому, щоб позначити його в мінорному релізі, чи не так? На жаль, був СБК, який чекав на засідку. Бібліотека повністю покладається на автовиявлення і робить ставку на те, що всі також дотримуються однієї і тієї ж філософії. Ну... Це не завжди так.

Що сталося? Коротше кажучи, здавалося б, нешкідлива 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 Toster:

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

Як бачите, пакет має залежність від Livewire. Це було неважко здогадатися, але ми повинні зробити цю залежність явною, як показано вище. Тепер не має значення, чи буде цей пакунок встановлено в додатку AD або NAD. Людям з NAD доведеться один раз зареєструватися ToasterServiceProvider вручну і з цього моменту забути про решту. Якщо я раптом вирішу скористатися іншою залежністю, це не підірве виробництво — ура!

Недостатнє використання контейнерних подій

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

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

Пояснюємо на прикладі: Інерція

Наведу приклад з піару, який я подав до Inertia пару років тому, який доведе справу до кінця.

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

Цей фрагмент коду може здатися знайомим. Врешті-решт, ми використовуємо вишукані фасади, щоб зареєструвати пару директив Blade. Єдине, з чим нам потрібно бути обережними, це зареєструвати його, а не register тому, boot що в іншому випадку сервіс може бути ще недоступним на той момент часу. Поки що все добре, то в чому проблема? Що ж, для цього нам спочатку потрібно зрозуміти, як протокол Inertia працює за лаштунками.

Якщо клієнт робить перший запит, тобто він ввів URL-адресу у вікні браузера і натискав клавішу Enter, то веб-сторінка буде показана як є. Коли надходить наступний запит, тобто користувач взаємодіє з інтерфейсом JavaScript, Inertia обходить всю логіку рендерингу HTML і видає у відповідь корисне навантаження JSON. Саме тут у гру вступила «втрата циклу процесора»: незважаючи на те, що компонент Blade від Laravel потрібен для першого запиту клієнта, він все одно вирішувався для кожного наступного запиту; Всі ці додаткові накладні витрати, в той час як заздалегідь прекрасно відомо, що вони не знадобляться! Це означає, що 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']));
}

Завдяки силі та могутності декораторів ми можемо додати поведінку під час виконання! Що це означає для Вас як споживача упаковки?

  • Ви можете продовжувати використовувати façade, як Toaster::error('Uh oh!')
  • Ви можете впровадити послугу, якщо вам більше подобається (конструктор / ін'єкція методу)
  • Ви можете змішувати та поєднувати пункти вище
  • Можна зайнятися чимось іншим

Можливості безмежні, а пакет дає можливість використовувати будь-який підхід, який ви забажаєте! Але що робити, якщо ви хочете продовжити якусь поведінку особисто?

Поясніть на прикладі: розширення поведінки Те, що декоратори надають нам безкоштовно, це той факт, що розширення існуючої поведінки

стає дитячою грою. Уявіть, що ви вирішили скористатися моїм пакетом Livewire Toaster і хотіли б відстежувати, скільки тостів надсилається щодня, щоб відобразити їх на інформаційній панелі адміністратора. Ви можете вирішити цю проблему спочатку, створивши новий клас:А потім розширивши поведінку у вашому AppServiceProvider:

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();
    }
}

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

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

Підбиття підсумків

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

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