JSON Patch i REST w Symfony2

W ostatnich wpisach budowaliśmy RESTful API w Symfony2 oraz poznaliśmy JSON Patch. Dziś połączymy obie technologie i wykorzystamy JSON Patch w praktyce.

Będę nadal bazował na przykładowym REST API w Symfony oraz na dokumencie opisującym pizzę w JSON z poprzedniego wpisu.

Potrzebujemy biblioteki PHP, która będzie dla naz wykonywać JSON Patch. Najpopularniejsza (według packagista) jest php-jsonpatch. Do pliku composer.json musimy dopisać:

"php-jsonpatch/php-jsonpatch": "1.1.*@dev",
"php-jsonpointer/php-jsonpointer": "1.1.*@dev"

W chwili pisania tego artykułu biblioteka była w wersji dev. Warto sprawdzić czy nie ma aktualizacji.

Po zainstalowaniu możemy przystąpić do działania.

Mime type dla JSON Patch

Na początek musimy nauczyć symfony (a właściwie bundla) nowego formatu danych. Klient powinien wysyłać dane o typie “application/json-patch+json”. Nasz bundle musi je rozumieć. W pliku app/config/config.yml zapisujemy:

fos_rest:
   view:
      mime_types:
         json_patch: ['application/json-patch+json']

Zdefiniowaliśmy typ mime, który w symfony będzie przedstawiał się jako “json_patch”. Klient musi nam wysłać dane tego typu. Ale skąd ma to wiedzieć? Do tego służy właśnie metoda OPTIONS. Specjalny nagłówek “Accept-patch” mówi klientowi jak ma patchować:

public function optionsPizzaAction() {
   $view = $this->view();
   $view->setHeader('Allow','OPTIONS,PATCH');
   $view->setHeader('Accept-Patch','application/json-patch+json');
   return $this->handleView($view);
}

Jeśli wywołamy teraz naszą metodę OPTIONS zobaczymy nagłówki:

curl -i -X OPTIONS  http://localhost/REST_Example/web/app_dev.php/api/pizza
HTTP/1.1 204 No Content
...
Allow: OPTIONS,PATCH
Accept-Patch: application/json-patch+json
...

Metoda patchująca

Możemy teraz przystąpić do pisania naszej metody PATCH. Tworzymy metodę patchPizzaAction(Request $request). Będzie ona obsługiwać wywołanie PATCH na zasobie /pizza.

Na początek musimy się upewnić, że klient faktycznie przysłał nam dokument JSON Patch a nie coś innego:

// sprawdzamy, czy klient przysłał nam json_patch
$contentType = $request->getContentType();
if ($contentType != 'json_patch') {
   $view = $this->view(null,415);
   $view->setHeader('Accept-Patch','application/json-patch+json');
   return $this->handleView($view);
}

Po to właśnie dodawaliśmy w konfigu definicję typu mime. Jeśli klient przysłał cokolwiek innego, odpowiadamy kodem 415 (nieobsługiwany typ) i jeszcze raz przypominamy mu, że akceptujemy tylko JSON Patch.

Możemy teraz przystąpić do patchowania. Jeśli w zmiennej $resource będzie nasz json, to wystarczy taki kod:

// pobieramy zamówienie na zmiany
$jsonPatch = $request->getContent();
try {
   // patchujemy
   $patch = new Patch($resource, $jsonPatch);
   $result = $patch->apply();
   $result = json_decode($result,true);
   // tu warto zapisać do bazy

   // nie powinniśmy zwracać danych, ale dla przykładu można
   $view = $this->view($result,200);
} catch (\Exception $e) {
   // błąd? pewnie problem klienta
   $view = $this->view($e->getMessage(),400);
}

Zwróćmy uwagę na kilka rzeczy:

Dokument powinniśmy zapisać w bazie. Patchowanie nie ma sensu jeśli nie prowadzi do trwałej modyfikacji dokumentu. W przykładzie pomijam obsługę bazy danych.

Zwracanie danych nie jest zgodne ze specyfikacją metody PATCH. Powinniśmy utworzyć widok podając null zamiast zasobu. Na szczęście nic złego się nie dzieje gdy podamy dane. Sprawdzenie działania patcha będzie łatwiejsze.

Obsługa błędów powinna być bardziej rozbudowana. Jeśli klient przesłał zły patch, odpowiemy 400 (Bad request). Jeśli problem jest u nas, odpowiemy 501 (Internal server error). Jeśli mamy więcej warunków, np. klient zechce dodać szynkę do pizzy a my prowadzimy lokal wegetariański, odpowiemy błędem 422 (Unprocessable Entity). Standardowy 404 też może się tu pojawić.

Jeśli używamy Etagów (pisałem o Etag w HTTP), możemy też zwracać większą gamę błędów. Np. błędy 412 i 409 oznaczają różne rodzaje konfliktów w danych.

Pełen kod metody patch wygląda tak:

public function patchPizzaAction(Request $request) {

   // sprawdzamy, czy klient przysłał nam json_patch
   $contentType = $request->getContentType();
   if ($contentType != 'json_patch') {
      $view = $this->view(null,415);
      $view->setHeader('Accept-Patch','application/json-patch+json');
      return $this->handleView($view);
   }

   // normalnie pobralibyśmy z bazy danych
   $resource = '
   {
      "pizza": {
         "size": "30cm",
         "thickness": "thick",
         "toppings": ["Olives","Mozarella","Mushrooms","Tomatoes","Onions","Basil"]
      },
      "extra": ["ketchup"]
   }';

   // pobieramy zamówienie na zmiany
   $jsonPatch = $request->getContent();
   try {
      // patchujemy
      $patch = new Patch($resource, $jsonPatch);
      $result = $patch->apply();
      $result = json_decode($result,true);
      // tu warto zapisać do bazy

      // nie powinniśmy zwracać danych, ale dla przykładu można
      $view = $this->view($result,200);
   } catch (\Exception $e) {
      // błąd? pewnie problem klienta
      $view = $this->view($e->getMessage(),400);
   }
   return $this->handleView($view);
}

Sprawdzamy czy działa

Możemy teraz wywołać naszą metodę podobnie jak wywołaliśmy OPTIONS. Żeby wszystko działało trzeba ustawić kilka nagłówków:

$ curl -i -X PATCH \
-H "Accept: application/xml" \
-H "Content-type: application/json-patch+json" \
-d '[{"op":"add", "path":"/pizza/toppings/-", "value":"Pepper"}]' \
http://localhost/REST_Example/web/app_dev.php/api/pizza

W odpowiedzi zobaczymy zmodyfikowany dokument JSON. Na pizzy będzie papryka.

Wszystkie przykłady z poprzedniego artykułu o JSON Patch będą tu działać. Wystarczy zmienić zawartość danych (-d) oraz usunąć komentarze z przykładów.

Co dalej

Mamy bardzo dobre REST API. Rozmawiamy z klientem poprzez kody HTTP i nagłówki. Wszystko jest poprawnie zdefiniowane. Prawie nie musimy nawet pisać dokumentacji. Znający się na REST programista tworzący klienta będzie w stanie sam rozpoznać formaty danych.

O JSON Patch możemy poczytać w RFC 6902. Sama metoda PATCH w HTTP jest opisana w RFC 5798.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *