Skip to content

Commit a5e1fb3

Browse files
committed
Merge 4.1
2 parents c2af797 + 85f198a commit a5e1fb3

File tree

16 files changed

+525
-32
lines changed

16 files changed

+525
-32
lines changed

src/Laravel/Eloquent/Extension/FilterQueryExtension.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public function apply(Builder $builder, array $uriVariables, Operation $operatio
6060

6161
$filter = $filterId instanceof FilterInterface ? $filterId : ($this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null);
6262
if ($filter instanceof FilterInterface) {
63-
$builder = $filter->apply($builder, $values, $parameter, $context + ($parameter->getFilterContext() ?? []));
63+
$builder = $filter->apply($builder, $values, $parameter->withKey($parameter->getExtraProperties()['_query_property'] ?? $parameter->getKey()), $context + ($parameter->getFilterContext() ?? []));
6464
}
6565
}
6666

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Laravel\Tests;
15+
16+
use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait;
17+
use Illuminate\Foundation\Testing\RefreshDatabase;
18+
use Illuminate\Support\Facades\DB;
19+
use Orchestra\Testbench\Concerns\WithWorkbench;
20+
use Orchestra\Testbench\TestCase;
21+
use Workbench\Database\Factories\ActiveBookFactory;
22+
23+
class OrderFilterTest extends TestCase
24+
{
25+
use ApiTestAssertionsTrait;
26+
use RefreshDatabase;
27+
use WithWorkbench;
28+
29+
public function testQueryParameterWithCamelCaseProperty(): void
30+
{
31+
ActiveBookFactory::new(['is_active' => true])->count(2)->create();
32+
ActiveBookFactory::new(['is_active' => false])->count(3)->create();
33+
34+
DB::enableQueryLog();
35+
$response = $this->get('/api/active_books?sort[isActive]=asc', ['Accept' => ['application/ld+json']]);
36+
$response->assertStatus(200);
37+
$this->assertEquals(\DB::getQueryLog()[1]['query'], 'select * from "active_books" order by "isActive" asc limit 30 offset 0');
38+
DB::flushQueryLog();
39+
$response = $this->get('/api/active_books?sort[isActive]=desc', ['Accept' => ['application/ld+json']]);
40+
$response->assertStatus(200);
41+
$this->assertEquals(DB::getQueryLog()[1]['query'], 'select * from "active_books" order by "isActive" desc limit 30 offset 0');
42+
}
43+
}

src/Laravel/Tests/EloquentTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
use Illuminate\Support\Str;
2020
use Orchestra\Testbench\Concerns\WithWorkbench;
2121
use Orchestra\Testbench\TestCase;
22+
use Workbench\App\Http\Requests\StoreSlotRequest;
2223
use Workbench\App\Models\PostWithMorphMany;
24+
use Workbench\Database\Factories\AreaFactory;
2325
use Workbench\Database\Factories\AuthorFactory;
2426
use Workbench\Database\Factories\BookFactory;
2527
use Workbench\Database\Factories\CommentMorphFactory;
@@ -614,4 +616,20 @@ public function testCreateDeliveryRequestWithPickupSlot(): void
614616
'note' => 'This is a test note.',
615617
]);
616618
}
619+
620+
public function testIriIsNotDenormalizedBeforeFormRequestValidation(): void
621+
{
622+
$area = AreaFactory::new()->create();
623+
624+
$this->postJson(
625+
'/api/slots',
626+
[
627+
'name' => 'Morning Slot',
628+
'area' => '/api/areas/'.$area->id, // @phpstan-ignore-line
629+
],
630+
['accept' => 'application/ld+json', 'content-type' => 'application/ld+json']
631+
)->assertStatus(201);
632+
633+
$this->assertSame(StoreSlotRequest::$receivedArea->name, $area->name); // @phpstan-ignore-line
634+
}
617635
}

