LARAVEL SERVICE CONTAINER (IOC) ORAZ ROUTE BINDING - DEVPARK
Development / Laravel

Laravel Service Container (IoC) oraz Route Binding – ich relacje i działanie.

Jedną z najlepszych cech Laravela jest implementacja Service Container (zwana również “IoC”). W tym artykule nie objaśnimy podstaw korzystania z niego – jeśli nie jesteś zaznajomiony z tematem, zalecamy obejrzenie tego filmu:
https://laracasts.com/series/laravel-5-fundamentals/episodes/26

W tym artykule spróbujemy wyjaśnić, jakie są relacje między Laravel Route Binding a IoC.
IoC dba o inicjowanie “pustych” obiektów dla w miejscach wstrzyknięcia zależności – lecz poza nim jest jeszcze jeden ważny element który z nim współpracuje – Route Binding.
Różnica polega na tym, że Route Binding jest przygotowane do użycia ściśle z Routes + Middleware + Controllers.

Co nam to daje?
Właściwe wyjaśnienie możesz znaleźć bezpośrednio w dokumentacji:
https://laravel.com/docs/5.4/routing#route-model-binding
która przedstawia, że finalnie otrzymasz dokładnie tę samą instancję wymaganego obiektu.

Jaka jest tego wartość?
Używając domyślnego wiązania, mamy możliwość uproszczenia kontrolera. Jeśli szukasz pojedynczego rekordu z tabeli, nie musisz pisać ani jednej linijki – Laravel zadba o to i sprawdzi, czy rekord występuje.
Ale prawdziwą moc można uzyskać z zadeklarowanym Route Bindingiem (explicit route binding). Dlaczego? Ponieważ będzie mieć bezpośredni wpływ na wydajność.
Wiele razy, zanim wywołasz jakąkolwiek logikę biznesową na obiekcie, chciałbyś go sprawdzić w pewnych warunkach. Nie mówimy tutaj o sprawdzaniu poprawności pola formularzy.
Przyjmijmy, iż posiadamy obiekt, który może być edytowany jedynie w konkretnych warunkach (np. użytkownik posiada do niego dostęp, rekord ma określony status, itp.). W takim przypadku prawdopodobnie będziesz chciał użyć warstwy Middleware, żeby to sprawdzić. W tym przypadku – w kilku miejscach konieczne będzie pobranie wartości obiektów z bazy danych – w Middleware, a później w warstwie kontrolera (lub gdzieś poniżej w warstwie logiki biznesowej).
Aby uniknąć takiego duplikowania pobierania danych z bazy, można użyć wyraźnego explicit route binding. W ten sposób obiekt zostanie zainicjowany raz i będziesz miał dostęp do instancji TEGO SAMEGO obiektu w Middleware, a później w kontrolerze.
Jak działa route binding?
Cóż – istnieje niewielka różnica między czystym użyciem kontenera serwisowego a wiązaniem trasy (route bindingiem). Ponieważ obiekt wiążący się z IoC (np. metodą wiązania) ma wpływ na całość zależności aplikacji – wiązanie trasy (route binding) ma wpływ tylko na oprogramowanie pośredniczące oraz kontrolery – to wszystko.
Dzięki temu możesz mieć pewność, że określony obiekt będzie używany tylko w tych dwóch miejscach! Jest to możliwe, ponieważ klasa routera jest “wyżej” od klasy kontenera.

Przejdźmy to krok po kroku.
Rozpatrzmy taką ścieżkę:

Pierwszym rozważanym krokiem jest to, że nic nie jest gotowe – w wyniku czego, instancja obiektu klasy “User” będzie inicjowana przez Service Container.
Więc $user będzie “pustą” instancją obiektu. “Pusty” oznacza tutaj, że obiekt nie będzie zawierać żadnych wartości (z wyjątkiem tych, które zostały zainicjowane w jego własnej konstrukcji).

Więc kiedy spróbujesz zdebugować $id otrzymasz wartość z adresu URL.
Kiedy zdebugujesz $user, zobaczysz, że jest to “pusty” obiekt klasy User.

Id jest przekazywane przez router, ale dla przypadku $user – wywoływany jest kontroler IoC, który jest odpowiedzialny za jego utworzenie. Jest to standardowe zachowanie, z którym powinni być zapoznani wszyscy programiści Laravel.
Weźmy pod uwagę kolejny krok – trasa zostanie zmieniona na to:

W końcu nasza metoda kontrolera będzie miała tylko parametr $user (tak, aby nazwa parametru route była równa nazwie parametru metody kontrolera).

Teraz robimy domyślne powiązanie trasy. Wynikiem tego będzie, że router z Laravel będzie próbował odnaleźć użytkownika w bazie danych i zainicjować obiekt klasy użytkownika. Więc teraz, gdy będziesz debugował, będziesz miał konkretny wypełniony obiekt. Miło, prawda? Nie musisz pisać zapytania, aby znaleźć rekord danych użytkownika.

