This is a rough draft of a guide I'll flesh out after Laracon Online as I continue to develop the best pattern for this.
See a practical (incomplete) example of this on my "JSON-API Examples" GitHub repo.
laravel new myproject && cd myproject
php artisan make:model Article -mfs
php artisan make:model Comment -mfs
guarded = ['id']
to both modelsarticle() { return $this->belongsTo(Article::class); }
to Comment
author() { return $this->belongsTo(User::class, 'author_id'); }
to Comment
comments() { return $this->hasMany(Comment::class); }
to Article
author() { return $this->belongsTo(User::class, 'author_id'); }
to Article
comments() { return $this->hasMany(Comment::class); }
to User
article() { return $this->hasMany(Article::class); }
to User
$table->text('body');
$table->unsignedBigInteger('article_id');
$table->foreign('article_id')->references('id')->on('articles');
$table->unsignedBigInteger('author_id');
$table->foreign('author_id')->references('id')->on('users');
$table->string('title');
$table->text('body');
$table->unsignedBigInteger('author_id');
$table->foreign('author_id')->references('id')->on('users');
php artisan make:resource Article
php artisan make:resource ArticleCollection
php artisan make:resource Comment
php artisan make:resource CommentCollection
php artisan make:controller "Api\ArticleController"
php artisan make:controller "Api\ArticleCommentController"
routes/api.php
:
Route::get('articles', 'Api\ArticleController@index');
Route::get('articles/{article}', 'Api\ArticleController@show');
Route::get('articles/{article}/comments', 'Api\ArticleCommentController@index');
composer require spatie/laravel-query-builder
ParsesIncludes
and ReturnsJsonApi
.ParsesIncludes:
trait ParsesIncludes
{
public function requestedIncludes($request)
{
if (! $request->input('include')) {
return collect([]);
}
$includes = collect(explode(',', $request->input('include')));
$includes->each(function ($include) {
if (! in_array($include, $this->allowedIncludes)) {
throw new Exception("Invalid include requested: {$include}");
}
});
return $includes;
}
}
ReturnsJsonApi:
trait ReturnsJsonApi
{
public function withResponse($request, $response)
{
$response->header('Content-Type', 'application/vnd.api+json');
}
}
use ParsesIncludes, ReturnsJsonApi;
public $allowedIncludes = [
'author',
'comments',
];
public function toArray($request)
{
return [
'type' => 'articles',
'id' => $this->id,
'attributes' => [
'title' => $this->title,
'body' => $this->body,
'created_at' => $this->created_at->format('c'),
'updated_at' => $this->created_at->format('c'),
],
$this->mergeWhen($this->requestedIncludes($request)->isNotEmpty(), [
'relationships' => $this->relationships($request),
]),
];
}
public function relationships()
{
// @todo, this is complicated -- see the tightenco/json-api-examples repo
// at the eloquent-resources branch, and I'll update this article ASAP
}
public function with()
{
// @todo, this is complicated -- see the tightenco/json-api-examples repo
// at the eloquent-resources branch, and I'll update this article ASAP
}
Check out the sample json-api-examples repo to see how I'm trying to make relationships work, and how I'm trying to make the included section work via the with method.
use ParsesIncludes, ReturnsJsonApi;
public $allowedIncludes = [
'author',
'comments',
];
public funtion toArray($request)
{
return [
'data' => parent::toArray($request),
$this->mergeWhen($this->requestedIncludes($request)->isNotEmpty(), [
'included' => $this->included($request),
]),
];
}
public function included($request)
{
// I haven't finished writing this method yet. Check back soon :)
$includes = $this->requestedIncludes($request);
if ($includes->isEmpty()) {
return [];
}
$included = [];
if ($includes->contains('author')) {
// @todo all authors, not just one.
// $included[] = new User($this->author);
}
// @todo flat map comments across all
return ['@todo'];
}
ArticleController
:
public function index()
{
$articles = QueryBuilder::for(Article::class)
->allowedIncludes(['author', 'comments'])
->allowedSorts(['created_at', 'title'])
->paginate();
return new \App\Http\Resources\ArticleCollection($articles)
}
public function show($articleId)
{
$article = QueryBuilder::for(Article::class)
->allowedIncludes(['author', 'comments', 'comments.author'])
->allowedSorts(['created_at', 'title'])
->findOrFail($articleId);
return new \App\Http\Resources\Article($article);
}
ArticleCommentController
:
public function index($articleId)
{
$comments = QueryBuilder::for(Comment::class)
->allowedIncludes(['author', 'article'])
->where('article_id', $articleI)
->paginate();
return new CommentCollection($comments)
}
With this complete, you have the basics of a Laravel-JSON-API structure. You haven't handled errors or complex content negotation, and your includes aren't done yet. You can either dig into flat includes yourself or using something like Fractal, or you can choose to just embed your includes into the relationships array--your call.
First, I'd recommend considering Fractal or Laravel-JSON-API instead. But if you really want to go down this road, check out the eloquent-resources branch of my json-api-examples repo, where I've started the process of trying to figure out how to do that.
For quick links to fresh content, and for more thoughts that don't make it to the blog.