Skip to content

📦 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 ResponseMacroServiceProvider

Complete 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

MacroUsageStatus Code
success()Success with data200
created()Resource created201
noContent()Success, no data204
error()Generic error400
unauthorized()Authentication error401
forbidden()Authorization error403
notFound()Resource not found404
validationError()Validation failed422

✅ 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.

Built with VitePress