• Czas czytania ~8 min
  • 11.02.2024

Jeśli mogę, to mam kilka uwag, które chciałbym wyrzucić z siebie. Chociaż nie można zaprzeczyć, że Laravel jest jedną z najlepiej prosperujących społeczności OSS, a Spatie jest niekwestionowanym mistrzem, jest kilka rzeczy, w których – myślę – możemy wspólnie zrobić lepiej. Osobiście byłem winny wszystkich kwestii poruszonych poniżej w przeszłości i będę pierwszym, który weźmie je pod uwagę.

Więc bez zbędnych ceregieli, zanurzmy się od razu!

Automatyczne wykrywanie nadmierne poleganie

na programistach Laravel, automatyczne wykrywanie (AD) niewątpliwie znacznie ułatwiło nam życie. Pamiętasz czasy, kiedy zawsze musieliśmy composer require i zawsze zapominaliśmy dodać paczkę ServiceProvider do config/app.php? Mimo że jest to niezwykle przydatna funkcja, która łagodzi nasze codzienne życie, nie jest darmowa. Każda decyzja w tworzeniu oprogramowania powinna być podejmowana świadomie, biorąc pod uwagę różne kompromisy.

Dlatego wciąż są ludzie, którzy nie decydują się na automatyczne wykrywanie pakietów ze względu na jego wkład w dłuższe czasy uruchamiania aplikacji. Problem polega na tym, że automatyczne wykrywanie ochoczo ładuje wszystkie pakietyServiceProvider, nawet jeśli możesz potrzebować tylko ułamka z nich dla bieżącego, aktywnie obsługiwanego żądania. Istnieją sposoby na leniwe załadowanie potrzebnych ServiceProviders i nie, Octane nie jest oczywistą odpowiedzią na ten problem. Ale to już temat na inny wpis na blogu. Jeśli mogę spojrzeć na korzyści związane z leniwym ładowaniem z perspektywy: byliśmy w stanie skrócić czas uruchamiania aplikacji do 80 ms, po prostu grając w grę rejestracji zależności w sprytny sposób! Po co rejestrować i uruchamiać Livewire, Blade, Horizon, Telescope, Nova itd. (rozumiesz, o co chodzi), jeśli masz zamiar odpowiedzieć zwykłym ładunkiem JSON?

Chodzi mi o to, że są programiści Laravel, którzy świadomie rezygnują z tej funkcji i niestety są pozostawieni w ciemności. "Ale jak?", możesz słusznie zapytać. I cieszę się, że pytasz! Pozwól, że wyjaśnię.

Zmiany

powodujące niezgodność uśpienia"Śpiący co?" – Teraz muszę przyznać, że właśnie ukułem pojęcie "Sleeper Breaking Changes" (SBC), ale myślę, że bardzo dobrze opisuje problem. Są to zmiany powodujące niezgodność, które są wprowadzane, mimo że zwracasz szczególną uwagę, aby ich nie wprowadzać, w wersjach pomocniczych tylko dlatego, że nie wiesz, że się zdarzają.

Wyjaśnione na przykładzie: Filament

Osobiście musiałem poradzić sobie z tym problemem z drobnym ulepszeniem filamentu. W pewnym momencie ktoś postanowił zmodularyzować zachowanie powiadomień panelu administracyjnego. Wszystko zostało stworzone tak, aby było kompatybilne wstecz, więc nie ma nic złego w otagowaniu tego w drobnym wydaniu, prawda? Niestety, na zasadzkę czekał SBC. Biblioteka w pełni opiera się na automatycznym odkrywaniu i stawia na to, że wszyscy również kierują się tą samą filozofią. Studnia... Nie zawsze tak jest.

Co się stało? Krótko mówiąc, pozornie nieszkodliwycomposer update, który został uruchomiony lokalnie i został wypchnięty do produkcji, zepsuł całą aplikację, ponieważ brakowało kluczowej zależności. Nie jest to również nielogiczne, ponieważ w ten sposób ludzie bez automatycznego wykrywania (NAD) muszą obecnie obejść problem (dodać bibliotekę i jej zależności podrzędne jeden po drugim):

'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,
];

Jeśli nowa zależność klucza jest dodawana przez bibliotekę, musi zostać dodana ręcznie przez użytkowników pakietów. Konsumenci opakowań często o tym zapominają, ponieważ w ogóle nie powinno tak być. Idealnie byłoby, gdybyśmy musieli zarejestrować pojedynczą zależność i nazwać ją dniem. W ten sposób:

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

