Appearance
📦 Response Macro Service Provider
Complete implementation of standardized JSON response formats using Laravel macros, JsonResource, and ResourceCollection.
Named Arguments (PHP 8.0+)
This implementation uses named arguments throughout for better code readability and maintainability. Named arguments make function calls self-documenting and allow you to skip optional parameters.
php
// ✅ Good: Named arguments (self-documenting)
return response()->success(
data: $user,
message: 'User created successfully',
status: 201
);
// ❌ Less clear: Positional arguments
return response()->success($user, 'User created successfully', 201);🎯 Service Provider Implementation
Create the Service Provider
bash
php artisan make:provider ResponseMacroServiceProviderComplete Implementation
php
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\ServiceProvider;
class ResponseMacroServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
//
}
/**
* Bootstrap services.
*/
public function boot(): void
{
$this->registerJsonMacro();
$this->registerSuccessMacro();
$this->registerErrorMacro();
$this->registerCreatedMacro();
$this->registerValidationErrorMacro();
$this->registerNotFoundMacro();
$this->registerUnauthorizedMacro();
$this->registerForbiddenMacro();
$this->registerNoContentMacro();
}
/**
* Override default json() response to use standard format
*/
private function registerJsonMacro(): void
{
Response::macro('json', function (
mixed $data = null,
int $status = 200,
array $headers = [],
int $options = 0
): JsonResponse {
// If data is already a JsonResponse, return it
if ($data instanceof JsonResponse) {
return $data;
}
// Format data based on type
$formattedData = $this->formatResponseData(data: $data, status: $status);
return response()->json(
data: $formattedData,
status: $status,
headers: $headers,
options: $options
);
});
}
/**
* Success response with data
*/
private function registerSuccessMacro(): void
{
Response::macro('success', function (
mixed $data = null,
string $message = 'Operation successful',
int $status = 200,
array $meta = []
): JsonResponse {
$response = [
'success' => true,
'message' => $message,
];
// Handle JsonResource
if ($data instanceof JsonResource) {
$response['data'] = $data->resolve();
}
// Handle ResourceCollection
elseif ($data instanceof ResourceCollection) {
$collection = $data->resolve();
$response['data'] = $collection['data'] ?? $collection;
// Add pagination meta if available
if (isset($collection['meta'])) {
$response['meta'] = array_merge($collection['meta'], $meta);
}
if (isset($collection['links'])) {
$response['links'] = $collection['links'];
}
}
// Handle array or null
else {
$response['data'] = $data;
}
// Add additional meta
if (!empty($meta) && !isset($response['meta'])) {
$response['meta'] = $meta;
}
return response()->json(data: $response, status: $status);
});
}
/**
* Error response
*/
private function registerErrorMacro(): void
{
Response::macro('error', function (
string $message = 'An error occurred',
?string $code = null,
int $status = 400,
?array $errors = null,
?array $meta = null
): JsonResponse {
$response = [
'success' => false,
'error' => [
'message' => $message,
'code' => $code ?? $this->generateErrorCode(message: $message),
],
];
if ($errors !== null) {
$response['error']['details'] = $errors;
}
if ($meta !== null) {
$response['meta'] = $meta;
}
return response()->json(data: $response, status: $status);
});
}
/**
* Created response (201)
*/
private function registerCreatedMacro(): void
{
Response::macro('created', function (
mixed $data = null,
string $message = 'Resource created successfully',
?string $location = null
): JsonResponse {
$response = response()->success(
data: $data,
message: $message,
status: 201
);
if ($location) {
$response->header(key: 'Location', values: $location);
}
return $response;
});
}
/**
* Validation error response (422)
*/
private function registerValidationErrorMacro(): void
{
Response::macro('validationError', function (
array $errors,
string $message = 'Validation failed'
): JsonResponse {
return response()->json(
data: [
'success' => false,
'error' => [
'message' => $message,
'code' => 'VALIDATION_ERROR',
'details' => $errors,
],
],
status: 422
);
});
}
/**
* Not found response (404)
*/
private function registerNotFoundMacro(): void
{
Response::macro('notFound', function (
string $resource = 'Resource',
?string $identifier = null,
?string $message = null
): JsonResponse {
$defaultMessage = $identifier
? "{$resource} with identifier '{$identifier}' not found"
: "{$resource} not found";
return response()->json(
data: [
'success' => false,
'error' => [
'message' => $message ?? $defaultMessage,
'code' => 'RESOURCE_NOT_FOUND',
],
],
status: 404
);
});
}
/**
* Unauthorized response (401)
*/
private function registerUnauthorizedMacro(): void
{
Response::macro('unauthorized', function (
string $message = 'Unauthorized',
?string $code = null
): JsonResponse {
return response()->json(
data: [
'success' => false,
'error' => [
'message' => $message,
'code' => $code ?? 'UNAUTHORIZED',
],
],
status: 401
);
});
}
/**
* Forbidden response (403)
*/
private function registerForbiddenMacro(): void
{
Response::macro('forbidden', function (
string $message = 'Forbidden',
?string $code = null
): JsonResponse {
return response()->json(
data: [
'success' => false,
'error' => [
'message' => $message,
'code' => $code ?? 'FORBIDDEN',
],
],
status: 403
);
});
}
/**
* No content response (204)
*/
private function registerNoContentMacro(): void
{
Response::macro('noContent', function (): JsonResponse {
return response()->json(data: null, status: 204);
});
}
/**
* Generate error code from message
*/
private function generateErrorCode(string $message): string
{
return strtoupper(str_replace(' ', '_', $message));
}
/**
* Format response data based on type
*/
private function formatResponseData(mixed $data, int $status): array
{
$isSuccess = $status >= 200 && $status < 300;
if ($data instanceof JsonResource) {
return [
'success' => $isSuccess,
'data' => $data->resolve(),
];
}
if ($data instanceof ResourceCollection) {
$collection = $data->resolve();
$response = [
'success' => $isSuccess,
'data' => $collection['data'] ?? $collection,
];
if (isset($collection['meta'])) {
$response['meta'] = $collection['meta'];
}
if (isset($collection['links'])) {
$response['links'] = $collection['links'];
}
return $response;
}
return [
'success' => $isSuccess,
'data' => $data,
];
}
}📝 Register Service Provider
Add to config/app.php:
php
'providers' => [
// Other providers...
App\Providers\ResponseMacroServiceProvider::class,
],Or in bootstrap/providers.php (Laravel 11+):
php
return [
App\Providers\AppServiceProvider::class,
App\Providers\ResponseMacroServiceProvider::class,
];🎨 API Resource Examples
Basic Resource
php
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'role' => $this->role,
'is_active' => $this->is_active,
'email_verified_at' => $this->email_verified_at?->toIso8601String(),
'created_at' => $this->created_at->toIso8601String(),
'updated_at' => $this->updated_at->toIso8601String(),
// Conditional relationships
'profile' => ProfileResource::make($this->whenLoaded('profile')),
'posts' => PostResource::collection($this->whenLoaded('posts')),
// Conditional attributes
'permissions' => $this->when(
$request->user()?->isAdmin(),
fn () => $this->permissions->pluck('name')
),
];
}
}Resource Collection
php
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* The resource that this resource collects.
*/
public $collects = UserResource::class;
/**
* Transform the resource collection into an array.
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total' => $this->total(),
'count' => $this->count(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'total_pages' => $this->lastPage(),
],
'links' => [
'first' => $this->url(1),
'last' => $this->url($this->lastPage()),
'prev' => $this->previousPageUrl(),
'next' => $this->nextPageUrl(),
],
];
}
/**
* Add additional meta information.
*/
public function with(Request $request): array
{
return [
'timestamp' => now()->toIso8601String(),
];
}
}🎮 Controller Usage Examples
Named Arguments Best Practice
All examples below use named arguments for clarity and maintainability. This makes the code self-documenting and easier to understand at a glance.
Using Success Macro with JsonResource
php
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreUserRequest;
use App\Http\Requests\UpdateUserRequest;
use App\Http\Resources\UserResource;
use App\Http\Resources\UserCollection;
use App\Services\UserService;
use Illuminate\Http\JsonResponse;
class UserController extends Controller
{
public function __construct(
private UserService $userService
) {}
/**
* Display a listing of users.
*/
public function index(): JsonResponse
{
$users = $this->userService->getPaginatedUsers();
return response()->success(
data: new UserCollection($users),
message: 'Users retrieved successfully'
);
}
/**
* Display the specified user.
*/
public function show(int $id): JsonResponse
{
try {
$user = $this->userService->getUserById(id: $id);
return response()->success(
data: new UserResource($user),
message: 'User retrieved successfully'
);
} catch (UserNotFoundException $e) {
return response()->notFound(
resource: 'User',
identifier: (string) $id
);
}
}
/**
* Store a newly created user.
*/
public function store(StoreUserRequest $request): JsonResponse
{
$user = $this->userService->createUser(data: $request->validated());
return response()->created(
data: new UserResource($user),
message: 'User created successfully',
location: route('api.users.show', $user->id)
);
}
/**
* Update the specified user.
*/
public function update(UpdateUserRequest $request, int $id): JsonResponse
{
try {
$user = $this->userService->updateUser(
id: $id,
data: $request->validated()
);
return response()->success(
data: new UserResource($user),
message: 'User updated successfully'
);
} catch (UserNotFoundException $e) {
return response()->notFound(
resource: 'User',
identifier: (string) $id
);
}
}
/**
* Remove the specified user.
*/
public function destroy(int $id): JsonResponse
{
try {
$this->userService->deleteUser(id: $id);
return response()->noContent();
} catch (UserNotFoundException $e) {
return response()->notFound(
resource: 'User',
identifier: (string) $id
);
}
}
}📊 Response Format Examples
Success with Single Resource
json
{
"success": true,
"message": "User retrieved successfully",
"data": {
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"role": "admin",
"is_active": true,
"email_verified_at": "2024-01-15T10:30:00Z",
"created_at": "2024-01-10T08:00:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
}Success with Collection (Paginated)
json
{
"success": true,
"message": "Users retrieved successfully",
"data": [
{
"id": 1,
"name": "John Doe",
"email": "[email protected]"
},
{
"id": 2,
"name": "Jane Smith",
"email": "[email protected]"
}
],
"meta": {
"total": 100,
"count": 2,
"per_page": 15,
"current_page": 1,
"total_pages": 7
},
"links": {
"first": "http://api.example.com/users?page=1",
"last": "http://api.example.com/users?page=7",
"prev": null,
"next": "http://api.example.com/users?page=2"
}
}Created Response (201)
json
{
"success": true,
"message": "User created successfully",
"data": {
"id": 1,
"name": "John Doe",
"email": "[email protected]"
}
}Validation Error (422)
json
{
"success": false,
"error": {
"message": "Validation failed",
"code": "VALIDATION_ERROR",
"details": {
"email": [
"The email field is required.",
"The email must be a valid email address."
],
"password": [
"The password must be at least 12 characters."
]
}
}
}Not Found Error (404)
json
{
"success": false,
"error": {
"message": "User with identifier '123' not found",
"code": "RESOURCE_NOT_FOUND"
}
}Unauthorized Error (401)
json
{
"success": false,
"error": {
"message": "Invalid credentials",
"code": "UNAUTHORIZED"
}
}Forbidden Error (403)
json
{
"success": false,
"error": {
"message": "You don't have permission to access this resource",
"code": "FORBIDDEN"
}
}Generic Error (400)
json
{
"success": false,
"error": {
"message": "Invalid operation",
"code": "INVALID_OPERATION",
"details": {
"reason": "Account is suspended"
}
}
}🔧 Exception Handler Integration
Update app/Exceptions/Handler.php:
php
<?php
declare(strict_types=1);
namespace App\Exceptions;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* Register the exception handling callbacks for the application.
*/
public function register(): void
{
$this->renderable(function (ValidationException $e, $request) {
if ($request->expectsJson()) {
return response()->validationError(
errors: $e->errors(),
message: $e->getMessage()
);
}
});
$this->renderable(function (NotFoundHttpException $e, $request) {
if ($request->expectsJson()) {
return response()->notFound(
resource: 'Resource',
identifier: null,
message: $e->getMessage() ?: 'The requested resource was not found'
);
}
});
$this->renderable(function (AuthenticationException $e, $request) {
if ($request->expectsJson()) {
return response()->unauthorized(
message: $e->getMessage() ?: 'Unauthenticated'
);
}
});
$this->renderable(function (AccessDeniedHttpException $e, $request) {
if ($request->expectsJson()) {
return response()->forbidden(
message: $e->getMessage() ?: 'Forbidden'
);
}
});
}
}📋 Available Macros
| Macro | Usage | Status Code |
|---|---|---|
success() | Success with data | 200 |
created() | Resource created | 201 |
noContent() | Success, no data | 204 |
error() | Generic error | 400 |
unauthorized() | Authentication error | 401 |
forbidden() | Authorization error | 403 |
notFound() | Resource not found | 404 |
validationError() | Validation failed | 422 |
✅ Benefits
- Consistency - All API responses follow the same structure
- Type Safety - Proper JsonResource and ResourceCollection usage
- Maintainability - Single source of truth for response formats
- Developer Experience - Easy to use macros with named arguments
- Frontend Friendly - Predictable response structure
- Documentation - Self-documenting API responses
- Readability - Named arguments make code intentions clear
🎯 Named Arguments Advantages
Using named arguments in response macros provides significant benefits:
1. Self-Documenting Code
php
// Clear what each parameter represents
return response()->error(
message: 'Payment failed',
code: 'PAYMENT_GATEWAY_ERROR',
status: 400,
errors: ['transaction_id' => $transactionId]
);2. Skip Optional Parameters
php
// No need to pass null for intermediate parameters
return response()->success(
data: $user,
message: 'User retrieved'
// status: 200 is default, no need to specify
// meta: [] is default, no need to specify
);3. Parameter Order Independence
php
// Parameters can be in any order
return response()->notFound(
identifier: '123',
resource: 'User', // Order doesn't matter
message: 'User not found'
);4. IDE Autocomplete Support
Named arguments provide better IDE autocomplete and inline documentation, reducing errors and improving developer productivity.
5. Code Reviews
Reviewers can immediately understand what each parameter does without referencing the function signature.
📝 Best Practices
✅ Always Use Named Arguments
php
// ✅ Excellent: Clear, self-documenting
return response()->success(
data: new UserResource($user),
message: 'User created successfully',
status: 201,
meta: ['total_users' => $totalUsers]
);
// ❌ Avoid: Less readable, order-dependent
return response()->success(
new UserResource($user),
'User created successfully',
201,
['total_users' => $totalUsers]
);✅ Consistent Error Codes
php
// Define error code constants
class ErrorCodes
{
public const VALIDATION_ERROR = 'VALIDATION_ERROR';
public const RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND';
public const UNAUTHORIZED = 'UNAUTHORIZED';
public const PAYMENT_FAILED = 'PAYMENT_FAILED';
}
// Use constants in responses
return response()->error(
message: 'Payment processing failed',
code: ErrorCodes::PAYMENT_FAILED,
status: 400
);✅ Always Use API Resources
php
// ✅ Good: Use JsonResource for transformation
return response()->success(
data: new UserResource($user),
message: 'User retrieved'
);
// ❌ Bad: Raw model exposure
return response()->success(
data: $user,
message: 'User retrieved'
);✅ Meaningful Messages
php
// ✅ Good: Descriptive, actionable message
return response()->validationError(
errors: $validator->errors(),
message: 'Please check the form data and try again'
);
// ❌ Bad: Generic, unhelpful message
return response()->validationError(
errors: $validator->errors(),
message: 'Error'
);📝 Response Standards: Always use these response macros with named arguments for API endpoints to maintain consistency, readability, and maintainability across the entire application.