RESTful API-ontwikkeling met PHP-framework Laravel

Peter Bond, MSc DoorPeter Bond, MSc

Al jarenlang zijn RESTful APIs ontzettend populair in de webontwikkeling. Veel moderne frameworks bieden ook uitgebreide mogelijkheden om in een korte tijd een dergelijke API tot stand te laten komen. In dit artikel nemen we de basisprincipes van het ontwikkelen van een RESTful API onder de loep met het populaire PHP-framework Laravel. Terwijl ik dit schreef werkte ik met Laravel versie 5.4, het kan dus zijn dat sommige stukken voorbeeldcode niet werken bij andere versies. We beginnen met een korte introductie over REST.

Representational State Transfer (REST)

REST is een software-architectuur bedacht door Roy Fielding die bijzonder nuttig is voor APIs. Enkele kenmerken staan hierbij op de voorgrond. Zo zijn RESTful APIs gebaseerd op resources die benaderd kunnen worden door een unieke URI. Een resource correspondeert in de regel hierbij met een individuele rij uit een databasetabel (met eventuele relaties), en de bijbehorende URI volgt het formaat /<resource>/<id>. Dit komt bijvoorbeeld neer op /transactions/5, waarbij een enkele transactie met ID 5 kan worden benaderd. Om de gehele groep resources te benaderen wordt de ID weggelaten, dus /<resource>.

Een ander kenmerk is dat de interactie met deze resources plaatsvindt door gebruik te maken van de HTTP verbs: GET, POST, PUT, PATCH, DELETE. Met GET kan een resource opgevraagd worden, met POST kan een nieuwe resource aangemaakt worden, met PUT kan een resource vervangen worden, met PATCH kan een resource bewerkt worden, en met DELETE kan een resource verwijderd worden.

Voortbordurend op het vorige voorbeeld met transacties, zou je een nieuwe transactie kunnen aanmaken met een POST-aanroep naar /transactions en zou je de transactie met ID 5 kunnen verwijderen met een DELETE-aanroep naar /transactions/5.

Ook verpakt een RESTful API zijn responses in een uniform formaat. Een veelvoorkomend formaat die hiervoor gebruikt wordt is JavaScript Object Notation (JSON). Zo kan de corresponderende databaserij van een resource er uitzien zoals hieronder:

ID amount status created_at updated_at
5 100.00 paid 2017-07-09 12:34:56 2017-07-09 12:35:56

Bij opvraag van deze resource met een GET-aanroep naar /transactions/5 die reageert met een JSON-representatie, zou dan het volgende teruggegeven worden:

{
  "id":5,
  "amount":100,
  "status":"paid",
  "created_at":"2017-07-09 12:34:56",
  "updated_at":"2017-07-09 12:35:56"
}

Tot slot is een RESTful API stateless. Dat wil zoveel zeggen als dat de respons en de afhandeling volledig afhankelijk zijn van de informatie die wordt geleverd met de aanroep, en dat de server dus geen informatie opslaat over de cliënt die invloed heeft op de respons. Er worden dus geen gebruikerssessies bijgehouden door de server, deze verantwoordelijkheid ligt volledig bij de cliënt. Er zijn nog enkele andere zaken die RESTful karakteriseren, maar deze vallen buiten het bestek van dit artikel.

Laravel controllers en routing

Laravel komt samen met de command-line interface Artisan, die de mogelijkheid biedt om met commando’s heel eenvoudig boilerplate code te genereren. Dit scheelt aardig wat typewerk en helpt om binnen de filosofie van het framework te programmeren.

Zo kun je met Artisan een resource controller genereren met het commando:
php artisan make:controller Api/\TransactionController –resource.

Dit commando genereert vervolgens een resource controller genaamd TransactionController binnen de namespace App\Http\Controllers\Api. Hierin zijn standaard de functies index, create, store, show, edit, update en destroy opgenomen. Omdat we een API ontwikkelen, kunnen we de functies create en edit schrappen. We hoeven immers geen view met een HTML-formulier te tonen aan de eindgebruiker. Per functie wordt zo de volgende functionaliteit geboden:

  • index – hiermee kan een API-gebruiker alle transactie resources opvragen met een GET-aanroep.
  • store – hiermee kan een API-gebruiker een nieuwe transactie aanmaken met een POST-aanroep.
  • show – hiermee kan een API-gebruiker een individuele transactie opvragen met een GET-aanroep.
  • update – hiermee kan een API-gebruiker een individuele transactie bewerken met een PATCH-aanroep.
  • delete – hiermee kan een API-gebruiker een individuele transactie verwijderen met een DELETE-aanroep.

Nu kun je deze individuele functies invullen. Zo haal je bij index bijvoorbeeld alle transacties op met Eloquent met Transaction::all(); en kun je een enkele transactie opvragen bij show met Transaction::findOrFail($id);.

Wat heel handig is aan Laravel, is dat een Eloquent-resultaat automatisch omzet naar JSON wanneer deze wordt geprint. Je hoeft dus niet in de controller de opgehaalde resource nog om te zetten naar JSON voordat je deze doorgeeft, noch hoef je een view aan te maken. Je kunt simpelweg gelijk de resource returnen. Laravel regelt de rest en geeft de JSON-representatie terug aan de eindgebruiker. De show-functie kan dus zo kort zijn als hieronder:

public function show($id)
{
    return Transaction::findOrFail($id);
}

Ook wanneer je de geldigheid van de meegegeven data wilt controleren, bij bijvoorbeeld de store endpoint, regelt Laravel een heleboel voor je, waardoor je controller clean blijft. Zo kan de functie in je controller er als volgt uitzien:

public function store(StoreTransactionRequest $request)
{
    return Transaction::create($request->all());
}

De StoreTransactionRequest-klasse genereer je vervolgens met Artisan met het commando:
php artisan make:request StoreTransactionRequest

Er verschijnt dan in in de app/Http/Requests folder de klasse StoreTransactionRequest. Hierin kun je vervolgens de validatieregels toevoegen voor een transactie:

public function rules()
{
    return [
        'amount' => 'required|integer',
        'status' => 'required|in:started,pending,paid,cancelled,refunded'
    ];
}

Wanneer er nu een POST-aanroep wordt gedaan naar de store endpoint, en de meegegeven data voldoet niet aan deze validatieregels, dan koppelt Laravel dit automatisch voor je terug aan de eindgebruiker in JSON. Zou je bijvoorbeeld een POST-aanroep doen zonder de verplichte parameters amount en status, dan geeft Laravel automatisch het volgende terug met de HTTP-statuscode 422 Unprocessable Entity:

{
  "amount":[
    "amount is verplicht."
  ],
  "state":[
    "state is verplicht."
  ]
}

Noot: de eindgebruiker moet wel in de headers van de POST-aanroep meegeven dat hij JSON als respons verwacht (Accept: application/json).

Wat nog rest is de koppeling tussen de functies van de resource controller en de daadwerkelijke URLs die de eindgebruiker kan aanroepen. Deze routes kun je toevoegen aan het standaard routesbestand voor APIs routes/api.php.

Maak in dit bestand een nieuwe routegroep aan, en je kunt handig gebruikmaken van de resourcewrapper die Laravel biedt om alle routes voor je resource controller te dekken:
Route::group(['prefix' => 'api', 'namespace' => 'Api'], function() {
    Route::resource('transactions', 'TransactionController');
});

En dat is alles, Laravel doet de rest en je hebt nu een volledig functionele API. Je kunt natuurlijk ook handmatig individuele routes beschrijven binnen de routegroep, zoals bijvoorbeeld Route::post(‘transactions’, [‘uses’ => ‘TransactionController@store’])->name(‘api.order.store’); voor als je routes wilt aanmaken met afwijkende namen die de resourcewrapper niet dekt.

Authenticatie

Meer dan eens is het de bedoeling dat een API niet publiekelijk toegankelijk is. Laravel biedt ook kant-en-klare mogelijkheden om API-authenticatie te implementeren. Sinds versie 5.3 is het Laravel-pakket Passport beschikbaar. Passport is een OAuth2 server-implementatie. In dit artikel zal ik een eenvoudigere token-gebaseerde oplossing behandelen die ook al in versie 5.2 van Laravel beschikbaar was.

De API-tokens corresponderen met gebruikers uit de standaard users-tabel die meegeleverd wordt met Laravel. Ìedere API-gebruiker heeft dus zijn eigen, unieke, API-token. Maak hiervoor een migration aan waarbij je een extra unieke kolom toevoegt aan de users-tabel genaamd api_token, met datatype string en lengte 60. Maak de migration aan met Artisan:
php artisan make:migration add_api_token_to_users_table

Plaats vervolgens onderstaande code in de up-functie:

Schema::table('users', function (Blueprint $table) {
    $table->string('api_token', 60)->unique();
});

En we vergeten natuurlijk niet de kolom weg te halen in de down-functie:

Schema::table('users', function (Blueprint $table) {
    $table->dropColumn('api_token');
});

Bij het aanmaken van een nieuwe API-gebruiker vul je deze kolom bijvoorbeeld met een willekeurig gegenereerde string.

De volgende stap is om Laravel te vertellen dat je API-routes worden afgehandeld door de juiste middleware. In routes/api.php passen we daarom de routegroep aan en voegen we de middleware api:auth toe:

Route::group(['prefix' => 'api', 'namespace' => 'Api', 'middleware' => 'auth:api'], function() {
    Route::resource('transactions', 'TransactionController');
});

Controleer ook voor de zekerheid of voor de API als guard de driver token staat ingesteld in config/auth.php:
'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
    'api' => [
        'driver' => 'token',
        'provider' => 'users',
    ],

],

Om nu een API-aanroep te verrichten naar de API moet de token meegegeven worden in de headers als volgt:
Authorization: Bearer <token>.

Een voorbeeld cURL-aanroep zou er dus als volgt uit kunnen zien:
curl -X POST –header “Content-Type: application/json” –header “Accept: application/json” -d ‘{“amount”:100, “state”:”started”}’ –header “Authorization: Bearer AclAaPztIWovPXYRjdiP0Nbca5wzGrK5yfQO3QySsvDkVccYHrWFftsKDvsG” http://url.app/api/transactions

Je hebt nu alle routes van je API, die gewrapped zijn in de routegroup met als middleware auth:api, afgeschermd met een API-token.

Over de auteur

Peter Bond, MSc

Peter Bond, MSc

Oprichter van Bond IT Solutions en software-ingenieur.

Laat een reactie achter