Section author: Vedran Miletić

Obrada HTTP zahtjeva i stvaranje odgovora u jeziku PHP

U ovom dijelu nastavljamo koristiti ugrađeni web poslužitelj interpretera PHP-a i pretpostavljamo da je pokrenut tako da poslužuje sadržaj direktorija public u kojem se nalazi datoteka index.php.

Usmjeravanje zahtjeva

Promijenimo kod u datoteci public/index.php tako da specijalno odgovara na sve zahtjeve upućene na putanju /proba/. To možemo izvesti korištenjem varijable $_SERVER["REQUEST_URI"] koja sadrži putanju u HTTP zahtjevu (dokumentacija) na način:

<?php

if ($_SERVER["REQUEST_URI"] == "/proba/") {
    echo "<p>Ovo nije generalna proba</p>\n";
} else {
    echo "<p>Hello, world!</p>\n";
}

Uvjerimo se da naše usmjeravanje zahtjeva upućenih na putanju /proba/ radi ispravno:

$ curl http://localhost:8000/proba/
<p>Ovo nije generalna proba</p>

Metodu koju HTTP zahtjev koristi možemo saznati iz varijable $_SERVER["REQUEST_METHOD"] i to iskoristiti na način:

<?php

if ($_SERVER["REQUEST_URI"] == "/proba/") {
    echo "<p>Ovo nije generalna proba</p>\n";
} else if ($_SERVER["REQUEST_METHOD"] == "POST") {
    echo "<p>Hello, post-world!</p>\n";
} else {
    echo "<p>Hello, world!</p>\n";
}

Uvjerimo se da provjera metode radi:

$ curl -X POST http://localhost:8000/
<p>Hello, post-world!</p>

Dodatno možemo postaviti statusni kod odgovora funkcijom http_response_code() (dokumentacija). Postavimo statusni kod na 206 Partial Content (više detalja o HTTP statusnom kodu 206 Partial Content na MDN-u) kod odgovora na zahtjeve upućene na putanju /proba/:

<?php

if ($_SERVER["REQUEST_URI"] == "/proba/") {
    http_response_code(206);
    echo "<p>Ovo nije generalna proba</p>\n";
} else if ($_SERVER["REQUEST_METHOD"] == "POST") {
    echo "<p>Hello, post-world!</p>\n";
} else {
    echo "<p>Hello, world!</p>\n";
}

Uvjerimo se da se statusni kod odgovora promijenio:

$ curl -v http://localhost:8000/proba/
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> GET /proba/ HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.72.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 206 Partial Content
< Host: localhost:8000
< Date: Mon, 02 Nov 2020 22:28:59 GMT
< Connection: close
< X-Powered-By: PHP/7.4.11
< Content-type: text/html; charset=UTF-8
<
  <p>Ovo nije generalna proba</p>
* Closing connection 0

Isti kod vidimo na strani poslužitelja:

[Mon Nov  2 23:28:59 2020] [::1]:52808 Accepted
[Mon Nov  2 23:28:59 2020] [::1]:52808 [206]: GET /proba/
[Mon Nov  2 23:28:59 2020] [::1]:52808 Closing

Provjere putanje i metode možemo i kombinirati korištenjem logičkih operatora negacije (!), “i” (&&) i “ili” (||) (dokumentacija). Primjerice, možemo u HTTP odgovoru postaviti statusni kod na 400 Bad Request (više detalja o HTTP statusnom kodu 400 Bad Request na MDN-u) ako se koristi metoda POST na putanji /proba/, za što nam treba logički operator “i” (&&):

<?php

if ($_SERVER["REQUEST_METHOD"] == "POST" && $_SERVER["REQUEST_URI"] == "/proba/") {
    http_response_code(400);
    echo "<p>Zahtjev metodom POST na /proba/ nije ispravan</p>\n";
} else {
    echo "<p>Hello, world!</p>\n";
}

Lako ćemo se uvjeriti da se postavljeni statusni kod javlja u HTTP odgovoru:

$ curl -v -X POST http://localhost:8000/proba/
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> POST /proba/ HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.72.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 400 Bad Request
< Host: localhost:8000
< Date: Tue, 03 Nov 2020 07:54:07 GMT
< Connection: close
< X-Powered-By: PHP/7.4.11
< Content-type: text/html; charset=UTF-8
<
  <p>Zahtjev metodom POST na /proba/ nije ispravan</p>
