Skip to content

Commit e53ac47

Browse files
authored
fix(router)!: return 404 when model binding fails (#2014)
1 parent a33b15a commit e53ac47

9 files changed

Lines changed: 122 additions & 6 deletions

File tree

docs/1-essentials/01-routing.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ use Tempest\Database\IsDatabaseModel;
171171

172172
final class Aircraft implements Bindable
173173
{
174-
public static function resolve(string $input): self
174+
public static function resolve(string $input): ?static
175175
{
176176
return query(self::class)->resolve($input);
177177
}
@@ -193,7 +193,7 @@ final class Aircraft implements Bindable
193193
#[IsBindingValue]
194194
public string $registrationNumber;
195195

196-
public static function resolve(string $input): self
196+
public static function resolve(string $input): ?static
197197
{
198198
return query(self::class)
199199
->where('registrationNumber', $input)

packages/database/src/Builder/QueryBuilders/QueryBuilder.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,9 @@ public function findById(string|int|PrimaryKey $id): ?object
169169
* query(User::class)->resolve(1);
170170
* ```
171171
*
172-
* @return TModel
172+
* @return TModel|null
173173
*/
174-
public function resolve(string|int|PrimaryKey $id): object
174+
public function resolve(string|int|PrimaryKey $id): ?object
175175
{
176176
if (! inspect($this->model)->hasPrimaryKey()) {
177177
throw ModelDidNotHavePrimaryColumn::neededForMethod($this->model, 'resolve');

packages/database/src/IsDatabaseModel.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public static function findById(string|int|PrimaryKey $id): ?static
7979
/**
8080
* Finds a model instance by its ID. Use through {@see Tempest\Router\Bindable}.
8181
*/
82-
public static function resolve(string $input): static
82+
public static function resolve(string $input): ?static
8383
{
8484
return static::queryBuilder()->resolve($input);
8585
}

packages/router/src/Bindable.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ interface Bindable
1010
/**
1111
* Resolves the implementing class through the given input.
1212
*/
13-
public static function resolve(string $input): self;
13+
public static function resolve(string $input): ?static;
1414
}

packages/upgrade/config/sets/tempest30.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use Rector\Configuration\Option;
55
use Rector\Configuration\Parameter\SimpleParameterProvider;
66
use Tempest\Upgrade\Tempest3\UpdateArrMapFunctionRector;
7+
use Tempest\Upgrade\Tempest3\UpdateBindableResolveReturnTypeRector;
78
use Tempest\Upgrade\Tempest3\UpdateCommandFunctionImportsRector;
89
use Tempest\Upgrade\Tempest3\UpdateContainerFunctionImportsRector;
910
use Tempest\Upgrade\Tempest3\UpdateEventFunctionImportsRector;
@@ -18,6 +19,7 @@
1819
SimpleParameterProvider::setParameter(Option::IMPORT_SHORT_CLASSES, value: true);
1920

2021
$config->rule(UpdateArrMapFunctionRector::class);
22+
$config->rule(UpdateBindableResolveReturnTypeRector::class);
2123
$config->rule(UpdateCommandFunctionImportsRector::class);
2224
$config->rule(UpdateContainerFunctionImportsRector::class);
2325
$config->rule(UpdateEventFunctionImportsRector::class);
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
namespace Tempest\Upgrade\Tempest3;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Identifier;
7+
use PhpParser\Node\Name;
8+
use PhpParser\Node\NullableType;
9+
use PhpParser\Node\Stmt\ClassMethod;
10+
use PhpParser\Node\Stmt\Interface_;
11+
use Rector\Rector\AbstractRector;
12+
13+
final class UpdateBindableResolveReturnTypeRector extends AbstractRector
14+
{
15+
public function getNodeTypes(): array
16+
{
17+
return [
18+
Node\Stmt\Class_::class,
19+
Interface_::class,
20+
];
21+
}
22+
23+
public function refactor(Node $node): ?int
24+
{
25+
if ($node instanceof Interface_) {
26+
if (! $this->hasBindableName($node->extends)) {
27+
return null;
28+
}
29+
30+
$this->refactorMethods($node->getMethods());
31+
32+
return null;
33+
}
34+
35+
if (! $node instanceof Node\Stmt\Class_) {
36+
return null;
37+
}
38+
39+
if (! $this->hasBindableName($node->implements)) {
40+
return null;
41+
}
42+
43+
$this->refactorMethods($node->getMethods());
44+
45+
return null;
46+
}
47+
48+
/**
49+
* @param Name[] $names
50+
*/
51+
private function hasBindableName(array $names): bool
52+
{
53+
foreach ($names as $name) {
54+
$value = ltrim($name->toString(), '\\');
55+
56+
if ($value === 'Tempest\\Router\\Bindable' || $value === 'Bindable') {
57+
return true;
58+
}
59+
}
60+
61+
return false;
62+
}
63+
64+
/**
65+
* @param ClassMethod[] $methods
66+
*/
67+
private function refactorMethods(array $methods): void
68+
{
69+
foreach ($methods as $method) {
70+
if ($method->name->toString() !== 'resolve' || ! $method->isStatic()) {
71+
continue;
72+
}
73+
74+
if ($method->returnType instanceof NullableType && $method->returnType->type instanceof Identifier && $method->returnType->type->toString() === 'static') {
75+
continue;
76+
}
77+
78+
$method->returnType = new NullableType(new Identifier('static'));
79+
}
80+
}
81+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
use Tempest\Router\Bindable;
4+
5+
final class UserBinding implements Bindable
6+
{
7+
public static function resolve(string $input): self
8+
{
9+
return new self();
10+
}
11+
}

packages/upgrade/tests/Tempest3/Tempest3RectorTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,12 @@ public function test_fully_qualified_map_iterable_call(): void
116116
->assertContains('use Tempest\Support\Arr\map;')
117117
->assertContains('return map($data, fn ($item) => $item * 2);');
118118
}
119+
120+
public function test_bindable_resolve_return_type_becomes_nullable(): void
121+
{
122+
$this->rector
123+
->runFixture(__DIR__ . '/Fixtures/BindableResolveReturnType.input.php')
124+
->assertContains('public static function resolve(string $input): ?static')
125+
->assertNotContains('public static function resolve(string $input): self');
126+
}
119127
}

tests/Integration/Route/RouterTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,20 @@ public function test_route_binding(): void
103103
$this->assertSame('Test', $response->body);
104104
}
105105

106+
public function test_route_binding_with_nonexistent_model_returns_404(): void
107+
{
108+
$this->database->migrate(
109+
CreateMigrationsTable::class,
110+
CreatePublishersTable::class,
111+
CreateAuthorTable::class,
112+
CreateBookTable::class,
113+
);
114+
115+
$this->http
116+
->get('/books/999')
117+
->assertNotFound();
118+
}
119+
106120
public function test_middleware(): void
107121
{
108122
$this

0 commit comments

Comments
 (0)