26 Aug

Konstruáljunk Web API-t!


Amikor a legtöbben meghallják azt a rövidítést, hogy API, rendkívül különféle dolgokra asszociálnak. Van akinek a Java Persistence API jut eszébe, van akinek a Facebook API, míg másoknak valami teljesen más...api

Na de mit is jelent maga a rövidítés?

Az API, az alkalmazás-programozási interfész rövidítése hivatott lenni, de aki eddig nem tudta mi az, azt most se hoztam közelebb a valósághoz. Ha az API kifejezést használjuk, akkor egy program azon funkcióit vagy szolgáltatásait soroljuk ide, amiket kívülről meg tudunk hívni és mindennek használatáról jó esetben dokumentáció is született. A lényeg, hogy nekünk nem kell tudnunk mi is történik a mélyben, mi csak használjuk a program nyújtotta szolgáltatásokat. Az előző példákat használva, a JPA (Java Persistence API) során nem kell tudnunk, hogy is kapcsolódik az adatbázishoz, hogy is menti le az entitásokat, stb. nekünk csak használni kell azt. Ugyanez van a Facebook API-val is. Nem tudjuk miben van tárolva az adat és nem tudjuk hol is van az, mi csak meghívunk egy webes URL-t adott paraméterrel és payloaddal és bumm, magic.



Mindezt használhatjuk egyazon programnyelvben is, amikor is létrehozunk egy csomagot, amit mások tudnak használni egy publikus API-n keresztül, de lehetséges mindez webes API-kon át.



Láthatjuk, hogy az egész kissé képlékeny, ezért nézzünk egy-egy példát.



Tegyük fel, hogy létrehoztunk egy tuti form validáló lib-et, amit boldogan használunk a saját kis applikációnkban. Egy idő után úgy döntünk, hogy jófejek leszünk és mindezt megosztjuk az open-source közösséggel. Felkerül packagist-re (vagy bárhova), a kód github-ra is. A githubos readme-ben szépen le van írva, hogy is tudják az emberek használni azt, tehát dokumentáltuk az API-t, amin keresztül el tudják érni a csomag nyújtotta szolgáltatásokat. Aki csak lehúzza azt magának függőségként, annak nem kell ismernie, hogy is működik, csak annyit kell tudnia, hogy mely publikus metódusokon keresztül éri el és azokat hogy kell használni. Emlékszünk még a facade patternre, ugye?



A másik opció, hogy készítettünk egy oldalt, ahol az emberek különféle csoportokat tudnak létrehozni, azokon belül pedig mindenféle szavazásokat csinálni. Így már legalább 100 scrum team létrehozott nálunk egy Slacker of the day szavazást, ahova gyűlik az infó. Na most ez eddig tök jó, viszont az oldalunk nagyon nem reszponzív, de megkeresnek minket, hogy csinálnának hozzá egy mobilapplikációt, csak nincs webes API hozzáírva. Na most backenddel jobban vagyunk, mint a design-al, így ismét csúcsra járatjuk a jófejségünket és írunk hozzá egy ún. REST (erről majd később) API-t, amin át a mobilapplikációkkal (vagy éppen egy másik webalkalmazással) is el tudják érni az oldalunk nyújtotta szolgáltatásokat, tehát tudnak majd szavazni, lekérdezni, stb.



Azért, hogy kicsit visszazuhanjunk az egyszerű valóságba, akkor jöjjön az, hogy aki bármit piszkált PHP-vel SQL adatbázisokban, az a PHP egyik MySQL kapcsolatokért felelős API-ját használta (mysql, mysqli, PDO).



Na de a mai témánk most a webes API lesz, ezért beszéljünk erről egy kicsit. Jelen esetben az API dokumentációja azt írja le, hogy is tudjuk piszkálni a szolgáltatásokat: milyen URI-n át, milyen HTTP metódusokkal, mi legyen a query stringben, milyen header-ökkel, mit küldjünk a request body-ban és milyen formában kapunk majd választ.



A web API-knak alapvetően két nagy formáját különböztetjük megy, az RPC (remote procedure call) és REST (representational state transfer) API-t.



