Podpora REST služeb v Nette 2.1

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
venus
Člen | 14
+
0
-

Bude prosím v Nette 2.1 přímá podpora pro REST v Route? Tedy tak, aby bylo při vytváření routy možné definovat i metodu (GET, POST, PUT, DELETE). Předem děkuji za odpověď.

David Grudl
Nette Core | 8111
+
0
-

Sám nic takového nechystám, ale pokud se toho někdo chopí a připraví RFC / implementaci, může být.

Erik Ferčák
Člen | 10
+
0
-

Potreboval som niečo podobné už v Netee 0.9.x, napísal som si na to vlastnú RestRoute:

<?php

class RestRoute extends Route
{
    const METHOD_POST = 4;
    const METHOD_GET = 8;
    const METHOD_PUT = 16;
    const METHOD_DELETE = 32;
    const RESTFUL = 64;

    public function match(IHttpRequest $httpRequest)
    {
        $httpMethod = $httpRequest->getMethod();

        if (($this->flags & self::RESTFUL) == self::RESTFUL) {
            $presenterRequest = parent::match($httpRequest);
            if ($presenterRequest != NULL) {
                switch ($httpMethod) {
                    case 'GET':
                        $action = 'default';
                        break;
                    case 'POST':
                        $action = 'create';
                        break;
                    case 'PUT':
                        $action = 'update';
                        break;
                    case 'DELETE':
                        $action = 'delete';
                        break;
                    default:
                        $action = 'default';
                }

                $params = $presenterRequest->getParams();
                $params['action'] = $action;
                $presenterRequest->setParams($params);
                return $presenterRequest;
            } else {
                return NULL;
            }
        }

        if (($this->flags & self::METHOD_POST) == self::METHOD_POST
            && $httpMethod != 'POST') {
                return NULL;
        }

        if (($this->flags & self::METHOD_GET) == self::METHOD_GET
            && $httpMethod != 'GET') {
                return NULL;
        }

        if (($this->flags & self::METHOD_PUT) == self::METHOD_PUT
            && $httpMethod != 'PUT') {
                return NULL;
        }

        if (($this->flags & self::METHOD_DELETE) == self::METHOD_DELETE
            && $httpMethod != 'DELETE') {
                return NULL;
        }

        return parent::match($httpRequest);
    }
}
?>

Použitie:

<?php

$router[] = new RestRoute('/1.0/resource/<id [0-9]+>', array(
    'presenter' => 'Resource',
    'action' => 'default',
), RestRoute::METHOD_GET);

$router[] = new RestRoute('/1.0/resource/<id [0-9]+>', array(
    'presenter' => 'Resource',
    'action' => 'delete'
), RestRoute::METHOD_DELETE);

?>
Tharos
Člen | 1030
+
0
-

Již nějaký měsíc používám lehce modifikovanou verzi té od Erika (už ji zde na fóru totiž jednou publikoval) a funguje výborně. Takže já bych klidně hlasoval za její přímé začlenění. :)

V abstraktním předkovi REST presenterů mám také takovou zkratku pro pohodlnější dolování proměnných předaných v těle HTTP požadavku:

/**
 * @param string $name
 * @return mixed
 */
protected function getRestParameter($name)
{
	$request = $this->getHttpRequest();
	if ($request->getMethod() === 'GET') { // TODO: zkonstantit
		return $request->getQuery($name);
	} else {
		return $request->getPost($name);
	}
}

To pochopitelně už nemá smysl cpát přímo do frameworku, ale uvádím to jen pro inspiraci, protože je to praktické.

Filip Procházka
Moderator | 4668
+
0
-

Mně se to nelíbí. Tohle by měl řešit presenter, né router. Respektive, nevidím důvod, proč by každá akce nemohla mít annotaci, jaké HTTP metody zpracovává (cca tak jako v RoR). To chcete mít milion rout?

Editoval HosipLan (9. 5. 2012 10:39)

Tharos
Člen | 1030
+
0
-

