Pomoc - Szukaj - Użytkownicy - Kalendarz
Pełna wersja: [LR] Budowanie API
Forum PHP.pl > Forum > PHP > Frameworki
markonix
Zabieram się do wystawienia API do istniejącej już aplikacji, API będzie zarówno pod appkę jak i strony www.
Chciałbym aby to było dobre, RESTowe, użyteczne i elastyczne API, a nie kilka sztywnych metod.
Jakieś doświadczenia jak zbudować dobre API, z którego byście sami z przyjemnością korzystali?

Kilka ważnych aspektów:
- autoryzacja
- wersjonowanie
- spójny format błędów
- kontrola nad zwracanymi danymi (zarówno wybór pól jak i relacji)
- limitowanie i paginacja
- możliwość generowanie zaawansowanych metod typu find, search (szukanie po polach za pomocą różnych warunków, które można użyć w where())
- dokumentacja (generator?)

Pod większością względów podoba mi się API wFirmy (system do faktur):
https://doc.wfirma.pl/#h2-Komunikacja-h3-Ko...nie-zapyta-find
Poza troszkę zagmatwanym formatem danych, zwracanych przez te API i brakiem wersjonowania to jest to dla mnie wzór, który chciałbym osiągnąć.

Zastanawiam się czy znajdę gotowy szkielet takiego API czy muszę to implementować wszystko samemu, na poziomie kontrolerów i repozytoriów?
W L5.5 jest kilka dodatków typowo pod API np. Responses ale i tak wciąż jest sporo pracy.
r4xz
Polecam po prostu zapoznać się z panującymi standardami. Jeśli o mnie chodzi to bardzo podobała mi się ta infografika.

Dokumentację polecam robić w swaggerze, po wygenerowaniu wygląda to bardzo dobrze i jest funkcjonalne (do tego jest to już sprawdzone i dosyć dojrzałe rozwiązanie). Sam przykład wykorzystania swaggera w projekcie:
  1. /**
  2.   * @SWG\Post(
  3.   * path="/schedules/{schedule_id}/close",
  4.   * consumes={"multipart/form-data"},
  5.   * tags={"Schedules"},
  6.   * security={{"api_key": {}}},
  7.   * @SWG\Parameter(in="path", name="schedule_id", required=true, type="integer"),
  8.   * @SWG\Response(response="200", description="Schedule successfully opened."),
  9.   * @SWG\Response(response="401", description="Unauthenticated."),
  10.   * @SWG\Response(response="403", description="Invalid permissions."),
  11.   * @SWG\Response(response="404", description="Schedule with given id not found."),
  12.   * @SWG\Response(response="409", description="Schedule has been already opened."),
  13.   * )
  14.   */
  15. public function close(Schedule $schedule)
  16. {
  17. // ...
  18. }

Oczywiście z tego kodu generuje się ładna dokumentacja, zobacz np. ten przykład. Jest też możliwość generowania klientów dla wielu języków programowania, dzięki temu nikt nie musi się zastanawiać co robi Twoje API, analizować dokumentacji itp. ponieważ ma to w postaci ładnych metod gotowych do użycia (choć wtedy trzeba to trochę łądniej napisać niż w moim przykłądzie).

Co do samej obsługi parametrów field, sort, filter itp. to faktycznie jest to problem, istnieją jakieś rozwiązania dla Laravel jednak nie wszystkie mnie satysfakcjonowały, właściwie to z każdego wydobyłbym coś innego. Swego czasu napisałem nawet bibliotekę która pomaga mi w pisaniu tego typu api (link do githuba). Oczywiście przez to że z niej korzystam to znam kilka słabych stron tej biblioteki, ale niestety ze względu na natłok obowiązków nie doczeka się ona drugiej wersji wcześniej jak w lipcu/sierpniu.

I na koniec, bo nie wiem czy słyszałeś, a tym bardziej myślałeś o GraphQL? Jeśli nie słyszałeś to też zobacz jakie to ma możliwości i podejmij decyzję REST vs GraphQL
markonix
Twoja biblioteka właśnie jest czymś czego szukam, fajnie by było znaleźć coś podobnego tylko bardziej popularnego/rozwijanego z community.
Wzorowałeś się może jakiejś innej bibliotece? Najczęściej trafiam na "Dingo" ale ma to dla mnie mało czytelną dokumentacje i nie jestem pewien czy to jest to czego szukam.