Zatrzymajmy się na chwilę i spróbujmy w prosty sposób wytłumaczyć ten proces.

Pierwszą rzeczą jest to, że router sprawdza parametry metody indeksowania. Jeśli odszuka, że nazwa parametru jest równa nazwie parametru na trasie ORAZ wskazuje że powinien on być przykładem pewnej klasy (User), to wtedy użyje tej klasy i odwoła się do metody findOrFail.

Poniższy diagram powinien to wytłumaczyć:

Powyższy diagram jest jedynie uproszczoną reprezentacją bardziej złożonego procesu – jednak jest to wystarczające, by zrozumieć cały algorytm.

W następnych etapach, nawet te uproszczone schematy, zaczną być bardziej złożone – jednak nadal będą tylko uproszczoną reprezentacją całego procesu. Ważna uwaga jest taka, że ukryte wiązania działają wyłącznie dla klasy, która rozszerza klasę EloquentModel.

Zobaczmy jak nasz proces poradzi sobie z przykładem, kiedy zwiążemy obiekt z wyraźnym wiązaniem (explicit binding).

W domyślnym wiązaniu nazwa parametru w metodzie kontrolera musi być taka sama, jak nazwa parametru w definicji trasy. Nie jest to wymagane dla wyraźnego powiązania trasy (explicit route binding). Aby to przedstawić, użyjemy tego kodu – jako ścieżki:

oraz dla kontrolera:

Jak widać powyżej – nazwy parametrów różnią się. W tym przypadku domyślne powiązanie trasy nie zadziała, w wyniku czego otrzymamy “pusty” przykład obiektu klasy użytkownika w parametrze $user. Dzieje się tak, ponieważ router użyje metody IoC container-> stwórz metodę inicjalizacji.
Ponieważ chcemy mieć wypełniony obiekt, musimy używać jawnego powiązania (explicit binding), a w RouteServiceProvider musimy dodać ten kod:

W ten sposób możemy przekazać aby Laravel wiedział, że znowu powinien znaleźć wiersz użytkownika w bazie danych z id = concreate_user.

Później router sprawdzi, czy metoda kontrolera wymaga przypadku klasy User w swoich parametrach i użyje tego przypadku (wiersza użytkownika, który odnalazł w bazie danych).
Ważna uwaga jest taka, że możemy to zrobić w inny sposób, a zamiast używać Route::model możemy związać tutaj cokolwiek chcemy – np. może to być contract:

Dzięki temu możemy mieć pełną kontrolę nad procesem i decydować dokładnie, co chcemy powiązać (oczywiście typ powiązanego elementu musi być taki sam, jak zadeklarowany w kontrolerze jako parametr metody). Również ważną rzeczą jest to, że możemy również użyć tego obiektu w Middleware.

Powyżej zabrakło nam jednej rzeczy – obiekt może zostać przekazany do metody kontrolera także za pomocą app()->instance method.

Nie pokażemy, jak to zrobić, ponieważ wyraźne powiązanie trasy (explicit route binding) jest właściwym sposobem na to – w każdym razie, trzeba zauważyć, że istnieje taka możliwość. Podsumowując – parametry w metodach kontrolera mogą mieć swoje wartości na kilka sposobów:

  1. 1. tylko przez przekazanie wartości parametru z żądania
  2. 2. mogą być zainicjowane jako “pusty” obiekt przez IoC
  3. 3. mogą być zainicjowane za pomocą app()->instancce method – odpowiadającej IoC
  4. 4. mogą być zainicjowane jako wypełniony obiekt przez domyślne powiązanie trasy
  5. 5. mogą być zainicjowane jako wypełniony obiekt lub coś innego przez wyraźne powiązanie trasy (explicit route binding).

Jak można osiągnąć ten punkt? To proste 🙂 Są dwie rzeczy, które musimy wiedzieć.
Zarówno router jak i kontener IoC mają szyk zawierający powiązane obiekty. W routerze (\Illuminate\Routing\Router.php) ten szereg nazywa się $binders. Router zawiera również $container, który jest przykładem kontenera IoC (\ Illuminate \ Container \ Container.php).

Szyk kontenera IoC jest nazwany $instances.
W obu szykach (powiązania i przypadki) (bindings and instances) klucze są nazwami klas, a wartości są inicjowanymi instancjami obiektów, które później będą używane jako parametry.

Proces działa tak, że najpierw router sprawdza własną tabelę “powiązań”, jeśli istnieje instancja obiektu wiązanego dla danej klasy. Jeśli coś znajdzie – użyje tej wartości. Jeśli nie – wywołuje metodę tworzenia kontenera (IoC).
Metoda tworzenia IoC najpierw sprawdza szereg $instances, jeśli istnieje instancja wymaganej klasy. Jeśli go znajdzie – zwróci jego wartość, jeśli nie – utworzy nowy “pusty” obiekt wymaganej klasy.