„HTTP verbs“, RESTful architektura
- PJK
- Člen | 70
Mám zásadní připomínku k návrhu Nette. Nejsem žádný RESTový úchyl, ale http metody jsou v Nette téměř ignorovány. To se v případě, že v Nette tvořím jen část aplikace (třeba jen backend, nebo řekněme že nechci používat AppForm a další možnosti) stává značným handicapem oproti konkurenci.
Navrhuji ke každé routě kromě cesty přidat ještě metodu (přesněji „http verb“). Nebo ještě lépe to řešit v presenterech. To je rozumné řešení, které z Nette neudělá extrém typu Recess, ale dá nám další možnost.
Výsledek bych si představoval třeba takhle:
<?php
//z routy typu <presenter>/<id>, action=default, id=NULL
class PostPresenter extends BasePresenter {
function actionGetDefault($id=NULL) {
//zobrazeni prispevku
}
function actionPutDefault($id) {
if (//autorizace) {
//update prispevku
$updatedPost=Environment::getHttpRequest()->getParam('postBody');
}
}
//+post a delete, to je jasné
}
?>
Přechod k něčemu takovému nebude jednoduchý a hodně z vás ho asi ani nebude chtít, říká si to o druhou vývojovou větev, ale mně osobně připadá škoda nemít tuto možnost. Také si plně uvědomuji, že pokušet se v PHP zpracovat cokoliv jiného než get a post je utrpení (php://input, $_SERVER[‚REQUEST_METHOD‘]), ale právě od toho frameworky jsou – udělat špinavou a rutinní práci za vývojáře.
Zajímalo by mě, jak se k tomu stavíte. A ti, co znají nette pod pokličkou ať klidně vyčtou, co všechno už nepůjde a co bude třeba překopat, aspoň si rozšířím obzory.
Díky, Pavel
- Mikulas Dite
- Člen | 756
Zajímavé. V nette jsem dělal i jenom API web, ale vzhledem k minimální velikosti a rychlosti celého Nette je celkem nemístné vyřazovat AppForm. Je pravda, že některé frameworky s http akcemi pracují – třeba rails takhle rovnou tvoří crud. Nicméně, protože Nette modelovou část nemá, nestará se o tyhle svázané záležitosti.
Každopádně, podle mě by určitě stačilo udělat si vlastní abstraktní ApiPresenter který se o http akce náležitě postará, jakákoliv úprava Nette je zbytečná. Nebo je v něčem problém?
- Honza Marek
- Člen | 1664
Možná by ti mohlo pomoct přepsání metody formatActionMethod v presenteru.
<?php
protected function formatActionMethod($action)
{
return "action" . Environment::getHttpRequest()->getMethod() . $action;
}
?>
- PJK
- Člen | 70
Honza Marek napsal(a):
Možná by ti mohlo pomoct přepsání metody formatActionMethod v presenteru.
Ano, to by mi pomohlo, díky :). A teď další věc: Chtěl bych důsledně oddělit identifikátory (ID produktu, článku) od dat (jméno produktu, nadpis) a ke všem datům se chovat stejně. HttpRequest->getPost() bych asi nahradil něčím jako getParam(‚jmeno‘), protože to, jakou metodou jsem data dostal, je vůbec první věc, o kterou se starám, a pak jsou si parametry vlastně rovny.
Mikulas Dite: Asi by to šlo. Nechtěl bys zkusit něco takového zhruba načrtnout? Když už zmiňuješ RoR, tak tam bych to rozhodně netlačil, nejsem nijak ortodoxní zastánce REST. Nette se mi až na tohle líbí, jak je. Bez konkrétní databázové vrsty a kompletně rozložitelné.
Editoval PJK (22. 6. 2010 20:58)
- PJK
- Člen | 70
kaja47 napsal(a):
<?php
Ano, to by také šlo. Spolu s tím, co navrhl Honza Marek můžeme problém pojmenovaní metod považovat za vyřešený.
Leda že by se zavedla další konvence, která by se starala o REST požadavky.
Hehé, zkus charakterizovat rest požadavek… Tím se dostáváme k tomu, že v pokud aplikaci nebo její část budu chtít restiodní, bude jeden presenter vlastně soubor metod pro správu jednoho zdroje (resource). A takové presentery se budou hodit pouze pro implementaci nějakého API, stejné akce od uživatele budeme muset implementovat znovu. Metody v modelu samozřejmě mám, ale třeba autorizaci si „dám znova“. Co s tím, abych se nemusel opakovat?
- Erik Ferčák
- Člen | 10
Metóda by sa mala podľa mňa definovať v route, a preto som si napísal toto:
<?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);
}
}
Príklad použitia:
$router = $application->getRouter();
$router[] = new RestRoute('/api/1.0/mailbox/', array(
'presenter' => 'Mailbox',
'action' => 'default',
), RestRoute::METHOD_GET);
$router[] = new RestRoute('/api/1.0/mailbox/', array(
'presenter' => 'Mailbox',
'action' => 'create',
), RestRoute::METHOD_POST);
- Erik Ferčák
- Člen | 10
Áno, alebo môžeš použiť RestRoute::RESTFUL flag:
/* priradí akcie k jednotlivým http metódam
GET => default
POST => create
PUT => update
DELETE => delete
*/
$router[] = new RestRoute('/api/1.0/mailbox/', array(
'presenter' => 'Mailbox'
), RestRoute::RESTFUL);
PJK napsal(a):
Erik Ferčák: Pokud tomu dobře rozumím, každá metoda jedné resource by měla vlastní routu, ano?
- Patrik Votoček
- Člen | 2221
A nebylo by jednodušší kdyby jsi mohl prostě a jednoduše definovat překladový slovník pro jednotlivé REST požadavky?
Pak by bylo prostě pole
//Method => Action
array(
'GET' => "default",
'POST' => "create",
'PUT' => "update",
'DELETE' => "delete"
);
- Erik Ferčák
- Člen | 10
vrtak-cz napsal(a):
A nebylo by jednodušší kdyby jsi mohl prostě a jednoduše definovat překladový slovník pro jednotlivé REST požadavky?
Pak by bylo prostě pole
//Method => Action array( 'GET' => "default", 'POST' => "create", 'PUT' => "update", 'DELETE' => "delete" );
Asi takto? (netestované)
<?php
class RestRoute extends Route
{
const METHOD_POST = 4;
const METHOD_GET = 8;
const METHOD_PUT = 16;
const METHOD_DELETE = 32;
const RESTFUL = 64;
protected static $restDictionary = array(
'GET' => 'default',
'POST' => 'create',
'PUT' => 'update',
'DELETE' => 'delete'
);
public static function setRestDictionary(array $dictionary)
{
self::$restDictionary = array_merge(self::$restDictionary, $dictionary);
}
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':
case 'POST':
case 'PUT':
case 'DELETE':
$action = self::$restDictionary[$httpMethod];
break;
default:
$action = self::$restDictionary['GET'];
}
$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);
}
}
// predefinovať akcie potom následovne:
RestRoute::setRestDictionary(array(
'GET' => 'list',
'POST' => 'add'
));
/*
výsledný slovník potom vyzerá takto:
'GET' => 'list',
'POST' => 'add',
'PUT' => 'update',
'DELETE' => 'delete'
*/
- PJK
- Člen | 70
vrtak-cz napsal(a):
A nebylo by jednodušší kdyby jsi mohl prostě a jednoduše definovat překladový slovník pro jednotlivé REST požadavky?
Pak by bylo prostě pole
//Method => Action array( 'GET' => "default", 'POST' => "create", 'PUT' => "update", 'DELETE' => "delete" );
Tak to už vypadá celkem stravitelně, děkuji.
A teď další věc: Chtěl bych důsledně oddělit identifikátory (ID produktu, článku) od dat (jméno produktu, nadpis) a ke všem datům se chovat stejně. HttpRequest->getPost() bych asi nahradil něčím jako getParam(‚jmeno‘), protože to, jakou metodou jsem data dostal, je vůbec první věc, o kterou se starám, a pak jsou si parametry vlastně rovny.
A je v Nette nějaká sofistikovanější metoda pro přístup k datům z PUT než php://input?
- Filip Procházka
- Moderator | 4668
Napadá mě, že by bylo asi nejlepší na to přiohnout
Http\Request
a Application\Request
- Filip Procházka
- Moderator | 4668
Přesně tak, jenom bych to možná trochu míň dědil a trochu víc upravil v Nette a poslal pull :)