GraphQL wygląda fajnie, generalnie czytałem parę artykułów o innych podejściach w API czy przy budowaniu repozytoriów niż takiego typowo RESTowe, ale na razie bym został na REST ze względu na ograniczony czas na zbudowanie API i przyzwyczajenie osób, które będą z tego API korzystały.
r4xz
Nie, niestety nie widziałem podobnej biblioteki, raczej patrzyłem na sposób definiowania GraphQL i zastanawiałem się czemu czegoś podobnego nie można znaleźć dla RESTa, aby tak z automatu mieć obsługę chociaż tych podstawowych rzeczy jak np. parametru fields/only, które mogłoby nawet naiwnie działać w post-processingu i wycinać przed wysłaniem do użytkownika okrojone dane.

Sprawdzam właśnie jak to robią w innych projektach:
Cachet - ręczna zabawa,
Laravel Horizon - może nie jest to fragment REST API, ale też idzie ręcznie
Flarum - https://github.com/tobscure/json-api
markonix
A jak Twoja biblioteka sobie radzi z L5.5?
r4xz
Raczej nie powinno być problemów, prawdę mówiąc pamiętam tylko o wersji Laravela w aktualnie rozwijanym projekcie i jest to wersja 5.4, a projekt do najmłodszych nie należy ponieważ ma już za sobą migrację z 5.3 smile.gif

Uprzedzam tylko, że są problemy z relacjami przechodnimi jak np. "user.messages", ale dla zwykłych relacji typu "user" lub "messages" działa poprawnie i nie napotkałem problemów. Np. ?limit=messages.5 zadziała, ale ?limit=messages.files.5 już nie jestem pewień, obsługa niektórych parametrów dla tego typu przypadków zawodzi.

PS Po magisterce musiałbym przysiąść trochę do tej biblioteki i faktycznie mogłoby powstać z tego coś wartościowego. Tzn. na moje potrzeby już jest i wykorzystuję ją zarówno na uczelni jak i w pracy (choć niestety tylko jeden z tych projektów generuje naprawdę ciekawe przypadki użycia).
markonix
Tak więc ostatecznie skorzystałem z Dingo, ma on tam dokumentowanie ale zgodnie z sugestią poszedłem w Swaggera, szkoda tylko, że trzyma się go na gałęzi 2.0.
Jeżeli chodzi o queries API to ciągle stoję w miejscu.

Nie potrafię znaleźć odpowiedniego narzędzia. W pewnym momencie myślałem, że nie będę musiał nawet go szukać bo mam go pod nosem bo używam Repository Pattern i biblioteki:
https://github.com/andersao/l5-repository#u...requestcriteria
No i mówię świetnie, wystarczy takie coś:
  1. public function index()
  2. {
  3. $this->playerRepository->pushCriteria(app('Prettus\Repository\Criteria\RequestCriteria'));
  4. return $this->playerRepository
  5. ->all();
  6. }

No i początkowo pełny entuzjazmu patrzę i faktycznie działa.. with fajnie, filter w porządku, search już się zaczynają schody, jakieś dziwne podejście do wyszukiwania po wielu polach, które musisz jeszcze zadeklarować tak jakby nie można było szukać po dowolnym atrybucie i w jakikolwiek sposób (nie tylko = ale też < > != itd.) Z sortowaniem też słabo bo nie da się posortować wg dwóch pól, a semantyka średnia. Brak limit(). Żeby limitować muszę już zamiast all() użyć paginate(), który jest nawet spoko bo dodaje linki prev/next, sumę itp. ale to już zmienia mocno format danych, co jest przesadą jakbym chciał np. ostatnie 5 rekordów posortowanych wg daty.