* Closing connection 0

Odgovarajuća poruka na strani poslužitelja je oblika:

[Tue Nov  3 08:54:07 2020] [::1]:52918 Accepted
[Tue Nov  3 08:54:07 2020] [::1]:52918 [400]: POST /proba/
[Tue Nov  3 08:54:07 2020] [::1]:52918 Closing

Zaglavlje odgovora možemo dopuniti dodatnim elementima korištenjem funkcije header() (dokumentacija). Primjerice, kod primanja statusnog koda 302 Found (više detalja o HTTP statusnom kodu 302 Found na MDN-u) klijent očekuje URL na kojem je sadržaj moguće pronaći u zaglavlju Location (više detalja o HTTP zaglavlju Location na MDN-u). Recimo da želimo sve zahtjeve na /proba preusmjeriti korištenjem statusnog koda 302 Found na /proba/; to ćemo izvesti postavljanjem statusnog koda 302 u odgovoru i dodavanjem zaglavlja Location koje sadrži vrijednost /proba/:

<?php

if ($_SERVER["REQUEST_URI"] == "/proba/") {
    http_response_code(206);
    echo "<p>Ovo nije generalna proba</p>\n";
} else if ($_SERVER["REQUEST_URI"] == "/proba") {
    http_response_code(302);
    header("Location: /proba/");
} else if ($_SERVER["REQUEST_METHOD"] == "POST") {
    echo "<p>Hello, post-world!</p>\n";
} else {
    echo "<p>Hello, world!</p>\n";
}

Uvjerimo se da preusmjeravanje radi korištenjem cURL-ovog parametra --location, odnosno -L koji će učiniti da cURL ponovi HTTP zahtjev na novoj lokaciji navedenoj u zaglavlju Location:

$ curl -v -L http://localhost:8000/proba
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> GET /proba HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.72.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Found
< Host: localhost:8000
< Date: Mon, 02 Nov 2020 22:38:20 GMT
< Connection: close
< X-Powered-By: PHP/7.4.11
< Location: /proba/
< Content-type: text/html; charset=UTF-8
<
* Closing connection 0
* Issue another request to this URL: 'http://localhost:8000/proba/'
* Hostname localhost was found in DNS cache
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#1)
> GET /proba/ HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.72.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 206 Partial Content
< Host: localhost:8000
< Date: Mon, 02 Nov 2020 22:38:20 GMT
< Connection: close
< X-Powered-By: PHP/7.4.11
< Content-type: text/html; charset=UTF-8
<
  <p>Ovo nije generalna proba</p>
* Closing connection 1

Na strani poslužitelja poruke su oblika:

[Mon Nov  2 23:38:20 2020] [::1]:52822 Accepted
[Mon Nov  2 23:38:20 2020] [::1]:52822 [302]: GET /proba
[Mon Nov  2 23:38:20 2020] [::1]:52822 Closing
[Mon Nov  2 23:38:20 2020] [::1]:52824 Accepted
[Mon Nov  2 23:38:20 2020] [::1]:52824 [206]: GET /proba/
[Mon Nov  2 23:38:20 2020] [::1]:52824 Closing

Relativne putanje u zaglavlju Location se rijetko koriste. Ako želimo u zaglavlje Location upisati čitav URL, možemo iskoristiti dodatne varijable:

  • $_SERVER["SERVER_NAME"] – ime poslužitelja (u našem slučaju localhost)

  • $_SERVER["SERVER_PORT"] – vrata poslužitelja (u našem slučaju 8000)

pa umjesto koda:

<?php

header("Location: /proba/");

staviti kod:

<?php

$name = $_SERVER["SERVER_NAME"];
$port = $_SERVER["SERVER_PORT"];
header("Location: http://$name:$port/proba/");

Uvjerimo se da sada u zaglavlju Location nalazi cjeloviti URL:

$ curl -v -L http://localhost:8000/proba
(...)
< Location: http://localhost:8000/proba/
< Content-type: text/html; charset=UTF-8
<
* Closing connection 0
* Issue another request to this URL: 'http://localhost:8000/proba/'
(...)

Dodatno možemo po potrebi koristiti i sljedeće podatke o web poslužitelju:

  • $_SERVER["SERVER_SOFTWARE"] – softver koju poslužitelj koristi (u našem slučaju PHP 7.4.11 Development Server)

  • $_SERVER["SERVER_PROTOCOL"] – protokol koji poslužitelj koristi (u našem slučaju HTTP/1.1)

