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