Próbowałem też inne ale większość wymaga aby przekazywać jako argumenty QueryBuilder'a. Częściowo dla mnie zrozumiałe ale nie bardzo bym chciał pomijać repozytoria. Czy u Ciebie da się używać repozytory pattern i Twojej biblioteki? Jakoś w przykładach u Ciebie nie do końca widzę jaką metodę wstrzykiwania przyjąłeś.

Chyba się udało z biblioteką:
https://github.com/marcelgwerder/laravel-api-handler

  1. return $this->myRepository->scopeQuery(function($query) {
  2. return ApiHandler::parseMultiple($query)->getBuilder();
  3. })->getBySomethingAndOtherLogic($someValue);


Można używać repo jednocześnie, i kontrolery względnie zostają czyste. Biblioteczka też widzę że jest od czasu do czasu aktualizowana.

Nawet jeszcze ładniej się da jak się utworzy własną metodę:
  1. return $this->myRepository->scopeQueryParametersMultiple()->getBySomethingAndOtherLogic($someValue);
r4xz
W mojej bibliotece działasz na QueryBuilder, zapytanie tworzysz normalnie tylko pomijasz etap pobrania wyników, biblioteka sama dokleja potrzebne warunki i pobiera wyniki:

  1. rest()->collection(User::where('kolumna', 'wartosc')->otherScopes());


W Twoim przykładzie trochę średnio zrozumiałem problem z limit, jeśli wpierw pobierasz wszystkie rekordy, a dopiero potem chcesz przycinać wyniki to jest to strasznie niewydajne, dlatego konieczne jest operowanie w przypadku parametru limit na QueryBuilder. Ale bardzo ciekawe jest to co tu piszesz, może zainpiruję się Twoimi przemyśleniami i w przyszłości zrobię wersję która pokrywa więcej przypadków użycia, póki co jest ściśle dopasowana pod moje wymagania smile.gif
markonix
O limitowanie mi chodzi o to, że w tym moim repozytorium nie ma w ogóle opcji limit jako tako czyli nie ograniczysz w prosty sposób listy zwróconych obiektów. Oczywiście wszystko na poziomie bazy, na razie nie myślałem o operowaniu na pobranej kolekcji ale już załamany powoli o tym myślałem (bo generalnie wszystkie where, limit, load - można zrobić na kolekcji, a na pewno byłoby to prostsze w osiągnięciu), na szczęście znalazłem ludzkie rozwiązanie.
Jeżeli chciałbym limitować kolekcję to muszę użyć paginacji czyli zupełnie innej metody niż zwykłe all() czy get(). Ta metoda ma już limit i offset ale zamiast kolekcji zwraca obiekt gdzie sama kolekcja ląduje w atrybucie data, a oprócz tego masz jeszcze atrybuty dotyczące paginacji czyli sumaryczna liczba rekordów, link do poprzedniej i następnej "strony" wyników co nawet i w API nawet może się przydać ale nie podoba mi się, że użycie limit powoduje takie duże zmiany w zwracanych danych. O wiele lepsze by było aby limit zostawić w spokoju, a dodać atrybut paginate=10,5, który już użytkownik API świadomie przesyłał i wiedział, że otrzyma zupełnie inny zestaw danych.

Jeżeli Twoja biblioteka ma metodę do zwrócenia jeszcze instancji QueryBuilder to prawdopodobnie też bym mógł jej użyć razem z repozytorium tak jak na przykładzie. Na modelach nie operuje zupełnie w kontrolerach.
Repozytoria u mnie już zwracają kolekcje, nie używam raczej get() czy first() w kontrolerach co też jeszcze lepiej je robi klarownymi.
r4xz
Cytat(markonix @ 16.12.2017, 17:09:08 ) *
O limitowanie mi chodzi o to, że w tym moim repozytorium nie ma w ogóle opcji limit jako tako czyli nie ograniczysz w prosty sposób listy zwróconych obiektów.

To jak to do tej pory realizowałeś dla zapytań które jednak musiały jakoś ograniczać liczbę wyników?

Cytat(markonix @ 16.12.2017, 17:09:08 ) *
Jeżeli Twoja biblioteka ma metodę do zwrócenia jeszcze instancji QueryBuilder to prawdopodobnie też bym mógł jej użyć razem z repozytorium tak jak na przykładzie. Na modelach nie operuje zupełnie w kontrolerach.

