Section author: Vedran Miletić

Implementacija REpresentational State Transfer (REST) aplikacijskog programskog sučelja u jeziku PHP

REpresentational State Transfer (REST) je danas najkorištenija softverska arhitektura koja koristi HTTP i namijenjena je za aplikacije temeljene na web servisima (više detalja o REST-u na MDN-u). Za web servis koji implementira REST kažemo da je RESTful; mi ćemo se u nastavku baviti takvim servisima, a aplikacijama koje se na njima temelje nekom drugom prilikom. RESTful web servisi su postali u praksi jako zastupljeni tijekom 2010-ih.

REST je razvio Roy Fielding 2000. godine u svom doktoratu i pritom kaže da se njegov razvoj temelji na HTTP-ovom modelu objekta iz 1994. godine (više detalja se može naći u poglavlju 5 pod naslovom Representational State Transfer (REST) i poglavlju 6 pod naslovom Experience and Evaluation).

Ključna apstrakcija podataka u REST-u je resurs; on može biti slika, dokument, zvuk, reprezentacija objekta iz stvarnosti (npr. osobe ili institucije), zbirka drugih resursa i slično. Stanje resursa u bilo kojem trenutku naziva se reprezentacija; ona sadrži podatke, metapodatke i poveznice prema drugim resursima. Za navođenje oblika reprezentacije koristi se već ranije spomenuti MIME tip.

Resursi se identificiraju korištenjem Uniform Resource Identifiera (URI). Recimo, možemo imati RESTful web servis koji putem URI-ja /persons omogućuje dohvaćanje popisa svih osoba koji je oblika:

{
  "count": 2,
  "results": [
    {
      "name": "Dennis MacAlistair Ritchie",
      "birth_year": 1941,
      "known_for": [
        "http://localhost:8000/technologies/1",
        "http://localhost:8000/technologies/2"
      ],
      "created": "2020-12-09T14:50:32.000000Z",
      "edited": "2020-12-20T21:14:07.000000Z",
      "url": "http://localhost:8000/persons/1"
    },
    {
      "name": "Kenneth Lane Thompson",
      "birth_year": 1943,
      "known_for": [
        "http://localhost:8000/technologies/2"
      ],
      "created": "2020-12-10T15:10:51.000000Z",
      "edited": "2020-12-20T21:17:55.000000Z",
      "url": "http://localhost:8000/persons/2"
    }
  ]
}

Za usporedbu, putem URI-ja /persons/1 može se dohvatiti osoba s identifikatorom 1 i primljeni odgovor će biti oblika:

{
  "name": "Dennis MacAlistair Ritchie",
  "birth_year": 1941,
  "known_for": [
    "http://localhost:8000/technologies/1",
    "http://localhost:8000/technologies/2"
  ],
  "created": "2020-12-09T14:50:32.000000Z",
  "edited": "2020-12-20T21:14:07.000000Z",
  "url": "http://localhost:8000/persons/1"
}

Napomene radi, tehnologija s identifikatorom 1 je programski jezik C, a tehnologija s identifikatorom 2 je Unix. Njihove reprezentacije možete zamisliti po želji.

U nastavku ćemo implementirati web servis koji na upite na te URI-je vraća odgovore s navedenim sadržajima i postavlja njihov MIME tip na application/json. Naravno, pored čitanja podataka, naš će web servis omogućavati i druge radnje nad podacima kao što su stvaranje, osvježavanje i brisanje (engl. create, read, update, and delete, kraće CRUD; više detalja o CRUD-u na MDN-u).

Radnje create, read, update i delete (CRUD) i pripadne HTTP metode

Korištenjem HTTP metoda i URI-ja možemo izvesti sve četiri navedene radnje nad podacima:

  • stvaranje (engl. create) – operacija POST na URI /persons

  • čitanje (engl. read) – operacija GET na URI-je /persons i persons/{id}, pri čemu je {id} identifikator osobe

  • osvježavanje (engl. update) – dvije mogućnosti:

    • čitav objekt osvježava se operacijom PUT na URI persons/{id}, pri čemu je {id} identifikator osobe

    • djelomični objekt osvježava se operacijom PATCH na URI persons/{id}, pri čemu je {id} identifikator osobe

  • brisanje (engl. delete) – operacija DELETE na URI persons/{id}, pri čemu je {id} identifikator osobe