RPCRPC-diagram


RPC esetében az esetek többségében egyetlen URI-t hivogatunk, mégpedig POST metódussal. Az, hogy mi is a cél, azt a küldött payload határozza meg. Ez általában egy struktúrált kérés, amiben benne lesz az adott művelet neve, valamint a paraméterek. Itt két módszert különböztetünk meg, XML-RPC-t és SOAP-ot. Ez utóbbiról regényeket lehetne írni, így arról most nem írnék, de ha lesz érdeklődés, akkor szívesen taglalom majd egy bejegyzés során. De nézzünk egy példát:
POST /xml-rpc HTTP/1.1
Content-Type: text/xml

<?xml version="1.0" encoding="utf-8"?>
<methodCall>
 <methodName>level.up</methodName>
  <params>
   <param>
     <value><integer>40</integer></value>
   </param>
  </params>
</methodCall>

A fenti példában egy POST kérést küldünk a /xml-rpc végpontra, a fent látható XML request body-val. Jól látszi, hogy a level.up metódust szeretnénk meghívni egy integer paraméterrel.  A gyakorlatban ez egyszerűen egy osztály egy adott metódusára mappelődik, hasonlóképpen:
class Level {
   public function up($level) {
        // black magic, ezt már az API használója nem tudja mit hogyan csinál 

   }
}

A fenti kérésre, miutá a handler osztályunk feldolgozta azt, hasonló válasz érkezhet:



HTTP/1.1 200 OK 
Content-Type: text/xml 
<?xml version="1.0" encoding="utf-8"?> 
<methodResponse> 
  <params> 
   <param> 
    <value><boolean>true</boolean></value> 
   </param> 
  </params> 
</methodResponse>

Látjuk, hogy a szintlépés sikeres volt, bármit is jelentsen az a mostani példában :) A lényeg, hogy hasonlóképpen működik, mintha csak a kódunkban hívnánk meg egy osztályunk egy metódusát, csak ezt egy távoli gépen tesszük, ennélfogva erőforrásigényesebb és lassabb lesz az.


Akkor nézzük, hogy összefoglalva mit tudunk az RPC-ről?
  • Egy végponton át, többféle művelet

  • POST kéréseket használ

  • Struktúrált request/response

  • Nincs HTTP caching, a HTTP válaszkódból nem állapítható meg, hogyha hiba volt, mindenképp vizsgálni kell azt

  • Nem használja ki a HTTP protokoll lehetőségeit

REST


A REpresentational State Transfer egy teljesen más megközelítése a dolgoknak. A lényege, hogy itt az adatbázisban szereplő entitások reprezentációja közlekedik. A HTTP protokollra épül, ezáltal próbálja annak minden szolgáltatását kihasználni, úgy mint:
  • Több végpont, minden URI egyedileg azonosítja az erőforrásokat.

  • A HTTP protokoll több metódusát használja

  • A kliensek megadhatják az általuk használt formátumot

  • Összekapcsolhatunk erőforrásokat, ezzel jelezve a kapcsolatot köztük

  • Alkalmazza az erőforrások cache-elését

  • Az egyes műveletek során linkeket is biztosít, hogy a kliens tudja mit is tud tenni ezután


Az RPC-vel szemben ez csupán iránymutatás, nincs kőbe vésve, hogy is kell mindezt implementálni, így egy REST API tervezésekor döntések tömkelegét kell meghoznunk:
  • Milyen formában fogjuk reprezentálni az adatainkat?

  • Ha egy kérést nem tudunk teljesíteni, akkor azt hogy közöljük a klienssel?

  • Ha valami hiba történt, ezt milyen formában adjuk tovább, milyen HTTP status code-okkal?

  • Hogy fogunk authentikálni? A HTTP stateless protokoll és habár a session sütik segítségünkre vannak a mindennapi böngészés során, de ezek használata itt nem javallott. Tehát HTTP-vel, OAuth-al vagy API tokennel fogunk authentikálni?


Pont emiatt a lazaság miatt, a REST rendkívül rugalmas és bővíthető, habár ugyanezért elég sok feladatot ró a fejlesztőre, hogy ezeket "megálmodja".

