Post on 17-Jul-2015
transcript
Bootstrap REST APIs with Laravel 5
Elena Kolevska
www.speedtocontact.comAutomated lead response system / call center in a
browser
Why REST?
SOAP!
POST http://www.stgregorioschurchdc.org/cgi/websvccal.cgi HTTP/1.1 Accept-Encoding: gzip,deflate Content-Type: text/xml;charset=UTF-8 SOAPAction: "http://www.stgregorioschurchdc.org/Calendar#easter_date" Content-Length: 479 Host: www.stgregorioschurchdc.org Connection: Keep-Alive User-Agent: Apache-HttpClient/4.1.1 (java 1.5) <?xml version="1.0"?> <soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:cal="http://www.stgregorioschurchdc.org/Calendar"> <soapenv:Header/> <soapenv:Body> <cal:easter_date soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <year xsi:type="xsd:short">2014</year> </cal:easter_date> </soapenv:Body> </soapenv:Envelope>
Basic AuthenticationALWAYS use SSL!
$header = 'Authorization: Basic' . base64_encode($username . ':' . $password);
Authentication Middleware<?php namespace App\Http\Middleware;
use App\User;use Closure;use Illuminate\Contracts\Auth\Guard;use Illuminate\Support\Facades\Hash;
class AuthenticateOnce {
public function handle($request, Closure $next) { if ($this->auth->onceBasic()) return response(['status'=>false, 'message'=>'Unauthorized'], 401, ['WWW-Authenticate' =>'Basic']);
return $next($request); }
protected $routeMiddleware = [ 'auth' => 'App\Http\Middleware\Authenticate', 'auth.basic' => 'Illuminate\Auth\Middleware\AuthenticateWithBasicAuth', 'auth.basic.once' => 'App\Http\Middleware\AuthenticateOnce', 'guest' => 'App\Http\Middleware\RedirectIfAuthenticated', ];
<?php
Route::group(array('prefix' => 'api/v1/examples','middleware' => 'auth.basic.once'), function () {
Route::get('1', 'ExamplesController@example1');
Route::get('2', 'ExamplesController@example2');
Route::get('3', 'ExamplesController@example3');
Route::get('4', 'ExamplesController@example4');
Route::get('5', 'ExamplesController@example5');
Route::get('6', 'ExamplesController@example6');
Route::get('7', 'ExamplesController@example7');
Route::get('8', 'ExamplesController@example8');
});
app/Http/routes.php
Response formatHTTP/1.1 200 OK Content-Type: application/json Connection: close X-Powered-By: PHP/5.6.3Cache-Control: no-cacheDate: Fri, 13 Apr 2015 16:37:57 GMT Transfer-Encoding: Identity
{"status":true,"data":{"k1":"value1","k2":"value2"},"message":"Zero imagination for funny messages"}
Most common HTTP status codes
• 200 OK
• 201 Created
• 204 No Content
• 400 Bad Request
• 401 Unauthorized
• 403 Forbidden
• 404 Not Found
• 409 Conflict
• 500 Internal Server Error
Response message format{ "status": true, "data": { "k1": "value1", "k2": "value2" }, "message": "Zero imagination for funny messages"}
{ "status": false, "errors": { "e1": "Nope, it can't be done", "e2": "That can't be done either" }, "error_code": 12345, "message": "Zero imagination for funny messages"}
Controller hierarchy
app/Http/Controllers/ApiController.php<?phpnamespace App\Http\Controllers;
use Illuminate\Http\Request;use Illuminate\Routing\ResponseFactory;use Illuminate\Auth\Guard;use App\User;
class ApiController extends Controller{
public $response; public $request; public $auth;
public function __construct(ResponseFactory $response, Request $request, Guard $auth) { $this->response = $response; $this->request = $request; $this->auth = $auth; $this->currentUser = $this->auth->user(); }}
app/Http/Controllers/ApiController.php
public function respond($data, $response_message, $status_code = 200) {
//If this is an internal request we only return the data if ($this->request->input('no-json')) return $data;
$message['status'] = true;
if (isset($data)) $message['data'] = $data;
if (isset($message)) $message['message'] = $response_message;
if (isset($error_code)) $message['error_code'] = $error_code;
return $this->response->json($message, $status_code);
}
app/Http/Controllers/ExamplesController.php<?php namespace App\Http\Controllers;
class ExamplesController extends ApiController {
/** * Send back response with data * * @return Response */ public function example1() { $sample_data_array = ['k1'=>'value1', 'k2'=>'value2']; return $this->respond($sample_data_array, 'Message'); }}
app/Http/Controllers/ApiController.php
public function respondWithError($errors, $error_code, $status_code = 400) { if (is_string($errors)) $errors = [$errors];
$message = [ 'status' => false, 'errors' => $errors, 'error_code' => $error_code ];
return $this->response->json($message, $status_code); }}
public function example3() { $error = "Can't be done";
return $this->respondWithError($error, 123, 500); }
app/Http/Controllers/ExamplesController.php
app/Http/Controllers/ApiController.php } /** * @param array $errors * @param int $status_code * @return Response */ public function respondWithValidationErrors($errors, $status_code = 400) {
$message = [ 'status' => false, 'message' => "Please double check your form", 'validation_errors' => [$errors] ];
return $this->response->json($message, $status_code); }}
app/Http/Controllers/ApiController.php public function respondCreated( $message = 'Resource created') { return $this->respond($message, 201); }
public function respondUnauthorized( $error_code, $message = 'You are not authorized for this') { return $this->respondWithError($message, $error_code, 401); }
public function respondNotFound( $error_code, $message = 'Resource not found') { return $this->respondWithError($message, $error_code, 404); }
public function respondInternalError( $error_code, $message = 'Internal error') { return $this->respondWithError($message, $error_code, 500); }
public function respondOk( $message = 'Done') { return $this->respond(null, $message, 200); }
Always use fake data for testing
https://github.com/fzaninotto/Faker
<?php
use Illuminate\Database\Seeder;
class UsersTableSeeder extends Seeder {
public function run() { Eloquent::unguard(); $faker = Faker\Factory::create();
for($i=1; $i < 20; $i++ ){ $data = [ 'name' => $faker->name, 'email' => $faker->email, 'password' => bcrypt('demodemo') ];
\App\User::create($data); }
}
}
database/seeds/UsersTableSeeder.php
Setting up the repositories
app/Providers/RepoBindingServiceProvider.php<?phpnamespace App\Providers;
use Illuminate\Support\ServiceProvider;
class RepoBindingServiceProvider extends ServiceProvider { public function register() { $app = $this->app;
$app->bind('\App\Repositories\Contracts\UsersInterface', function() { $repository = new \App\Repositories\UsersRepository(new \App\User); return $repository; });
}}
* Register the service provider in the list of autoloaded service providers in config/app.php
app/Repositories/BaseRepository.php<?phpnamespace App\Repositories;
use Illuminate\Database\Eloquent\Model;
class BaseRepository { public function __construct(Model $model) { $this->model = $model; }
public function create($data) { return $this->model->create($data); }
public function find($id) { return $this->model->find($id); }
public function delete($id) { return $this->model->destroy($id); }
public function all() { return $this->model->all(); }
app/Repositories/BaseRepository.php public function update($record, $data) { if (is_int($record)){ $this->model->find($record); $id = $record; } else { $this->model = $record; $id = $record->id; } return $this->model->where('id',$id)->update($data); }
public function getById($id, $user_id = null, $with = null) { if (is_array($id)){ $result = $this->model->whereIn('id', $id); }else{ $result = $this->model->where('id', $id); }
if ($user_id) $result->where('user_id', $user_id);
if ($with) $result->with($with);
if (is_array($id)){ return $result->get();
return $result->first(); }
public function example5() { $data = \App::make('\App\Repositories\Contracts\UsersInterface')->getById(3,null,['courses']); return $this->respond($data,"All users"); }
{ "status": true, "data": { "id": 3, "name": "Asia Towne DVM", "email": "emmett42@welch.com", "api_token":"543bjk6h3uh34n5j45nlk34j5k43n53j4b5jk34b5jk34", "created_at": "2015-04-14 18:09:48", "updated_at": "2015-04-14 18:09:48", "courses": [ { "id": 3, "name": "Forro", "pivot": { "user_id": 3, "course_id": 3 } }, { "id": 4, "name": "Jiu Jitsu", "pivot": { "user_id": 3, "course_id": 4 } } ] }, "message": "User with the id of 3"}
app/Http/Controllers/ExamplesController.php
Data TransformersYou need them
<?php
namespace App\DataTransformers;
abstract class DataTransformer {
public function transformCollection($items, $method = 'transform') { return array_map([$this, $method], $items); }
public abstract function transform($item);}
app/DataTransformers/DataTransformer.php
<?phpnamespace App\DataTransformers;
class UserTransformer extends DataTransformer{
public function transform($user) { return [ 'id' => $user['id'], 'name' => $user['name'], 'email' => $user['email'], ]; }}
app/DataTransformers/UserTransformer.php
{ "status": true, "data": [ { "id": 20, "name": "Vance Jacobs", "email": "callie.zieme@hotmail.com" }, { "id": 19, "name": "Chesley Swift", "email": "giovani72@schumm.com" }, { "id": 18, "name": "Frederick Hilpert", "email": "dameon.macejkovic@gmail.com" } ], "message": "Latest 3 users"}
<?phpnamespace App\DataTransformers;
class UserTransformer extends DataTransformer{
public function transform($user) { return [ 'id' => $user['id'], 'name' => $user['name'], 'email' => $user['email'], 'courses' => (isset($user['courses']) && count($user['courses'])) ? array_map([$this,'transformCourses'], $user['courses']) : null, ]; }
public function transformCourses($course){ return [ 'id' => $course['id'], 'name' => $course['name'] ]; }}
What about nested resources?
{ "status": true, "data": [ { "id": 20, "name": "Vance Jacobs", "email": "callie.zieme@hotmail.com", "courses": null }, { "id": 19, "name": "Chesley Swift", "email": "giovani72@schumm.com", "courses": [ { "id": 2, "name": "Samba" }, { "id": 3, "name": "Forro" } ] }, { "id": 18, "name": "Frederick Hilpert", "email": "dameon.macejkovic@gmail.com", "courses": [
{ "id": 4, "name": "Jiu Jitsu" } ] } ], "message": "Latest 3 users"}
Pretty error messages
public function render($request, Exception $e) { if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException){ $message = [ 'status' => false, 'error_code' => 2234, 'errors' => ["That resource doesn't exist"] ]; return response($message, 404); }
if ($e instanceof \Symfony\Component\HttpKernel\Exception\NotFoundHttpException){ $message = [ 'status' => false, 'error_code' => 1235, 'errors' => ["We don't have that kind of resources"] ]; return response($message, 404); }
if ($e instanceof \Exception){ $message = [ 'status' => false, 'message' => $e->getMessage() ]; return response($message, $e->getCode()); }
return parent::render($request, $e); }
}
app/Exceptions/Handler.php
STATUS 404 Not Found
{ "status": false, "error_code": 1235, "errors": [ "We don't have that kind of resources" ]}
STATUS 404 Not Found
{ "status": false, "error_code": 2234, "errors": [ "That resource doesn't exist" ]}
STATUS 418 I'm a teapot
{ "status": false, "message": "I'm a teapot"}
STATUS 500 Internal Server Error
{ "status": false, "message": "Class 'User' not found"}
Internal dispatcherFor your (probably) most important consumer
<?php
namespace App\Http;
class InternalDispatcher {
public function release( $url, $method = 'GET', $input, $no_json) { // Store the original input of the request $originalInput = \Request::input();
// Create request to the API, adding the no-json parameter, since it's an internal request $request = \Request::create($url, $method);
// Replace the input with the request instance input \Request::replace($input);
// Fetch the response if ($no_json){ $content = \Route::dispatch($request)->getContent(); $result = json_decode($content, 1); }else{ $result = \Route::dispatch($request)->getContent(); }
// Replace the input again with the original request input. \Request::replace($originalInput);
return $result; }
public function withNoInput($url, $method = 'GET', $no_json = true){ $input = ['no-json'=>$no_json]; return $this->release($url, $method = 'GET', $input, $no_json); }}
app/Http/InternalDispatcher.php
STATUS 200 OK{ "status": true, "data": { "latest_users": [ { "id": 20, "name": "Vance Jacobs", "email": "callie.zieme@hotmail.com", "courses": [] }, { "id": 18, "name": "Frederick Hilpert", "email": "dameon.macejkovic@gmail.com", "courses": [] } ], "youngest_user": { "id": 3, "name": "Asia Towne DVM", "email": "emmett42@welch.com", "courses": [ { "id": 3, "name": "Forro" }, { "id": 4, "name": "Jiu Jitsu" } ] } }, "message": "Good data"}
Thank you for listening!
Let the discussion begin :)