@HosipLan: Jasně, ale tohle můžeš říct o celém routování. :) Proč by každá akce nemohla mít anotaci, jakou masku „matchne“… když to přeženu (jasně, že by to bylo komplikovanější). Mně se to Erikovo řešení líbí, protože HTTP sloveso považuji za informaci pro router, skoro mi to přijde jako detašovaná součást masky. :)

Myslím, že existují rozumné důvody pro oba přístupy. Namátkou vyjmenuji nějaké výhody zde zmíněného řešení: že vše ohledně routování je na jednom místě, nejsou zapotřebí anotace.

Jinak osobně bych dokázal bych žít s oběma variantami a poděkoval bych tomu, kdo by v duchu jakékoliv z nich připravil pull. :) Protože si myslím, že by lepší podpora REST architektury Nette slušela.

Editoval Tharos (9. 5. 2012 10:48)

Filip Procházka
Moderator | 4668
+
0
-

Máš pravdu, že u rout je potřeba rozlišovat, jestli je možné danou akci vůbec zpracovat, to tedy musí být na úrovni routování. Jak se ale zbavit potřeby, mít routu pro každou hloupost?

Tharos
Člen | 1030
+
0
-

No, to je právě to, že těch rout v praxi nevzniká až tolik, kolik by se mohlo na první pohled zdát. :) Stručné vysvětlení je, že uvedená routa ke konci matche stejně volá parent::match(...), takže potřeba mít routu pro každou hloupost je úplně stejná, jako u routování pomocí vestavěné Route.

Dále – velmi užitečný je ten příznak RESTFUL, který Ti v jedné routě vyřeší CRUD nad jednou „záležitostí“ /entitou, objektem, prvkem… určitě víš, co tím myslím :)/ a naroutuje Ti jej na individuální akce. Dokážu si představit jednu jedinou routu s maskou /<presenter> s příznakem RESTFUL, se kterou bych si vystačil pro kompletní CRUD nad uživateli, články, vzkazy, produkty…

Hodně záleží na designu API. Pokud jej chceš mít vyloženě CRUDové, vystačíš si s minimálním počtem rout. Pokud jej chceš mít více use-case lazené, tak se bohužel více routám nevyhneš, ale ty bys ta vstupní URL stejně někde nějak musel naroutovat.

Editoval Tharos (9. 5. 2012 13:09)

David Grudl
Nette Core | 8111
+
0
-

REST je forma routování používající pro akci HTTP metodu. Cílem REST routeru tedy není rozlišovat routy podle metody, ale podle metod stanovit action. V REST routě tak stačí uvést jen zdroj, se kterým pracuju, a to je všechno.

$routes[] = new RestRoute(‚posts‘);

A umožní mi to následující:

GET    /posts        | index             | Render a list of all post
GET    /posts/new    | new               | Render a form for creating a single new post
POST   /posts        | create            | Create a single new post from the received data
GET    /posts/1      | show              | Render a single existing post
GET    /posts/1/edit | edit              | Render a form for editing a single existing post
PUT    /posts/1      | update            | Update a single existing post based on the received data
DELETE /posts/1      | destroy           | Destroy a single existing post

Používání HTTP metod, jako je tomu třeba v Rails, je dle mého omyl, protože takové odkazy nelze vytvářet. Leda AJAXem a ne v každém browseru. Proto musí fungovat tohle:

GET    /posts           | index             | Render a list of all post
GET    /posts/new       | new               | Render a form for creating a single new post
GET    /posts/create    | create            | Create a single new post from the received data
GET    /posts/1         | show              | Render a single existing post
GET    /posts/1/edit    | edit              | Render a form for editing a single existing post
GET    /posts/1/update  | update            | Update a single existing post based on the received data
GET    /posts/1/destroy | destroy           | Destroy a single existing post

Což můžeme zapsat cca jako

$router[] = new Route('<resource>[/<id>][/<action>]');

RestRoute by mělo mít oproti Route navíc logiku, která podle přítomnosti/nepřítomnosti id zvolí mezi show a index apod. Samozřejmě může taky respektovat HTTP metodu.

paranoiq
Člen | 392
+
0
-

definice metod, jak ji píšeš mi nějak nesedí. dle mě:

POST = update (UPDATE v SQL, jazyce mého kmene)

PUT = create (INSERT nebo REPLACE v SQL)