src/Laravel/Tests/ValidationTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,14 @@ public function testRouteWithRequirements(): void
7575
$response = $this->get('api/issue_7194_requirements/1', ['accept' => 'application/ld+json']);
7676
$response->assertStatus(200);
7777
}
78+
79+
public function testGetCollectionWithFormRequestValidation(): void
80+
{
81+
$response = $this->get('/api/slots/dropoff', ['accept' => 'application/ld+json']);
82+
$response->assertStatus(422);
83+
$response->assertJsonFragment(['violations' => [
84+
['propertyPath' => 'pickupDate', 'message' => 'The pickup date field is required.'],
85+
['propertyPath' => 'pickupSlotId', 'message' => 'The pickup slot id field is required.'],
86+
]]);
87+
}
7888
}

src/Laravel/config/api-platform.php

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
'routes' => [
2626
'domain' => null,
2727
// Global middleware applied to every API Platform routes
28-
// 'middleware' => []
28+
// 'middleware' => [],
2929
],
3030

3131
'resources' => [
@@ -78,13 +78,13 @@
7878
'introspection' => ['enabled' => true],
7979
'max_query_complexity' => 500,
8080
'max_query_depth' => 200,
81-
// 'middleware' => null
81+
// 'middleware' => null,
8282
],
8383

8484
'graphiql' => [
8585
// 'enabled' => true,
8686
// 'domain' => null,
87-
// 'middleware' => null
87+
// 'middleware' => null,
8888
],
8989

9090
// set to null if you want to keep snake_case
@@ -98,48 +98,47 @@
9898
'swagger_ui' => [
9999
'enabled' => true,
100100
// 'apiKeys' => [
101-
// 'api' => [
102-
// 'type' => 'Bearer',
103-
// 'name' => 'Authentication Token',
104-
// 'in' => 'header'
105-
// ]
101+
// 'api' => [
102+
// 'name' => 'Authorization',
103+
// 'type' => 'header',
104+
// ],
106105
// ],
107106
// 'oauth' => [
108-
// 'enabled' => true,
109-
// 'type' => 'oauth2',
110-
// 'flow' => 'authorizationCode',
111-
// 'tokenUrl' => '',
112-
// 'authorizationUrl' =>'',
113-
// 'refreshUrl' => '',
114-
// 'scopes' => ['scope1' => 'Description scope 1'],
115-
// 'pkce' => true
107+
// 'enabled' => true,
108+
// 'type' => 'oauth2',
109+
// 'flow' => 'authorizationCode',
110+
// 'tokenUrl' => '',
111+
// 'authorizationUrl' =>'',
112+
// 'refreshUrl' => '',
113+
// 'scopes' => ['scope1' => 'Description scope 1'],
114+
// 'pkce' => true,
116115
// ],
117116
// 'license' => [
118-
// 'name' => 'Apache 2.0',
119-
// 'url' => 'https://www.apache.org/licenses/LICENSE-2.0.html',
117+
// 'name' => 'Apache 2.0',
118+
// 'url' => 'https://www.apache.org/licenses/LICENSE-2.0.html',
120119
// ],
121120
// 'contact' => [
122-
// 'name' => 'API Support',
123-
// 'url' => 'https://www.example.com/support',
124-
// 'email' => '[email protected]',
121+
// 'name' => 'API Support',
122+
// 'url' => 'https://www.example.com/support',
123+
// 'email' => '[email protected]',
125124
// ],
126125
// 'http_auth' => [
127-
// 'Personal Access Token' => [
128-
// 'scheme' => 'bearer',
129-
// 'bearerFormat' => 'JWT'
130-
// ]
131-
// ]
126+
// 'Personal Access Token' => [
127+
// 'scheme' => 'bearer',
128+
// 'bearerFormat' => 'JWT',
129+
// ],
130+
// ],
132131
],
133132

134133
// 'openapi' => [
135-
// 'tags' => []
134+
// 'tags' => [],
136135
// ],
137136

138137
'url_generation_strategy' => UrlGeneratorInterface::ABS_PATH,
139138