Az előző cikkemben egy hibrid mobilapplikációt készítettünk, ami statikus adatokat használt. Most jöjjön az, hogy megírjuk a hozzá tartozó backendet, hogy valahol az ottani módosításokat letároljuk. Az egyszerűtől fogunk indulni, szimplán todo-kat szolgálunk ki, lehetővé tesszük azok módosítását, törlését, hozzáadását. Azután bevezetünk egy OAuth2-es authentikációt, az egyes todokat listába szervezzük, a listákat emberekhez rendeljük, ahogy azt a Wunderlist is csinálja.

Apigility


A fentiekhez nem mást, mint a Zend csapata által készített Apigility-t fogjuk használni. Ez egy webes API builder tool, amivel könnyedén tudjuk összekattintgatni az API nagy részét, ezáltal sok terhet levesz a vállunkról. Ráadásul nem csak Zend keretrendszerbe tudjuk a kapott kódot beilleszteni, hanem máshova is.

Kezdjük azzal, hogy letöltjük azt innen.

Csomagoljuk ki valahova és vagy állítsunk a public mappára egy VHOST-ot, vagy szimplán a projekt gyökeréből indítsunk egy PHP-s built-in webszervert:
$ php -S 0.0.0.0:8888 -t public public/index.php

Ezután csapjuk fel a localhost:8888-at és nézzük miből élünk!
A module mappára adjunk írási jogot a webszerver felhasználójának, különben a scaffolding nem fog menni!

A felületen fogad pár menüpont felül:
  • Content negotiation: itt lehet testreszabni az általunk kezelt formátumokat, hogy mely content-type-ra, mivel is reagáljunk.

  • Authentication : itt lehet felvenni/szerkeszteni az authentikációs adapterjeinket

  • Database : ha már meglévő adatbázishoz kapcsolódunk, itt tudjuk felvenni az ahhoz tartozó kapcsolatot (ez fontos lesz majd nekünk)

  • Documentation : a generált/általunk kitöltött adatok alapján összeállított API dokumentációt találhatjuk itt.

  • Package: az elkészült API-t itt tudjuk valamilyen formában becsomagolni a későbbi deployra

  • About: az aminek látszik


A sidebaron láthatjuk, hogy fel tudunk venni új API-t, ezért hozzunk is létre egyet. Ez a module mappában fog létrehozni egy modult a számunkra és ezt tudjuk majd később becsomagolni. Legyen a neve mondjuk TodoBackend.Selection_003

Itt láthatjuk az API-t védő authentikációt, a hozzá tartozó REST és RPC szolgáltatásokat (egyelőre 0), valamint egy igen fontos dolgot, mégpedig a verziót. Ez fontos lehet, ha supportálni akarunk régebbi klienseket is, ahogy az alkalmazás változik.

Ha ezzel kész vagyunk, akkor jöjjön az, ami igazán meggyorsíthatja majd a dolgunkat!

Először is hozzunk létre egy adatbázist és adjunk hozzá egy felhasználót a megfelelő jogosultságokkal!
CREATE DATABASE todo COLLATE utf8_hungarian_ci;
CREATE USER 'todo'@'localhost' IDENTIFIED BY 'password';
GRANT ALL ON todo.* TO 'todo'@'localhost';

Hozzuk létre a todos táblát:



CREATE TABLE `todo`.`todo` ( `id` BIGINT NOT NULL AUTO_INCREMENT ,`name` VARCHAR(100) NOT NULL , `done` BOOLEAN, PRIMARY KEY (`id`)) ENGINE = InnoDB


Ha ez megvan, akkor jöhet egy kis mágia!




Menjünk a már korábban említett Database menüpontra és adjunk hozzá egy új db adaptert, pl. local MySQL néven. Használjuk a PDO_Mysql drivert, a todo adatbázist a megfelelő userrel/jelszóval és okézzuk le.



A Charset mezőben az utf8-at írva default azt fogja használni, ezzel elkerüljük a kódolási gondokat


Selection_004


