Skip to content

Commit 4d1218d

Browse files
authored
Merge pull request #2 from eufaturo/feature/responsable-and-query-builder-responses
Make responses Responsable and return SuccessResponse from QueryBuilder
2 parents 45d9295 + 9e33a8a commit 4d1218d

18 files changed

Lines changed: 127 additions & 88 deletions

File tree

.github/scripts/setup-php.sh

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,10 @@ sudo add-apt-repository ppa:ondrej/php
55
sudo apt-get update
66
sudo apt-get install -y \
77
php${PHP_VERSION} \
8-
php${PHP_VERSION}-curl \
9-
php${PHP_VERSION}-cgi \
108
php${PHP_VERSION}-cli \
119
php${PHP_VERSION}-curl \
12-
php${PHP_VERSION}-dev \
13-
php${PHP_VERSION}-fpm \
1410
php${PHP_VERSION}-intl \
1511
php${PHP_VERSION}-mbstring \
16-
php${PHP_VERSION}-mysql \
12+
php${PHP_VERSION}-sqlite3 \
1713
php${PHP_VERSION}-xml \
1814
php${PHP_VERSION}-zip

.github/workflows/tests.yml

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,6 @@ jobs:
2727

2828
runs-on: ubuntu-latest
2929

30-
services:
31-
mysql:
32-
image: mysql:8
33-
env:
34-
MYSQL_ALLOW_EMPTY_PASSWORD: yes
35-
MYSQL_DATABASE: eufaturo_testing
36-
ports:
37-
- 3306:3306
38-
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
39-
4030
steps:
4131
- name: Checkout Code
4232
uses: actions/checkout@v6

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,11 @@ Use it in a controller:
3232
```php
3333
final class ListController
3434
{
35-
public function __invoke(Request $request, Response $response): JsonResponse
35+
public function __invoke(Request $request)
3636
{
37-
$products = QueryBuilder::for(Product::class, $request)
37+
return QueryBuilder::for(Product::class, $request)
3838
->fromResource(ProductResource::class)
3939
->paginate();
40-
41-
return $response->success($products, ProductResource::class)->respond();
4240
}
4341
}
4442
```

docs/introduction.mdx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,16 @@ final class ProductResource extends Resource
6262
}
6363
```
6464

65-
Use the QueryBuilder and Response in your controller:
65+
Use the QueryBuilder in your controller:
6666

6767
```php
6868
final class ListController
6969
{
70-
public function __invoke(Request $request, Response $response): JsonResponse
70+
public function __invoke(Request $request)
7171
{
72-
$products = QueryBuilder::for(Product::class, $request)
72+
return QueryBuilder::for(Product::class, $request)
7373
->fromResource(ProductResource::class)
7474
->paginate();
75-
76-
return $response->success($products, ProductResource::class)->respond();
7775
}
7876
}
7977
```

docs/query-builder/overview.mdx

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: Overview
33
description: Fluent query builder that ties everything together
44
---
55

6-
The `QueryBuilder` is a fluent wrapper that applies filtering, sorting, includes, and pagination from the request. It reads defaults from your [Resource](/resources/overview) and allowing overrides.
6+
The `QueryBuilder` is a fluent wrapper that applies filtering, sorting, includes, and pagination from the request. It reads defaults from your [Resource](/resources/overview) and allows overrides.
77

88
## Why use it?
99

@@ -18,13 +18,14 @@ $query = Product::query();
1818
(new IncludeParser(allowed: [...]))->apply($request, $query);
1919

2020
$results = (new OffsetPaginator())->paginate($request, $query);
21+
22+
return $response->success($results, ProductResource::class)->respond();
2123
```
2224

2325
With the QueryBuilder:
2426

