Skip to content

Commit 229dc96

Browse files
committed
feat: wip
1 parent 299ef1f commit 229dc96

27 files changed

+1088
-217
lines changed

.github/workflows/tests.yml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: tests
2+
3+
on: [ workflow_dispatch, push, pull_request ]
4+
5+
jobs:
6+
test:
7+
runs-on: ${{ matrix.os }}
8+
strategy:
9+
fail-fast: false
10+
matrix:
11+
os: [ ubuntu-latest ]
12+
php: [ 7.4, 8.3 ]
13+
dependency-version: [ prefer-stable ]
14+
laravel: [ 8.23.*, 9.*, 10.*, 11.* ]
15+
include:
16+
- laravel: 8.23.*
17+
testbench: 6.*
18+
- laravel: 9.*
19+
testbench: 7.*
20+
- laravel: 10.*
21+
testbench: 8.*
22+
- laravel: 11.*
23+
testbench: 9.*
24+
exclude:
25+
- php: 7.4
26+
laravel: 9.*
27+
- php: 7.4
28+
laravel: 10.*
29+
- php: 7.4
30+
laravel: 11.*
31+
- php: 8.3
32+
laravel: 8.23.*
33+
34+
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
35+
36+
steps:
37+
- name: Checkout code
38+
uses: actions/checkout@v4
39+
40+
- name: Cache dependencies
41+
uses: actions/cache@v4
42+
with:
43+
path: ~/.composer/cache/files
44+
key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
45+
46+
- name: Setup PHP
47+
uses: shivammathur/setup-php@v2
48+
with:
49+
php-version: ${{ matrix.php }}
50+
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick
51+
coverage: xdebug
52+
53+
- name: Install dependencies
54+
run: |
55+
composer remove "brainmaestro/composer-git-hooks" --no-interaction --no-scripts --dev --ansi -v
56+
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update --ansi -v
57+
composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-scripts --ansi -W -v
58+
59+
- name: Execute tests
60+
run: composer test-coverage
61+
62+
- name: Upload coverage to Codecov
63+
uses: codecov/codecov-action@v4
64+
with:
65+
files: ./.build/phpunit/clover.xml
66+
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
67+
fail_ci_if_error: true # optional (default = false)
68+
verbose: true # optional (default = false)

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ clover.xml
1111
phpunit.xml
1212
psalm.xml
1313
vendor/
14-
tests.*
14+
/tests.*
1515
.DS_Store
1616
.history/
1717
composer.phar

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
"require": {
3131
"php": ">=7.4",
3232
"ext-json": "*",
33-
"illuminate/support": "^8.23 || ^9.0 || ^10.0 || ^11.0"
33+
"illuminate/support": "^8.23 || ^9.0 || ^10.0 || ^11.0",
34+
"spatie/laravel-package-tools": "^1.12"
3435
},
3536
"require-dev": {
3637
"brainmaestro/composer-git-hooks": "^2.8 || ^3.0",

config/api-response.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
/** @noinspection LaravelFunctionsInspection */
4+
35
declare(strict_types=1);
46

57
/**
@@ -10,3 +12,48 @@
1012
*
1113
* @see https://github.com/guanguans/laravel-api-response
1214
*/
15+
16+
use Symfony\Component\HttpFoundation\Response;
17+
18+
return [
19+
/**
20+
* @see \Guanguans\LaravelApiResponse\ApiResponseServiceProvider::registerRenderUsing()
21+
*/
22+
'render_using_factory' => Guanguans\LaravelApiResponse\RenderUsingFactory::class,
23+
24+
/**
25+
* @see \Guanguans\LaravelApiResponse\ApiResponse::mapException()
26+
*/
27+
'exception_map' => [
28+
Illuminate\Auth\AuthenticationException::class => [
29+
'code' => Response::HTTP_UNAUTHORIZED,
30+
],
31+
// Illuminate\Database\QueryException::class => [
32+
// 'message' => '',
33+
// 'code' => Response::HTTP_INTERNAL_SERVER_ERROR,
34+
// ],
35+
// Illuminate\Validation\ValidationException::class => [
36+
// 'code' => Response::HTTP_UNPROCESSABLE_ENTITY,
37+
// ],
38+
// Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class => [
39+
// 'message' => '',
40+
// ],
41+
// Illuminate\Database\Eloquent\ModelNotFoundException::class => [
42+
// 'message' => '',
43+
// ],
44+
],
45+
46+
'pipes' => [
47+
/*
48+
* Before...
49+
*/
50+
Guanguans\LaravelApiResponse\Pipes\DataPipe::class,
51+
Guanguans\LaravelApiResponse\Pipes\MessagePipe::with(),
52+
Guanguans\LaravelApiResponse\Pipes\ErrorPipe::with(/* ! app()->hasDebugModeEnabled() */),
53+
54+
/*
55+
* After...
56+
*/
57+
// Guanguans\LaravelApiResponse\Pipes\SetStatusCodePipe::class::with(),
58+
],
59+
];

phpstan-baseline.neon

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,29 @@
11
parameters:
22
ignoreErrors:
33
-
4-
message: "#^Anonymous function has invalid return type Guanguans\\\\LaravelApiResponse\\\\ApiResponseManager\\.$#"
4+
message: "#^Call to an undefined method Illuminate\\\\Support\\\\Collection\\:\\:unshift\\(\\)\\.$#"
55
count: 1
6-
path: src/ApiResponseServiceProvider.php
6+
path: src/ApiResponse.php
77

88
-
9-
message: "#^Anonymous function has invalid return type Guanguans\\\\LaravelApiResponse\\\\CollectorManager\\.$#"
9+
message: "#^Offset 'code' on array\\{message\\: string, code\\: int, error\\: array\\|null, headers\\: array\\} on left side of \\?\\? always exists and is not nullable\\.$#"
1010
count: 1
11-
path: src/ApiResponseServiceProvider.php
11+
path: src/ApiResponse.php
1212

1313
-
14-
message: "#^Call to static method reportIf\\(\\) on an unknown class Guanguans\\\\LaravelApiResponse\\\\Facades\\\\ApiResponse\\.$#"
14+
message: "#^Offset 'headers' on array\\{message\\: string, code\\: int, error\\: array\\|null, headers\\: array\\} on left side of \\?\\? always exists and is not nullable\\.$#"
1515
count: 1
16-
path: src/ApiResponseServiceProvider.php
16+
path: src/ApiResponse.php
1717

1818
-
19-
message: "#^Class Guanguans\\\\LaravelApiResponse\\\\ApiResponseManager not found\\.$#"
20-
count: 4
21-
path: src/ApiResponseServiceProvider.php
22-
23-
-
24-
message: "#^Class Guanguans\\\\LaravelApiResponse\\\\CollectorManager not found\\.$#"
25-
count: 4
26-
path: src/ApiResponseServiceProvider.php
27-
28-
-
29-
message: "#^Class Guanguans\\\\LaravelApiResponse\\\\Commands\\\\TestCommand not found\\.$#"
30-
count: 5
31-
path: src/ApiResponseServiceProvider.php
32-
33-
-
34-
message: "#^Class Guanguans\\\\LaravelApiResponse\\\\Facades\\\\ApiResponse not found\\.$#"
35-
count: 1
36-
path: src/ApiResponseServiceProvider.php
37-
38-
-
39-
message: "#^Instantiated class Guanguans\\\\LaravelApiResponse\\\\ApiResponseManager not found\\.$#"
19+
message: "#^Offset 'message' on array\\{message\\: string, code\\: int, error\\: array\\|null, headers\\: array\\} on left side of \\?\\? always exists and is not nullable\\.$#"
4020
count: 1
41-
path: src/ApiResponseServiceProvider.php
21+
path: src/ApiResponse.php
4222

4323
-
44-
message: "#^Instantiated class Guanguans\\\\LaravelApiResponse\\\\CollectorManager not found\\.$#"
24+
message: "#^Right side of && is always true\\.$#"
4525
count: 1
46-
path: src/ApiResponseServiceProvider.php
26+
path: src/ApiResponse.php
4727

4828
-
4929
message: "#^Method Guanguans\\\\LaravelApiResponse\\\\Rectors\\\\ToInternalExceptionRector\\:\\:refactor\\(\\) should return 1\\|2\\|3\\|4\\|array\\<PhpParser\\\\Node\\>\\|PhpParser\\\\Node\\|null but empty return statement found\\.$#"

rector.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
// ->withPhpSets()
5858
// ->withPreparedSets()
5959
->withSets([
60-
// DowngradeLevelSetList::DOWN_TO_PHP_74,
60+
DowngradeLevelSetList::DOWN_TO_PHP_74,
6161
LevelSetList::UP_TO_PHP_74,
6262
])
6363
->withSets([

src/ApiResponse.php

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Copyright (c) 2021-2024 guanguans<ityaozm@gmail.com>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*
11+
* @see https://github.com/guanguans/laravel-api-response
12+
*/
13+
14+
namespace Guanguans\LaravelApiResponse;
15+
16+
use Guanguans\LaravelApiResponse\Concerns\ConcreteHttpStatusMethods;
17+
use Guanguans\LaravelApiResponse\Concerns\HasExceptionMap;
18+
use Guanguans\LaravelApiResponse\Concerns\HasPipes;
19+
use Illuminate\Contracts\Debug\ExceptionHandler;
20+
use Illuminate\Http\JsonResponse;
21+
use Illuminate\Pipeline\Pipeline;
22+
use Illuminate\Support\Collection;
23+
use Illuminate\Support\Traits\Conditionable;
24+
use Illuminate\Support\Traits\Dumpable;
25+
use Illuminate\Support\Traits\Macroable;
26+
use Illuminate\Support\Traits\Tappable;
27+
use Illuminate\Validation\ValidationException;
28+
use Symfony\Component\HttpFoundation\Response;
29+
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
30+
31+
/**
32+
* @see https://github.com/dingo/api
33+
* @see https://github.com/f9webltd/laravel-api-response-helpers
34+
* @see https://github.com/flugg/laravel-responder
35+
* @see https://github.com/jiannei/laravel-response
36+
* @see https://github.com/MarcinOrlowski/laravel-api-response-builder
37+
*
38+
* @method array convertExceptionToArray(\Throwable $throwable)
39+
*/
40+
class ApiResponse implements Contracts\ApiResponse
41+
{
42+
// use Dumpable;
43+
use ConcreteHttpStatusMethods;
44+
use Conditionable;
45+
use HasExceptionMap;
46+
use HasPipes;
47+
use Macroable;
48+
use Tappable;
49+
50+
public function __construct(?Collection $pipes = null, ?Collection $exceptionMap = null)
51+
{
52+
$this->pipes = collect($pipes);
53+
$this->exceptionMap = collect($exceptionMap);
54+
}
55+
56+
/**
57+
* @param mixed $data
58+
*/
59+
public function success($data = null, string $message = '', int $code = Response::HTTP_OK): JsonResponse
60+
{
61+
return $this->json(true, $code, $message, $data);
62+
}
63+
64+
public function error(string $message = '', int $code = Response::HTTP_BAD_REQUEST, ?array $error = null): JsonResponse
65+
{
66+
return $this->json(false, $code, $message, null, $error);
67+
}
68+
69+
/**
70+
* @see \Illuminate\Foundation\Exceptions\Handler::render()
71+
* @see \Illuminate\Foundation\Exceptions\Handler::prepareException()
72+
* @see \Illuminate\Foundation\Exceptions\Handler::convertExceptionToArray()
73+
* @see \Illuminate\Database\QueryException
74+
*/
75+
public function throw(\Throwable $throwable): JsonResponse
76+
{
77+
$newThrowable = $this->mapException($throwable);
78+
$newThrowable instanceof \Throwable and $throwable = $newThrowable;
79+
80+
/** @noinspection PhpCastIsUnnecessaryInspection */
81+
$code = (int) $throwable->getCode() ?: Response::HTTP_INTERNAL_SERVER_ERROR;
82+
$message = app()->hasDebugModeEnabled() ? $throwable->getMessage() : '';
83+
$error = (fn (): array => $this->convertExceptionToArray($throwable))->call(app(ExceptionHandler::class));
84+
$headers = [];
85+
86+
if ($throwable instanceof HttpExceptionInterface) {
87+
$message = $throwable->getMessage();
88+
$code = $throwable->getStatusCode();
89+
$headers = $throwable->getHeaders();
90+
}
91+
92+
if ($throwable instanceof ValidationException) {
93+
$message = $throwable->getMessage();
94+
$code = $throwable->status;
95+
$error = $throwable->errors();
96+
}
97+
98+
if (\is_array($newThrowable) && $newThrowable) {
99+
$message = $newThrowable['message'] ?? null ?: $message;
100+
$code = $newThrowable['code'] ?? null ?: $code;
101+
$error = $newThrowable['error'] ?? null ?: $error;
102+
$headers = $newThrowable['headers'] ?? null ?: $headers;
103+
}
104+
105+
return $this->error($message, $code, $error)->withHeaders($headers);
106+
}
107+
108+
/**
109+
* @param int<100, 599>|int<10000, 59999> $code
110+
* @param mixed $data
111+
* @param null|array<string, mixed> $error
112+
*/
113+
public function json(bool $status, int $code, string $message = '', $data = null, ?array $error = null): JsonResponse
114+
{
115+
return (new Pipeline(app()))
116+
->send(['status' => $status, 'code' => $code, 'message' => $message, 'data' => $data, 'error' => $error])
117+
->through($this->pipes())
118+
->then(static fn (array $data): JsonResponse => new JsonResponse(
119+
$data,
120+
200,
121+
[],
122+
\JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_LINE_TERMINATORS
123+
| \JSON_HEX_TAG | \JSON_HEX_APOS | \JSON_HEX_AMP | \JSON_HEX_QUOT
124+
));
125+
}
126+
}

0 commit comments

Comments
 (0)