Wszystko inne jest szczegółem implementacji pakietu Filament. Jeśli za kulisami pojawi się coś nowego, nie powinniśmy być tego świadomi w postaci bezlitosnych 500 eksplozji na produkcji. "Ale czy ten problem jest w ogóle do rozwiązania?" Pewnie! To proste jak bułka z masłem.

Wpisz ...AggregateServiceProvider

Chwalebny AggregateServiceProvider

Lekarstwo AggregateServiceProviderna ten ból leży w . Myślę, że ta klasa jest najważniejszym plikiem w całej bazie kodu Laravel! Ma prosty cel, czy zgadniesz, co to jest? Dam ci 5 sekund...

... agregowanie innych dostawców usług (czytaj: zależności). To było trudne. Zasadniczo pozwala nam to zebrać wszystkie wymagane zależności pod jednym, nadrzędnym dachem. Ma to wiele zalet:

  • Zależności pakietów są natychmiast jasne.
  • Zadbano o każdy rodzaj aplikacji (AD vs. NAD).
  • Pozwala nam zbudować mentalny model współzależności pakietów.
  • Kontener IoC Laravel jest na tyle inteligentny, że pomija już zarejestrowane ServiceProviders, więc nie będzie wielu rejestracji.

Jeśli byłeś na Laracon EU '24 i widziałeś wykład Mateusa na temat monolitów modułowych, to musisz wiedzieć o tej klasie, ponieważ jest to najostrzejsze narzędzie w szopie, jeśli chodzi o modularyzację aplikacji Laravel!

Proponowane rozwiązanie

Jeśli jesteś twórcą pakietów, użyj chwalebnego AggregateServiceProviderpliku . Chętnie podam przykład z mojego pakietu Livewire Toaster :

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

Jak widać, pakiet jest zależny od Livewire. Nie było to trudne do odgadnięcia, ale powinniśmy wyjaśnić tę zależność, jak pokazano powyżej. To, czy ten pakiet zostanie następnie zainstalowany w aplikacji AD czy NAD, nie ma teraz znaczenia. Ludzie z NAD będą musieli raz ręcznie zarejestrować ToasterServiceProvider się i od tego momentu zapomnieć o reszcie. Jeśli nagle zdecyduję się skorzystać z innej zależności, nie wysadzi to produkcji w powietrze – hurra!

Niedostateczne wykorzystanie zdarzeń

kontenera Czy kiedykolwiek słyszałeś o " zdarzeniach kontenera"? Nie? Cóż, nie winię Cię, ponieważ jest to tak drobny temat w oficjalnej dokumentacji Laravel, mimo że może pomóc przyspieszyć czas uruchamiania naszej aplikacji o tonę. Mówiąc najprościej, są one sposobem na podpięcie się do cyklu życia rozpoznawania obiektów kontenera różnych usług, które mogą być używane przez cały okres istnienia przychodzącego żądania internetowego. Dlaczego warto wiedzieć? Cóż, jedno słowo: wydajność.

Widzisz, większość bibliotek open-source w rzeczywistości ulepsza istniejące komponenty Laravel, rozszerzając ich funkcjonalność, zamiast być całkowicie niezależnym komponentem. Tak też powinno być, bo po co wymyślać koło na nowo, skoro już istnieje? Chociaż nadal powinniśmy uważać na nasze otoczenie i nie prosić o coś, czego nie będziemy potrzebować.

Wyjaśnienie na przykładzie: Bezwładność

Pozwólcie, że podam przykład z PR, który przesłałem do Inertii kilka lat temu, który doprowadzi do sedna sprawy.

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

Ten fragment kodu może wyglądać znajomo. W końcu korzystamy z wykwintnych fasad, aby zarejestrować kilka dyrektyw Blade'a. Jedyne, na co musimy uważać, to zarejestrowanie go zamiast boot register tego, ponieważ w przeciwnym razie usługa może nie być jeszcze dostępna w tym momencie. Jak na razie jest dobrze, więc w czym problem? Cóż, w tym przypadku musimy najpierw zrozumieć, jak protokół bezwładności działa za kulisami.

