„HTTP verbs“, RESTful architektura

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

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
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

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);
PJK
Člen | 70
+
0
-

Erik Ferčák: Pokud tomu dobře rozumím, každá metoda jedné resource by měla vlastní routu, ano?

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

Á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
+
0
-

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
+
0
-

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
+
0
-

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?

blacksun
Člen | 177
+
0
-

Řeším zrovna implementaci jedné webové služby a zde uváděná REST routa mi vyhovuje, akorát bych se zeptal stejně jako PJK, je nějaký lepší způsob, jak přistoupit k datům z PUT requestu?

Filip Procházka
Moderator | 4668
+
0
-

Napadá mě, že by bylo asi nejlepší na to přiohnout Http\RequestApplication\Request

blacksun
Člen | 177
+
0
-

V jakým smyslu přiohnout?
Podědit a přidělat metody jako „getPut“ a „getDelete“, ve kterých požadovaný data dostat z php://input a naplnit potřebný pole?

Filip Procházka
Moderator | 4668
+
0
-

Přesně tak, jenom bych to možná trochu míň dědil a trochu víc upravil v Nette a poslal pull :)

grey
Člen | 94
+
0
-

Jsem jednoznačně pro, aby to bylo přímo v nette! V asp.net mvc se to řeší anotacema, s čímž by ale tady byl problém, protože by kolidovaly jména metod…