Installation:
composer require open-southeners/laravel-apiable
php artisan apiable:install
The apiable:install command publishes the config file and creates a default ApiableServiceProvider.
Initial Setup:
config/apiable.php (e.g., default namespace, JSON:API version).config/app.php under providers:
OpenSoutheners\Apiable\ApiableServiceProvider::class,
First Use Case:
Create a JSON:API resource for a Post model:
php artisan make:apiable Post
This generates:
app/Http/Resources/PostResource.php (default JSON:API resource).app/Http/Resources/PostCollection.php (for paginated responses).Use it in a controller:
use OpenSoutheners\Apiable\Http\Resources\PostResource;
public function show(Post $post)
{
return new PostResource($post);
}
Key Files to Review:
config/apiable.php: Global settings (e.g., api_version, default_namespace).app/Http/Resources/: Auto-generated resource classes.app/Providers/ApiableServiceProvider.php: Custom logic (e.g., global resource macros).Resource Creation:
make:apiable for models or manually extend OpenSoutheners\Apiable\Http\Resources\JsonApiResource.namespace App\Http\Resources;
use OpenSoutheners\Apiable\Http\Resources\JsonApiResource;
class CustomPostResource extends JsonApiResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'custom_field' => $this->customField, // Override default fields
];
}
}
Relationships:
public function toArray($request)
{
return [
'id' => $this->id,
'author' => new UserResource($this->whenLoaded('author')),
];
}
whenLoaded() to avoid N+1 queries.Collections:
JsonApiResourceCollection for paginated responses:
namespace App\Http\Resources;
use OpenSoutheners\Apiable\Http\Resources\JsonApiResourceCollection;
class PostCollection extends JsonApiResourceCollection
{
public function toArray($request)
{
return [
'data' => $this->collection,
'meta' => ['total' => $this->collection->total()],
];
}
}
API Versioning:
config/apiable.php:
'api_version' => 'v1',
ApiableVersionMiddleware).Request Handling:
public function toArray($request)
{
$include = $request->input('include', []);
return [...];
}
Macros and Extensions:
ApiableServiceProvider:
\OpenSoutheners\Apiable\Http\Resources\JsonApiResource::macro('addMeta', function ($key, $value) {
$this->additional['meta'][$key] = $value;
return $this;
});
return (new PostResource($post))->addMeta('status', 'published');
Error Handling:
ApiableServiceProvider:
\OpenSoutheners\Apiable\Exceptions\Handler::render($request, throwable: $exception);
Namespace Conflicts:
default_namespace in config/apiable.php matches your app/Http/Resources path.'default_namespace' => 'App\Http\Resources'.Relationship Loading:
with() in queries or whenLoaded() in resources:
// Bad: Loads all relationships unconditionally
$posts = Post::with('author', 'comments')->get();
// Good: Load only requested relationships
$posts = Post::query();
if ($request->has('include')) {
$posts->with(explode(',', $request->input('include')));
}
Pagination Meta:
links, meta) may not match your API design. Override in PostCollection:
public function toArray($request)
{
return [
'data' => $this->collection,
'links' => [], // Customize or remove
'meta' => ['custom_meta' => 'value'],
];
}
Caching Resources:
toArray():
public function toArray($request)
{
return Cache::remember("post.{$this->id}", now()->addMinutes(10), function () {
return [
'id' => $this->id,
'title' => $this->title,
];
});
}
Middleware Order:
ApiableVersionMiddleware runs before ApiableMiddleware in app/Http/Kernel.php:
protected $middleware = [
\OpenSoutheners\Apiable\Http\Middleware\ApiableVersionMiddleware::class,
\OpenSoutheners\Apiable\Http\Middleware\ApiableMiddleware::class,
];
Testing:
JsonApiTestCase for assertions:
use OpenSoutheners\Apiable\Testing\JsonApiTestCase;
class PostTest extends JsonApiTestCase
{
public function test_post_show()
{
$response = $this->getJson('/api/v1/posts/1');
$response->assertJsonApiResource('posts');
}
}
Enable JSON:API Debugging:
'debug' => true in config/apiable.php to log resource processing.Check Resource Output:
dd($resource->resolve($request)) to inspect raw resource data before serialization.Validate Requests:
Accept: application/vnd.api+json header is present. Add middleware to enforce:
$request->headers->set('Accept', 'application/vnd.api+json');
Common Errors:
Class not found: Verify the resource class exists and namespace is correct.Method not allowed: Ensure routes use ApiableRouteMiddleware.data key: Confirm the resource extends JsonApiResource and returns an array.Custom Serializers:
serializeRelationship() for custom relationship handling:
protected function serializeRelationship($name, $value, $request)
{
if ($name === 'author') {
return new UserResource($value);
}
return parent::serializeRelationship($name, $value, $request);
}
Dynamic Attributes:
mergeWhen() to conditionally add fields:
public function toArray($request)
{
return $this->mergeWhen(
$request->user()->can('view_hidden'),
['hidden_field' => $this->hiddenField]
);
}
API Documentation:
/**
* @OA\Get(
* path="/api/v1/posts/{id}",
* @OA\Response(response="200", description="Returns a post")
* )
*/
GraphQL-like Includes:
include parameter to load nested relationships dynamically:
public function toArray($request)
{
$includes = explode(',', $request->input('include', ''));
if (in_array('author', $includes)) {
$this->load('author');
}
return [...];
}
Custom Headers:
withResponse():
return (new PostResource($post))
->withResponse($request, function ($response) {
$response->header('X-Custom-Header', 'value');
});
How can I help you explore Laravel packages today?