Most menjünk a TodoBackend API-ra és adjunk hozzá egy új service-t, mégpedig a DB Connected fülről. Itt válasszuk ki az imént létrehozott adaptert és ha mindent jól csináltunk, akkor bizony feldobja nekünk a todos táblát, a megfelelő fieldekkel. Csekkoljuk be mellette a checkboxot és save.Selection_005


Na most meg is jelent a bal oldali listában, hogy ez alá az API alá tartozik egy todo nevű szolgáltatás. Ha megnyitjuk, akkor látjuk, hogy mindezt a /todo[/:todo_id] URL-re mappelte.Selection_006


Na várjunk csak, akkor ez most működik is? Ennyi lenne egy egyszerű REST szolgáltatást csinálni? Próbáljuk ki!



Kétféle végpontot különböztetünk meg alapvetően. Az egyik az egész collection-t azonosítja, ez lesz esetünkben a /todo, míg a másik fajta kérés egy adott entitást azonosít, itt a /todo/[identifier] lesz az adott URI. Az elvégzendő művelet függ mindkettőtől. Egy GET kérést indítva a collection-re, az egész collection-t szeretnénk lekérni, míg mindez egy entitás esetében az adott entitást adja vissza. Ha DELETE-el hívjuk meg az entitást, akkor az törölni fogja, ha PUT-al, akkor módosítani szeretnénk. A collection végpontjára pedig POST-ot küldve tudunk új elemet felvenni. Persze ezek az alapok és mi szabadon bővíthetjük ezt (csak dokumentáljuk le :) )


Lőjünk fel egy PostMan-t és küldjünk egy GET kérést a localhost:8888/todo végpontunkra. Ha jól csináltuk, akkor a válaszban ez lesz:

{  
   "_links":{  
      "self":{  
         "href":"http://localhost:8888/todo"
      }
   },
   "_embedded":{  
      "todo":[  

      ]
   },
   "page_count":0,
   "page_size":25,
   "total_items":0,
   "page":0
}

Wow, ez úgy néz ki működik, látjuk a saját URL-t, látjuk ,hogy 25-ösével lapoz, 0 elem van egyelőre, 0 oldal és mi is azon vagyunk. Na de akkor hozzunk létre egy új elemet, elvileg azt is tudnunk kellene, nem? A REST elvek szerint ha ugyanerre az URL-re küldünk egy POST kérést, azzal tudunk létrehozni egy új elemet.


Küldjünk egy POST kérést ugyanerre az URL-re, de a form data mezők közé vegyük fel a 'name' : 'debug that shit' és a 'done' : 0 kulcs-érték párt.



Ugyanezt küldhetjük JSON formátumban a form body-ban, ha a Content-type mező értékének beállítjuk az application/json-t.


Ennek kellene visszajönnie:

{  
   "id":"1",
   "name":"debug that shit",
   "done":0,
   "_links":{  
      "self":{  
         "href":"http://localhost:8888/todo/1"
      }
   }
}

Ha belelesünk az adatbázisba, akkor láthatjuk, hogy ott van az általunk megadott rekord, tehát beillesztettük az adatbázisba az elemet, valamint visszajött a hozzá tartozó ID, sőt az erre az elemre mutató link is, ha később szükségünk lenne rá. De ha már ideadta, nézzük meg, mit kapunk, ha arra a címre küldünk egy GET kérést? Igen, ugyanezt, mivel a válaszban megkapjuk a kész entitást, így megspórol nekünk a REST egy kérést. :)

Most ha küldünk egy GET kérést a /todo címre, akkor már merőben más fogad:
{  
   "_links":{  
      "self":{  
         "href":"http://localhost:8888/todo?page=1"
      },
      "first":{  
         "href":"http://localhost:8888/todo"
      },
      "last":{  
         "href":"http://localhost:8888/todo?page=1"
      }
   },
   "_embedded":{  
      "todo":[  
         {  
            "id":"1",
            "name":"debug that shit",
            "done":"0",
            "_links":{  
               "self":{  
                  "href":"http://localhost:8888/todo/1"
               }
            }
         }
      ]
   },
   "page_count":1,
   "page_size":25,
   "total_items":1,
   "page":1
}