REST ne definira način pohrane podataka i prepušta to implementaciji. Podaci mogu biti pohranjeni u relacijskoj bazi, nerelacijskoj bazi ili datoteci, a mi ćemo ih pohranjivati u datoteci radi jednostavnosti.

Čitanje podataka

Recimo da imamo datoteku persons.json sadržaja:

[
  {
    "name": "Dennis MacAlistair Ritchie",
    "birth_year": 1941,
    "known_for": [
      "http://localhost:8000/technologies/1",
      "http://localhost:8000/technologies/2"
    ],
    "created": "2020-12-09T14:50:32.000000Z",
    "edited": "2020-12-20T21:14:07.000000Z",
    "url": "http://localhost:8000/persons/1"
  },
  {
    "name": "Kenneth Lane Thompson",
    "birth_year": 1943,
    "known_for": [
      "http://localhost:8000/technologies/2"
    ],
    "created": "2020-12-10T15:10:51.000000Z",
    "edited": "2020-12-20T21:17:55.000000Z",
    "url": "http://localhost:8000/persons/2"
  }
]

Kod pokretanja ćemo funkcijom file_exists() (dokumentacija) provjeriti ako postoji datoteka sa spremljenim podacima od ranije te ih učitati funkcijom file_get_contents() (dokumentacija) i dekodirati dobiveni JSON u polje funkcijom json_decode(). Ako datoteka ne postoji, inicijalizirat ćemo popis osoba na prazno polje. Kod je oblika:

<?php

$datoteka = "persons.json";
if (file_exists($datoteka)) {
    $j = file_get_contents($datoteka);
    $persons = json_decode($j, true);
} else {
    $persons = [];
}

Dohvaćanje svih unosa

<?php

$datoteka = "persons.json";
if (file_exists($datoteka)) {
    $j = file_get_contents($datoteka);
    $persons = json_decode($j, true);
} else {
    $persons = [];
}

if ($_SERVER["REQUEST_URI"] == "/persons") {
   $response_body_array = ["count" => count($persons), "results" => $persons];
   $response_body = json_encode($response_body_array);
   header("Content-type: application/json");
   echo $response_body;
}

Isprobajmo

curl -v localhost:8000/persons
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> GET /persons HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.74.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8000
< Date: Mon, 15 Feb 2021 14:25:54 GMT
< Connection: close
< X-Powered-By: PHP/7.4.15
< Content-type: application/json
<
* Closing connection 0
{"count":2,"results":[{"name":"Dennis MacAlistair Ritchie","birth_year":1941,"known_for":["http:\/\/localhost:8000\/technologies\/1","http:\/\/localhost:8000\/technologies\/2"],"created":"2020-12-09T14:50:32.000000Z","edited":"2020-12-20T21:14:07.000000Z","url":"http:\/\/localhost:8000\/persons\/1"},{"name":"Kenneth Lane Thompson","birth_year":1943,"known_for":["http:\/\/localhost:8000\/technologies\/2"],"created":"2020-12-10T15:10:51.000000Z","edited":"2020-12-20T21:17:55.000000Z","url":"http:\/\/localhost:8000\/persons\/2"}]}

Dohvaćanje pojedinog unosa

Kod stvaranja odgovora moramo se pobrinuti da postavimo statusni kod 404 Not found

preg_match() (dokumentacija)

Regularni izraz koji ćemo koristiti je /^\/persons\/[1-9][0-9]*$/. Raščlanimo njegove elemente:

  • / – znak za početak regularnog izraza

  • ^ – znak za početak znakovnog niza; želimo da se naš navedeni regularni izraz poklapa s danim znakovnim nizom od početka

  • \/ – znak kose crte ima specijalno značenje pa, kad nam doslovno treba znak kose crte, unosimo ga na ovaj način

  • persons – doslovni tekst

  • \/ – isto kao iznad

  • [1-9] – jedna znamenka od 1 do 9

  • [0-9]* – znamenka od 0 do 9 ponovljena proizvoljan broj puta, uključujući i nijednom

  • $ – znak za kraj znakovnog niza; želimo da se naš navedeni regularni izraz poklapa s danim znakovnim nizom do kraja

  • / – znak za kraj regularnog izraza

