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
ipersons/{id}
, pri čemu je{id}
identifikator osobeosvježavanje (engl. update) – dvije mogućnosti:
čitav objekt osvježava se operacijom
PUT
na URIpersons/{id}
, pri čemu je{id}
identifikator osobedjelomični objekt osvježava se operacijom
PATCH
na URIpersons/{id}
, pri čemu je{id}
identifikator osobe
brisanje (engl. delete) – operacija
DELETE
na URIpersons/{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činpersons
– 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()
.