├── pint.json ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── phpstan.neon ├── .editorconfig ├── src ├── Response.php ├── Http │ ├── Exceptions │ │ └── Handler.php │ └── Middleware │ │ ├── SetAcceptHeader.php │ │ ├── Etag.php │ │ └── ThrottleRequests.php ├── Providers │ ├── LumenServiceProvider.php │ └── LaravelServiceProvider.php ├── Support │ ├── Facades │ │ ├── Format.php │ │ └── Response.php │ ├── Traits │ │ ├── JsonResponseTrait.php │ │ └── ExceptionTrait.php │ └── Format.php └── Contract │ └── ResponseFormat.php ├── .all-contributorsrc ├── LICENSE ├── composer.json ├── config └── response.php ├── README.md └── README-EN.md /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel" 3 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "composer" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/larastan/larastan/extension.neon 3 | 4 | parameters: 5 | level: 6 6 | paths: 7 | - src 8 | treatPhpDocTypesAsCertain: false -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false 10 | 11 | [*.{vue,js,scss}] 12 | charset = utf-8 13 | indent_style = space 14 | indent_size = 2 15 | end_of_line = lf 16 | insert_final_newline = true 17 | trim_trailing_whitespace = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Jiannei\Response\Laravel; 13 | 14 | use Jiannei\Response\Laravel\Support\Traits\JsonResponseTrait; 15 | 16 | class Response 17 | { 18 | use JsonResponseTrait; 19 | } 20 | -------------------------------------------------------------------------------- /src/Http/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Jiannei\Response\Laravel\Http\Exceptions; 13 | 14 | use Jiannei\Response\Laravel\Support\Traits\ExceptionTrait; 15 | 16 | class Handler extends \Illuminate\Foundation\Exceptions\Handler 17 | { 18 | use ExceptionTrait; 19 | } 20 | -------------------------------------------------------------------------------- /src/Providers/LumenServiceProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Jiannei\Response\Laravel\Providers; 13 | 14 | class LumenServiceProvider extends LaravelServiceProvider 15 | { 16 | protected function setupConfig(): void 17 | { 18 | $path = dirname(__DIR__, 2).'/config/response.php'; 19 | 20 | $this->mergeConfigFrom($path, 'response'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "vanthao03596", 10 | "name": "Pham Thao", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/34786441?v=4", 12 | "profile": "https://github.com/vanthao03596", 13 | "contributions": [ 14 | "design" 15 | ] 16 | }, 17 | { 18 | "login": "guanguans", 19 | "name": "guanguans", 20 | "avatar_url": "https://avatars.githubusercontent.com/u/22309277?v=4", 21 | "profile": "https://www.guanguans.cn", 22 | "contributions": [ 23 | "bug" 24 | ] 25 | } 26 | ], 27 | "contributorsPerLine": 7, 28 | "projectName": "laravel-response", 29 | "projectOwner": "jiannei", 30 | "repoType": "github", 31 | "repoHost": "https://github.com", 32 | "skipCi": true 33 | } 34 | -------------------------------------------------------------------------------- /src/Http/Middleware/SetAcceptHeader.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Jiannei\Response\Laravel\Http\Middleware; 13 | 14 | use Closure; 15 | use Illuminate\Http\Request; 16 | use Illuminate\Support\Str; 17 | 18 | class SetAcceptHeader 19 | { 20 | /** 21 | * Handle an incoming request. 22 | * 23 | * @param \Closure(\Illuminate\Http\Request): mixed $next 24 | * @return mixed 25 | */ 26 | public function handle(Request $request, Closure $next, string $type = 'json') 27 | { 28 | Str::contains($request->header('Accept') ?? '', $contentType = "application/$type") or 29 | $request->headers->set('Accept', $contentType); 30 | 31 | return $next($request); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 jiannei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Support/Facades/Format.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Jiannei\Response\Laravel\Support\Facades; 13 | 14 | use Illuminate\Contracts\Pagination\Paginator; 15 | use Illuminate\Http\JsonResponse; 16 | use Illuminate\Http\Resources\Json\JsonResource; 17 | use Illuminate\Http\Resources\Json\ResourceCollection; 18 | use Illuminate\Pagination\AbstractCursorPaginator; 19 | use Illuminate\Pagination\AbstractPaginator; 20 | use Illuminate\Support\Facades\Facade as IlluminateFacade; 21 | 22 | /** 23 | * @method static \Jiannei\Response\Laravel\Support\Format data(mixed $data = null, string $message = '', int|\BackedEnum $code = 200, $error = null) 24 | * @method static array|null get() 25 | * @method static array paginator(AbstractPaginator|AbstractCursorPaginator|Paginator $resource) 26 | * @method static array resourceCollection(ResourceCollection $collection) 27 | * @method static array jsonResource(JsonResource $resource) 28 | * @method static JsonResponse response() 29 | * 30 | * @see \Jiannei\Response\Laravel\Support\Format 31 | */ 32 | class Format extends IlluminateFacade 33 | { 34 | protected static function getFacadeAccessor() 35 | { 36 | return \Jiannei\Response\Laravel\Contract\ResponseFormat::class; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Providers/LaravelServiceProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Jiannei\Response\Laravel\Providers; 13 | 14 | use Illuminate\Support\ServiceProvider; 15 | use Jiannei\Response\Laravel\Contract\ResponseFormat; 16 | use Jiannei\Response\Laravel\Http\Exceptions\Handler; 17 | use Jiannei\Response\Laravel\Support\Format; 18 | 19 | class LaravelServiceProvider extends ServiceProvider 20 | { 21 | public function register() 22 | { 23 | $this->setupConfig(); 24 | 25 | $this->app->singleton( 26 | \Illuminate\Contracts\Debug\ExceptionHandler::class, 27 | Handler::class 28 | ); 29 | 30 | $this->app->singleton(ResponseFormat::class, function ($app) { 31 | $formatter = $app->config->get('response.format.class'); 32 | $config = $app->config->get('response.format.config'); 33 | 34 | return match (true) { 35 | class_exists($formatter) && is_subclass_of($formatter, ResponseFormat::class) => new $formatter($config), 36 | default => new Format($config), 37 | }; 38 | }); 39 | } 40 | 41 | protected function setupConfig(): void 42 | { 43 | $path = dirname(__DIR__, 2).'/config/response.php'; 44 | 45 | if ($this->app->runningInConsole()) { 46 | $this->publishes([$path => config_path('response.php')], 'response'); 47 | } 48 | 49 | $this->mergeConfigFrom($path, 'response'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jiannei/laravel-response", 3 | "description": "Laravel api response data format.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "jiannei", 8 | "email": "longjian.huang@foxmail.com" 9 | } 10 | ], 11 | "require": { 12 | "php": "^8.1", 13 | "ext-json": "*" 14 | }, 15 | "require-dev": { 16 | "orchestra/testbench": "^9.0|^10.0", 17 | "pestphp/pest": "^3.11", 18 | "jiannei/laravel-enum": "^4.0", 19 | "laravel/pint": "^1.18.1", 20 | "larastan/larastan": "^3.0" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Jiannei\\Response\\Laravel\\": "src" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "Jiannei\\Response\\Laravel\\Tests\\": "tests" 30 | } 31 | }, 32 | "extra": { 33 | "laravel": { 34 | "providers": [ 35 | "Jiannei\\Response\\Laravel\\Providers\\LaravelServiceProvider" 36 | ], 37 | "aliases": { 38 | "Response": "Jiannei\\Response\\Laravel\\Support\\Facades\\Response" 39 | } 40 | } 41 | }, 42 | "scripts": { 43 | "test": "vendor/bin/pest", 44 | "test-coverage": "vendor/bin/pest --coverage", 45 | "test-coverage-html": "vendor/bin/pest --coverage-html coverage", 46 | "style": "vendor/bin/pint", 47 | "analyse": "vendor/bin/phpstan analyse" 48 | }, 49 | "minimum-stability": "dev", 50 | "prefer-stable" : true, 51 | "config": { 52 | "allow-plugins": { 53 | "pestphp/pest-plugin": true 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Support/Facades/Response.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Jiannei\Response\Laravel\Support\Facades; 13 | 14 | use Illuminate\Http\JsonResponse; 15 | use Illuminate\Support\Facades\Facade as IlluminateFacade; 16 | 17 | /** 18 | * @method static JsonResponse accepted($data = null, string $message = '', string $location = '') 19 | * @method static JsonResponse created($data = null, string $message = '', string $location = '') 20 | * @method static JsonResponse noContent(string $message = '') 21 | * @method static JsonResponse localize(int|\BackedEnum $code = 200) 22 | * @method static JsonResponse ok(string $message = '', int|\BackedEnum $code = 200) 23 | * @method static JsonResponse success($data = null, string $message = '', int|\BackedEnum $code = 200) 24 | * @method static JsonResponse errorBadRequest(?string $message = '') 25 | * @method static JsonResponse errorUnauthorized(string $message = '') 26 | * @method static JsonResponse errorForbidden(string $message = '') 27 | * @method static JsonResponse errorNotFound(string $message = '') 28 | * @method static JsonResponse errorMethodNotAllowed(string $message = '') 29 | * @method static JsonResponse fail(string $message = '', int|\BackedEnum $code = 500, $errors = null) 30 | * 31 | * @see \Jiannei\Response\Laravel\Response 32 | */ 33 | class Response extends IlluminateFacade 34 | { 35 | protected static function getFacadeAccessor() 36 | { 37 | return \Jiannei\Response\Laravel\Response::class; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Contract/ResponseFormat.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Jiannei\Response\Laravel\Contract; 13 | 14 | use Illuminate\Contracts\Pagination\Paginator; 15 | use Illuminate\Http\JsonResponse; 16 | use Illuminate\Http\Resources\Json\JsonResource; 17 | use Illuminate\Http\Resources\Json\ResourceCollection; 18 | use Illuminate\Pagination\AbstractCursorPaginator; 19 | use Illuminate\Pagination\AbstractPaginator; 20 | 21 | interface ResponseFormat 22 | { 23 | public function response(): JsonResponse; 24 | 25 | /** 26 | * Get formatted data. 27 | * 28 | * @return array|null 29 | */ 30 | public function get(): ?array; 31 | 32 | /** 33 | * Format data structures. 34 | * 35 | * @return $this 36 | */ 37 | public function data(mixed $data = null, string $message = '', int|\BackedEnum $code = 200, mixed $error = null): static; 38 | 39 | /** 40 | * Format paginator data. 41 | * 42 | * @param AbstractPaginator|AbstractCursorPaginator|Paginator $resource 43 | * @return array 44 | */ 45 | public function paginator(AbstractPaginator|AbstractCursorPaginator|Paginator $resource): array; 46 | 47 | /** 48 | * Format collection resource data. 49 | * 50 | * @return array 51 | */ 52 | public function resourceCollection(ResourceCollection $collection): array; 53 | 54 | /** 55 | * Format JsonResource Data. 56 | * 57 | * @return array 58 | */ 59 | public function jsonResource(JsonResource $resource): array; 60 | } 61 | -------------------------------------------------------------------------------- /src/Http/Middleware/Etag.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Jiannei\Response\Laravel\Http\Middleware; 13 | 14 | use Closure; 15 | use Illuminate\Http\Request; 16 | 17 | class Etag 18 | { 19 | /** 20 | * Implement Etag support. 21 | * 22 | * @param \Illuminate\Http\Request $request the HTTP request 23 | * @param \Closure $next closure for the response 24 | * @return mixed 25 | */ 26 | public function handle(Request $request, Closure $next) 27 | { 28 | // If this was not a get or head request, just return 29 | if (! $request->isMethod('get') && ! $request->isMethod('head')) { 30 | return $next($request); 31 | } 32 | 33 | // Get the initial method sent by client 34 | $initialMethod = $request->method(); 35 | 36 | // Force to get in order to receive content 37 | $request->setMethod('get'); 38 | 39 | // Get response 40 | $response = $next($request); 41 | 42 | // Generate Etag 43 | $etag = md5(json_encode($response->headers->get('origin')).$response->getContent()); 44 | 45 | // Load the Etag sent by client 46 | $requestEtag = str_replace('"', '', $request->getETags()); 47 | 48 | // Check to see if Etag has changed 49 | if ($requestEtag && $requestEtag[0] == $etag) { 50 | $response->setNotModified(); 51 | } 52 | 53 | // Set Etag 54 | $response->setEtag($etag); 55 | 56 | // Set back to original method 57 | $request->setMethod($initialMethod); // set back to original method 58 | 59 | // Send response 60 | return $response; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /config/response.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | return [ 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Set the http status code when the response fails 16 | |-------------------------------------------------------------------------- 17 | | 18 | | the reference options are false, 200, 500 19 | | 20 | | false, stricter http status codes such as 404, 401, 403, 500, etc. will be returned 21 | | 200, All failed responses will also return a 200 status code 22 | | 500, All failed responses return a 500 status code 23 | */ 24 | 25 | 'error_code' => false, 26 | 27 | // lang/zh_CN/enums.php 28 | 'locale' => 'enums', // enums.\Jiannei\Enum\Laravel\Support\Enums\HttpStatusCode::class 29 | 30 | // You can set some attributes (eg:code/message/header/options) for the exception, and it will override the default attributes of the exception 31 | 'exception' => [ 32 | Illuminate\Validation\ValidationException::class => [ 33 | 'code' => 422, 34 | ], 35 | Illuminate\Auth\AuthenticationException::class => [ 36 | ], 37 | Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class => [ 38 | 'message' => '', 39 | ], 40 | Illuminate\Database\Eloquent\ModelNotFoundException::class => [ 41 | 'message' => '', 42 | ], 43 | ], 44 | 45 | // Any key that returns data exists supports custom aliases and display. 46 | 'format' => [ 47 | 'class' => Jiannei\Response\Laravel\Support\Format::class, 48 | 'config' => [ 49 | // key => config 50 | 'status' => ['alias' => 'status', 'show' => true], 51 | 'code' => ['alias' => 'code', 'show' => true], 52 | 'message' => ['alias' => 'message', 'show' => true], 53 | 'error' => ['alias' => 'error', 'show' => true], 54 | 'data' => ['alias' => 'data', 'show' => true], 55 | 'data.data' => ['alias' => 'data.data', 'show' => true], // rows/items/list 56 | ], 57 | ], 58 | ]; 59 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | tests: 11 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ ubuntu-latest ] 17 | php: [8.1, 8.2, 8.3] 18 | laravel: [11.*, 12.*] 19 | dependency-version: [ prefer-stable ] 20 | include: 21 | - laravel: 11.* 22 | testbench: 9.* 23 | pest: ^3.11 24 | enum: ^4.0 25 | - laravel: 12.* 26 | testbench: 10.* 27 | pest: ^3.11 28 | enum: ^4.0 29 | exclude: 30 | # Laravel 12 requires PHP 8.2+ 31 | - php: 8.1 32 | laravel: 12.* 33 | # Laravel 11 requires PHP 8.2+ 34 | - php: 8.1 35 | laravel: 11.* 36 | 37 | steps: 38 | - name: Checkout code 39 | uses: actions/checkout@v4 40 | 41 | - name: Validate composer.json and composer.lock 42 | run: composer validate --strict 43 | 44 | - name: Security audit 45 | run: composer audit --no-dev || true 46 | 47 | - name: Cache Composer packages 48 | uses: actions/cache@v4 49 | with: 50 | path: vendor 51 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 52 | restore-keys: | 53 | ${{ runner.os }}-php- 54 | 55 | - name: Setup PHP 56 | uses: shivammathur/setup-php@v2 57 | with: 58 | php-version: ${{ matrix.php }} 59 | extensions: curl, mbstring, zip, pcntl, sqlite, pdo_sqlite, iconv 60 | coverage: none 61 | 62 | - name: Install dependencies - L${{ matrix.laravel }} 63 | run: | 64 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "pestphp/pest:${{ matrix.pest }}" "jiannei/laravel-enum:${{ matrix.enum }}" --no-interaction --no-update 65 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction 66 | 67 | - name: Run code style check 68 | run: vendor/bin/pint --test 69 | 70 | - name: Execute tests 71 | run: vendor/bin/pest -------------------------------------------------------------------------------- /src/Support/Traits/JsonResponseTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Jiannei\Response\Laravel\Support\Traits; 13 | 14 | use Illuminate\Http\JsonResponse; 15 | use Jiannei\Response\Laravel\Support\Facades\Format; 16 | 17 | trait JsonResponseTrait 18 | { 19 | /** 20 | * Respond with an accepted response and associate a location and/or content if provided. 21 | * 22 | * @param array $data 23 | */ 24 | public function accepted(array $data = [], string $message = '', string $location = ''): JsonResponse 25 | { 26 | return tap($this->success($data, $message, 202), function ($response) use ($location) { 27 | if ($location) { 28 | $response->header('Location', $location); 29 | } 30 | }); 31 | } 32 | 33 | /** 34 | * Respond with a created response and associate a location if provided. 35 | * 36 | * @param array $data 37 | */ 38 | public function created(array $data = [], string $message = '', string $location = ''): JsonResponse 39 | { 40 | return tap($this->success($data, $message, 201), function ($response) use ($location) { 41 | if ($location) { 42 | $response->header('Location', $location); 43 | } 44 | }); 45 | } 46 | 47 | /** 48 | * Respond with a no content response. 49 | */ 50 | public function noContent(string $message = ''): JsonResponse 51 | { 52 | return $this->success(message: $message, code: 204); 53 | } 54 | 55 | /** 56 | * Alias of success method, no need to specify data parameter. 57 | */ 58 | public function ok(string $message = '', int|\BackedEnum $code = 200): JsonResponse 59 | { 60 | return $this->success(message: $message, code: $code); 61 | } 62 | 63 | /** 64 | * Alias of the successful method, no need to specify the message and data parameters. 65 | * You can use ResponseCodeEnum to localize the message. 66 | */ 67 | public function localize(int|\BackedEnum $code = 200): JsonResponse 68 | { 69 | return $this->ok(code: $code); 70 | } 71 | 72 | /** 73 | * Return a 400 bad request error. 74 | */ 75 | public function errorBadRequest(string $message = ''): JsonResponse 76 | { 77 | return $this->fail($message, 400); 78 | } 79 | 80 | /** 81 | * Return a 401 unauthorized error. 82 | */ 83 | public function errorUnauthorized(string $message = ''): JsonResponse 84 | { 85 | return $this->fail($message, 401); 86 | } 87 | 88 | /** 89 | * Return a 403 forbidden error. 90 | */ 91 | public function errorForbidden(string $message = ''): JsonResponse 92 | { 93 | return $this->fail($message, 403); 94 | } 95 | 96 | /** 97 | * Return a 404 not found error. 98 | */ 99 | public function errorNotFound(string $message = ''): JsonResponse 100 | { 101 | return $this->fail($message, 404); 102 | } 103 | 104 | /** 105 | * Return a 405 method not allowed error. 106 | */ 107 | public function errorMethodNotAllowed(string $message = ''): JsonResponse 108 | { 109 | return $this->fail($message, 405); 110 | } 111 | 112 | /** 113 | * Return an fail response. 114 | */ 115 | public function fail(string $message = '', int|\BackedEnum $code = 500, mixed $errors = null): JsonResponse 116 | { 117 | return Format::data(message: $message, code: $code, error: $errors)->response(); 118 | } 119 | 120 | /** 121 | * Return a success response. 122 | */ 123 | public function success(mixed $data = [], string $message = '', int|\BackedEnum $code = 200): JsonResponse 124 | { 125 | return Format::data(data: $data, message: $message, code: $code)->response(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Support/Traits/ExceptionTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Jiannei\Response\Laravel\Support\Traits; 13 | 14 | use Exception; 15 | use Illuminate\Auth\AuthenticationException; 16 | use Illuminate\Http\Exceptions\HttpResponseException; 17 | use Illuminate\Http\JsonResponse; 18 | use Illuminate\Http\Request; 19 | use Illuminate\Support\Arr; 20 | use Illuminate\Support\Facades\Config; 21 | use Illuminate\Validation\ValidationException; 22 | use Jiannei\Response\Laravel\Support\Facades\Response; 23 | use Throwable; 24 | 25 | trait ExceptionTrait 26 | { 27 | /** 28 | * The response builder callback. 29 | * 30 | * @var callable|null 31 | */ 32 | protected static $responseBuilder; 33 | 34 | /** 35 | * Custom Normal Exception response. 36 | * 37 | * @param Request $request 38 | * @param Throwable|Exception $e 39 | * @return JsonResponse 40 | */ 41 | protected function prepareJsonResponse($request, $e) 42 | { 43 | // 要求请求头 header 中包含 /json 或 +json,如:Accept:application/json 44 | // 或者是 ajax 请求,header 中包含 X-Requested-With:XMLHttpRequest; 45 | $exceptionConfig = Config::get('response.exception.'.get_class($e)); 46 | 47 | if ($exceptionConfig === false) { 48 | return parent::prepareJsonResponse($request, $e); 49 | } 50 | 51 | $isHttpException = $this->isHttpException($e); 52 | 53 | $message = is_array($exceptionConfig) && isset($exceptionConfig['message']) && is_scalar($exceptionConfig['message']) ? (string) $exceptionConfig['message'] : ($isHttpException ? $e->getMessage() : 'Server Error'); 54 | $code = is_array($exceptionConfig) && isset($exceptionConfig['code']) ? $exceptionConfig['code'] : ($isHttpException && method_exists($e, 'getStatusCode') ? $e->getStatusCode() : 500); 55 | $header = is_array($exceptionConfig) && isset($exceptionConfig['header']) ? $exceptionConfig['header'] : ($isHttpException && method_exists($e, 'getHeaders') ? $e->getHeaders() : []); 56 | $options = is_array($exceptionConfig) && isset($exceptionConfig['options']) ? $exceptionConfig['options'] : (JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 57 | 58 | return Response::fail($message, $code, $this->convertExceptionToArray($e)) 59 | ->withHeaders($header) 60 | ->setEncodingOptions($options); 61 | } 62 | 63 | /** 64 | * Custom Failed Validation Response for Lumen. 65 | * 66 | * @param array $errors 67 | * @return mixed 68 | * 69 | * @throws HttpResponseException 70 | */ 71 | protected function buildFailedValidationResponse(Request $request, array $errors) 72 | { 73 | if (isset(static::$responseBuilder)) { 74 | return (static::$responseBuilder)($request, $errors); 75 | } 76 | 77 | $firstMessage = Arr::first($errors, null, ''); 78 | $message = $this->extractStringFromMessage($firstMessage); 79 | 80 | $exceptionConfig = Config::get('response.exception'); 81 | $code = is_array($exceptionConfig) ? Arr::get($exceptionConfig, ValidationException::class.'.code', 422) : 422; 82 | $codeValue = is_numeric($code) ? (int) $code : 422; 83 | 84 | return Response::fail($message, $codeValue, $errors); 85 | } 86 | 87 | /** 88 | * Custom Failed Validation Response for Laravel. 89 | * 90 | * @param Request $request 91 | * @return JsonResponse 92 | */ 93 | protected function invalidJson($request, ValidationException $exception) 94 | { 95 | $exceptionConfig = Config::get('response.exception.'.ValidationException::class); 96 | 97 | if ($exceptionConfig !== false) { 98 | $firstError = $exception->validator->errors()->first(); 99 | $message = (string) $firstError; 100 | $code = is_array($exceptionConfig) ? Arr::get($exceptionConfig, 'code', 422) : 422; 101 | $codeValue = is_numeric($code) ? (int) $code : 422; 102 | 103 | return Response::fail($message, $codeValue, $exception->errors()); 104 | } 105 | 106 | return parent::invalidJson($request, $exception); 107 | } 108 | 109 | /** 110 | * Extract string message from mixed message format. 111 | */ 112 | private function extractStringFromMessage(mixed $message): string 113 | { 114 | if (is_string($message)) { 115 | return $message; 116 | } 117 | 118 | if (is_array($message)) { 119 | $firstElement = Arr::first($message); 120 | 121 | return is_scalar($firstElement) ? (string) $firstElement : ''; 122 | } 123 | 124 | return is_scalar($message) ? (string) $message : ''; 125 | } 126 | 127 | /** 128 | * Custom Failed Authentication Response for Laravel. 129 | * 130 | * @param Request $request 131 | * @return \Illuminate\Http\RedirectResponse|JsonResponse|\Illuminate\Http\Response 132 | */ 133 | protected function unauthenticated($request, AuthenticationException $exception) 134 | { 135 | $exceptionConfig = Config::get('response.exception.'.AuthenticationException::class); 136 | 137 | return $exceptionConfig !== false && $request->expectsJson() 138 | ? Response::errorUnauthorized(is_array($exceptionConfig) && is_string($exceptionConfig['message'] ?? null) ? $exceptionConfig['message'] : $exception->getMessage()) 139 | : parent::unauthenticated($request, $exception); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Http/Middleware/ThrottleRequests.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Jiannei\Response\Laravel\Http\Middleware; 13 | 14 | use Closure; 15 | use Illuminate\Cache\RateLimiter; 16 | use Illuminate\Http\Exceptions\ThrottleRequestsException; 17 | use Illuminate\Support\InteractsWithTime; 18 | use Illuminate\Support\Str; 19 | use Symfony\Component\HttpFoundation\Response; 20 | 21 | class ThrottleRequests 22 | { 23 | use InteractsWithTime; 24 | 25 | /** 26 | * The rate limiter instance. 27 | * 28 | * @var \Illuminate\Cache\RateLimiter 29 | */ 30 | protected $limiter; 31 | 32 | /** 33 | * Create a new request throttler. 34 | * 35 | * @return void 36 | */ 37 | public function __construct(RateLimiter $limiter) 38 | { 39 | $this->limiter = $limiter; 40 | } 41 | 42 | /** 43 | * Handle an incoming request. 44 | * 45 | * @param \Illuminate\Http\Request $request 46 | * @param int|string $maxAttempts 47 | * @param float|int $decayMinutes 48 | * @param string $prefix 49 | * @return \Symfony\Component\HttpFoundation\Response 50 | * 51 | * @throws \Illuminate\Http\Exceptions\ThrottleRequestsException 52 | */ 53 | public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1, $prefix = '') 54 | { 55 | $key = $prefix.$this->resolveRequestSignature($request); 56 | 57 | $maxAttempts = $this->resolveMaxAttempts($request, $maxAttempts); 58 | 59 | if ($this->limiter->tooManyAttempts($key, $maxAttempts)) { 60 | throw $this->buildException($key, $maxAttempts); 61 | } 62 | 63 | $this->limiter->hit($key, (int) ($decayMinutes * 60)); 64 | 65 | $response = $next($request); 66 | 67 | return $this->addHeaders( 68 | $response, $maxAttempts, 69 | $this->calculateRemainingAttempts($key, $maxAttempts) 70 | ); 71 | } 72 | 73 | /** 74 | * Resolve the number of attempts if the user is authenticated or not. 75 | * 76 | * @param \Illuminate\Http\Request $request 77 | * @return int 78 | */ 79 | protected function resolveMaxAttempts($request, int|string $maxAttempts) 80 | { 81 | if (Str::contains((string) $maxAttempts, '|')) { 82 | $maxAttempts = explode('|', (string) $maxAttempts, 2)[$request->user() ? 1 : 0]; 83 | } 84 | 85 | if (! is_numeric($maxAttempts) && $request->user()) { 86 | $maxAttempts = $request->user()->{$maxAttempts}; 87 | } 88 | 89 | return (int) $maxAttempts; 90 | } 91 | 92 | /** 93 | * Resolve request signature. 94 | * 95 | * @param \Illuminate\Http\Request $request 96 | * @return string 97 | * 98 | * @throws \RuntimeException 99 | */ 100 | protected function resolveRequestSignature($request) 101 | { 102 | if ($user = $request->user()) { 103 | $identifier = $user->getAuthIdentifier(); 104 | 105 | return sha1(is_scalar($identifier) ? (string) $identifier : ''); 106 | } 107 | 108 | $fingerprint = $request->fingerprint(); 109 | 110 | return sha1((string) $fingerprint); 111 | } 112 | 113 | /** 114 | * Create a 'too many attempts' exception. 115 | * 116 | * @param string $key 117 | * @param int $maxAttempts 118 | * @return \Illuminate\Http\Exceptions\ThrottleRequestsException 119 | */ 120 | protected function buildException($key, $maxAttempts) 121 | { 122 | $retryAfter = $this->getTimeUntilNextRetry($key); 123 | 124 | $headers = $this->getHeaders( 125 | $maxAttempts, 126 | $this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter), 127 | $retryAfter 128 | ); 129 | 130 | return new ThrottleRequestsException( 131 | 'Too Many Attempts.', null, $headers 132 | ); 133 | } 134 | 135 | /** 136 | * Get the number of seconds until the next retry. 137 | * 138 | * @param string $key 139 | * @return int 140 | */ 141 | protected function getTimeUntilNextRetry($key) 142 | { 143 | return $this->limiter->availableIn($key); 144 | } 145 | 146 | /** 147 | * Add the limit header information to the given response. 148 | * 149 | * @param int $maxAttempts 150 | * @param int $remainingAttempts 151 | * @param int|null $retryAfter 152 | * @return \Symfony\Component\HttpFoundation\Response 153 | */ 154 | protected function addHeaders(Response $response, $maxAttempts, $remainingAttempts, $retryAfter = null) 155 | { 156 | $response->headers->add( 157 | $this->getHeaders($maxAttempts, $remainingAttempts, $retryAfter) 158 | ); 159 | 160 | return $response; 161 | } 162 | 163 | /** 164 | * Get the limit headers information. 165 | * 166 | * @param int $maxAttempts 167 | * @param int $remainingAttempts 168 | * @param int|null $retryAfter 169 | * @return array 170 | */ 171 | protected function getHeaders($maxAttempts, $remainingAttempts, $retryAfter = null) 172 | { 173 | $headers = [ 174 | 'X-RateLimit-Limit' => $maxAttempts, 175 | 'X-RateLimit-Remaining' => $remainingAttempts, 176 | ]; 177 | 178 | if (! is_null($retryAfter)) { 179 | $headers['Retry-After'] = $retryAfter; 180 | $headers['X-RateLimit-Reset'] = $this->availableAt($retryAfter); 181 | } 182 | 183 | return $headers; 184 | } 185 | 186 | /** 187 | * Calculate the number of remaining attempts. 188 | * 189 | * @param string $key 190 | * @param int $maxAttempts 191 | * @param int|null $retryAfter 192 | * @return int 193 | */ 194 | protected function calculateRemainingAttempts($key, $maxAttempts, $retryAfter = null) 195 | { 196 | if (is_null($retryAfter)) { 197 | return $this->limiter->retriesLeft($key, $maxAttempts); 198 | } 199 | 200 | return 0; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/Support/Format.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Jiannei\Response\Laravel\Support; 13 | 14 | use Illuminate\Contracts\Pagination\LengthAwarePaginator; 15 | use Illuminate\Contracts\Pagination\Paginator; 16 | use Illuminate\Contracts\Support\Arrayable; 17 | use Illuminate\Http\JsonResponse; 18 | use Illuminate\Http\Resources\Json\JsonResource; 19 | use Illuminate\Http\Resources\Json\ResourceCollection; 20 | use Illuminate\Pagination\AbstractCursorPaginator; 21 | use Illuminate\Pagination\AbstractPaginator; 22 | use Illuminate\Pagination\CursorPaginator; 23 | use Illuminate\Support\Arr; 24 | use Illuminate\Support\Facades\Config; 25 | use Illuminate\Support\Facades\Lang; 26 | use Illuminate\Support\Traits\Macroable; 27 | use Jiannei\Response\Laravel\Contract\ResponseFormat; 28 | 29 | class Format implements ResponseFormat 30 | { 31 | use Macroable; 32 | 33 | /** 34 | * @var array|null 35 | */ 36 | protected ?array $data = null; 37 | 38 | protected int $statusCode = 200; 39 | 40 | /** 41 | * @param array $config 42 | */ 43 | public function __construct(protected array $config = []) {} 44 | 45 | /** 46 | * Return a new JSON response from the application. 47 | */ 48 | public function response(): JsonResponse 49 | { 50 | return tap(new JsonResponse($this->data, $this->statusCode), function () { 51 | $this->data = null; 52 | $this->statusCode = 200; 53 | }); 54 | } 55 | 56 | /** 57 | * Get formatted data. 58 | * 59 | * @return array|null 60 | */ 61 | public function get(): ?array 62 | { 63 | return $this->data; 64 | } 65 | 66 | /** 67 | * Core format. 68 | */ 69 | public function data(mixed $data = null, string $message = '', int|\BackedEnum $code = 200, mixed $error = null): static 70 | { 71 | $bizCode = $this->formatBusinessCode($code); 72 | 73 | $this->data = $this->formatDataFields([ 74 | 'status' => $this->formatStatus($bizCode), 75 | 'code' => $bizCode, 76 | 'message' => $this->formatMessage($bizCode, $message), 77 | 'data' => $this->formatData($data), 78 | 'error' => $this->formatError(null), 79 | ]); 80 | 81 | $this->statusCode = $this->formatStatusCode($bizCode); 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Format paginator data. 88 | * 89 | * @param AbstractPaginator|AbstractCursorPaginator|Paginator $resource 90 | * @return array 91 | */ 92 | public function paginator(AbstractPaginator|AbstractCursorPaginator|Paginator $resource): array 93 | { 94 | $data = method_exists($resource, 'toArray') ? $resource->toArray()['data'] : $resource->items(); 95 | 96 | return [ 97 | 'data' => $data, 98 | 'meta' => $this->formatMeta($resource), 99 | ]; 100 | } 101 | 102 | /** 103 | * Format collection resource data. 104 | * 105 | * @return array 106 | */ 107 | public function resourceCollection(ResourceCollection $collection): array 108 | { 109 | return [ 110 | 'data' => $collection->resolve(), 111 | 'meta' => array_merge_recursive($this->formatMeta($collection->resource), $collection->with(request()), $collection->additional), 112 | ]; 113 | } 114 | 115 | /** 116 | * Format JsonResource Data. 117 | * 118 | * @return array 119 | */ 120 | public function jsonResource(JsonResource $resource): array 121 | { 122 | return value($this->formatJsonResource(), $resource); 123 | } 124 | 125 | /** 126 | * Format data. 127 | * 128 | * @return array|object 129 | */ 130 | protected function formatData(mixed $data): array|object 131 | { 132 | return match (true) { 133 | $data instanceof ResourceCollection => $this->resourceCollection($data), 134 | $data instanceof JsonResource => $this->jsonResource($data), 135 | $data instanceof AbstractPaginator, $data instanceof AbstractCursorPaginator => $this->paginator($data), 136 | $data instanceof Arrayable, (is_object($data) && method_exists($data, 'toArray')) => $data->toArray(), 137 | empty($data) => (object) $data, 138 | default => Arr::wrap($data) 139 | }; 140 | } 141 | 142 | /** 143 | * Format return message. 144 | */ 145 | protected function formatMessage(int $code, string $message = ''): ?string 146 | { 147 | $localizationKey = implode('.', [Config::get('response.locale', 'enums'), $code]); 148 | 149 | return match (true) { 150 | ! $message && Lang::has($localizationKey) => Lang::get($localizationKey), 151 | default => $message 152 | }; 153 | } 154 | 155 | /** 156 | * Format business code. 157 | */ 158 | protected function formatBusinessCode(int|\BackedEnum $code): int 159 | { 160 | return $code instanceof \BackedEnum ? (int) $code->value : $code; 161 | } 162 | 163 | /** 164 | * Format http status description. 165 | */ 166 | protected function formatStatus(int $bizCode): string 167 | { 168 | $statusCode = (int) substr((string) $bizCode, 0, 3); 169 | 170 | return match (true) { 171 | ($statusCode >= 400 && $statusCode <= 499) => 'error',// client error 172 | ($statusCode >= 500 && $statusCode <= 599) => 'fail',// service error 173 | default => 'success' 174 | }; 175 | } 176 | 177 | /** 178 | * Http status code. 179 | */ 180 | protected function formatStatusCode(int $code): int 181 | { 182 | $errorCode = Config::get('response.error_code'); 183 | $codeToUse = is_numeric($errorCode) ? $errorCode : $code; 184 | 185 | return (int) substr((string) $codeToUse, 0, 3); 186 | } 187 | 188 | /** 189 | * Get JsonResource resource data. 190 | */ 191 | protected function formatJsonResource(): \Closure 192 | { 193 | // vendor/laravel/framework/src/Illuminate/Http/Resources/Json/ResourceResponse.php 194 | // toResponse 195 | return function (JsonResource $resource) { 196 | return array_merge_recursive($resource->resolve(request()), $resource->with(request()), $resource->additional); 197 | }; 198 | } 199 | 200 | /** 201 | * Format paginator data. 202 | * 203 | * @return array 204 | */ 205 | protected function formatMeta(mixed $collection): array 206 | { 207 | // vendor/laravel/framework/src/Illuminate/Http/Resources/Json/PaginatedResourceResponse.php 208 | return match (true) { 209 | $collection instanceof CursorPaginator => [ 210 | 'cursor' => [ 211 | 'current' => $collection->cursor()?->encode(), 212 | 'prev' => $collection->previousCursor()?->encode(), 213 | 'next' => $collection->nextCursor()?->encode(), 214 | 'count' => count($collection->items()), 215 | ], 216 | ], 217 | $collection instanceof LengthAwarePaginator => [ 218 | 'pagination' => [ 219 | 'count' => $collection->lastItem(), 220 | 'per_page' => $collection->perPage(), 221 | 'current_page' => $collection->currentPage(), 222 | 'total' => $collection->total(), 223 | 'total_pages' => $collection->lastPage(), 224 | 'links' => array_filter([ 225 | 'previous' => $collection->previousPageUrl(), 226 | 'next' => $collection->nextPageUrl(), 227 | ]), 228 | ], 229 | ], 230 | $collection instanceof Paginator => [ 231 | 'pagination' => [ 232 | 'count' => $collection->lastItem(), 233 | 'per_page' => $collection->perPage(), 234 | 'current_page' => $collection->currentPage(), 235 | 'links' => array_filter([ 236 | 'previous' => $collection->previousPageUrl(), 237 | 'next' => $collection->nextPageUrl(), 238 | ]), 239 | ], 240 | ], 241 | default => [], 242 | }; 243 | } 244 | 245 | /** 246 | * Format error. 247 | * 248 | * @param array|null $error 249 | * @return object|array 250 | */ 251 | protected function formatError(?array $error): object|array 252 | { 253 | return $error ?: (object) []; 254 | } 255 | 256 | /** 257 | * Format response data fields. 258 | * 259 | * @param array $data 260 | * @return array 261 | */ 262 | protected function formatDataFields(array $data): array 263 | { 264 | foreach ($this->config as $key => $config) { 265 | if (! is_string($key) || ! Arr::has($data, $key)) { 266 | continue; 267 | } 268 | 269 | $show = is_array($config) ? ($config['show'] ?? true) : true; 270 | $alias = is_array($config) ? ($config['alias'] ?? '') : ''; 271 | 272 | if (! $show) { 273 | unset($data[$key]); 274 | 275 | continue; 276 | } 277 | 278 | if ($alias && $alias !== $key) { 279 | $data[$alias] = Arr::get($data, $key); 280 | unset($data[$key]); 281 | } 282 | } 283 | 284 | return $data; 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

laravel-response

2 | 3 | [简体中文](README.md) | [ENGLISH](README-EN.md) 4 | 5 | > 为 Laravel 和 Lumen API 项目提供一个规范统一的响应数据格式。 6 | 7 | > **🎉 最新更新:现已支持 Laravel 12!** 支持 Laravel 5.5 ~ 12.x 全版本,PHP 7.0 ~ 8.3。 8 | 9 | ![Test](https://github.com/Jiannei/laravel-response/workflows/Test/badge.svg) 10 | [![Pest](https://img.shields.io/badge/Tests-Pest-green?style=flat)](https://pestphp.com) 11 | [![PHPStan](https://img.shields.io/badge/PHPStan-Level%206-brightgreen?style=flat)](https://phpstan.org) 12 | [![Code Coverage](https://img.shields.io/badge/Coverage-100%25-brightgreen?style=flat)](https://github.com/Jiannei/laravel-response) 13 | [![Pint](https://img.shields.io/badge/Code%20Style-Pint-orange?style=flat)](https://laravel.com/docs/pint) 14 | [![Latest Stable Version](https://poser.pugx.org/jiannei/laravel-response/v)](https://packagist.org/packages/jiannei/laravel-response) 15 | [![Total Downloads](https://poser.pugx.org/jiannei/laravel-response/downloads)](https://packagist.org/packages/jiannei/laravel-response) 16 | [![Monthly Downloads](https://poser.pugx.org/jiannei/laravel-response/d/monthly)](https://packagist.org/packages/jiannei/laravel-response) 17 | [![License](https://poser.pugx.org/jiannei/laravel-response/license)](https://packagist.org/packages/jiannei/laravel-response) 18 | 19 | ## 社区讨论文章 20 | 21 | - [是时候使用 Lumen 8 + API Resource 开发项目了!](https://learnku.com/articles/45311) 22 | - [教你更优雅地写 API 之「路由设计」](https://learnku.com/articles/45526) 23 | - [教你更优雅地写 API 之「规范响应数据」](https://learnku.com/articles/52784) 24 | - [教你更优雅地写 API 之「枚举使用」](https://learnku.com/articles/53015) 25 | - [教你更优雅地写 API 之「记录日志」](https://learnku.com/articles/53669) 26 | - [教你更优雅地写 API 之「灵活地任务调度」](https://learnku.com/articles/58403) 27 | 28 | ## 介绍 29 | 30 | `laravel-response` 主要用来统一 API 开发过程中「成功」、「失败」以及「异常」情况下的响应数据格式。 31 | 32 | 实现过程简单,在原有的 `\Illuminate\Http\JsonResponse`进行封装,使用时不需要有额外的心理负担。 33 | 34 | 遵循一定的规范,返回易于理解的 HTTP 状态码,并支持定义 `ResponseEnum` 来满足不同场景下返回描述性的业务操作码。 35 | 36 | ## 概览 37 | 38 | - 统一的数据响应格式,固定包含:`code`、`status`、`data`、`message`、`error` (响应格式设计源于:[RESTful服务最佳实践](https://www.cnblogs.com/jaxu/p/7908111.html#a_8_2) ) 39 | - 你可以继续链式调用 `JsonResponse` 类中的所有 public 方法,比如 `Response::success()->header('X-foo','bar');` 40 | - 合理地返回 Http 状态码,默认为 restful 严格模式,可以配置异常时返回 200 http 状态码(多数项目会这样使用) 41 | - 支持格式化 Laravel 的 `Api Resource`、`Api Resource Collection`、`Paginator`(简单分页)、`LengthAwarePaginator`(普通分页)、`Eloquent\Model`、`Eloquent\Collection`,以及简单的 `array` 和 `string`等格式数据返回 42 | - 根据 debug 开关,合理返回异常信息、验证异常信息等 43 | - 支持修改 Laravel 特地异常的状态码或提示信息,比如将 `No query results for model` 的异常提示修改成 `数据未找到` 44 | - 支持配置返回字段是否显示,以及为她们设置别名,比如,将 `message` 别名设置为 `msg`,或者 分页数据第二层的 `data` 改成 `list`(res.data.data -> res.data.list) 45 | - 分页数据格式化后的结果与使用 `league/fractal` (DingoApi 使用该扩展进行数据转换)的 transformer 转换后的格式保持一致,也就是说,可以顺滑地从 Laravel Api Resource 切换到 `league/fractal` 46 | - 内置 Http 标准状态码支持,同时支持扩展 ResponseEnum 来根据不同业务模块定义响应码(可选,需要安装 `jiannei/laravel-enum`) 47 | - 响应码 code 对应描述信息 message 支持本地化,支持配置多语言(可选,需要安装 `jiannei/laravel-enum`) 48 | 49 | 50 | ## 安装 51 | 52 | 支持 Laravel 5.5.* ~ Laravel 12.* 版本,自定义业务操作码部分依赖于 [jiannei/laravel-enum](https://github.com/Jiannei/laravel-enum),需要先进行安装。 53 | 54 | | laravel 版本 | lumen 版本 | response 版本 | enum 版本 | PHP 版本要求 | 55 | |------------|------------|-------------|---------|------------| 56 | | 5.5.* | 5.5.* | ~1.8 | ~1.4 | ^7.0 | 57 | | 6.* | 6.* | ^2.0 | ~1.4 | ^7.2 | 58 | | 7.* | 7.* | ^3.0 | ^2.0 | ^7.2.5 | 59 | | 8.* | 8.* | ^4.0 | ^3.0 | ^7.3 | 60 | | 9.* - 10.* | 9.* - 10.* | ^5.0 | ^3.0 | ^8.0 | 61 | | 11.* | 不支持 | ^6.0 | ^4.0 | ^8.2 | 62 | | 12.* | 不支持 | ^6.0 | ^4.0 | ^8.2 | 63 | 64 | > **📝 版本说明:** 65 | > - Laravel 11+ 版本内置了更好的异常处理机制,可以省略手动配置异常处理步骤 66 | > - Lumen 从 Laravel 9 开始不再维护,建议使用 Laravel 进行 API 开发 67 | > - 推荐使用最新版本以获得更好的性能和安全性 68 | 69 | ```shell 70 | # laravel 5.5 71 | 72 | composer require jiannei/laravel-response "~1.8" -vvv 73 | composer require jiannei/laravel-enum "~1.4" -vvv # 可选 74 | 75 | # laravel 6.x 76 | 77 | composer require jiannei/laravel-response "^2.0" -vvv 78 | composer require jiannei/laravel-enum "~1.4" -vvv # 可选 79 | 80 | # laravel 7.x 81 | 82 | composer require jiannei/laravel-response "^3.0" -vvv 83 | composer require jiannei/laravel-enum "^2.0" -vvv # 可选 84 | 85 | # laravel 8.x 86 | 87 | composer require jiannei/laravel-response "^4.0" -vvv 88 | composer require jiannei/laravel-enum "^3.0" -vvv # 可选 89 | 90 | # laravel 9.x - 10.x 91 | 92 | composer require jiannei/laravel-response "^5.0" -vvv 93 | composer require jiannei/laravel-enum "^3.0" -vvv # 可选 94 | 95 | # laravel 11.x 96 | 97 | composer require jiannei/laravel-response "^6.0" -vvv 98 | composer require jiannei/laravel-enum "^4.0" -vvv # 可选 99 | 100 | # laravel 12.x 101 | 102 | composer require jiannei/laravel-response "^6.0" -vvv 103 | composer require jiannei/laravel-enum "^4.0" -vvv # 可选 104 | ``` 105 | 106 | ## 配置 107 | 108 | ### Laravel 109 | 110 | - 发布配置文件 111 | 112 | ```shell 113 | $ php artisan vendor:publish --provider="Jiannei\Response\Laravel\Providers\LaravelServiceProvider" 114 | ``` 115 | 116 | - 格式化异常响应(Laravel 11+ 可省略这一步) 117 | 118 | 119 | ```php 120 | // app/Exceptions/Handler.php 121 | // 引入以后对于 API 请求产生的异常都会进行格式化数据返回 122 | // 要求请求头 header 中包含 /json 或 +json,如:Accept:application/json 123 | // 或者是 ajax 请求,header 中包含 X-Requested-With:XMLHttpRequest; 124 | 125 | configure('response'); 153 | ``` 154 | 155 | - 格式化异常响应 156 | 157 | 在 `app/Exceptions/Handler.php` 中 引入 `use Jiannei\Response\Laravel\Support\Traits\ExceptionTrait;` 158 | 159 | 在 `app/Http/Controllers/Controller.php` 中引入 `use Jiannei\Response\Laravel\Support\Traits\ExceptionTrait;` 160 | 161 | - 注册服务容器 162 | 163 | ```php 164 | $app->register(\Jiannei\Response\Laravel\Providers\LumenServiceProvider::class); 165 | ``` 166 | 167 | ## 使用 168 | 169 | 扩展包本身提供了丰富的单元测试用例[tests](https://github.com/Jiannei/laravel-response/tree/main/tests) ,也可以通过查看测试用例来解锁使用方法。 170 | 171 | 或者查看相应的模板项目: 172 | 173 | - [laravel-api-starter](https://github.com/Jiannei/laravel-api-starter) 174 | - [lumen-api-starter](https://github.com/Jiannei/lumen-api-starter) 175 | 176 | ### 成功响应 177 | 178 | #### 示例代码 179 | 180 | ```php 181 | 'Jiannel', 214 | 'email' => 'longjian.huang@foxmail.com' 215 | ],'', ResponseEnum::SERVICE_REGISTER_SUCCESS); 216 | } 217 | ``` 218 | 219 | #### 返回全部数据 220 | 221 | 支持自定义内层 data 字段名称,比如 rows、list 222 | 223 | ```json 224 | { 225 | "status": "success", 226 | "code": 200, 227 | "message": "操作成功", 228 | "data": { 229 | "data": [ 230 | { 231 | "nickname": "Joaquin Ondricka", 232 | "email": "lowe.chaim@example.org" 233 | }, 234 | { 235 | "nickname": "Jermain D'Amore", 236 | "email": "reanna.marks@example.com" 237 | }, 238 | { 239 | "nickname": "Erich Moore", 240 | "email": "ernestine.koch@example.org" 241 | } 242 | ] 243 | }, 244 | "error": {} 245 | } 246 | ``` 247 | 248 | #### 分页数据 249 | 250 | 支持自定义内层 data 字段名称,比如 rows、list 251 | 252 | ```json 253 | { 254 | "status": "success", 255 | "code": 200, 256 | "message": "操作成功", 257 | "data": { 258 | "data": [ 259 | { 260 | "nickname": "Joaquin Ondricka", 261 | "email": "lowe.chaim@example.org" 262 | }, 263 | { 264 | "nickname": "Jermain D'Amore", 265 | "email": "reanna.marks@example.com" 266 | }, 267 | { 268 | "nickname": "Erich Moore", 269 | "email": "ernestine.koch@example.org" 270 | }, 271 | { 272 | "nickname": "Eva Quitzon", 273 | "email": "rgottlieb@example.net" 274 | }, 275 | { 276 | "nickname": "Miss Gail Mitchell", 277 | "email": "kassandra.lueilwitz@example.net" 278 | } 279 | ], 280 | "meta": { 281 | "pagination": { 282 | "count": 5, 283 | "per_page": 5, 284 | "current_page": 1, 285 | "total": 12, 286 | "total_pages": 3, 287 | "links": { 288 | "previous": null, 289 | "next": "http://laravel-api.test/api/users/paginate?page=2" 290 | } 291 | } 292 | } 293 | }, 294 | "error": {} 295 | } 296 | ``` 297 | 298 | #### 返回简单分页数据 299 | 300 | 支持自定义内层 data 字段名称,比如 rows、list 301 | 302 | ```json 303 | { 304 | "status": "success", 305 | "code": 200, 306 | "message": "操作成功", 307 | "data": { 308 | "data": [ 309 | { 310 | "nickname": "Joaquin Ondricka", 311 | "email": "lowe.chaim@example.org" 312 | }, 313 | { 314 | "nickname": "Jermain D'Amore", 315 | "email": "reanna.marks@example.com" 316 | }, 317 | { 318 | "nickname": "Erich Moore", 319 | "email": "ernestine.koch@example.org" 320 | }, 321 | { 322 | "nickname": "Eva Quitzon", 323 | "email": "rgottlieb@example.net" 324 | }, 325 | { 326 | "nickname": "Miss Gail Mitchell", 327 | "email": "kassandra.lueilwitz@example.net" 328 | } 329 | ], 330 | "meta": { 331 | "pagination": { 332 | "count": 5, 333 | "per_page": 5, 334 | "current_page": 1, 335 | "links": { 336 | "previous": null, 337 | "next": "http://laravel-api.test/api/users/simple-paginate?page=2" 338 | } 339 | } 340 | } 341 | }, 342 | "error": {} 343 | } 344 | ``` 345 | 346 | #### 返回单条数据 347 | 348 | ```json 349 | { 350 | "status": "success", 351 | "code": 200, 352 | "message": "操作成功", 353 | "data": { 354 | "nickname": "Joaquin Ondricka", 355 | "email": "lowe.chaim@example.org" 356 | }, 357 | "error": {} 358 | } 359 | ``` 360 | 361 | #### 其他快捷方法 362 | 363 | ```php 364 | Response::ok();// 无需返回 data,只返回 message 情形的快捷方法 365 | Response::localize(200101);// 无需返回 data,message 根据响应码配置返回的快捷方法 366 | Response::accepted(); 367 | Response::created(); 368 | Response::noContent(); 369 | ``` 370 | 371 | ### 失败响应 372 | 373 | #### 不指定 message 374 | 375 | ```php 376 | public function fail() 377 | { 378 | return Response::fail(); 379 | } 380 | ``` 381 | 382 | - 未配置多语言响应描述 383 | 384 | ```json 385 | { 386 | "status": "fail", 387 | "code": 500, 388 | "message": "Http internal server error", 389 | "data": {}, 390 | "error": {} 391 | } 392 | ``` 393 | 394 | - 配置多语言描述 395 | 396 | ```json 397 | { 398 | "status": "fail", 399 | "code": 500, 400 | "message": "操作失败", 401 | "data": {}, 402 | "error": {} 403 | } 404 | ``` 405 | 406 | #### 指定 message 407 | 408 | ```php 409 | public function fail() 410 | { 411 | return Response::fail('error'); 412 | } 413 | ``` 414 | 415 | 返回数据 416 | 417 | ```json 418 | { 419 | "status": "fail", 420 | "code": 500, 421 | "message": "error", 422 | "data": {}, 423 | "error": {} 424 | } 425 | ``` 426 | 427 | #### 指定 code 428 | 429 | ```php 430 | public function fail() 431 | { 432 | return Response::fail('',ResponseEnum::SERVICE_LOGIN_ERROR); 433 | } 434 | ``` 435 | 436 | 返回数据 437 | 438 | ```json 439 | { 440 | "status": "fail", 441 | "code": 500102, 442 | "message": "登录失败", 443 | "data": {}, 444 | "error": {} 445 | } 446 | ``` 447 | 448 | #### 其他快捷方法 449 | 450 | ```php 451 | Response::errorBadRequest(); 452 | Response::errorUnauthorized(); 453 | Response::errorForbidden(); 454 | Response::errorNotFound(); 455 | Response::errorMethodNotAllowed(); 456 | Response::errorInternal(); 457 | ``` 458 | 459 | ### 异常响应 460 | 461 | #### 表单验证异常 462 | 463 | ```json 464 | { 465 | "status": "error", 466 | "code": 422, 467 | "message": "验证失败", 468 | "data": {}, 469 | "error": { 470 | "email": [ 471 | "The email field is required." 472 | ] 473 | } 474 | } 475 | ``` 476 | 477 | #### Controller 以外抛出异常 478 | 479 | 可以使用 abort 辅助函数抛出 HttpException 异常 480 | 481 | ```php 482 | abort(500102,'登录失败'); 483 | 484 | // 返回数据 485 | 486 | { 487 | "status": "fail", 488 | "code": 500102, 489 | "message": "登录失败", 490 | "data": {}, 491 | "error": {} 492 | } 493 | ``` 494 | 495 | #### 其他异常 496 | 497 | 开启 debug(`APP_DEBUG=true`) 498 | 499 | ```json 500 | { 501 | "status": "error", 502 | "code": 404, 503 | "message": "Http not found", 504 | "data": {}, 505 | "error": { 506 | "message": "", 507 | "exception": "Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException", 508 | "file": "/home/vagrant/code/laravel-api-starter/vendor/laravel/framework/src/Illuminate/Routing/AbstractRouteCollection.php", 509 | "line": 43, 510 | "trace": [ 511 | { 512 | "file": "/home/vagrant/code/laravel-api-starter/vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php", 513 | "line": 162, 514 | "function": "handleMatchedRoute", 515 | "class": "Illuminate\\Routing\\AbstractRouteCollection", 516 | "type": "->" 517 | }, 518 | { 519 | "file": "/home/vagrant/code/laravel-api-starter/vendor/laravel/framework/src/Illuminate/Routing/Router.php", 520 | "line": 646, 521 | "function": "match", 522 | "class": "Illuminate\\Routing\\RouteCollection", 523 | "type": "->" 524 | } 525 | ] 526 | } 527 | } 528 | ``` 529 | 530 | 关闭 debug 531 | 532 | ```json 533 | { 534 | "status": "error", 535 | "code": 404, 536 | "message": "Http not found", 537 | "data": {}, 538 | "error": {} 539 | } 540 | ``` 541 | 542 | ### 自定义业务码 543 | 544 | ```php 545 | 200001,也就是有 001 ~ 999 个编号可以用来表示业务成功的情况,当然你可以根据实际需求继续增加位数,但必须要求是 200 开头 556 | // 举个栗子:你可以定义 001 ~ 099 表示系统状态;100 ~ 199 表示授权业务;200 ~ 299 表示用户业务... 557 | case SERVICE_REGISTER_SUCCESS = 200101; 558 | case SERVICE_LOGIN_SUCCESS = 200102; 559 | 560 | // 业务操作错误码(外部服务或内部服务调用...) 561 | case SERVICE_REGISTER_ERROR = 500101; 562 | case SERVICE_LOGIN_ERROR = 500102; 563 | 564 | // 客户端错误码:400 ~ 499 开头,后拼接 3 位 565 | case CLIENT_PARAMETER_ERROR = 400001; 566 | case CLIENT_CREATED_ERROR = 400002; 567 | case CLIENT_DELETED_ERROR = 400003; 568 | 569 | // 服务端操作错误码:500 ~ 599 开头,后拼接 3 位 570 | case SYSTEM_ERROR = 500001; 571 | case SYSTEM_UNAVAILABLE = 500002; 572 | case SYSTEM_CACHE_CONFIG_ERROR = 500003; 573 | case SYSTEM_CACHE_MISSED_ERROR = 500004; 574 | case SYSTEM_CONFIG_ERROR = 500005; 575 | } 576 | ``` 577 | 578 | ### 本地化业务码描述 579 | 580 | ```php 581 | [ 589 | // 标准 HTTP 状态码 590 | HttpStatusCode::HTTP_OK->value => '操作成功', 591 | HttpStatusCode::HTTP_UNAUTHORIZED->value => '授权失败', 592 | 593 | // 业务操作成功 594 | ResponseEnum::SERVICE_REGISTER_SUCCESS->value => '注册成功', 595 | ResponseEnum::SERVICE_LOGIN_SUCCESS->value => '登录成功', 596 | 597 | // 业务操作失败:授权业务 598 | ResponseEnum::SERVICE_REGISTER_ERROR->value => '注册失败', 599 | ResponseEnum::SERVICE_LOGIN_ERROR->value => '登录失败', 600 | 601 | // 客户端错误 602 | ResponseEnum::CLIENT_PARAMETER_ERROR->value => '参数错误', 603 | ResponseEnum::CLIENT_CREATED_ERROR->value => '数据已存在', 604 | ResponseEnum::CLIENT_DELETED_ERROR->value => '数据不存在', 605 | 606 | // 服务端错误 607 | ResponseEnum::SYSTEM_ERROR->value => '服务器错误', 608 | ResponseEnum::SYSTEM_UNAVAILABLE->value => '服务器正在维护,暂不可用', 609 | ResponseEnum::SYSTEM_CACHE_CONFIG_ERROR->value => '缓存配置错误', 610 | ResponseEnum::SYSTEM_CACHE_MISSED_ERROR->value => '缓存未命中', 611 | ResponseEnum::SYSTEM_CONFIG_ERROR->value => '系统配置错误', 612 | ], 613 | ], 614 | ]; 615 | ``` 616 | 617 | ## 由 JetBrains 赞助 618 | 619 | 非常感谢 Jetbrains 为我提供的 IDE 开源许可,让我完成此项目和其他开源项目上的开发工作。 620 | 621 | [![](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg)](https://www.jetbrains.com/?from=https://github.com/jiannei) 622 | 623 | ## Stargazers over time 624 | 625 | [![Stargazers over time](https://starchart.cc/jiannei/laravel-response.svg)](https://starchart.cc/jiannei/laravel-response) 626 | 627 | ## 协议 628 | 629 | MIT 许可证(MIT)。有关更多信息,请参见[协议文件](LICENSE)。 -------------------------------------------------------------------------------- /README-EN.md: -------------------------------------------------------------------------------- 1 |

laravel-response

2 | 3 | [简体中文](README.md) | [ENGLISH](README-EN.md) 4 | 5 | > Provide a standardized and unified response data format for Laravel and Lumen API projects. 6 | 7 | > **🎉 Latest Update: Now supports Laravel 12!** Supports Laravel 5.5 ~ 12.x all versions, PHP 7.0 ~ 8.3. 8 | 9 | ![Test](https://github.com/Jiannei/laravel-response/workflows/Test/badge.svg) 10 | [![Pest](https://img.shields.io/badge/Tests-Pest-green?style=flat)](https://pestphp.com) 11 | [![PHPStan](https://img.shields.io/badge/PHPStan-Level%206-brightgreen?style=flat)](https://phpstan.org) 12 | [![Code Coverage](https://img.shields.io/badge/Coverage-100%25-brightgreen?style=flat)](https://github.com/Jiannei/laravel-response) 13 | [![Pint](https://img.shields.io/badge/Code%20Style-Pint-orange?style=flat)](https://laravel.com/docs/pint) 14 | [![Latest Stable Version](https://poser.pugx.org/jiannei/laravel-response/v)](https://packagist.org/packages/jiannei/laravel-response) 15 | [![Total Downloads](https://poser.pugx.org/jiannei/laravel-response/downloads)](https://packagist.org/packages/jiannei/laravel-response) 16 | [![Monthly Downloads](https://poser.pugx.org/jiannei/laravel-response/d/monthly)](https://packagist.org/packages/jiannei/laravel-response) 17 | [![License](https://poser.pugx.org/jiannei/laravel-response/license)](https://packagist.org/packages/jiannei/laravel-response) 18 | 19 | ## Introduce 20 | 21 | `laravel-response` It is mainly used to unify the response data format of "success", "failure" and "exception" in the process of API development. 22 | 23 | It is encapsulated in the original `\Illuminate\Http\JsonResponse`, there is nothing complicated. 24 | 25 | Follow certain specifications, return HTTP status codes that are easy to understand, and support the definition of 'enum' to meet the return of descriptive business operation codes in different scenarios. 26 | 27 | ## Features 28 | 29 | - Unified data response format, including:`code`、`status`、`data`、`message`、`error` 30 | - You can continue to chain call all public methods in the `JsonResponse` class, such as `Response::success()->header('x-foo ',' bar ')` 31 | - Reasonably return the HTTP status code. The default is restful strict mode. You can configure to return 200 HTTP status code in case of exception (this is how most projects use it) 32 | - It supports the formatting of `Api Resource`、`Api Resource Collection`、`Paginator`、`LengthAwarePaginator`、`Eloquent\Model`、`Eloquent\Collection`,as well as the return of data in simple formats such as `array` 和 `string` 33 | - According to the debug config, reasonably return the exception information, verify the exception information 34 | - It supports modifying the status code or prompt information of laravel's special exception, such as modifying the exception prompt of `No query results for model` to `data not found` 35 | - Any returned field can be displayed or hidden, and aliases can be set, such as alia `message` to `msg` 36 | - The formatted result of paging data is consistent with the format converted by the transformer using `league/fractal` (DingoApi uses this extension for data conversion), that is, it can be smoothly switched from laravel API resource to `league/fractal` 37 | - Built in HTTP standard status code support, and support the extension of ResponseCodeEnum to define response codes according to different business modules (optional, need to install `jiannei/laravel-enum`) 38 | 39 | ## Install 40 | 41 | Support for Laravel 5.5.* ~ Laravel 12.*, the user-defined business operation code partially depends on [jiannei/laravel-enum](https://github.com/Jiannei/laravel-enum). 42 | 43 | | laravel version | lumen version | response version | enum version | PHP version | 44 | |------------| ---- |-------------| ---- |------------| 45 | | 5.5.* | 5.5.* | ~1.8 | ~1.4 | ^7.0 | 46 | | 6.* | 6.* | ^2.0 | ~1.4 | ^7.2 | 47 | | 7.* | 7.* | ^3.0 | ^2.0 | ^7.2.5 | 48 | | 8.* | 8.* | ^4.0 | ^3.0 | ^7.3 | 49 | | 9.* - 10.* | 9.* - 10.* | ^5.0 | ^3.0 | ^8.0 | 50 | | 11.* | Not supported | ^6.0 | ^4.0 | ^8.2 | 51 | | 12.* | Not supported | ^6.0 | ^4.0 | ^8.2 | 52 | 53 | 54 | ```shell 55 | # laravel 5.5 56 | 57 | composer require jiannei/laravel-response "~1.8" -vvv 58 | composer require jiannei/laravel-enum "~1.4" -vvv # optional 59 | 60 | # laravel 6.x 61 | 62 | composer require jiannei/laravel-response "^2.0" -vvv 63 | composer require jiannei/laravel-enum "~1.4" -vvv # optional 64 | 65 | # laravel 7.x 66 | 67 | composer require jiannei/laravel-response "^3.0" -vvv 68 | composer require jiannei/laravel-enum "^2.0" -vvv # optional 69 | 70 | # laravel 8.x 71 | 72 | composer require jiannei/laravel-response "^4.0" -vvv 73 | composer require jiannei/laravel-enum "^3.0" -vvv # optional 74 | 75 | # laravel 9.x - 10.x 76 | 77 | composer require jiannei/laravel-response "^5.0" -vvv 78 | composer require jiannei/laravel-enum "^3.0" -vvv # optional 79 | 80 | # laravel 11.x 81 | 82 | composer require jiannei/laravel-response "^6.0" -vvv 83 | composer require jiannei/laravel-enum "^4.0" -vvv # optional 84 | 85 | # laravel 12.x 86 | 87 | composer require jiannei/laravel-response "^6.0" -vvv 88 | composer require jiannei/laravel-enum "^4.0" -vvv # optional 89 | ``` 90 | 91 | > **📝 Version Notes:** 92 | > - Laravel 11+ has built-in better exception handling mechanism, you can skip manual exception handling configuration 93 | > - Lumen is no longer maintained since Laravel 9, it's recommended to use Laravel for API development 94 | > - It's recommended to use the latest version for better performance and security 95 | 96 | ## Configuration 97 | 98 | ### Laravel 99 | 100 | - publish config file 101 | 102 | ```shell 103 | $ php artisan vendor:publish --provider="Jiannei\Response\Laravel\Providers\LaravelServiceProvider" 104 | ``` 105 | 106 | - format exception response (Laravel 11+ can skip this step) 107 | 108 | ```php 109 | // app/Exceptions/Handler.php 110 | // The exceptions generated by API requests will be formatted and returned 111 | // The request header is required to contain /json or +json, such as Accept:application/json 112 | // Or Ajax request, the header contains X-Requested-With:XMLHttpRequest; 113 | 114 | configure('response'); 142 | ``` 143 | 144 | - format exception response 145 | 146 | In `app/Exceptions/Handler.php` file `use Jiannei\Response\Laravel\Support\Traits\ExceptionTrait;` 147 | 148 | In `app/Http/Controllers/Controller.php` file `use Jiannei\Response\Laravel\Support\Traits\ExceptionTrait;` 149 | 150 | - register service provider 151 | 152 | ```php 153 | $app->register(\Jiannei\Response\Laravel\Providers\LumenServiceProvider::class); 154 | ``` 155 | 156 | ## Usage 157 | 158 | The extension package itself provides rich unit test cases,you can unlock usage by viewing test cases [tests](https://github.com/Jiannei/laravel-response/tree/main/tests) 159 | 160 | Or view the corresponding template projects: 161 | 162 | - [laravel-api-starter](https://github.com/Jiannei/laravel-api-starter) 163 | - [lumen-api-starter](https://github.com/Jiannei/lumen-api-starter) 164 | 165 | ### Successful response 166 | 167 | #### Sample code 168 | 169 | ```php 170 | 'Jiannel', 204 | 'email' => 'longjian.huang@foxmail.com' 205 | ],'', ResponseCodeEnum::SERVICE_REGISTER_SUCCESS); 206 | } 207 | 208 | 209 | ``` 210 | 211 | #### Return all rows data 212 | 213 | Support custom inner `data` field names, such as `rows` or `list` 214 | 215 | ```json 216 | { 217 | "status": "success", 218 | "code": 200, 219 | "message": "操作成功", 220 | "data": { 221 | "data": [ 222 | { 223 | "nickname": "Joaquin Ondricka", 224 | "email": "lowe.chaim@example.org" 225 | }, 226 | { 227 | "nickname": "Jermain D'Amore", 228 | "email": "reanna.marks@example.com" 229 | }, 230 | { 231 | "nickname": "Erich Moore", 232 | "email": "ernestine.koch@example.org" 233 | } 234 | ] 235 | }, 236 | "error": {} 237 | } 238 | ``` 239 | 240 | #### Paginator 241 | 242 | Support custom inner `data` field names, such as `rows` or `list` 243 | 244 | ```json 245 | { 246 | "status": "success", 247 | "code": 200, 248 | "message": "操作成功", 249 | "data": { 250 | "data": [ 251 | { 252 | "nickname": "Joaquin Ondricka", 253 | "email": "lowe.chaim@example.org" 254 | }, 255 | { 256 | "nickname": "Jermain D'Amore", 257 | "email": "reanna.marks@example.com" 258 | }, 259 | { 260 | "nickname": "Erich Moore", 261 | "email": "ernestine.koch@example.org" 262 | }, 263 | { 264 | "nickname": "Eva Quitzon", 265 | "email": "rgottlieb@example.net" 266 | }, 267 | { 268 | "nickname": "Miss Gail Mitchell", 269 | "email": "kassandra.lueilwitz@example.net" 270 | } 271 | ], 272 | "meta": { 273 | "pagination": { 274 | "count": 5, 275 | "per_page": 5, 276 | "current_page": 1, 277 | "total": 12, 278 | "total_pages": 3, 279 | "links": { 280 | "previous": null, 281 | "next": "http://laravel-api.test/api/users/paginate?page=2" 282 | } 283 | } 284 | } 285 | }, 286 | "error": {} 287 | } 288 | ``` 289 | 290 | #### Simple Paginator 291 | 292 | Support custom inner `data` field names, such as `rows` or `list` 293 | 294 | ```json 295 | { 296 | "status": "success", 297 | "code": 200, 298 | "message": "操作成功", 299 | "data": { 300 | "data": [ 301 | { 302 | "nickname": "Joaquin Ondricka", 303 | "email": "lowe.chaim@example.org" 304 | }, 305 | { 306 | "nickname": "Jermain D'Amore", 307 | "email": "reanna.marks@example.com" 308 | }, 309 | { 310 | "nickname": "Erich Moore", 311 | "email": "ernestine.koch@example.org" 312 | }, 313 | { 314 | "nickname": "Eva Quitzon", 315 | "email": "rgottlieb@example.net" 316 | }, 317 | { 318 | "nickname": "Miss Gail Mitchell", 319 | "email": "kassandra.lueilwitz@example.net" 320 | } 321 | ], 322 | "meta": { 323 | "pagination": { 324 | "count": 5, 325 | "per_page": 5, 326 | "current_page": 1, 327 | "links": { 328 | "previous": null, 329 | "next": "http://laravel-api.test/api/users/simple-paginate?page=2" 330 | } 331 | } 332 | } 333 | }, 334 | "error": {} 335 | } 336 | ``` 337 | 338 | #### Return one row data 339 | 340 | ```json 341 | { 342 | "status": "success", 343 | "code": 200, 344 | "message": "操作成功", 345 | "data": { 346 | "nickname": "Joaquin Ondricka", 347 | "email": "lowe.chaim@example.org" 348 | }, 349 | "error": {} 350 | } 351 | ``` 352 | 353 | #### Other shortcuts 354 | 355 | ```php 356 | Response::ok();// There is no need to return data, but only message 357 | Response::localize(200101);// There is no need to return data, return message according to the response code 358 | Response::accepted(); 359 | Response::created(); 360 | Response::noContent(); 361 | ``` 362 | 363 | ### Failure response 364 | 365 | #### Do not specify message 366 | 367 | ```php 368 | public function fail() 369 | { 370 | Response::fail();// 不需要加 return 371 | } 372 | ``` 373 | 374 | - localization is not configured 375 | 376 | ```json 377 | { 378 | "status": "fail", 379 | "code": 500, 380 | "message": "Http internal server error", 381 | "data": {}, 382 | "error": {} 383 | } 384 | ``` 385 | 386 | - localization is configured 387 | 388 | ```json 389 | { 390 | "status": "fail", 391 | "code": 500, 392 | "message": "操作失败", 393 | "data": {}, 394 | "error": {} 395 | } 396 | ``` 397 | 398 | #### Specify message 399 | 400 | ```php 401 | public function fail() 402 | { 403 | Response::fail('error');// 不需要加 return 404 | } 405 | ``` 406 | 407 | return response 408 | 409 | ```json 410 | { 411 | "status": "fail", 412 | "code": 500, 413 | "message": "error", 414 | "data": {}, 415 | "error": {} 416 | } 417 | ``` 418 | 419 | #### Specify code 420 | 421 | ```php 422 | public function fail() 423 | { 424 | Response::fail('',ResponseCodeEnum::SERVICE_LOGIN_ERROR); 425 | } 426 | ``` 427 | 428 | return response 429 | 430 | ```json 431 | { 432 | "status": "fail", 433 | "code": 500102, 434 | "message": "登录失败", 435 | "data": {}, 436 | "error": {} 437 | } 438 | ``` 439 | 440 | #### Other shortcuts 441 | 442 | ```php 443 | Response::errorBadRequest(); 444 | Response::errorUnauthorized(); 445 | Response::errorForbidden(); 446 | Response::errorNotFound(); 447 | Response::errorMethodNotAllowed(); 448 | Response::errorInternal(); 449 | ``` 450 | 451 | ### Exception response 452 | 453 | #### Form validation exception 454 | 455 | ```json 456 | { 457 | "status": "error", 458 | "code": 422, 459 | "message": "验证失败", 460 | "data": {}, 461 | "error": { 462 | "email": [ 463 | "The email field is required." 464 | ] 465 | } 466 | } 467 | ``` 468 | 469 | #### An exception is thrown outside the controller 470 | 471 | You can throw an `HttpException` using the `abort` helper function 472 | 473 | ```php 474 | abort(500102,'登录失败'); 475 | 476 | // return response 477 | { 478 | "status": "fail", 479 | "code": 500102, 480 | "message": "登录失败", 481 | "data": {}, 482 | "error": {} 483 | } 484 | ``` 485 | 486 | #### Other exceptions 487 | 488 | enable debug mode(`APP_DEBUG=true`) 489 | 490 | ```json 491 | { 492 | "status": "error", 493 | "code": 404, 494 | "message": "Http not found", 495 | "data": {}, 496 | "error": { 497 | "message": "", 498 | "exception": "Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException", 499 | "file": "/home/vagrant/code/laravel-api-starter/vendor/laravel/framework/src/Illuminate/Routing/AbstractRouteCollection.php", 500 | "line": 43, 501 | "trace": [ 502 | { 503 | "file": "/home/vagrant/code/laravel-api-starter/vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php", 504 | "line": 162, 505 | "function": "handleMatchedRoute", 506 | "class": "Illuminate\\Routing\\AbstractRouteCollection", 507 | "type": "->" 508 | }, 509 | { 510 | "file": "/home/vagrant/code/laravel-api-starter/vendor/laravel/framework/src/Illuminate/Routing/Router.php", 511 | "line": 646, 512 | "function": "match", 513 | "class": "Illuminate\\Routing\\RouteCollection", 514 | "type": "->" 515 | } 516 | ] 517 | } 518 | } 519 | ``` 520 | 521 | disable debug mode(`APP_DEBUG=false`) 522 | 523 | ```json 524 | { 525 | "status": "error", 526 | "code": 404, 527 | "message": "Http not found", 528 | "data": {}, 529 | "error": {} 530 | } 531 | ``` 532 | 533 | ### Custom business code 534 | 535 | ```php 536 | [ 572 | // 成功 573 | ResponseCodeEnum::HTTP_OK => '操作成功', // 自定义 HTTP 状态码返回消息 574 | ResponseCodeEnum::HTTP_INTERNAL_SERVER_ERROR => '操作失败', // 自定义 HTTP 状态码返回消息 575 | ResponseCodeEnum::HTTP_UNAUTHORIZED => '授权失败', 576 | 577 | // 业务操作成功 578 | ResponseCodeEnum::SERVICE_REGISTER_SUCCESS => '注册成功', 579 | ResponseCodeEnum::SERVICE_LOGIN_SUCCESS => '登录成功', 580 | 581 | // 客户端错误 582 | ResponseCodeEnum::CLIENT_PARAMETER_ERROR => '参数错误', 583 | ResponseCodeEnum::CLIENT_CREATED_ERROR => '数据已存在', 584 | ResponseCodeEnum::CLIENT_DELETED_ERROR => '数据不存在', 585 | ResponseCodeEnum::CLIENT_VALIDATION_ERROR => '表单验证错误', 586 | 587 | // 服务端错误 588 | ResponseCodeEnum::SYSTEM_ERROR => '服务器错误', 589 | ResponseCodeEnum::SYSTEM_UNAVAILABLE => '服务器正在维护,暂不可用', 590 | ResponseCodeEnum::SYSTEM_CACHE_CONFIG_ERROR => '缓存配置错误', 591 | ResponseCodeEnum::SYSTEM_CACHE_MISSED_ERROR => '缓存未命中', 592 | ResponseCodeEnum::SYSTEM_CONFIG_ERROR => '系统配置错误', 593 | 594 | // 业务操作失败:授权业务 595 | ResponseCodeEnum::SERVICE_REGISTER_ERROR => '注册失败', 596 | ResponseCodeEnum::SERVICE_LOGIN_ERROR => '登录失败', 597 | ], 598 | ]; 599 | ``` 600 | 601 | ## Project supported by JetBrains 602 | 603 | Many thanks to Jetbrains for kindly providing a license for me to work on this and other open-source projects. 604 | 605 | [![](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg)](https://www.jetbrains.com/?from=https://github.com/jiannei) 606 | 607 | ## Contributors ✨ 608 | 609 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 |

Pham Thao

🎨

guanguans

🐛
620 | 621 | 622 | 623 | 624 | 625 | 626 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 627 | 628 | ## Stargazers over time 629 | 630 | [![Stargazers over time](https://starchart.cc/jiannei/laravel-response.svg)](https://starchart.cc/jiannei/laravel-response) 631 | 632 | ## License 633 | 634 | The MIT License (MIT). Please see [License File](LICENSE) for more information. --------------------------------------------------------------------------------