ABONAMENTE VIDEO REDACȚIA
RO
EN
×
▼ LISTĂ EDIȚII ▼
Numărul 43
Abonament PDF

De la Zero la RESTful în patru pași. Design de API

Georgiana Gligor
Owner @Tekkie Consulting
PROGRAMARE

Acest articol este partea a doua dintr-o serie în care arătăm cum se pregătește, planifică și implementează un API RESTful. După ce am pus bazele în prima parte, de data aceasta ne vom uita la cum se face designul unui API, care sunt aspectele la care trebuie să fim atenți. Dar mai întâi ne vom uita la ce înseamnă REST și la constrângerile arhitecturale pe care le impune asupra sistemului.

Ce este REST?

Representational State Transfer este un stil de arhitectură de aplicație care, în loc să impună decizii asupra tehnologiei, preferă să definească un set de constrângeri la care sistemul să adere. În felul acesta, detaliile de implementare se pot schimba ulterior, dar să se păstreze avantajele care decurg din abordarea RESTful.

Concepte REST cheie

O resursă reprezintă orice informație care poate fi denumită. De obicei acestea sunt concepte din domeniul aplicației, indiferent că se referă la concepte concrete (ex: persoane) sau la unele abstracte (ex: fișiere). Un bun de pornire în "vizualizarea" acestora pentru cei familiarizați cu OOP este să folosească o mapare unu-la-unu cu clasele ce compun modelul domeniului. Deoarece noi construim o aplicație de monitorizat datele despre vehicule, putem exemplifica două concepte: vehicul și asigurare. Dificultatea explicării termenului de resursă provine din faptul că aspecte particulare de business pot dicta deviații de la cel mai intuitiv mod de reprezentare, fără ca acest lucru să înlăture validitatea.

REST a fost descris în mod formal de Roy J. Fielding în teza sa de doctorat. El pleacă de la un sistem care nu are delimitări clare între componente, și aplică incremental cinci constrângeri obligatorii și una opțională asupra elementelor care compun arhitectura:

  1. client-server: separarea aspectelor interfeței utilizator de stocarea datelor

  2. fără stare: păstrarea detaliilor despre sesiune strict la client, eliberând astfel serverul de povara managementului sesiunilor, pentru a aduce scalabilitate, siguranță, și vizibilitate; dezavantajul este că fiecare cerere va trebui să conțină suficiente informații încât să poată fi procesată corect

  3. cache: datele care compun un răspuns trebuie să fie etichetate ca și cache-uibile sau non-cache-uibile

  4. interfață uniformă între componente, așa cum e definită de următoarele constrângeri secundare: identificarea resurselorș manipularea resurselor prin intermediul reprezentărilor acestoraș mesaje auto-descriptive; și hypermedia ca motor al stării aplicației (aka HATEOAS)

  5. sistem stratificat: componenta poate să "vadă" și să interacționeze doar cu straturile din imediata sa apropiere; spre exemplu, clienții nu pot presupune că interacționează direct cu sursa de date, deoarece pot comunica cu un nivel de cache

  6. [opțional] cod-la-cerere: funcționalitatea clientului poate fi extinsă prin descărcarea și execuția de cod extern; aceasta înseamnă că nu este necesar să se pornească execuția în client atunci când tot codul este disponibil, deoarece el poate fi obținut ulterior la cerere; imaginați-vă cum este adăugată funcționalitate prin injectarea de cod Javascript în browser

Reprezentarea este o parte a stării resursei care este transferată între client și server. Se referă de obicei la starea curentă a resursei, dar poate indica și starea dorită, ne putem gândi la acest lucru ca la un dry-run atunci când se face cererea.

Deși nu constituie o constrângere în sine, mecanismele de comunicare oferite de HTTP sunt alegerea celor mai mulți dezvoltatori care implementează REST. Și noi vom folosi HTTP în acest proiect, iar verbele sale ne vor ajuta să definim operațiile care se efectuează asupra resurselor noastre. În cele ce urmează vom folosi paradigma puternică a calendarului pentru a ilustra ușor diferența dintre resursă și reprezentarea acesteia, prin studierea reprezentării .ics în prima fază:

GET /calendar/123sample
Host example.dev
Accept: text/calendar

