├── .gitignore ├── README.md ├── composer.json ├── composer.lock ├── config └── api-response.php └── src ├── ApiResponseServiceProvider.php ├── Exceptions └── ExceptionHandler.php ├── Http ├── ApiResponseHandler.php └── Middleware │ └── EnsureApiResponse.php ├── Interfaces └── HandlesResponse.php └── Traits └── HasApiResponse.php /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /public/hot 3 | /public/storage 4 | /storage/*.key 5 | /vendor 6 | .env 7 | .env.backup 8 | .phpunit.result.cache 9 | Homestead.json 10 | Homestead.yaml 11 | npm-debug.log 12 | yarn-error.log 13 | /.idea 14 | /.vscode 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel API Response 2 | 3 | A Laravel package that provides a standardized, consistent way to format API responses with proper status codes, messages, and error handling. 4 | 5 | ## Features 6 | 7 | - Modern PHP 8.0+ implementation with type hints and return types 8 | - Consistent JSON response structure 9 | - Automatic handling of Laravel Models, Collections, and Paginated responses 10 | - Configurable resource naming for collections 11 | - Comprehensive exception handling 12 | - Debug mode with stack traces 13 | - Support for custom headers and messages 14 | 15 | ## Installation 16 | 17 | Install the package via composer: 18 | 19 | ```bash 20 | composer require faridibin/laravel-api-response 21 | ``` 22 | 23 | ## Configuration 24 | 25 | Publish the configuration file: 26 | 27 | ```bash 28 | php artisan vendor:publish --tag="api-response-config" 29 | ``` 30 | 31 | ### Configuration Options 32 | 33 | The `config/api-response.php` file includes: 34 | 35 | ```php 36 | return [ 37 | // Exception handlers 38 | 'exceptions' => [ 39 | ModelNotFoundException::class => [ 40 | 'setStatusCode' => 404, 41 | 'setModelNotFoundMessage' => 'Resource not found' 42 | ], 43 | ValidationException::class => function($exception, $handler) { 44 | $handler 45 | ->setStatusCode(422) 46 | ->setMessage('Validation failed') 47 | ->mergeErrors($exception->errors()); 48 | } 49 | ], 50 | 51 | // Enable stack traces in local environment 52 | 'trace' => env('APP_ENV') === 'local', 53 | 54 | // Use plural resource names for collections 55 | 'resource_name' => true, 56 | ]; 57 | ``` 58 | 59 | ## Basic Usage 60 | 61 | Add the middleware to your API routes in `app/Http/Kernel.php`: 62 | 63 | ```php 64 | protected $middlewareGroups = [ 65 | 'api' => [ 66 | \Faridibin\LaravelApiResponse\Http\Middleware\EnsureApiResponse::class, 67 | ], 68 | ]; 69 | ``` 70 | 71 | ### Response Structure 72 | 73 | The package provides a consistent response structure: 74 | 75 | ```json 76 | { 77 | "data": {}, 78 | "message": "Optional message", 79 | "errors": [], 80 | "success": true, 81 | "status": "success", 82 | "status_code": 200, 83 | "status_text": "OK" 84 | } 85 | ``` 86 | 87 | ## Examples 88 | 89 | ### Returning Models 90 | 91 | #### Single Model 92 | 93 | ```php 94 | public function show(User $user) 95 | { 96 | return $user; 97 | } 98 | ``` 99 | 100 | Response: 101 | 102 | ```json 103 | { 104 | "data": { 105 | "user": { 106 | "id": 1, 107 | "name": "Crystal Farrell", 108 | "email": "berenice.bednar@example.org", 109 | "email_verified_at": "2025-02-02T02:20:17.000000Z", 110 | "created_at": "2025-02-02T02:20:17.000000Z", 111 | "updated_at": "2025-02-02T02:20:17.000000Z" 112 | } 113 | }, 114 | "errors": [], 115 | "success": true, 116 | "status": "success", 117 | "status_code": 200, 118 | "status_text": "OK" 119 | } 120 | ``` 121 | 122 | ### Collection Handling 123 | 124 | #### Basic Collection 125 | 126 | ```php 127 | public function index() 128 | { 129 | return User::all(); 130 | } 131 | ``` 132 | 133 | Response includes automatically pluralized resource name: 134 | 135 | ```json 136 | { 137 | "data": { 138 | "users": [ 139 | { 140 | "id": 1, 141 | "name": "Crystal Farrell", 142 | "email": "berenice.bednar@example.org", 143 | "email_verified_at": "2025-02-02T02:20:17.000000Z", 144 | "created_at": "2025-02-02T02:20:17.000000Z", 145 | "updated_at": "2025-02-02T02:20:17.000000Z" 146 | } 147 | ] 148 | }, 149 | "success": true, 150 | "status": "success", 151 | "status_code": 200, 152 | "status_text": "OK" 153 | } 154 | ``` 155 | 156 | #### Pagination 157 | 158 | ```php 159 | public function index() 160 | { 161 | return User::paginate(); 162 | } 163 | ``` 164 | 165 | ## Upgrading from laravel-json-response 166 | 167 | Key changes when upgrading from the previous version: 168 | 169 | 1. Namespace Change: 170 | 171 | - Old: `Faridibin\LaravelJsonResponse` 172 | - New: `Faridibin\LaravelApiResponse` 173 | 174 | 2. Middleware: 175 | 176 | - Old: `OutputJsonResponse` 177 | - New: `EnsureApiResponse` 178 | 179 | 3. Trait: 180 | 181 | - Old: `HasJson` 182 | - New: `HasApiResponse` 183 | 184 | 4. Method Changes: 185 | 186 | ```php 187 | // Old 188 | json_response()->error('message'); 189 | 190 | // New 191 | $this->setMessage('message')->mergeErrors(['error']); 192 | ``` 193 | 194 | ## Troubleshooting 195 | 196 | ### Common Issues 197 | 198 | 1. Response Not Formatting 199 | 200 | ```php 201 | // Ensure middleware is registered correctly in Kernel.php 202 | protected $middlewareGroups = [ 203 | 'api' => [ 204 | \Faridibin\LaravelApiResponse\Http\Middleware\EnsureApiResponse::class, 205 | ], 206 | ]; 207 | ``` 208 | 209 | 2. Resource Names Not Working 210 | 211 | ```php 212 | // Verify config/api-response.php has: 213 | 'resource_name' => true, 214 | ``` 215 | 216 | 3. Exception Handler Not Working 217 | 218 | ```php 219 | // Check exception configuration: 220 | 'exceptions' => [ 221 | YourException::class => [ 222 | 'setStatusCode' => 404, 223 | 'setMessage' => 'Custom message' 224 | ] 225 | ] 226 | ``` 227 | 228 | 4. Stack Traces Not Showing 229 | - Ensure your environment is set to 'local' 230 | - Check `trace` config is enabled 231 | 232 | ## Contributing 233 | 234 | Contributions are welcome! Please feel free to submit a Pull Request. 235 | 236 | ## License 237 | 238 | This package is open-sourced software licensed under the MIT license. 239 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "faridibin/laravel-api-response", 3 | "description": "A Laravel package for consistently formatted API responses with support for JSON, XML, and YAML.", 4 | "type": "library", 5 | "authors": [ 6 | { 7 | "name": "Farid Adam", 8 | "email": "faridibin@gmail.com" 9 | } 10 | ], 11 | "license": "MIT", 12 | "require": { 13 | "php": "^8.0", 14 | "symfony/yaml": "^7.2" 15 | }, 16 | "require-dev": { 17 | "orchestra/testbench": "^9.6" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "Faridibin\\LaravelApiResponse\\": "src/" 22 | }, 23 | "files": [] 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Faridibin\\LaravelApiResponse\\Tests\\": "tests/" 28 | } 29 | }, 30 | "scripts": { 31 | "post-autoload-dump": [ 32 | "@php ./vendor/bin/testbench package:discover --ansi" 33 | ] 34 | }, 35 | "extra": { 36 | "laravel": { 37 | "providers": [ 38 | "Faridibin\\LaravelApiResponse\\ApiResponseServiceProvider" 39 | ] 40 | } 41 | }, 42 | "minimum-stability": "stable" 43 | } 44 | -------------------------------------------------------------------------------- /config/api-response.php: -------------------------------------------------------------------------------- 1 | 'json', 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | Response Case 28 | |-------------------------------------------------------------------------- 29 | | 30 | | This option allows you to set the case of the response data. 31 | | The supported cases are 'camel', 'snake'. 32 | | 33 | */ 34 | 35 | 'case' => 'snake', 36 | 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | Use Resource Name 40 | |-------------------------------------------------------------------------- 41 | | 42 | | This option allows you to set the resource name in the response data. 43 | | When set to true, the API will include the resource name in the response data. 44 | | 45 | */ 46 | 47 | 'resource_name' => true, 48 | 49 | /* 50 | |-------------------------------------------------------------------------- 51 | | The Exceptions To Handle 52 | |-------------------------------------------------------------------------- 53 | | 54 | | This is a list of exceptions that the API should handle. 55 | | When an exception is thrown, the API will catch it and return a response 56 | | based on the exception. 57 | | 58 | */ 59 | 60 | 'exceptions' => [ 61 | 62 | /** 63 | * The AuthenticationException is thrown when the user is not authenticated. 64 | */ 65 | AuthenticationException::class => [ 66 | 'setMessage' => 'Unauthenticated.', 67 | 'setStatusCode' => 401, 68 | ], 69 | 70 | /** 71 | * The AuthorizationException is thrown when the user is not authorized to perform an action. 72 | */ 73 | AuthorizationException::class => [ 74 | 'setMessage' => 'This action is unauthorized.', 75 | 'setStatusCode' => 403, 76 | ], 77 | 78 | /** 79 | * The ModelNotFoundException is thrown when a model is not found. 80 | */ 81 | ModelNotFoundException::class => [ 82 | 'setModelNotFoundMessage' => 'No query results for model.', 83 | 'setStatusCode' => 404, 84 | ], 85 | 86 | /** 87 | * The ValidationException is thrown when a validation fails. 88 | */ 89 | ValidationException::class => function (ValidationException $e, HandlesResponse $json): void { 90 | $json 91 | ->setMessage($e->getMessage()) 92 | ->mergeErrors($e->errors()) 93 | ->setStatusCode(Response::HTTP_UNPROCESSABLE_ENTITY); 94 | }, 95 | 96 | /** 97 | * The Exception is thrown when an error occurs while processing a request. 98 | */ 99 | \Exception::class => [ 100 | 'setMessage' => 'An error occurred while processing your request.', 101 | 'setStatusCode' => 500, 102 | ], 103 | 104 | ], 105 | 106 | /* 107 | |-------------------------------------------------------------------------- 108 | | Response Data Tracing 109 | |-------------------------------------------------------------------------- 110 | | 111 | | This option allows you to trace the response data. 112 | | When set to true, the API will trace the response data. 113 | | 114 | */ 115 | 116 | 'trace' => (bool) env('APP_DEBUG', false), 117 | ]; 118 | -------------------------------------------------------------------------------- /src/ApiResponseServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom( 15 | __DIR__ . '/../config/api-response.php', 16 | 'api-response' 17 | ); 18 | } 19 | 20 | /** 21 | * Bootstrap services. 22 | */ 23 | public function boot(): void 24 | { 25 | $this->publishes([ 26 | __DIR__ . '/../config/api-response.php' => config_path('api-response.php'), 27 | ], 'api-response-config'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Exceptions/ExceptionHandler.php: -------------------------------------------------------------------------------- 1 | getPrevious()) { 26 | $exception = $exception->getPrevious(); 27 | } 28 | 29 | if (isset($exception->status)) { 30 | $this->statusCode = $exception->status; 31 | } 32 | 33 | if (isset($exceptions[get_class($exception)])) { 34 | $handlers = $exceptions[get_class($exception)]; 35 | 36 | if (is_array($handlers)) { 37 | foreach ($handlers as $handler => $params) { 38 | match (true) { 39 | is_callable([$this, $handler]) => $this->$handler($params), 40 | is_callable($handler) => $handler($params, $this), 41 | default => null, 42 | }; 43 | } 44 | } elseif (is_callable($handlers)) { 45 | $handlers($exception, $this); 46 | } else if ((new ReflectionClass($handlers))->hasMethod('__invoke')) { 47 | (new $handlers)($exception, $this); 48 | } 49 | } 50 | 51 | if ($this->shouldTrace($exception)) { 52 | $this->mergeErrors(['trace' => $exception->getTrace()]); 53 | } 54 | 55 | if ($this->getStatusCode() < 400) { 56 | $this->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR); 57 | } 58 | 59 | return response()->json( 60 | $this->getResponse(), 61 | $this->getStatusCode(), 62 | $this->getHeaders(), 63 | ); 64 | } 65 | 66 | /** 67 | * Set response message for the ModelNotFoundException. 68 | * @param string $message 69 | * @return $this 70 | */ 71 | public function setModelNotFoundMessage(string $message): static 72 | { 73 | // TODO: Implement setModelNotFoundMessage() method. Make it dynamic. 74 | $this->message = $message; 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * Determine if the response should trace the exception. 81 | * @param Exception $exception 82 | * @return bool 83 | */ 84 | protected function shouldTrace(Exception $exception): bool 85 | { 86 | if (!in_array(get_class($exception), config('api-response.excluded_trace'))) { 87 | return config('api-response.trace', app()->environment('local')); 88 | } 89 | 90 | return false; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Http/ApiResponseHandler.php: -------------------------------------------------------------------------------- 1 | getOriginalContent(); 26 | 27 | match (true) { 28 | $content instanceof Model => $this->handleModel($content), 29 | $content instanceof Arrayable => $this->handleArrayable($content), 30 | is_array($content) => $this->set($content), 31 | default => $this->mergeData([$content]) 32 | }; 33 | 34 | if (method_exists($response, 'getStatusCode')) { 35 | $this->setStatusCode($response->getStatusCode()); 36 | } 37 | 38 | return response()->json( 39 | $this->getResponse(), 40 | $this->getStatusCode(), 41 | $this->getHeaders(), 42 | ); 43 | } 44 | 45 | /** 46 | * Handle a model response. 47 | * 48 | * @param \Illuminate\Database\Eloquent\Model $model 49 | * @return void 50 | */ 51 | protected function handleModel(Model $model): void 52 | { 53 | $key = Str::snake(class_basename($model)); 54 | 55 | $this->set($key, $model->toArray()); 56 | } 57 | 58 | /** 59 | * Handle an arrayable response. 60 | * 61 | * @param \Illuminate\Contracts\Support\Arrayable $arrayable 62 | * @return void 63 | */ 64 | protected function handleArrayable(LengthAwarePaginator|Arrayable $arrayable): void 65 | { 66 | $key = 'data'; 67 | 68 | if (config('api-response.resource_name')) { 69 | $key = $this->getResourceName($arrayable); 70 | } 71 | 72 | if ($arrayable instanceof LengthAwarePaginator) { 73 | $this->handlePaginator($arrayable, $key); 74 | } else { 75 | $this->set($key, $arrayable->toArray()); 76 | } 77 | } 78 | 79 | /** 80 | * Handle a paginator response. 81 | * 82 | * @param \Illuminate\Pagination\LengthAwarePaginator $paginator 83 | * @param string $key 84 | * @return void 85 | */ 86 | protected function handlePaginator(LengthAwarePaginator $paginator, string $key): void 87 | { 88 | $data = $paginator->toArray(); 89 | 90 | if ($key !== 'data') { 91 | $data[$key] = $data['data']; 92 | unset($data['data']); 93 | } 94 | 95 | $this->set($data); 96 | } 97 | 98 | /** 99 | * Get the resource name. 100 | * 101 | * @param \Illuminate\Contracts\Support\Arrayable $arrayable 102 | * @return string 103 | */ 104 | protected function getResourceName(Arrayable $arrayable): string 105 | { 106 | if ($arrayable->isEmpty()) { 107 | $resource = basename($arrayable->path()); 108 | } else { 109 | $first = $arrayable->first(); 110 | 111 | if ($first instanceof Model) { 112 | $resource = class_basename($first); 113 | } else { 114 | $resource = class_basename($arrayable); 115 | 116 | $resource = str_replace(['Collection', 'Resource'], '', $resource); 117 | } 118 | } 119 | 120 | return (string) Str::of($resource)->plural()->lower(); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Http/Middleware/EnsureApiResponse.php: -------------------------------------------------------------------------------- 1 | exception instanceof \Exception) { 24 | return $this->handleException($response->exception); 25 | } 26 | 27 | return $this->handleResponse($response); 28 | } 29 | 30 | /** 31 | * Handle a successful response. 32 | * 33 | * @param \Exception|\Symfony\Component\HttpFoundation\Response $response 34 | * @return \Symfony\Component\HttpFoundation\Response 35 | */ 36 | private function handleResponse(Response $response): Response 37 | { 38 | return (new ApiResponseHandler)($response); 39 | } 40 | 41 | /** 42 | * Handle an exception response. 43 | * 44 | * @param \Exception $exception 45 | * @return \Symfony\Component\HttpFoundation\Response 46 | */ 47 | private function handleException(Exception $exception): Response 48 | { 49 | return (new ExceptionHandler)($exception); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Interfaces/HandlesResponse.php: -------------------------------------------------------------------------------- 1 | headers = $headers; 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * Get the response headers. 59 | * 60 | * @return array 61 | */ 62 | public function getHeaders(): array 63 | { 64 | return $this->headers; 65 | } 66 | 67 | /** 68 | * Sets the HTTP status code to be used for the response. 69 | * 70 | * @param int $statusCode 71 | * @return $this 72 | */ 73 | public function setStatusCode(int $statusCode): static 74 | { 75 | $this->statusCode = $statusCode; 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * Get the response status code. 82 | * 83 | * @return int 84 | */ 85 | public function getStatusCode(): int 86 | { 87 | return $this->statusCode; 88 | } 89 | 90 | /** 91 | * Set the response message. 92 | * 93 | * @param string $message 94 | */ 95 | public function setMessage(string $message): static 96 | { 97 | $this->message = $message; 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * Set a data value. 104 | * 105 | * @param mixed $key 106 | * @param mixed $value 107 | * @return static 108 | */ 109 | public function set(mixed $key, mixed $value = null): static 110 | { 111 | match (true) { 112 | is_array($key) => $this->data = array_merge($this->data, $key), 113 | default => $this->data[$key] = $value 114 | }; 115 | 116 | return $this; 117 | } 118 | 119 | public function getData(): array 120 | { 121 | return $this->data; 122 | } 123 | 124 | /** 125 | * Merge the response errors. 126 | * 127 | * @param array $errors 128 | * @return static 129 | */ 130 | public function mergeErrors(array $errors): static 131 | { 132 | $this->errors = array_merge($this->errors, $errors); 133 | 134 | return $this; 135 | } 136 | 137 | public function mergeData(array $data): static 138 | { 139 | $this->data = array_merge($this->data, $data); 140 | 141 | return $this; 142 | } 143 | 144 | /** 145 | * Is response successful? 146 | * 147 | * @final 148 | */ 149 | public function isSuccessful(): bool 150 | { 151 | return $this->statusCode >= 200 && $this->statusCode < 300; 152 | } 153 | 154 | /** 155 | * Get the response data. 156 | * 157 | * @return array 158 | */ 159 | protected function getResponse(): array 160 | { 161 | // TODO: Implement case and return type handling. 162 | 163 | $response = [ 164 | 'data' => $this->getData(), 165 | 'message' => $this->message, 166 | 'errors' => $this->errors, 167 | 'success' => $this->isSuccessful(), 168 | 'status' => $this->statusCode === 200 ? 'success' : 'error', 169 | 'status_code' => $this->statusCode, 170 | 'status_text' => Response::$statusTexts[$this->statusCode] ?? 'unknown status' 171 | ]; 172 | 173 | return array_filter($response, fn($value) => !is_null($value)); 174 | } 175 | } 176 | --------------------------------------------------------------------------------