koukni na „idempotentní metody“: https://cs.wikipedia.org/…fer_Protocol

Vojtěch Dobeš
Gold Partner | 1316
+
0
-

paranoiq Hm, Davidem odkazovaný článek zase tvrdí opak. PUT jako UPDATE a POST jako CREATE implementuje například i Backbone.js, a předpokládám, že tomu tak bude s naprostou většinou knihoven využívajícíh RESTový CRUD.

Filip Procházka
Moderator | 4668
+
0
-

Já to znám taky jako PUT === UPDATE :)

Tharos
Člen | 1030
+
0
-

Rozhodně PUT === UPDATE, nebo alespoň pro ty, co chtějí být kompatibilní s REST specifikací a 99,99% světa. :)

@David Grudl: Přiznám se, že jsem z toho Tvého popisu trochu zmaten a přemýšlím, jestli jsem o pár příspěvků výše nemluvil o koze a Ty teď o voze. :)

Všechna čistokrevná REST API, se kterými jsem se kdy setkal, rozlišovala to, co se má dít, právě podle kombinace metody a URL. Zkrátka když volám URL /posts metodou GET, čtu data, když volám URL /posts metodou POST, vytvářím nový záznam. IMHO tam nepatří žádné /posts/create a podobné „berličky“ (to ještě za chvíli rozvedu).

Že na to nelze vytvořit odkaz přece vůbec nevadí, protože kdo by odkazoval na webu přímo na nějaké REST API? K čemu by to mělo být dobré? K REST API se většinou vytváří pouze API dokumentace a v té se zpravidla jako API metoda považuje kombinace metody a URL adresy.

A také mi moc nedává smysl Tebou zmíněná URL /posts/new, která by měla vykreslovat nějaký formulář? REST API typicky vrací nějakou JSON nebo XML strukturu. Nějaké HTML přece patří až do aplikaci, která je nad API postavená.

Ještě k těm berličkám… Již jsem zažil taková REST API, ve kterých se vše volalo GET metodou a URL měly právě tvar /posts/create, /posts/delete a tak podobně, ale tohle už není REST API v pravém slova smyslu.

No, přiznám se, že pokud by se do Nette mělo dostat nativně něco, co není REST v pravém slova smyslu… tak to bych tam asi radši osobně nedával nic. :)

Editoval Tharos (16. 5. 2012 22:45)

Vojtěch Dobeš
Gold Partner | 1316
+
0
-

Mohl bych poprosit o nějaké příklady API využívajících PUT k updatu? Pro rozšíření obzorů :)

Honza Marek
Člen | 1664
+
0
-

Ostatně může fungovat pravý rest + berličky najednou. DELETE /posts/1 a třeba GET /posts/1/delete nebo GET /posts/1?method=delete by routa hodila na tu samou akci.

Vykreslování formuláře mi přijde taky navíc.

Tharos
Člen | 1030
+
0
-

@vojtech.dobes: No tak obzory Ti rozšířím snadno :), namátkou třeba REST API od project trackeru JIRA (ono to tu asi nikdo znát nebude, ale třeba s tím jsem právě přišel do styku…). Pokud se Ti na tom webu nechce API doc dlouze hledat, koukni rovnou sem.

No a pak třeba viz originální práce od Roye Fieldinga, Wikipedia… Kde vůbec vzniklo, že by k vytváření záznamu měl sloužit PUT?

Jinak nekoukejte na „REST API“ velkých sociálních sítí ala Twitter, protože ty jsou fakt plná berliček (v podstatě se omezují pouze na metody GET a POST a chybějící výrazivo nahrazují právě něčím v URL…).

Vojtěch Dobeš
Gold Partner | 1316
+
0
-

Díky Tharos :) Eh… PUT, UPDATE, POST… spletl jsem si to, teprve teď mi došlo, že ty, Hosiplan i já říkáme to samé :)

David Grudl
Nette Core | 8111
+
0
-

Tharos napsal(a):

@David Grudl: Přiznám se, že jsem z toho Tvého popisu trochu zmaten a přemýšlím, jestli jsem o pár příspěvků výše nemluvil o koze a Ty teď o voze. :)

