Skip to content

Commit 7cf0517

Browse files
authored
refactor(mapper): mapper improvements (#992)
1 parent 8ef314c commit 7cf0517

File tree

8 files changed

+72
-22
lines changed

8 files changed

+72
-22
lines changed

src/Tempest/Router/src/Mappers/PsrRequestToRequestMapper.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,6 @@ public function map(mixed $from, mixed $to): array|object
4343
}
4444
}
4545

46-
$data = arr($data)
47-
->map(fn (mixed $value) => $value === '' ? null : $value)
48-
->toArray();
49-
5046
$headersAsString = array_map(
5147
fn (array $items) => implode(',', $items),
5248
$from->getHeaders(),

src/Tempest/Router/src/Mappers/RequestToObjectMapper.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@
66

77
use Tempest\Mapper\Mapper;
88
use Tempest\Router\Request;
9+
use Tempest\Validation\Exceptions\PropertyValidationException;
10+
use Tempest\Validation\Exceptions\ValidationException;
11+
use Tempest\Validation\Validator;
912

1013
use function Tempest\map;
14+
use function Tempest\reflect;
1115

1216
final readonly class RequestToObjectMapper implements Mapper
1317
{
@@ -19,6 +23,30 @@ public function canMap(mixed $from, mixed $to): bool
1923
public function map(mixed $from, mixed $to): array|object
2024
{
2125
/** @var Request $from */
22-
return map($from->body)->to($to);
26+
$object = map($from->body)->to($to);
27+
28+
// We perform a new round of validation on the newly constructed object
29+
// because we want to be sure that required uninitialized properties are also validated.
30+
// This doesn't happen in the ArrayToObject mapper because we are more lenient there by design
31+
// TODO: The better approach would be to have this RequestToObjectMapper be totally independent of ArrayToObjectMapper
32+
$validator = new Validator();
33+
34+
$failingRules = [];
35+
36+
foreach (reflect($object)->getPublicProperties() as $property) {
37+
$value = $property->isInitialized($object) ? $property->getValue($object) : null;
38+
39+
try {
40+
$validator->validateProperty($property, $value);
41+
} catch (PropertyValidationException $validationException) {
42+
$failingRules[$property->getName()] = $validationException->failingRules;
43+
}
44+
}
45+
46+
if ($failingRules !== []) {
47+
throw new ValidationException($object, $failingRules);
48+
}
49+
50+
return $object;
2351
}
2452
}

tests/Fixtures/Modules/Posts/PostRequest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ final class PostRequest implements Request
1515

1616
public string $title;
1717

18-
public ?string $text;
18+
public string $text;
1919
}

tests/Integration/Mapper/MapperTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithMapToCollisions;
3030
use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithMapToCollisionsJsonSerializable;
3131
use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithMultipleMapFrom;
32+
use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithNotNullInferenceA;
3233
use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithStrictOnClass;
3334
use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithStrictProperty;
3435
use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithStringProperty;

tests/Integration/Mapper/PsrRequestToRequestMapperTest.php

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,22 +47,6 @@ public function test_map_with(): void
4747
$this->assertEquals(['x-test' => 'test'], $request->headers);
4848
}
4949

50-
public function test_empty_strings_are_converted_to_null(): void
51-
{
52-
$mapper = new PsrRequestToRequestMapper();
53-
54-
/** @var PostRequest $request */
55-
$request = $mapper->map(
56-
from: $this->http->makePsrRequest(
57-
uri: '/',
58-
body: ['title' => 'a', 'text' => ''],
59-
),
60-
to: PostRequest::class,
61-
);
62-
63-
$this->assertNull($request->text);
64-
}
65-
6650
public function test_map_with_with_missing_data(): void
6751
{
6852
$this->expectException(MissingValuesException::class);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Tests\Tempest\Integration\Route\Fixtures;
4+
5+
final class RequestObjectA
6+
{
7+
public RequestObjectB $b;
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Tests\Tempest\Integration\Route\Fixtures;
4+
5+
final class RequestObjectB
6+
{
7+
public string $name;
8+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Tests\Tempest\Integration\Route;
4+
5+
use Tempest\Http\Method;
6+
use Tempest\Router\GenericRequest;
7+
use Tempest\Validation\Exceptions\ValidationException;
8+
use Tempest\Validation\Rules\NotNull;
9+
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
10+
use Tests\Tempest\Integration\Route\Fixtures\RequestObjectA;
11+
use function Tempest\map;
12+
13+
final class RequestToObjectMapperTest extends FrameworkIntegrationTestCase
14+
{
15+
public function test_request(): void
16+
{
17+
$request = new GenericRequest(method: Method::POST, uri: '/', body: []);
18+
19+
try {
20+
map($request)->to(RequestObjectA::class);
21+
} catch (ValidationException $validationException) {
22+
$this->assertInstanceOf(NotNull::class, $validationException->failingRules['b'][0]);
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)