140139
'serializer' => [
141140
'hydra_prefix' => false,
142-
// 'datetime_format' => \DateTimeInterface::RFC3339
141+
// 'datetime_format' => \DateTimeInterface::RFC3339,
143142
],
144143

145144
// we recommend using "file" or "acpu"
@@ -161,5 +160,5 @@
161160
// 'request_options' => [],
162161
// 'purger' => ApiPlatform\HttpCache\SouinPurger::class,
163162
// ],
164-
// ]
163+
// ],
165164
];
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Workbench\App\ApiResource;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use ApiPlatform\Metadata\GetCollection;
18+
use Workbench\App\Http\Requests\GetDropOffSlotsRequest;
19+
20+
#[ApiResource(
21+
operations: [
22+
new GetCollection(
23+
uriTemplate: '/slots/dropoff',
24+
rules: GetDropOffSlotsRequest::class,
25+
provider: [self::class, 'provide'],
26+
output: SlotsForDate::class,
27+
),
28+
],
29+
)]
30+
class SlotsForDate
31+
{
32+
public int $id = 1;
33+
public string $name = 'Morning Slot';
34+
public string $note = 'This is a morning slot';
35+
36+
public static function provide()
37+
{
38+
return [];
39+
}
40+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Workbench\App\Http\Requests;
15+
16+
use ApiPlatform\Laravel\State\ValidationErrorTrait;
17+
use Illuminate\Contracts\Validation\Validator;
18+
use Illuminate\Foundation\Http\FormRequest;
19+
20+
class GetDropOffSlotsRequest extends FormRequest
21+
{
22+
use ValidationErrorTrait;
23+
24+
public function authorize(): bool
25+
{
26+
return true;
27+
}
28+
29+
protected function prepareForValidation(): void
30+
{
31+
$this->merge([
32+
'pickupDate' => $this->query('pickupDate'),
33+
'pickupSlotId' => $this->query('pickupSlotId'),
34+
]);
35+
}
36+
37+
/**
38+
* Get the validation rules that apply to the request.
39+
*
40+
* @docs/guides/return-the-iri-of-your-resources-relations.php array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
41+
*/
42+
public function rules(): array
43+
{
44+
return [
45+
'pickupDate' => 'required|date|after_or_equal:today',
46+
'pickupSlotId' => 'required|string',
47+
];
48+
}
49+
50+
// to match api plaform validation response
51+
protected function failedValidation(Validator $validator): void
52+
{
53+
$violations = collect($validator->errors())
54+
->map(fn ($m, $f) => ['propertyPath' => $f, 'message' => $m[0]]) // ** @phpstan-ignore-line */
55+
->values()->all();
56+
57+
throw new \ApiPlatform\Laravel\ApiResource\ValidationError($violations[0]['message'] ?? 'Validation failed.', hash('xxh3', implode(',', array_column($violations, 'propertyPath'))), new \Illuminate\Validation\ValidationException($validator), $violations);
58+
}
59+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Workbench\App\Http\Requests;
15+
16+
use ApiPlatform\Metadata\IriConverterInterface;
17+
use Illuminate\Foundation\Http\FormRequest;
18+
use Illuminate\Support\Facades\App;
19+
use Workbench\App\Models\Area;
20+
21+
class StoreSlotRequest extends FormRequest
22+
{
23+
public static ?Area $receivedArea = null;
24+
25+
public function authorize(): bool
26+
{
27+
return true;
28+
}
29+
30+
public function rules(): array
31+
{
32+
$iriConverter = App::get(IriConverterInterface::class);
33+
34+
return [
35+
'name' => ['required', 'string', 'max:255'],
36+
'area' => [
37+
'required',
38+
'string',
39+
function (string $attribute, mixed $value, \Closure $fail) use ($iriConverter): void {
40+
if (!(self::$receivedArea = $iriConverter->getResourceFromIri($value))) {
41+
$fail("The {$attribute} is invalid.");
42+
}
43+
},
44+
],
45+
];
46+
}
47+
}

0 commit comments

Comments
 (0)