Ez már azért egy fokkal hosszabb válasz. A _links alatt találjuk a paginationre vonatkozó linkeket, az _embedded alatt pedig a collectionben rejlő információt.
Akadnak esetek, mikor komplex, nagy adathalmazok érkeznének, ilyenkor a collection tartalma nem érkezik meg így, csupán az egyes elemekre mutató linkek.

Na és mi a helyzet akkor, ha módosítani szeretnénk egy ilyen elemet? Azt szintén az adott entitásra mutató PUT kéréssel tudjuk elérni, mégpedig úgy, hogy küldjük a módosítandó mezőket, JSON formátumban.

Küldjünk hát egy PUT kérést, ezzel jelezve, hogy mi ezt az entitást szeretnénk módosítani:
PUT /todo/1 HTTP/1.1
Content-type : application/json

{
  "name" : "debug this shit",
  "done" : 1
}

Erre pedig a válasz:
{  
   "id":"1",
   "name":"debug this shit",
   "done":"1",
   "_links":{  
      "self":{  
         "href":"http://localhost:8888/todo/1"
      }
   }
}

Itt is megkapjuk tehát az entitás módosítás utáni állapotát.

Az entitás törléséhez pedig egy DELETE kérést kell indítanunk erre a címre és volt-nincs todo.

Láthatjuk hát, hogy a legtöbb linket a rendelkezésünkre bocsátja a rendszer, így a kliensnek csak az elvégzendő művelethez tartozó HTTP verb-el kell tisztában lennie, a szükséges linkeket megkapja a rendszerből. Ennélfogva ha nem égetjük azokat bele, hanem mindig a válasz alapján dolgozunk, akkor a szerveren szabadon módosíthatjuk az egyes resource-okon belüli elérési utakat, mert nem borítja meg a klienseket.

Na de jöjjön egy kis dokumentáció, majd nézzük hogy is lehet ezt deployolni!

Ha a Service-ünkre kattintunk, akkor láthatjuk a hozzá tartozó menüket:Selection_002

A General Settings alatt tudjuk testreszabni a hozzá tartozó route-ot, hogy mely HTTP metódusokat engedlyük a klienseknek, mi lesz a hydrator típusa, mi lesz a collection neve (amit a fenti válaszban láttunk az _embedded mező alatt), az Entity és Collection objektumaink osztályait is lehet megadni, a pagination kezeléséhez szükséges paramétert, a táblában lévő primary key-t.Selection_007

A Database settings alatt tudjuk a service-t egy másik táblára vagy éppen adatbázisra átállítani.Selection_008

A Content Negitiation alatt lehet beállítani, hogy milyen kérésekre, milyen válaszokkal tudunk válaszolni (Accept) , és milyen Content-type-ot fogadunk el.Selection_009

A Fields tab alatt az entity-hez tartozó mezőket láthatjuk, és újakat is hozzáadhatunk akár, ha nem DB connected alapján vettük fel azt, vagy megváltozott időközben a séma. Különböző validátorokat rendelhetünk hozzájuk. Filtereket, amikkel pl. trimmelhetjük a stringeket, castolhatjuk a változókat, stb.Selection_011

De nézzük meg mit is tudunk szerkeszteni a mezőkön, ugyanis itt lesz majd a dokumentációnak egy kis része:

Selection_010

Itt kell beállítani az egyes mezőkhöz tartozó leírást, a típusát is megadhatjuk, valamint ha ezen validáció során valami hiba történik, akkor a hibaüzenet is testreszabható.

Az Authorization tab alatt lehet a resource-ra vonatkozóan authorizációhoz kötni a kéréseket. Jelenleg ez üres, mert nem használunk semmiféle authentikációt sem, így most hagyjuk így.Selection_012

Na és itt jön a lényeg, a Documentation tab, ami alatt generált dokumentációt tudjuk létrehozni. Megadhatjuk a REST service leírását, a collection leírását, hogy az egyes metódusokkal pontosan mit is érhetünk el, milyen választ fogunk kapni és milyen payloadot várnak. A konfiguráció alapján ki tud nekünk generálni a rendszer a szükséges helyeken egy példa választ és kérést is, amit akár példa adatokkal fel is tölthetünk.Selection_013

