Response Analysis Guide
Laravel Spectrum automatically analyzes and documents API resources, Fractal transformers, pagination, and various response patterns.
ðĶ API Resourcesâ
Basic API Resourceâ
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'role' => $this->role,
'avatar_url' => $this->avatar_url,
'created_at' => $this->created_at->toISOString(),
'updated_at' => $this->updated_at->toISOString(),
];
}
}
Generated OpenAPI schema:
{
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"email": { "type": "string", "format": "email" },
"role": { "type": "string" },
"avatar_url": { "type": "string", "format": "uri" },
"created_at": { "type": "string", "format": "date-time" },
"updated_at": { "type": "string", "format": "date-time" }
}
}
Nested Resourcesâ
class PostResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'content' => $this->content,
'author' => new UserResource($this->author),
'comments' => CommentResource::collection($this->whenLoaded('comments')),
'tags' => TagResource::collection($this->tags),
'meta' => [
'views_count' => $this->views_count,
'likes_count' => $this->likes_count,
'is_featured' => $this->is_featured,
],
];
}
}
Conditional Attributesâ
class UserResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
// Show only to authenticated users
'phone' => $this->when($request->user(), $this->phone),
// Show only to admins
'internal_notes' => $this->when(
$request->user()?->isAdmin(),
$this->internal_notes
),
// Conditionally merge
$this->mergeWhen($request->user()?->id === $this->id, [
'private_settings' => $this->private_settings,
'notification_preferences' => $this->notification_preferences,
]),
// Only when relation is loaded
'posts' => PostResource::collection($this->whenLoaded('posts')),
'posts_count' => $this->whenCounted('posts'),
];
}
}
Metadata and Wrappingâ
class UserResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
];
}
public function with($request)
{
return [
'meta' => [
'version' => '1.0',
'api_version' => config('app.api_version'),
'generated_at' => now()->toISOString(),
],
];
}
// Custom wrapping
public static $wrap = 'user';
}
ð Collection Resourcesâ
Basic Collectionâ
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
public function toArray($request)
{
return [
'data' => $this->collection,
'meta' => [
'total_active' => $this->collection->where('is_active', true)->count(),
'total_inactive' => $this->collection->where('is_active', false)->count(),
],
];
}
public function with($request)
{
return [
'links' => [
'self' => route('users.index'),
],
'meta' => [
'generated_at' => now()->toISOString(),
],
];
}
}
Paginated Collectionâ
// Controller
public function index(Request $request)
{
$users = User::query()
->when($request->search, function ($query, $search) {
$query->where('name', 'like', "%{$search}%");
})
->paginate($request->input('per_page', 15));
return UserResource::collection($users);
}
Automatically detected pagination structure:
{
"data": [...],
"links": {
"first": "http://api.example.com/users?page=1",
"last": "http://api.example.com/users?page=10",
"prev": null,
"next": "http://api.example.com/users?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 10,
"path": "http://api.example.com/users",
"per_page": 15,
"to": 15,
"total": 150
}
}
ðĶī Fractal Transformersâ
Basic Transformerâ
namespace App\Transformers;
use App\Models\Product;
use League\Fractal\TransformerAbstract;
class ProductTransformer extends TransformerAbstract
{
protected array $availableIncludes = ['category', 'reviews'];
protected array $defaultIncludes = ['brand'];
public function transform(Product $product)
{
return [
'id' => (int) $product->id,
'name' => $product->name,
'slug' => $product->slug,
'price' => [
'amount' => (float) $product->price,
'currency' => $product->currency,
'formatted' => $product->formatted_price,
],
'in_stock' => (bool) $product->in_stock,
'created_at' => $product->created_at->toIso8601String(),
];
}
public function includeCategory(Product $product)
{
return $this->item($product->category, new CategoryTransformer);
}
public function includeReviews(Product $product)
{
return $this->collection($product->reviews, new ReviewTransformer);
}
public function includeBrand(Product $product)
{
return $this->item($product->brand, new BrandTransformer);
}
}
Using Fractal Managerâ
use League\Fractal\Manager;
use League\Fractal\Resource\Collection;
use League\Fractal\Resource\Item;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
class ProductController extends Controller
{
private Manager $fractal;
public function __construct(Manager $fractal)
{
$this->fractal = $fractal;
$this->fractal->setSerializer(new \League\Fractal\Serializer\ArraySerializer());
}
public function index(Request $request)
{
$paginator = Product::paginate(20);
$products = new Collection($paginator->items(), new ProductTransformer);
$products->setPaginator(new IlluminatePaginatorAdapter($paginator));
if ($request->has('include')) {
$this->fractal->parseIncludes($request->include);
}
return $this->fractal->createData($products)->toArray();
}
public function show($id)
{
$product = Product::findOrFail($id);
$resource = new Item($product, new ProductTransformer);
return $this->fractal->createData($resource)->toArray();
}
}
ð Complex Response Patternsâ
Polymorphic Relationsâ
class ActivityResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'type' => $this->type,
'description' => $this->description,
'subject' => $this->whenMorphLoaded('subject', function () {
return match ($this->subject_type) {
Post::class => new PostResource($this->subject),
Comment::class => new CommentResource($this->subject),
User::class => new UserResource($this->subject),
default => null,
};
}),
'created_at' => $this->created_at->toISOString(),
];
}
}
Recursive Structuresâ
class CategoryResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'parent_id' => $this->parent_id,
'children' => CategoryResource::collection($this->whenLoaded('children')),
'products_count' => $this->whenCounted('products'),
];
}
}
Custom Response Formatsâ
class ApiResponse
{
public static function success($data, string $message = '', int $code = 200)
{
return response()->json([
'success' => true,
'message' => $message,
'data' => $data,
'timestamp' => now()->toISOString(),
], $code);
}
public static function error(string $message, array $errors = [], int $code = 400)
{
return response()->json([
'success' => false,
'message' => $message,
'errors' => $errors,
'timestamp' => now()->toISOString(),
], $code);
}
public static function paginated($paginator, $resource)
{
return response()->json([
'success' => true,
'data' => $resource::collection($paginator),
'pagination' => [
'total' => $paginator->total(),
'per_page' => $paginator->perPage(),
'current_page' => $paginator->currentPage(),
'last_page' => $paginator->lastPage(),
'from' => $paginator->firstItem(),
'to' => $paginator->lastItem(),
],
'timestamp' => now()->toISOString(),
]);
}
}
ðŊ Type Inference and Schema Generationâ
Type Inference from Model Attributesâ
Laravel Spectrum infers types from:
-
Database Schema
- Migration files
- Model's
$casts
property
-
Model Casts
protected $casts = [
'is_active' => 'boolean',
'metadata' => 'array',
'published_at' => 'datetime',
'price' => 'decimal:2',
'settings' => 'json',
]; -
Accessors and Mutators
// Attribute casting
protected function price(): Attribute
{
return Attribute::make(
get: fn ($value) => $value / 100,
set: fn ($value) => $value * 100,
);
}
ðĄ Best Practicesâ
1. Consistent Response Structureâ
// Common base class for all resources
abstract class BaseResource extends JsonResource
{
protected function formatTimestamp($timestamp): ?string
{
return $timestamp?->toISOString();
}
protected function formatMoney($amount, string $currency = 'JPY'): array
{
return [
'amount' => $amount,
'currency' => $currency,
'formatted' => number_format($amount) . ' ' . $currency,
];
}
}
2. Explicit Type Castingâ
public function toArray($request)
{
return [
'id' => (int) $this->id, // Explicitly cast to int
'name' => (string) $this->name,
'price' => (float) $this->price,
'is_active' => (bool) $this->is_active,
'tags' => $this->tags->pluck('name')->toArray(), // Return as array
];
}
3. Proper Relation Handlingâ
public function toArray($request)
{
return [
// Avoid N+1 problem
'comments_count' => $this->whenCounted('comments'),
// Include only when loaded
'comments' => CommentResource::collection(
$this->whenLoaded('comments')
),
// Provide default value
'author' => $this->whenLoaded(
'author',
fn() => new UserResource($this->author),
fn() => null
),
];
}
4. Unified Error Responsesâ
trait ApiResponses
{
protected function successResponse($data, string $message = '', int $code = 200)
{
return response()->json([
'success' => true,
'message' => $message,
'data' => $data,
], $code);
}
protected function errorResponse(string $message, int $code = 400, array $errors = [])
{
return response()->json([
'success' => false,
'message' => $message,
'errors' => $errors,
], $code);
}
protected function validationErrorResponse($validator)
{
return $this->errorResponse(
'Validation failed',
422,
$validator->errors()->toArray()
);
}
}
ð Troubleshootingâ
Response Structure Not Detectedâ
-
Check Return Statement
// â Detected
return new UserResource($user);
return UserResource::collection($users);
// â May not be detected
return response()->json(new UserResource($user)); -
Resource Class Namespace
use App\Http\Resources\UserResource; // Correct namespace
-
Clear Cache
php artisan spectrum:cache clear
Nesting Too Deep Warningâ
// config/spectrum.php
'analysis' => [
'max_depth' => 5, // Adjust maximum nesting depth
],
ð Related Documentationâ
- API Resources - Laravel resources in detail
- Pagination - Pagination support
- Error Handling - Error responses