ar putea returna ceva similar cu:

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Tekkie Consulting//123sample//EN
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Bucharest
END:VTIMEZONE
BEGIN:VEVENT
UID:123456789@example.dev
DTSTART;TZID=Z:20150311T123456
DTEND;TZID=Z:20150311T125959
END:VEVENT
END:VCALENDAR

Dacă vom cere o reprezentare JSON a aceleași resurse

GET /calendar/123sample
Host example.dev
Accept: application/json

ea ar putea arăta ca în exemplul de mai jos:

{
  "version": "2.0",
  "creator": {"company": "Tekkie Consulting", "product": "123sample"},
  "type": "Gregorian",
  "language": "English",
  "timezone": {
    "id": "Europe/Bucharest"
  },
    "events": [{
      "id": "123456789@example.dev",
      "start": "2015-03-11T12:34:56.000Z",
      "end": "2015-03-11T12:59:59.000Z"
  }]
}

În continuarea acestei serii de articole vom întrebuința JSON ca reprezentare implicită a resurselor noastre.

BDD în PHP

Pentru a specifica felul în care se comportă APIul nostru, ne dorim să folosim Behaviour Driven Development. În această secțiune vom introduce câteva concepte și instrumente care să ne ajute. Această pregătire ne va fi de mare folos atunci când vom scrie cerințele în forma scenariilor de utilizare.

Sunt două cuvinte cheie care apar de fiecare dată în discuțiile despre BDD în PHP. Cel mai popular este, fără îndoială Behat, care folosește Gherkin pentru specificarea cerințelor în limbaj apropiat de cel business. Celălalt este Codeception, care întrebuințează cod PHP ușor de citit pentru a obține același lucru, și care oferă în plus instrumente pentru a facilita scrierea tuturor testelor în același mod (atât cele de acceptanță, funcționale, cât și testele unitare).

În trecut am folosit Behat destul de mult la proiectele mele Symfony, așa că în mod natural am încercat să merg pe calea cea mai ușoară și să il instalez folosind $ composer install behat/behat. Am observat conflicte de pachete deoarece Behat dorea să folosească symfony/event-dispatcher ~2.1 iar eu aveam deja instalat 3.0.1. Așa că, în loc să încerc să rezolv problema prin impunerea unor cerințe mai stricte asupra pachetelor, am decis că e momentul potrivit să încerc Codeception, mai ales că am primit feedback extrem de călduros de la cei care îl folosesc deja în munca lor de zi cu zi. Am lăsat în urmă frumoasele fișiere Gherkin numai bune pentru oamenii de business, am revenit la 100% PHP!

Așadar am instalat folosind metoda Composer care a funcționat perfect din prima încercare:

$ composer install codeception/codeception

apoi am rulat comanda de bootstrap

$ vendor/bin/codecept bootstrap

Se observă un folder numit /tests/ în rădăcina proiectului, care a fost populat cu tot feluld e bunătățuri.

Deoarece suntem în mediu de dezvoltare, pornim aplicația local folosind serverul PHP built-in:

$ php -S localhost:12345 -t web

și ne asigurăm că acest URL este configurat corect în fișierul tests/acceptance.suite.yml.

În continuare vom genera un test de acceptanță simplu, rulând comanda:

$ vendor/bin/codecept generate:cept acceptance Welcome

Fișierul tests/acceptance/WelcomeCept.php nou creat va conține un test simplu, menit doar să verifice ruta de status:

$I = new AcceptanceTester($scenario);
$I->wantTo('check the status route');
$I->amOnPage('/');
$I->see('Up and running.');

Iar rezultatele sunt într-adevăr cele așteptate:

$ vendor/bin/codecept run
Codeception PHP Testing Framework v2.1.5
Powered by PHPUnit 4.8.21 by Sebastian Bergmann and contributors.

Acceptance Tests (1) ---------------------------------------------
Check the status route (WelcomeCept)                        Ok
------------------------------------------------------------------
Functional Tests (0) ------------------------
---------------------------------------------

Unit Tests (0) ------------------------------
---------------------------------------------

Time: 214 ms, Memory: 11.25Mb

OK (1 test, 1 assertion)