Dohvaćanje i obrada zaglavlja zahtjeva

Možemo dohvatiti i sljedeća zaglavlja HTTP zahtjeva:

Primjerice, poslužitelj može biti postavljen da podatke o studentu šalje u različitom obliku ovisno o navedenoj vrijednosti u zaglavlju Accept HTTP zahtjeva. Konkretno, poslužitelj može razlikovati JSON (poznati format za razmjenu podataka među aplikacijama; službena stranica, više detalja o JSON-u na MDN-u) koji ima MIME tip application/json, HTML koji ima MIME tip text/html i čisti tekst koji ima MIME tip text/plain (popis često korištenih MIME tipova na MDN-u).

<?php

if ($_SERVER["HTTP_ACCEPT"] == "application/json") {
    echo '{"ime:"Ivan","prezime":"Horvat","studij":"Informatika"}';
} else if ($_SERVER["HTTP_ACCEPT"] == "text/html") {
    echo "<p>Ivan Horvat (Informatika)</p>\n";
} else if ($_SERVER["HTTP_ACCEPT"] == "text/plain") {
    echo "Ivan,Horvat,Informatika\n";
} else {
    http_response_code(400);
    echo "<p>Nejasan zahtjev. Podržani MIME tipovi su application/json, text/html i text/plain.</p>\n";
}

Uočimo da kod JSON-a za imenovanje polja i navođenje njihovih vrijednosti koristimo dvostruke navodnike pa za ispis ovog objekta naredbom echo koristimo jednostruke.

Zatražimo cURL-om prvo JSON navođenjem odgovarajuće vrijednosti zaglavlja Accept parametrom -H:

$ curl -v -H "Accept: application/json" localhost:8000
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> GET / HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.72.0
> Accept: application/json
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8000
< Date: Fri, 13 Nov 2020 13:50:52 GMT
< Connection: close
< X-Powered-By: PHP/7.4.11
< Content-type: text/html; charset=UTF-8
<
* Closing connection 0
{"ime:"Ivan","prezime":"H^Cvat","studij":"Informatika"}

Zatim zatražimo HTML pa čisti tekst:

$ curl -v -H "Accept: text/html" localhost:8000
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> GET / HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.72.0
> Accept: text/html
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8000
< Date: Fri, 13 Nov 2020 13:51:03 GMT
< Connection: close
< X-Powered-By: PHP/7.4.11
< Content-type: text/html; charset=UTF-8
<
<p>Ivan Horvat (Informatika)</p>
* Closing connection 0

$ curl -v -H "Accept: text/plain" localhost:8000
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> GET / HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.72.0
> Accept: text/plain
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8000
< Date: Fri, 13 Nov 2020 13:51:14 GMT
< Connection: close
< X-Powered-By: PHP/7.4.11
< Content-type: text/html; charset=UTF-8
<
Ivan,Horvat,Informatika
* Closing connection 0

Uočimo da smo u sva tri slučaja dobili očekivane odgovore. Provjerimo da u slučaju da ne navedemo neku od te tri vrijednosti zaglavlja Accept u zahtjevu, poslužitelj odgovara porukom o pogrešci:

$ curl -v localhost:8000
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> GET / HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.72.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 400 Bad Request
< Host: localhost:8000
< Date: Fri, 13 Nov 2020 13:51:24 GMT
< Connection: close
< X-Powered-By: PHP/7.4.11
< Content-type: text/html; charset=UTF-8
<
<p>Nejasan zahtjev. Podržani MIME tipovi su application/json, text/html i text/plain.</p>
* Closing connection 0

Osim spomenutih zaglavlja HTTP zahtjeva, možemo obrađivati i sva druga. Sva zaglavlja koja zahtjev sadrži dohvatit ćemo funkcijom getallheaders() (dokumentacija).

<?php

$request_headers = getallheaders();

Todo

Ovaj dio treba napisati.

Specifičnosti metode GET

Varijable navedene kod slanja zahtjeva HTTP metodom GET možemo dohvatiti putem varijable $_GET (dokumentacija). Primjerice, recimo da skripta index.php očekuje ime i prezime na način da dajete zatjeve na putanju /profil?ime=Ivan&prezime=Horvat. Možemo se ponadati da bismo obradu takvih zahtjeva mogli izvesti kodom:

<?php

if ($_SERVER["REQUEST_METHOD"] == "GET" && $_SERVER["REQUEST_URI"] == "/profil") {
    $ime = $_GET["ime"];
    $prezime = $_GET["prezime"];
    echo "<p>Vi ste $ime $prezime.</p>\n";
}

Međutim, $_SERVER["REQUEST_URI"] će uključivati čitavu putanju u kojoj se nalazi i upit (u našem slučaju ?ime=Ivan&prezime=Horvat) pa nam kod neće raditi kako očekujemo. Putanja bez upita je sadržana u varijabli $_SERVER["PATH_INFO"] pa je ispravan kod oblika:

<?php

if ($_SERVER["REQUEST_METHOD"] == "GET" && $_SERVER["PATH_INFO"] == "/profil") {
    $ime = $_GET["ime"];
    $prezime = $_GET["prezime"];
    echo "<p>Vi ste $ime $prezime.</p>\n";
}

Kako ljuska tretira ? i & kao posebne znakove, URL je kod izvođenja zahtjeva potrebno staviti pod navodnike na način:

$ curl -v "http://localhost:8000/profil?ime=Ivan&prezime=Horvat"
*   Trying 127.0.0.1:8000...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET /profil?ime=Ivan&prezime=Horvat HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8000
< Date: Tue, 03 Nov 2020 12:55:51 GMT
< Connection: close
< X-Powered-By: PHP/7.4.3
< Content-type: text/html; charset=UTF-8
<
  <p>Vi ste Ivan Horvat.</p>
* Closing connection 0

Specifičnosti metode POST

Podatke možemo primati i metodom POST i dohvatiti ih putem varijable $_POST (dokumentacija) na način:

<?php

if ($_SERVER["REQUEST_METHOD"] == "POST" && $_SERVER["REQUEST_URI"] == "/profil") {
    $ime = $_POST["ime"];
    $prezime = $_POST["prezime"];
    echo "<p>Vi ste $ime $prezime.</p>\n";
}

Ovdje putanja ne sadrži upit pa nam $_SERVER["REQUEST_URI"] ostaje /profil pa ne moramo koristiti $_SERVER["PATH_INFO"]. Zahtjev izvodimo na način:

$ curl -v -X POST -d "ime=Ivan" -d "prezime=Horvat" http://localhost:8000/profil
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 127.0.0.1:8000...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8000 (#0)
> POST /profil HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.68.0
> Accept: */*
> Content-Length: 23
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 23 out of 23 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8000
< Date: Tue, 03 Nov 2020 13:02:47 GMT
< Connection: close
< X-Powered-By: PHP/7.4.3
< Content-type: text/html; charset=UTF-8
<
  <p>Vi ste Ivan Horvat.</p>
* Closing connection 0

Provjera poslanih varijabli u zahtjevima metodom GET i POST

Dodatno možemo provjeriti jesu li u zahtjevu poslane varijable koje želimo iskoristiti, što ćemo izvesti funkcijom isset() (dokumentacija). Zatim ćemo formirati adekvatan odgovor u slučaju da varijable nisu postavljene; primjerice, postavit ćemo već ranije korišteni statusni kod 400 Bad Request i poslati pripadnu poruku:

<?php

if ($_SERVER["REQUEST_URI"] == "/profil" && $_SERVER["REQUEST_METHOD"] == "POST") {
    $ime = $_POST["ime"];
    $prezime = $_POST["prezime"];
    if (isset($ime) && isset($prezime)) {
      echo "<p>Vi ste $ime $prezime.</p>\n";
    } else {
      http_response_code(400);
      echo "<p>Niste poslali ime ili prezime.</p>\n";
    }
}

Uvjerimo se da smo uhvatili sve situacije kad u zahtjevu nedostaje neka od varijabli:

$ curl -X POST http://localhost:8000/profil
<p>Niste poslali ime ili prezime.</p>
$ curl -X POST -d "ime=Ivan" http://localhost:8000/profil
<p>Niste poslali ime ili prezime.</p>
$ curl -X POST -d "prezime=Horvat" http://localhost:8000/profil
<p>Niste poslali ime ili prezime.</p>

Na analogan način provjerili bismo poslane varijable u slučaju kad se koristi metoda GET.

Dohvaćanje i obrada tijela zahtjeva

Todo

Ovaj dio treba napisati.