Upgrade to Pro — share decks privately, control downloads, hide ads and more …

APIs with Laravel - Laracon 2018

TJ
July 26, 2018

APIs with Laravel - Laracon 2018

TJ

July 26, 2018
Tweet

Other Decks in Programming

Transcript

  1. { "data": [{ "type": "articles", "id": "1", "attributes": { "title":

    "JSON API paints my bikeshed!", "body": "The shortest article. Ever.", "created": "2015-05-22T14:56:29.000Z", "updated": "2015-05-22T14:56:28.000Z" }, "relationships": { "author": { "data": {"id": "42", "type": "people"} } } }], "included": [ { "type": "people", "id": "42", "attributes": { "name": "John", "age": 80, "gender": "male" } } ] } @SLXLIV3
  2. { "class": [ "order" ], "properties": { "orderNumber": 42, "itemCount":

    3, "status": "pending" }, "entities": [ { "class": [ "info", "customer" ], "rel": [ "http: //x.io/rels/customer" ], "properties": { "customerId": "pj123", "name": "Peter Joseph" }, "links": [ { "rel": [ "self" ], "href": "http: //api.x.io/customers/pj123" } ] } ], "actions": [ { "name": "add-item", "title": "Add Item", "method": "POST", "href": "http: //api.x.io/orders/42/items", "type": "application/x- www-form-urlencoded", "fields": [ { "name": "orderNumber", "type": "hidden", "value": "42" }, ] } ], "links": [ { "rel": [ "self" ], "href": "http: //api.x.io/orders/42" }, ] } @SLXLIV3
  3. { "status" : "success", "data" : { "posts" : [

    { "id" : 1, "title" : "A blog post", "body" : "Some useful content" }, { "id" : 2, "title" : "Another blog post", "body" : "More content" } ] } } @SLXLIV3
  4. openapi: 3.0.0 info: version: '2018-07-01' title: Laracon Blog license: name:

    MIT servers: - url: 'https: //api.sixlabs.io' paths: /posts: get: summary: List all posts operationId: post.index tags: - Posts parameters: - in: query name: published schema: type: boolean responses: '200': description: A list of posts content: application/json: schema: $ref: 'schemas/post.index.json' default: description: unexpected error content: application/json: schema: $ref: 'schemas/error.json' @SLXLIV3
  5. openapi: 3.0.0 info: version: '2018-07-01' title: Laracon Blog license: name:

    MIT servers: - url: 'https: //api.sixlabs.io' paths: /posts: get: summary: List all posts operationId: post.index tags: - Posts parameters: - in: query name: published schema: type: boolean responses: '200': description: A list of posts content: application/json: schema: $ref: 'schemas/post.index.json' default: description: unexpected error content: application/json: schema: $ref: 'schemas/error.json' @SLXLIV3
  6. openapi: 3.0.0 info: version: '2018-07-01' title: Laracon Blog license: name:

    MIT components: securitySchemes: bearerAuth: type: http scheme: bearer security: - bearerAuth: [] servers: - url: 'https: //api.sixlabs.io' @SLXLIV3
  7. openapi: 3.0.0 info: version: '2018-07-01' title: Laracon Blog license: name:

    MIT servers: - url: 'https: //api.sixlabs.io' paths: /posts: get: summary: List all posts operationId: post.index tags: - Posts parameters: - in: query name: published schema: type: boolean responses: '200': description: A list of posts content: application/json: schema: $ref: 'schemas/post.index.json' default: description: unexpected error content: application/json: schema: $ref: 'schemas/error.json' @SLXLIV3
  8. paths: /posts: get: summary: List all posts operationId: post.index tags:

    - Posts parameters: - in: query name: published schema: type: boolean responses: '200': description: A list of posts content: application/json: schema: $ref: 'schemas/post.index.json' default: description: unexpected error content: application/json: schema: $ref: 'schemas/error.json' @SLXLIV3
  9. openapi: 3.0.0 info: version: '2018-07-01' title: Laracon Blog license: name:

    MIT servers: - url: 'https: //api.sixlabs.io' paths: /posts: get: summary: List all posts operationId: post.index tags: - Posts parameters: - in: query name: published schema: type: boolean responses: '200': description: A list of posts content: application/json: schema: $ref: 'schemas/post.index.json' default: description: unexpected error content: application/json: schema: $ref: 'schemas/error.json' @SLXLIV3
  10. paths: /posts: get: summary: List all posts operationId: post.index tags:

    - Posts parameters: - in: query name: published schema: type: boolean @SLXLIV3
  11. paths: '/posts/{id}': get: summary: Details for a specific post operationId:

    post.show tags: - Posts parameters: - name: id in: path required: true description: The id of the post to retrieve schema: type: string @SLXLIV3
  12. openapi: 3.0.0 info: version: '2018-07-01' title: Laracon Blog license: name:

    MIT servers: - url: 'https: //api.sixlabs.io' paths: /posts: get: summary: List all posts operationId: post.index tags: - Posts parameters: - in: query name: published schema: type: boolean responses: '200': description: A list of posts content: application/json: schema: $ref: 'schemas/post.index.json' default: description: unexpected error content: application/json: schema: $ref: 'schemas/error.json' @SLXLIV3
  13. responses: '200': description: A list of posts content: application/json: schema:

    $ref: 'schemas/post.index.json' default: description: unexpected error content: application/json: schema: $ref: 'schemas/error.json' @SLXLIV3
  14. /posts: post: summary: Create posts operationId: post.create tags: - Posts

    requestBody: required: true content: application/json: schema: $ref: 'schemas/post.create.json' responses: '201': description: Null response @SLXLIV3
  15. responses: '200': description: A list of posts content: application/json: schema:

    $ref: 'schemas/post.index.json' default: description: unexpected error content: application/json: schema: $ref: 'schemas/error.json' @SLXLIV3
  16. schemas ├── error.json ├── post.create.json ├── post.index.json ├── post.json ├──

    post.show.json ├── user.index.json ├── user.json └── user.show.json @SLXLIV3
  17. { "data": [ { "id": 1, "title": "Nova Introduction", "post_body":

    "Zurfs Up !", "user_id": 1 }, { "id": 2, "title": "Nova Part Two", "post_body": null, "user_id": 1 } ] } @SLXLIV3
  18. { "properties": { "id": { "format": "int64", "type": "integer" },

    "post_body": { "type": [ "string", "null" ] }, "title": { "type": [ "string", "null" ] }, "user_id": { "format": "int64", "type": "integer" } }, "required": [ "id", "user_id", "title", "post_body" ], "type": "object" } @SLXLIV3
  19. { "properties": { "id": { "format": "int64", "type": "integer" },

    "post_body": { "type": [ "string", "null" ] } }, "required": [ "id", "post_body" ], "type": "object" } @SLXLIV3
  20. { "properties": { "data": { "items": { "$ref": "post.json" },

    "type": "array" } }, "type": "object" } @SLXLIV3
  21. ├── app ├── artisan ├── bootstrap ├── composer.json ├── config

    ├── database ├── openapi.yml ├── package.json ├── phpunit.xml ├── public ├── resources ├── routes ├── schemas ├── server.php ├── storage ├── tags ├── tests ├── vendor └── webpack.mix.js @SLXLIV3
  22. protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies ::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse

    ::class, \Illuminate\Session\Middleware\StartSession ::class, // \Illuminate\Session\Middleware\AuthenticateSession ::class, \Illuminate\View\Middleware\ShareErrorsFromSession ::class, \App\Http\Middleware\VerifyCsrfToken ::class, \Illuminate\Routing\Middleware\SubstituteBindings ::class, ], 'api' => [ 'throttle:60,1', 'bindings', ], ]; @SLXLIV3
  23. REMOVE MIDDLEWARE FILES └── Middleware ├── CheckForMaintenanceMode.php ├── EncryptCookies.php ├──

    RedirectIfAuthenticated.php ├── TrimStrings.php ├── TrustProxies.php └── VerifyCsrfToken.php @SLXLIV3
  24. REMOVE WEB ROUTES FILE ├── routes │ ├── api.php │

    ├── channels.php │ ├── console.php │ └── web.php @SLXLIV3
  25. public function map() { $this ->mapApiRoutes(); Route ::middleware('throttle:60,1') ->namespace($this ->namespace)

    ->group(function () { Route ::get('status', 'SystemStatusController'); }); } @SLXLIV3
  26. ├── tests │ ├── CreatesApplication.php │ ├── Feature │ │

    └── UserApiTest.php │ └── TestCase.php @SLXLIV3
  27. /** @test */ function fetch_a_user_index() { $user = factory(User ::class,

    2) ->create(); $this ->get(route('users.index')) ->assertOk(); } @SLXLIV3
  28. /** @test */ function fetch_a_user_index() { $user = factory(User ::class)

    ->create(); $this ->get(route('users.index')) ->assertOk() ->assertJsonFragment([ 'id' => $user ->id, 'name' => $user ->name, 'email' => $user ->email, ]); } @SLXLIV3
  29. => App\User {#2898 name: "David Hemphill", email: "[email protected]", api_token: "edslIo7q1CW6",

    updated_at: "2018-07-21 01:17:26", created_at: "2018-07-21 01:17:26", id: 1, } @SLXLIV3
  30. class UserController extends Controller { public function index() { return

    response() ->json([ 'data' => User ::all() ->toArray() ]); } } @SLXLIV3
  31. { "data": [{ "id": 1, "email": "[email protected]", "name": "David Hemphill",

    "api_token": "edslIo7q1CW6", "created_at": "2018-07-21 01:17:26", "updated_at": "2018-07-21 01:17:26" }] } @SLXLIV3
  32. class UserController extends Controller { public function index() { $users

    = User ::select([ 'id', 'name', 'created_at', ]) ->get(); return response() ->json([ 'data' => $users, ]); } } @SLXLIV3
  33. class UserController extends Controller { public function index() { $users

    = User ::select([ 'id', 'name', 'created_at', ]) ->get(); return response() ->json([ 'data' => $users, ]); } public function show($id) { $user = User ::select([ 'id', 'name', 'created_at', ]) ->findOrFail($id); return response() ->json([ 'data' => $user, ]); } } @SLXLIV3
  34. class PostController extends Controller { public function show($id) { $post

    = Post ::with('author') ->findOrFail($id); // Formatting for Post AND User return response() ->json(['data' => $post]); } } @SLXLIV3
  35. class UserResource extends JsonResource { public function toArray($request) { return

    [ 'id' => $this ->id, 'email' => $this ->email, 'name' => $this ->name, 'created_at' => $this ->created_at, ]; } } @SLXLIV3
  36. use App\Http\Resources\UserResource; class UserController extends Controller { public function index()

    { return UserResource ::collection(User ::all()); } public function show($id) { return UserResource ::make(User ::findOrFail($id)); } } @SLXLIV3
  37. class PostResource extends JsonResource { public function toArray($request) { return

    [ 'id' => $this ->id, 'title' => $this ->title, 'post_body' => $this ->post_body, 'author' => UserResource ::make($this ->whenLoaded('author')), ]; } } @SLXLIV3
  38. class PostResource extends JsonResource { public function toArray($request) { return

    [ 'id' => $this ->id, 'title' => $this ->title, 'post_body' => $this ->post_body, 'author' => UserResource ::make($this ->whenLoaded('author')), ]; } } @SLXLIV3
  39. class PostController extends Controller { public function show($id) { return

    PostResource ::make( Post ::with('author') ->findOrFail($id) ); } } @SLXLIV3
  40. class UserResource extends JsonResource { public function toArray($request) { return

    [ 'id' => $this ->id, 'email' => $this ->email, 'name' => $this ->name, 'api_token' => $this ->when( $this ->canViewApiToken(), $this ->api_token ), 'created_at' => $this ->created_at, ]; } private function canViewApiToken() { return auth() ->user() ->can('user.api_token.view', $this); } } @SLXLIV3
  41. class UserResource extends JsonResource { public function toArray($request) { return

    [ 'id' => $this ->id, 'email' => $this ->email, 'name' => $this ->name, 'api_token' => $this ->when( $this ->canViewApiToken(), $this ->access_token ), 'created_at' => $this ->created_at, ]; } } @SLXLIV3
  42. class PostController extends Controller { public function index() { return

    PostResource ::collection(Post ::all() ->paginate()); } } @SLXLIV3
  43. { "data": [ { "id": 1, "post_body": "Zurfs Up !",

    "title": "Nova Introduction", "user_id": 1 }, { "id": 2, "post_body": null, "title": "Nova Part Two", "user_id": 1 } ], "links": { "first": "https: //api.sixlabs.io/posts?page=1", "last": "https: //api.sixlabs.io/posts?page=1", "next": null, "prev": null }, "meta": { "current_page": 1, "from": 1, "last_page": 1, "path": "https: //api.sixlabs.io/posts", "per_page": 15, "to": 1, "total": 1 } } @SLXLIV3
  44. /** @test */ function fetch_a_user_index() { $user = factory(User ::class)

    ->create(); $this ->get(route('users.index')) ->assertOk() ->assertJsonFragment([ 'id' => $user ->id, 'name' => $user ->name, 'email' => $user ->email, ]) } @SLXLIV3
  45. class UserResource extends JsonResource { public function toArray($request) { return

    [ 'id' => $this ->id, 'email' => $this ->email, 'name' => $this ->name, 'created_at' => $this ->created_at, ]; } } @SLXLIV3
  46. /** @test */ function fetch_a_user_index() { $user = factory(User ::class)

    ->create(); $this ->get(route('users.index')) ->assertOk() ->assertJsonFragment([ 'id' => $user ->id, 'name' => $user ->name, 'email' => $user ->email, ]) } @SLXLIV3
  47. /** @test */ function fetch_a_user_index() { $user = factory(User ::class)

    ->create(); $this ->get(route('users.index')) ->assertOk() ->assertJsonFragment([ 'id' => $user ->id, 'name' => $user ->name, 'email' => $user ->email, ]) ->assertJsonSchema('user.index'); } @SLXLIV3
  48. class CreateUsersTable extends Migration { public function up() { Schema

    ::create('users', function (Blueprint $table) { $table ->increments('id'); $table ->string('name'); $table ->string('email') ->unique(); $table ->string('password'); $table ->string('api_token', 64); $table ->rememberToken(); $table ->timestamps(); }); } } @SLXLIV3
  49. 'guards' => [ 'web' => [ 'driver' => 'session', 'provider'

    => 'users', ], 'api' => [ 'driver' => 'token', 'provider' => 'users', ], ], @SLXLIV3
  50. 'guards' => [ 'web' => [ 'driver' => 'token', 'provider'

    => 'users', ], 'api' => [ 'driver' => 'token', 'provider' => 'users', ], ], @SLXLIV3
  51. /** @test */ function fields_are_required_to_create_a_post() { $user = factory(User ::class)

    ->create(); $this ->actingAs($user); $response = $this ->json('POST', route('posts.store'), []); $response ->assertStatus(Response ::HTTP_UNPROCESSABLE_ENTITY) ->assertJsonValidationErrors([ 'title', 'post_body' ]); } @SLXLIV3
  52. /** @test */ public function a_user_can_create_a_post() { $user = factory(User

    ::class) ->create(); $this ->actingAs($user); $response = $this ->json('POST', route('posts.store'), [ 'title' => 'Laracon Spotlight: Wes Bos', 'post_body' => 'Wicked!' ]); $response ->assertStatus(Response ::HTTP_CREATED) ->assertJsonFragment([ 'post_body' => 'Wicked!', 'title' => 'Laracon Spotlight: Wes Bos', 'user_id' => $user ->id, ]) ->assertJsonSchema('post.show'); } @SLXLIV3
  53. public function store() { request() ->validate([ 'title' => 'required', 'post_body'

    => 'required', ]); $post = request() ->user() ->posts() ->create( request() ->only(['title', 'post_body']) ); return PostResource ::make($post); } @SLXLIV3
  54. https: //localhost/posts?filter[published]=true /** @test */ public function can_filter_by_published() { $user

    = factory(User ::class) ->create(); $posts = factory(Post ::class, 2) ->create([ 'user_id' => $user ->id, 'published' => true, ]); $posts[1] ->update(['published' => false]); $route = route('posts.index', [ 'filter' => [ 'published' => true ] ]); $this ->get($route) ->assertJsonCount(1, 'data') ->assertJsonFragment([ 'title' => $posts[0] ->title, 'post_body' => $posts[0] ->post_body, ]) ->assertJsonSchema('post.index'); } @SLXLIV3
  55. https: //localhost/posts?filter[published]=true /** @test */ public function can_filter_by_published() { $user

    = factory(User ::class) ->create(); $posts = factory(Post ::class, 2) ->create([ 'user_id' => $user ->id, 'published' => true, ]); $posts[1] ->update(['published' => false]); $route = route('posts.index', [ 'filter' => [ 'published' => true ] ]); $this ->get($route) ->assertJsonCount(1, 'data') ->assertJsonFragment([ 'title' => $posts[0] ->title, 'post_body' => $posts[0] ->post_body, ]) ->assertJsonSchema('post.index'); } @SLXLIV3
  56. https: //localhost/posts?filter[published]=true /** @test */ public function can_filter_by_published() { $user

    = factory(User ::class) ->create(); $posts = factory(Post ::class, 2) ->create([ 'user_id' => $user ->id, 'published' => true, ]); $posts[1] ->update(['published' => false]); $route = route('posts.index', [ 'filter' => [ 'published' => true ] ]); $this ->get($route) ->assertJsonCount(1, 'data') ->assertJsonFragment([ 'title' => $posts[0] ->title, 'post_body' => $posts[0] ->post_body, ]) ->assertJsonSchema('post.index'); } @SLXLIV3
  57. https: //localhost/posts?filter[published]=true /** @test */ public function can_filter_by_published() { $user

    = factory(User ::class) ->create(); $posts = factory(Post ::class, 2) ->create([ 'user_id' => $user ->id, 'published' => true, ]); $posts[1] ->update(['published' => false]); $route = route('posts.index', [ 'filter' => [ 'published' => true ] ]); $this ->get($route) ->assertJsonCount(1, 'data') ->assertJsonFragment([ 'title' => $posts[0] ->title, 'post_body' => $posts[0] ->post_body, ]) ->assertJsonSchema('post.index'); } @SLXLIV3
  58. class PostController extends Controller { public function index() { $posts

    = Post ::all(); return PostResource ::collection($posts); } } @SLXLIV3
  59. use Spatie\QueryBuilder\QueryBuilder; class PostController extends Controller { public function index()

    { $posts = QueryBuilder ::for(Post ::class) ->allowedFilters('published') ->get(); return PostResource ::collection($posts); } } @SLXLIV3
  60. RECOMMENDED PACKAGES ▸ Laravel CSP (spatie/laravel-csp) ▸ Laravel CORS (spatie/laravel-cors)

    ▸ Laravel Query Builder (spatie/laravel-query-builder) ▸ Bouncer (silber/bouncer) ▸ JSON Schema Assertions (sixlive/laravel-json-schema-assertions) @SLXLIV3