Testele de acceptanță Codeception sunt de regulă mai lente decât cele funcționale, deoarece este nevoie de un server web pentru a rula testele. Din fericire, există teste funcționale, care ne vor fi de mare folos în descrierea funcționalității APIului, așa că le vom folosi pe acelea. Mai întâi activăm modulul Silex, adăugându-l în tests/functional.suite.yml:

class_name: FunctionalTester
modules:
    enabled:
      - Silex:
          app: 'app/bootstrap.php'
      - \Helper\Functional

Cum facem design de API?

Așa cum am văzut în secțiunea introductivă REST, un concept cheie în acest stil arhitectural sunt resursele, toate celelalte lucruri gravitând în jurul lor. De aceea recomand să trecem prin lista conceptelor domeniului mai întâi, pentru a identifica resursele noastre:

Acum că am clarificat care sunt resursele aplicației noastre, ce operații putem defini pe fiecare? Cu siguranță ne dorim să putem crea, modifica și obține date despre vehicule. Apoi avem nevoie să adăugăm și obținem detalii despre taxa de drum, verificările periodice, precum și despre asigurare.

Toate operațiile de mai sus arată foarte asemănător cu cele CRUD (o parte din ele lipsesc din motive care țin de domeniu). Așadar, haideți să definim lista completă de operații CRUD pe o resursă de tip vehicul:

Formatul de date din exemplul de mai sus este conform standardului ISO 8601, pentru a folosi un format care să conțină timezone. Facebook a fost nevoit să schimbe în 2012 API-ul de evenimente din cauza unor aspecte legate de timezone.

Nu vom detalia mai mult operațiile ce pot fi efectuate asupra celorlalte tipuri de resurse, deoarece ele sunt similare cu cele detaliate mai sus.

Păstrarea consistenței APIului

Este important să oferim un API consistent, nu doar pentru noi, ci și pentru terții care îl consumă. E foarte ușor de explicat folosind distincția între public și publicat pe care o face Martin Fowler. O dată ce un lucru este publicat, efectuarea de modificări asupra sa necesită un proces complex de deprecare a funcționalității existente, oferirea celei noi la alt URI, lucruri care iau mai mult timp și implică efort suplimentar fată de o modificare de cod obișnuită.

Un aspect important care trebuie avut în vedere încă de la început este versionarea APIului. Unii designeri de API preferă să folosească versiunea în URLul de bază, ca GitHub care întrebuințează numărul versiunii, sau ca Twilio care folosește data lansării https://api.twilio.com/2010-04-01 ca punct de pornire. WePay au și ei timestampul lansării în URL pentru a diferenția versiunile între ele. Aproape oricine a ales versionarea într-un fel sau altul, iar dacă vreți să jucați la siguranță e nevoie doar să începeți cu/v1/. Cel mai mare avantaj al acestei abordări este posibilitatea de a avea un aPI foarte diferit în următoarea versiune. Cu toate acestea, e important să vă întrebați dacă APIul în sine e cel care va suferi modificări, sau resursele, sau reprezentările acestora. Strategiile de modificare a structurii sau operațiilor unei resurse non-versionate sunt puțin mai complexe, de obicei se oferă noile date la un URI diferit, și se marchează ca deprecated cel existent. Așa cum arată și Stefan Tilkov, REST este mult mai mult decât unele șabloane URI și verbele din HTTP. Alegerea strategiei optime este foarte dependentă de problema care trebuie rezolvată, și cu toții am auzit exemple de sub- sau supra-inginerie a APIurilor.

Când ne construim URIurile, trebuie să avem în vedere să nu amestecăm singularele și pluralurile. Așadar, nu vom expune simultan/vehicles/{id} și /tax/{id} în aceeași aplicație, vom alege să folosim taxes în cel de-al doilea caz. Se recomandă în general folosirea formei plural în detrimentul celei singulare.

Deoarece URIurile identifică resurse, este considerată o bună practică folosirea în exclusivitatea a substantivelor în compunerea acestora, și evitarea verbelor. Pentru a descrie acțiunile care se pot efectua asupra resursei, se folosesc verbele HTTP. Spre exemplu, /vehicles/create ar trebui să fie de fapt un POST către /vehicles.

Pentru mai multe exemple aplicate, recomand standardele Casei Albe care sunt foarte bine scrise, ele conținând multe exemple concrete de ce anume este considerat "rău" și ce "bun". Sunt indicii bune și în tutorialul REST API, dar recomand prudență la navigarea prin acest site, deoarece nu toate informațiile de acolo aderă strict la principiile REST pe care le-am prezentat la începutul acestui articol.

