W poprzednim artykule opisałem jak działają standardowe filtry dostarczane z Api Platform. Jednak, często zdarza się, że to co domyślnie jest dostarczane pozostaje niewystarczające. Dlatego Api platform daje nam możliwość tworzenia własnych filtrów – zgodnie z naszymi potrzebami.
Implementacja interfejsu filtrów
Pierwszym, a zarazem ładniejszym sposobem jest implementacja interfejsu dostarczonego przez twórców Api Platform: ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractFilter.
Interfejs ten posiada dwie metody, których ciało musimy zaimplementować:
1. filterProperty – odpowiada za działanie filtra, a dokładnie za modyfikację queryBuildera w sposób, który odpowiada naszym oczekiwaniom.
2. getDescription – ta metoda zwraca dokumentacje dla naszego filtra. Zwraca tablice, która następnie jest serializowana do standardu OpenApi. Dzięki temu nasz filtr będzie widoczny w swager.
Przykładowa implementacja
W poniższym przykładzie założyłem posiadanie dwóch zasobów. Film i Użytkownik. Użytkownik posiada relacje ManyToMany do filmów. Oznacza ona filmy, w których grał użytkownik. Film posiada właściwość `type` która określa typ filmu. Kod jest dostępny na moim githubie
Stwórzmy filtr, który dla użytkowników będzie znajdować tylko tych, którzy występowali w filmach odpowiedniego typu.
Zanim przejdziemy do implementacji interfejsu wspomnę o abstrakcyjnej klasie \ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractFilter, która jest udostępniana przez ApiPlatform. Implementuje ona nasz interfejs, a dodatkowo ma wstrzyknięte serwisy, które mogą okazać się użyteczne przy tworzeniu filtrów.
Implementacja metody `getDescription`
W zasadzie moglibyśmy zwracać w tej metodzie pustą tablicę. Filtr by i tak zadziałał. Jednak, jeżeli chcemy mieć porządek w dokumentacji naszego api, zalecam dodać implementacje.
W naszym przypadku wygląda to tak:
<?php public function getDescription(string $resourceClass): array { $description = []; $description["typesOfFilms"] = [ 'property' => null, 'required' => false, 'type' => 'string', 'is_collection' => false, ]; return $description; }
Klucz zwracanej tablicy oznacza nazwę filtra.
Parametr tablicy`property` określa, dla której właściwości zasobu odnosi się filtr. Może się tak zdarzyć, że skonfigurujemy filtr w taki sposób, aby działał dla konkretnej właściwości. W naszym przypadku filtr będzie działał w kontekście zasobu. Dlatego ustawiona jest wartość null.
Kolejno, pole`require`- mówi nam czy filtr jest wymagany. Dla niektórych zapytań możemy wymagać podania wartości w filtrach. U nas zasób będzie działał zarówno z zaaplikowanym filtrem jak i bez.
Następnie, właściwość ‘type’ określa typ wartości, które możemy wpisać w filtrze. Typy są zgodne ze standardem OpenApi ‘string’, ‘bool’, ‘array’ itp. W sytuacji, gdy podamy niepoprawną wartość lub w ogóle jej nie podamy, domyślnie zostanie wybrany typ ‘string’.
Parametr `is_collection` określa czy dany filtr może być aplikowany wielokrotnie. Zobrazuję to przykładem:
Jeżeli tę wartość ustawilibyśmy na true to moglibyśmy wysyłać takie żądania:
“https://example.com/api/users?typesOfFilms[]=action&typesOfFilms[]=drama”
Dla przypomnienia dodam, że filtry są dodawana za pomocą operatora logicznego AND, dlatego przykład powyżej powinien zwrócić wszystkich aktorów, którzy grali w dramatach i filmach akcji.
Implementacja filtru. Metoda “filterProperty”
<?php protected function filterProperty( string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null ) { $rootAlias = $queryBuilder->getRootAliases()[0]; $joinAlias = $queryNameGenerator->generateJoinAlias('movies'); $queryBuilder ->leftJoin($rootAlias.'.movies', $joinAlias) ->andWhere($joinAlias . ".type = :type") ->setParameter('type', $value); }
W parametrach tej metody dostajemy kolejno:
- $property zasobu, jeżeli ustaliliśmy filtr dla konkretnej property
- $value wartość przekazywaną przez filtr
- $queryBuilder z częściowo utworzonym zapytaniem do bazy danych
- $queryNameGenerator obiekt, który pomoże nam stworzyć potrzebne aliasy dla naszego filtra
- $resourceClass to klasa/zasób, do której zapięty jest filtr
- opcjonalnie $operationName czyli metoda żądania z protokołu HTTP
Ciało metody jest bardzo proste. Po pierwsze – odczytuję główny alias naszego zasobu. Następnie, tworzę alias dla relacji z `movie`.
W tym momencie mamy już komplet danych, dzięki którym możemy zmodyfikować zapytanie w queryBuilderze. Należy pamiętać, aby dodać ‘join’ gdyż Api Platform może domyślnie nie dodawać relacji do każdego zasobu.
Rejestracja filtra
Ostatnią rzeczą, którą należy zrobić, aby nasz filtr był widoczny w systemie jest jego rejestracja. W tym celu klasę należy zarejestrować jako filtr z tagiem `api_platform.filter`.
Jeżeli mamy włączoną autokonfigurację wystarczy sama rejestracja serwisu. Symfony po interfejsie sam rozpozna jaki tag musi nadać.
U mnie wygląda to tak, że rejestruje wszystkie klasy znajdujące się w namespace App/Filter jako serwisy.
# config/services.yaml App\Filter\: resource: '../src/Filter'
Podsumowanie
Dzięki temu, że możemy modyfikować bezpośrednio zapytanie do bazy danych mechanizm filtrów jest bardzo elastyczny. Implementacja dostarczonego interfejsu sprawia, że kod jest czysty i spójny.
Jeżeli jednak i to by nam nie wystarczyło Api Platform w swojej dokumentacji opisuje metodę tworzenia filtrów z wykorzystaniem doctrine bezpośrednio. Ale o tym w innym artykule.
Pokazałem tutaj tylko jeden ze sposobów na stworzenie własnego filtra. Po więcej informacje odsyłam do dokumentacji.