funkcija explode() (dokumentacija) npr. za /persons/1 dobivamo polje ["", "persons", "1"].

https://www.php.net/manual/en/language.types.type-juggling.php

else if (preg_match("/^\/persons\/[1-9][0-9]*$/", $_SERVER["REQUEST_URI"])) {
   $uri_parts = explode("/", $_SERVER["REQUEST_URI"]);
   $id = (int) $uri_parts[2];
}

Za stvaranje novih unosa moramo prvo naučiti obraditi tijelo zahtjeva.

Obrada tijela zahtjeva i stvaranje tijela odgovora

Tijelo HTTP zahtjeva nam je u jeziku PHP dostupno putanji php://input (dokumentacija) i ponaša se kao datoteka iz koje možemo čitati. Ovaj zapis putanje tijela HTTP zahtjeva ne treba mistificirati jer naprosto radi o konvenciji koja se koristi; postoji analogna putanja php://output u koju možemo kao u datoteku zapisivati sadržaj tijela odgovora, odnosno na drugačiji način izvesti isto što već rutinski radimo naredbom echo.

Na sličnim putanjama dostupni su i drugi ulazno-izlazni tokovi: standardni ulaz, standardni izlaz i standardni izlaz za greške operacijskog sustava redom pod php://stdin, php://stdout i php://stderr (dokumentacija), opisnici otvorenih datoteka pod php://fd itd.

U nastavku ćemo koristiti dvije funkcije iz dijela Filesystem: sadržaj zaglavlja ćemo dohvatiti funkcijom file_get_contents() (dokumentacija), a sadržaj tijela odgovora ćemo u slučaju potrebe puniti analognom funkcijom file_put_contents().

Napravimo poslužitelj koji na zahtjev tipa POST sa sadržajem “Kako ide?” koji je MIME tipa text/plain odgovara sadržajem “A evo, dobro.” također MIME tipa text/plain.

<?php

if ($_SERVER["REQUEST_METHOD"] == "POST") {
    if ($_SERVER["HTTP_CONTENT_TYPE"] == "text/plain") {
        $request_body = file_get_contents("php://input");
        if ($request_body == "Kako ide?") {
            header("Content-type: text/plain");
            $response_body = "A evo, dobro.";
            file_put_contents("php://output", $response_body);
        }
    }
}

Napravimo zahtjev cURL-om metodom POST MIME tipa text/plain sa sadržajem tijela zahtjeva "Kako ide?":

$ curl -v -X POST -H "Content-Type: text/plain" -d "Kako ide?" http://localhost:8000/
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> POST / HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.72.0
> Accept: */*
> Content-Type: text/plain
> Content-Length: 9
>
* upload completely sent off: 9 out of 9 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8000
< Date: Wed, 30 Dec 2020 19:20:13 GMT
< Connection: close
< X-Powered-By: PHP/8.0.0
< Content-type: text/plain;charset=UTF-8
<
* Closing connection 0
A evo, dobro.

Stvaranje podataka

Kako je HTTP protokol koji ne održava stanje (engl. stateless protocol), svaki zahtjev se obrađuje kodom danim iznad neovisno o prethodnima. Čak nakon pohrane podataka iz varijable $student u polje $studenti, to polje i time svi podaci u njemu prestaju postojati nakon slanja odgovora na primljeni zahtjev. Dakle, kod novog zahtjeva ponovno se izvršava isti kod ispočetka u kojem se inicijalizira prazno polje $studenti i prethodno pohranjene podatke ne možemo dohvatiti.

Zbog toga moramo podatke serijalizirane u nekom obliku trajno pohraniti u datoteku funkcijom file_put_contents(). Ponovno ćemo iskoristiti serijalizaciju u oblik JSON funkcijom json_encode() te po potrebi deserializaciju iz JSON-a u podatke funkcijom json_decode().