Ha kitöltöttük őket, akkor már csak a Source code fül marad, ami jelenleg két elemből áll, a Collection és az Entity classból. Gyakorlatilag ezeket "piszkálhatjuk", habár nem a rendszeren keresztül, mert itt szerkeszteni nem lehet azokat.

Na és most nézzük meg a generált dokumentációt a navbarban található Documentation menü alatt!Selection_014

Láthatjuk hogy jelenleg csak egy API van, a TodoBackend, az is a v1-es verzióval, ami linkre kattintva láthatjuk a részleteket is:Selection_015

Felsorolva az összes HTTP verb, amit elfogadunk és azokra kattintva lenyílnak az imént kreált dokumentációk is:

Selection_016

Láthatjuk, hogy le van írva milyen válasz fejlécekkel, status kódokkal térhetünk vissza, mit reprezentálnak az egyes mezők, amiket visszakapunk, no meg mire is jó ez a resource?Selection_017

Most, hogy készítettünk egy minimál dokumentációt az API-hoz, nézzük hogy is tudjuk ezt kirakni a helyére!
A dokumentációt alapesetben az apigility/documentation címen érjük majd el a deployolt packageben.

Jöjjön a Package fül a navbaron!Selection_018

A package krelálása elég egyértelmű a fentiek alapján, kiválasztjuk a formátumát (a ZIP a legtöbb telepítéssel működik), hogy melyik API-kat szeretnénk benne, futtassa-e a composert (ezzel lehetnek problémák), valamint rákérdez, hogy milyen config fájlokat mellékeljük. Egyelőre hagyjuk ZIP-en, válasszuk ki a TodoBackendet és nyomjuk egy generate-re. Ez a komplett applikációt, vendor mappa nélkül betömöríti egy ZIP-be, utána letölti azt. Ezt tömörítsük ki a helyére, állítsunk rá egy vhost-ot, de előtte a gyökerében hajtsunk végre egy
$ composer install

parancsot. Ha rákérdez a Zend/Validatorra, akkor a module configba injektáljuk azt.

Ha most ránézünk, akkor a főoldalon már nem irányít át az admin UI-ra, hanem egy figyelmeztető oldal jön be, miszerint jelenleg production módban van az alkalmazás és ha szeretnénk folytatni a fejlesztést, akkor ahhoz bizony a composer kell.Selection_019

Viszont ha itt meghívjuk a korábban tesztelt URI-t, akkor biza autentikusan csecsre fut. Hát persze, mert a konfigurációs fájlokat nem mellékeltük, honnan tudná a rendszer, hogy melyik adatbázishoz csatlakozzon? Hozzunk létre egy local.php-t a config/autoload mappán belül és töltsük fel!
<?php
return [
    'db' => [
        'adapters' => [
            'Local MySQL' => [
                'charset' => 'utf8',
                'database' => 'todo',
                'driver' => 'PDO_Mysql',
                'hostname' => 'boszme_mysql_host',
                'username' => 'todo',
                'password' => 'password',
                'port' => '3306',
            ]
        ],
    ],
];

Természetesen írjuk át a megfelelő elérésre és felhasználónév/jelszó kombóra. Ha ezt megtettük, akkor elméleti síkon működnie kell, viszont még mindig van egy kis apróság, ami gondot okozhat (legalábbis engem beszívatott Ubuntu alatt). Ezért, a config/application.config.php-ben a
'config_glob_paths' => 
array (
0 => '/tmp/ZFDeploy_57c06a6d3c68a/config/autoload/{,*.}{global,local}.php',
),

írjuk át:
'config_glob_paths' => [realpath(__DIR__) . '/autoload/{,*.}{global,local}.php'],

Ha ezzel is megvagyunk és a config is jó, akkor minden fasz és működik a cucc!

Legközelebb belőjük az authentikációt, összelőjük az Ionicos projekttel és kicsit tovább bővítjük a dolgot!

Hozzászólások betöltése
2014-2018 © Letscode.hu. Minden jog fenntartva. Build verzió: 1.2.11