Tak jak napisałem u mnie to działa trochę inaczej, wpierw trzeba przygotować odpowiedni QueryBuilder który przekazuje się w wywołaniu metody, doklejane są do niego odpowiednie warunki i zwracany wynik. Ty widzę szukasz bardziej czegoś co wpierw doklei Ci wszystkie warunki, zwróci QueryBuilder, a dopiero potem będziesz na nim operował. Takie rozwiązanie ma pewne wady jeśli chodzi o implementację tego jako bibliotekę, ponieważ łatwo o sytuację w której programista wykluczy pola które są potrzebne do złączenia relacji. W swojej implementacji tyle pól ile tylko możliwe staram się już wycinać na poziomie zapytania, jednak jest też pewien mały postprocessing który usuwa np. id (a którego nie mogłem wcześnieje usunąć ponieważ Eloquent nie umiałby powiązać relacji).
Pyton_000
A ja tak z innej beczki trochę skoro już o api mowa.
Stosujecie Transformatory? Chodzi mi o transformacę danych z BD na dane do API włącznie z przemianowaniem pól. (tak to się bodaj nazywało)
markonix
Cytat(r4xz @ 17.12.2017, 10:49:56 ) *
To jak to do tej pory realizowałeś dla zapytań które jednak musiały jakoś ograniczać liczbę wyników?

  1. $repository->getLastPosts($limit = 10)

A potem w kodzie repozytorium możesz operować na Eloquent także więc nie ma aż takiego dużego problemu.


Cytat(r4xz @ 17.12.2017, 10:49:56 ) *
Tak jak napisałem u mnie to działa trochę inaczej, wpierw trzeba przygotować odpowiedni QueryBuilder który przekazuje się w wywołaniu metody, doklejane są do niego odpowiednie warunki i zwracany wynik. Ty widzę szukasz bardziej czegoś co wpierw doklei Ci wszystkie warunki, zwróci QueryBuilder, a dopiero potem będziesz na nim operował. Takie rozwiązanie ma pewne wady jeśli chodzi o implementację tego jako bibliotekę, ponieważ łatwo o sytuację w której programista wykluczy pola które są potrzebne do złączenia relacji. W swojej implementacji tyle pól ile tylko możliwe staram się już wycinać na poziomie zapytania, jednak jest też pewien mały postprocessing który usuwa np. id (a którego nie mogłem wcześnieje usunąć ponieważ Eloquent nie umiałby powiązać relacji).

Tzn. przez programistę rozumiesz użytkownika API, który selectem/filterem wybierze zbyt mało pól. Masz racje - przetestowałem i faktycznie filtr powoduje, że zamiast relacji mamy null gdy wytniemy klucz.
Jednak co dziwne autor tej biblioteki sam napisał:
Cytat
Note: Whenever you limit the fields with _fields in combination with _with. Under the hood the fields are extended with the primary/foreign keys of the relation. Eloquent needs the linking keys to get related models.

Choć to nie działa albo chodziło mu tylko o to, że _fields nie wytnie atrybutów, które są relacjami, ale co mi z nich gdy zawierają null..

Co do transformatorów, które są dostępne od ręki w L5.5 jako Responses nie korzystałem bo użytkownicy API póki co nie narzekali na format zwracanych danych. Myślę, że to jednak od nich powinna wyjść ta sugestia np. gdyby mój format nie był zgodny z jakimś standardem (mówię np. o datach czy liczbach). Jak zwracam im za dużo to zawsze mają możliwość użycia wspomnianego limitowania. Nie wiem czy pod transformatory podchodzą dekoratory, które doklejają dane do odpowiedzi i to pewnie użyję właśnie w paginacji gdzie będą dołączone meta dane.
To jest wersja lo-fi głównej zawartości. Aby zobaczyć pełną wersję z większą zawartością, obrazkami i formatowaniem proszę kliknij tutaj.
Invision Power Board © 2001-2025 Invision Power Services, Inc.