Jeśli klient wyśle żądanie po raz pierwszy, co oznacza, że wpisał adres URL w oknie przeglądarki i nacisnął Enter, strona internetowa zostanie wyświetlona bez zmian. Gdy nadejdzie kolejne żądanie, co oznacza, że użytkownik wszedł w interakcję z interfejsem użytkownika JavaScript, Inertia pomija całą logikę renderowania HTML i wypluwa ładunek JSON jako odpowiedź. W tym miejscu w grę wchodziło również "marnotrawstwo cyklu procesora": mimo że komponent Blade firmy Laravel był potrzebny do pierwszego żądania klienta, nadal był rozwiązywany dla każdego kolejnego żądania; Wszystko to dodatkowe koszty ogólne, podczas gdy doskonale wiadomo z góry, że nie będzie to potrzebne! Oznacza to, że Blade jest rozwiązywany bez powodu podczas 99% wszystkich żądań!

Proponowane rozwiązanie

Jeśli jesteś twórcą pakietów, skorzystaj ze zdarzeń kontenera:

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

Bezwładność została już załatwiona, więc teraz Twoja kolej!

Ponadto, aby wyjaśnić, zdarzenia kontenera mogą być używane z dowolną usługą, która musi zostać rozwiązana za pośrednictwem kontenera. Blade to tylko jeden z wielu przykładów.

Zjawienie wzoru

dekoratora To będzie krótka sekcja, ponieważ poświęciłem już cały wpis na blogu wzorowi dekoratora. Jeśli masz jakieś uprzedzenia wobec tego wzorca, uprzejmie proszę o przeczytanie najpierw tego wpisu na blogu .

Wydaje się, że generalnie unikamy wzorca projektowego dekoratora. Ale dlaczego? To potężne narzędzie w naszym zestawie narzędzi. Być może dlatego, że wzajemnie się wyklucza dzięki zastosowaniu fasad Laravel? Nic bardziej mylnego. Jeśli spojrzysz w dół w poniższym przykładzie, możesz zauważyć malutki, mały interfejs o nazwie 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;
    }
}

Czy wiesz, jak jest używany w odpowiednim ServiceProvider? Nie? Cóż, oznacza to, że nie czytałeś mojego poprzedniego wpisu na blogu, więc proszę, zrób to!

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']));
}

Dzięki potędze i potędze dekoratorów jesteśmy w stanie dodać zachowanie w czasie wykonywania! Co to oznacza dla Ciebie jako konsumenta pakietu?

  • Możesz nadal korzystać z elewacji jak Toaster::error('Uh oh!')
  • Możesz wstrzyknąć usługę, jeśli wolisz (wstrzykiwanie konstruktora/metody)
  • Możesz mieszać i dopasowywać powyższe punkty
  • Możesz zrobić coś innego

Możliwości są nieograniczone, a pakiet umożliwia zastosowanie dowolnego podejścia! Ale co, jeśli chcesz osobiście rozszerzyć niektóre zachowania?

Wyjaśnienie na przykładzie: rozszerzanie zachowania

Coś, co dekoratorzy zapewniają nam za darmo, to fakt, że rozszerzanie istniejących zachowań staje się dziecinnie proste. Wyobraź sobie, że zdecydowałeś się skorzystać z mojego pakietu Livewire Toaster i chciałbyś śledzić, ile tostów jest wysyłanych dziennie do wyświetlenia na pulpicie administratora. Sposobem na rozwiązanie tego problemu jest najpierw utworzenie nowej klasy:A następnie rozszerzenie zachowania w swoim 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)
    );
}

To jest to! Nie ma nic innego, co trzeba modyfikować, aby to działało! To takie proste. Mam nadzieję, że jest to wystarczający dowód na to, że nieszablonowe myślenie może być korzystne dla nas wszystkich.

Podsumowanie

Ten wpis na blogu nie powinien być odczytywany jako zgłoszenie skargi, ale raczej jako przepis na ulepszenie, abyśmy mogli być inkluzywni i jeszcze bardziej podnieść poprzeczkę. Żadne z powyższych odniesień nie powinno być postrzegane jako atak na autorów pakietów! Są one używane wyłącznie ze względu na moje osobiste zaangażowanie i służą jako przykłady z życia wzięte. Nie zapomnij okazać autorom trochę miłości, oznaczając ich repozytoria gwiazdkami!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

O

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...

O autorze CrazyBoy49z
WORK EXPERIENCE
Kontakt
Ukraine, Lutsk
+380979856297