2527
```php
26-
// With QueryBuilder
27-
$results = QueryBuilder::for(Product::class, $request)
28+
return QueryBuilder::for(Product::class, $request)
2829
->fromResource(ProductResource::class)
2930
->paginate();
3031
```
@@ -34,23 +35,23 @@ $results = QueryBuilder::for(Product::class, $request)
3435
```php
3536
use Eufaturo\ApiToolkit\QueryBuilder;
3637

37-
$products = QueryBuilder::for(Product::class, $request)
38+
return QueryBuilder::for(Product::class, $request)
3839
->fromResource(ProductResource::class)
3940
->paginate();
40-
41-
return $response->success($products, ProductResource::class)->respond();
4241
```
4342

4443
The QueryBuilder reads `allowedFilters()`, `allowedSorts()`, `defaultSort()`, and `allowedIncludes()` from the Resource and applies them automatically.
4544

45+
The terminal methods (`paginate()`, `cursorPaginate()`, `get()`) return a `SuccessResponse` that implements Laravel's `Responsable` interface. You can return it directly from controllers without calling `->respond()`.
46+
4647
## Starting from an existing query
4748

4849
You can pass an existing `Builder` instance instead of a model class:
4950

5051
```php
5152
$query = Product::query()->where('company_id', $companyId);
5253

53-
$products = QueryBuilder::for($query, $request)
54+
return QueryBuilder::for($query, $request)
5455
->fromResource(ProductResource::class)
5556
->paginate();
5657
```
@@ -59,21 +60,31 @@ $products = QueryBuilder::for($query, $request)
5960

6061
| Method | Returns | Description |
6162
|--------|---------|-------------|
62-
| `paginate()` | `LengthAwarePaginator` | Offset-based pagination |
63-
| `cursorPaginate()` | `CursorPaginator` | Cursor-based pagination |
64-
| `get()` | `Collection` | All results (no pagination) |
63+
| `paginate()` | `SuccessResponse` | Offset-based pagination |
64+
| `cursorPaginate()` | `SuccessResponse` | Cursor-based pagination |
65+
| `get()` | `SuccessResponse` | All results (no pagination) |
66+
67+
All terminal methods return a `SuccessResponse` which is `Responsable`. You can chain `->meta()` or `->links()` before returning:
68+
69+
```php
70+
return QueryBuilder::for(Product::class, $request)
71+
->fromResource(ProductResource::class)
72+
->paginate()
73+
->meta(['request_id' => $requestId]);
74+
```
6575

6676
## Accessing the underlying query
6777

68-
Use `getQuery()` to access the Eloquent `Builder` before executing:
78+
Use `apply()` + `getQuery()` when you need direct access to the Eloquent Builder:
6979

7080
```php
7181
$builder = QueryBuilder::for(Product::class, $request)
72-
->fromResource(ProductResource::class);
82+
->fromResource(ProductResource::class)
83+
->apply();
7384

7485
$query = $builder->getQuery();
75-
// Add custom logic
7686
$query->whereHas('inventory', fn ($q) => $q->where('quantity', '>', 0));
7787

78-
$products = $builder->paginate();
88+
// Use Response class directly when working with raw queries
89+
return $response->success($query->paginate(20), ProductResource::class);
7990
```

docs/quickstart.mdx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,20 +65,16 @@ final class ProductResource extends Resource
6565
```php app/Http/Controllers/Products/ListController.php
6666
use App\Http\Resources\ProductResource;
6767
use App\Models\Product;
68-
use Eufaturo\ApiToolkit\Http\Response;
6968
use Eufaturo\ApiToolkit\QueryBuilder;
70-
use Illuminate\Http\JsonResponse;
7169
use Illuminate\Http\Request;
7270

7371
final class ListController
7472
{
75-
public function __invoke(Request $request, Response $response): JsonResponse
73+
public function __invoke(Request $request)
7674
{
77-
$products = QueryBuilder::for(Product::class, $request)
75+
return QueryBuilder::for(Product::class, $request)
7876
->fromResource(ProductResource::class)
7977
->paginate();
80-
81-
return $response->success($products, ProductResource::class)->respond();
8278
}
8379
}
8480
```
@@ -89,13 +85,12 @@ final class ListController
8985
use App\Http\Resources\ProductResource;
9086
use App\Models\Product;
9187
use Eufaturo\ApiToolkit\Http\Response;
92-
use Illuminate\Http\JsonResponse;
9388