Snažím se říct jen to, že implementace by měla být taková, aby

  • stačila jen jedna routa
  • existoval fallback pro případy, že nelze použít jiné HTTP metody, protože to dost často nelze
jasir
Člen | 746
+
0
-

Jestli jsem pochopil správně tu Erikovu routu, tak přeci jediná stačí obsloužit všechny REST presentery, například takto:

<?php
$router[] = new RestRoute('rest/<presenter>/<id [0-9]+>', array(
    'presenter' => 'Resource',
    'action' => 'default',
), RestRoute::RESTFUL);
?>

Tedy funguje úplně stejně normální routování, akorát action se přířadí podle metody.

Editoval jasir (17. 5. 2012 1:26)

Patrik Votoček
Člen | 2221
+
0
-

Express.js se používá jako fallback parametr _method pomocí kterého lze „změnit“ HTTP metodu tam kde to nejde nativně. Převod se pak provádí na nejnižší možné úrovni spracování HTTP požadavku ve frameworku (tj. ostatní části systému jsou od toho celkem dobře odstíněny).

Vojtěch Dobeš
Gold Partner | 1316
+
0
-

Pro pestřejší paletu, Backbone.js zase využívá hlavičky X-HTTP-Method-Override, kde uvádí název metody (PUT nebo DELETE), ve skutečnosti realizované skrze POST.

Lopo
Člen | 277
+
0
-

inac serial o REST co som teraz zacal citat: http://phpmaster.com/…n-spell-it-1

mishak
Člen | 94
+
0
-

Shodou okolností nebo protože nic neexistovalo sem na něco podobného dělal.
https://gist.github.com/2781069 není to dokončené, ale už tak obsluhuji, některé požadavky.

David Grudl
Nette Core | 8111
+
0
-

jasir napsal(a):

Jestli jsem pochopil správně tu Erikovu routu, tak přeci jediná stačí obsloužit všechny REST presentery

Máš pravdu, umí to.

patriksima
Člen | 58
+
0
-

Tharos napsal(a):

Již nějaký měsíc používám lehce modifikovanou verzi té od Erika (už ji zde na fóru totiž jednou publikoval) a funguje výborně. Takže já bych klidně hlasoval za její přímé začlenění. :)

V abstraktním předkovi REST presenterů mám také takovou zkratku pro pohodlnější dolování proměnných předaných v těle HTTP požadavku…

Ahoj, mě to v PHP 5.3.5 nefunguje. Musím například PUT request číst takto:

parse_str(file_get_contents("php://input"),$put_vars);

možná jdou pro čtení používat i tyto funkce http://php.net/…est-body.php

Editoval patriksima (25. 7. 2012 9:13)

patriksima
Člen | 58
+
0
-

A kdyby někdo potřeboval výjimky zobrazovat v jsonu, tak to jde v presenteru udělat nějak takhle:

protected function startup()
{
    parent::startup();

    // debug off, jinak nam do jsonu poleze debug bar
    Debugger::enable(Debugger::PRODUCTION);

    // json exception handler
    set_exception_handler(function($e){
        echo json_encode(array('error'=>$e->getMessage()));
    });
}

Editoval patriksima (25. 7. 2012 9:57)

Zdeněk Topič
Člen | 14
+
0
-

Zdravím, jak toto dopadlo prosím?

Dále se chci zeptat, nebylo by také dobré, když už teda framework si je vědom toho, že zpracovává požadavek na REST službu, aby předal i data do parametru metody? Použiju Davidům příklad:

GET    /posts			| actionDefault($id = null, $data = null)	| Render a list of all post
GET    /posts?offset=10	| actionDefault($id = null, $data = null)	| Render a form for creating a single new post
POST   /posts			| actionPostDefault($data)					| Create a single new post from the received data
GET    /posts/1			| actionDefault($id = null, $data = null)	| Render a single existing post
PUT    /posts/1			| actionPut($id, $data = null)				| Update a single existing post based on the received

Samozřejmě proměnná $data tedy bude vždy obsahovat pole/strukturu s daty dané metody.

Editoval Zdeněk Topič (15. 11. 2012 23:59)