Sfaturi chiar și mai granulare privesc convețiile de numire și folosirea snake-camel case, atât pentru URIuri cât și pentru structuri JSON. ASigurați-vă că alegeți unul și îl utilizați peste tot.

Definirea comportamentului aplicației

Să trecem la pregătirea comportamentului pentru resursele de tip vehicul prin definirea unor teste funcționale pentru acestea. Vom folosi clase Cest deoarece ulterior ne vor ajuta la o aranjare mai facilă a testelor noastre. Cest-urile sunt clase simple care grupează funcționalitatea din Cept-uri într-o manieră OOP, și ne vor facilita gruparea testelor împreună.

Generăm primul nostru Cest folosind:

$ vendor/bin/codecept generate:cest functional Vehicles
Test was created in /Users/g/Sites/learn/silex-tutorial/tests/functional/VehiclesCept.php

Apoi definim cum va arăta o resursă de tip vehicul. Spunem practic că "la execuția unei cereri POST către /vehicles care primește un nume pentru noul item, îl vom crea și returna întreaga înregistrare, inclusiv IDul acesteia". Pe baza acestui ID vom efectua ulterior operații de modificare, la adresa /vehicles/ID.

wantTo('create a new vehicle');
      $I->sendPOST('/vehicles', [
        'name' => 'Pansy'
      ]);
      // we mark scenario as not implemented
      $scenario->incomplete('work in progress');

      $I->seeResponseCodeIs(200);
      $I->seeResponseIsJson();
      $I->seeResponseJsonMatchesXpath('//id');
      $I->seeResponseJsonMatchesXpath('//name');
      $I->seeResponseMatchesJsonType([
        'id' => 'integer',
        'name' => 'string'
      ]);
      $I->seeResponseContainsJson([
        'id' => 123,
        'name' => 'Pansy'
      ]);
    }
}

Să observăm că, cel puțin pentru moment, marcăm scenariul curent ca incomplet, pentru a preveni execuția aserțiilor noastre. Vom elimina acest lucru în partea următoare a tutorialului nostru, atunci când vom trece la implementarea efectivă a funcționalității.

Mai multe detalii despre celelalte operații definite pentru vehicule, precum și aserțiile corespunzătoare, pot fi găsite pe GitHub.

Rularea testelor funcționale indică faptul că avem câteva definite și că ele sunt încă incomplete, iată cele mai importante elemente de la rularea testelor:

$ vendor/bin/codecept run

Functional Tests (4) ----------------------------------------------------
Check the status route (StatusRouteCest::checkTheStatusRoute)      Ok
Create a new vehicle (VehiclesCest::createItem)                    Incomplete
Retrieve current vehicles (VehiclesCest::retrieveItems)            Incomplete
Modify an existing item (VehiclesCest::updateItem)                 Incomplete
-------------------------------------------------------------------------

Time: 372 ms, Memory: 12.00Mb

OK, but incomplete, skipped, or risky tests!
Tests: 4, Assertions: 1, Incomplete: 3.

Definirea celorlalte cazuri de utilizare rămâne ca exercițiu pentru cititor.

Concluzii

Am aflat ce este stilul arhitectural REST, am învățat diferența între resurse și reprezentările lor, am pregătit Codeception pentru a ne fi de folos la definirea cazurilor de utilizare, am învățat cum se face design de API din punct de vedere practic, și am văzut la ce trebuie să fim atenți în cadrul acestui proces. În articolul următor vom rafina cazurile de utilizare și vom trece la partea de implementare.

LANSAREA NUMĂRULUI 88

Prezentări articole și
Panel: Robot Process Automation

Miercuri, 16 Octombrie, ora 18:30
Clădirea Casino parc, Cluj-Napoca

Înregistrează-te

Facebook Meetup

Conferință

Sponsori

  • comply advantage
  • ntt data
  • 3PillarGlobal
  • Betfair
  • Telenav
  • Accenture
  • Siemens
  • Bosch
  • FlowTraders
  • MHP
  • Connatix
  • UIPatj
  • MetroSystems
  • Globant
  • Colors in projects