9489
final class ViewController
9590
{
96-
public function __invoke(Product $product, Response $response): JsonResponse
91+
public function __invoke(Product $product, Response $response)
9792
{
98-
return $response->success($product, ProductResource::class)->respond();
93+
return $response->success($product, ProductResource::class);
9994
}
10095
}
10196
```

docs/resources/collections.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ When you pass an array, `Collection`, or paginator to a success response, the to
1414
```php
1515
$products = Product::query()->paginate(20);
1616

17-
return $response->success($products, ProductResource::class)->respond();
17+
return $response->success($products, ProductResource::class);
1818
```
1919

2020
## Supported data types

docs/resources/links-and-meta.mdx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,7 @@ In addition to resource-level links and meta, you can add response-level meta an
101101
return $response
102102
->success($products, ProductResource::class)
103103
->meta(['request_id' => $requestId])
104-
->links(['documentation' => 'https://docs.example.com/products'])
105-
->respond();
104+
->links(['documentation' => 'https://docs.example.com/products']);
106105
```
107106

108107
These are added to the top-level JSON:API document, separate from the individual resource objects.

docs/responses/error-responses.mdx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ title: Error Responses
33
description: Build JSON:API error responses
44
---
55

6+
`ErrorResponse` implements Laravel's `Responsable` interface. The HTTP status code is derived from the status you pass to `error()`, so you can return it directly without calling `->respond()`.
7+
68
## Basic error
79

810
```php
9-
return $this->response->error('Not Found', 'Product does not exist', 404)->respond();
11+
return $this->response->error('Not Found', 'Product does not exist', 404);
1012
```
1113

1214
```json
@@ -26,8 +28,7 @@ return $this->response->error('Not Found', 'Product does not exist', 404)->respo
2628
```php
2729
return $this->response
2830
->error('Validation Error', 'Name is required', 422)
29-
->code('validation_error')
30-
->respond();
31+
->code('validation_error');
3132
```
3233

3334
```json
@@ -51,22 +52,20 @@ Point to the field that caused the error:
5152
return $this->response
5253
->error('Validation Error', 'Name is required', 422)
5354
->code('validation_error')
54-
->source(['pointer' => '/data/attributes/name'])
55-
->respond();
55+
->source(['pointer' => '/data/attributes/name']);
5656
```
5757

5858
## With meta
5959

6060
```php
6161
return $this->response
6262
->error('Rate Limited', 'Too many requests', 429)
63-
->meta(['retry_after' => 60])
64-
->respond();
63+
->meta(['retry_after' => 60]);
6564
```
6665

67-
## Custom status code on respond
66+
## Custom headers
6867

69-
Override the status code at response time:
68+
Use `->respond()` when you need custom headers:
7069

7170
```php
7271
return $this->response

docs/responses/overview.mdx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,24 @@ final class ProductController
2020
}
2121
```
2222

23+
## Responsable
24+
25+
Both `SuccessResponse` and `ErrorResponse` implement Laravel's `Responsable` interface. You can return them directly from controllers without calling `->respond()`:
26+
27+
```php
28+
// No ->respond() needed
29+
return $this->response->success($product, ProductResource::class);
30+
31+
// Also works with errors
32+
return $this->response->error('Not Found', 'Product does not exist', 404);
33+
```
34+
35+
Use `->respond()` when you need a custom status code or headers:
36+
37+
```php
38+
return $this->response->success($product, ProductResource::class)->respond(201);
39+
```
40+
2341
## Inject via constructor
2442

2543
The `Response` class is registered as a singleton in the service container. Inject it via constructor dependency injection:

0 commit comments

Comments
 (0)