Skip to content

Commit 1260979

Browse files
committed
Extract a shared cast-field resolver for entity and model typing
1 parent 57da04e commit 1260979

6 files changed

Lines changed: 99 additions & 56 deletions

File tree

extension.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ services:
5656
castTypeResolver:
5757
class: CodeIgniter\PHPStan\Database\Schema\CastTypeResolver
5858

59+
castFieldTypeResolver:
60+
class: CodeIgniter\PHPStan\Database\Schema\CastFieldTypeResolver
61+
5962
columnTypeResolver:
6063
class: CodeIgniter\PHPStan\Database\Schema\ColumnTypeResolver
6164

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) 2023 CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\PHPStan\Database\Schema;
15+
16+
use PHPStan\Reflection\ParametersAcceptorSelector;
17+
use PHPStan\Reflection\ReflectionProvider;
18+
use PHPStan\Type\MixedType;
19+
use PHPStan\Type\Type;
20+
use PHPStan\Type\TypeCombinator;
21+
22+
/**
23+
* Resolves a `$casts` entry to the type its handler reads back, using the framework built-ins and
24+
* reflecting custom `$castHandlers` for cast names the framework does not define.
25+
*/
26+
final class CastFieldTypeResolver
27+
{
28+
public function __construct(
29+
private readonly ReflectionProvider $reflectionProvider,
30+
private readonly CastTypeResolver $castTypeResolver,
31+
) {}
32+
33+
/**
34+
* @param array<string, string> $castHandlers
35+
*/
36+
public function resolve(string $cast, array $castHandlers): Type
37+
{
38+
return $this->castTypeResolver->resolve($cast) ?? $this->resolveCustomHandler($cast, $castHandlers);
39+
}
40+
41+
/**
42+
* @param array<string, string> $castHandlers
43+
*/
44+
private function resolveCustomHandler(string $cast, array $castHandlers): Type
45+
{
46+
$nullable = str_starts_with($cast, '?');
47+
48+
if ($nullable) {
49+
$cast = substr($cast, 1);
50+
}
51+
52+
$handler = $castHandlers[$this->castName($cast)] ?? null;
53+
54+
if ($handler === null || ! $this->reflectionProvider->hasClass($handler)) {
55+
return new MixedType();
56+
}
57+
58+
$handlerReflection = $this->reflectionProvider->getClass($handler);
59+
60+
if (! $handlerReflection->hasNativeMethod('get')) {
61+
return new MixedType();
62+
}
63+
64+
$type = ParametersAcceptorSelector::combineAcceptors($handlerReflection->getNativeMethod('get')->getVariants())->getReturnType();
65+
66+
return $nullable ? TypeCombinator::addNull($type) : $type;
67+
}
68+
69+
private function castName(string $cast): string
70+
{
71+
if (preg_match('/\A(.+)\[.+\]\z/', $cast, $matches) === 1) {
72+
return $matches[1];
73+
}
74+
75+
return $cast;
76+
}
77+
}

src/Reflection/EntityPropertiesClassReflectionExtension.php

Lines changed: 3 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,16 @@
1616
use CodeIgniter\Entity\Entity;
1717
use CodeIgniter\I18n\Time;
1818
use CodeIgniter\PHPStan\Database\ModelTableMapProvider;
19-
use CodeIgniter\PHPStan\Database\Schema\CastTypeResolver;
19+
use CodeIgniter\PHPStan\Database\Schema\CastFieldTypeResolver;
2020
use CodeIgniter\PHPStan\Database\Schema\Column;
2121
use CodeIgniter\PHPStan\Database\Schema\ColumnTypeResolver;
2222
use CodeIgniter\PHPStan\Database\SchemaProvider;
2323
use PHPStan\Reflection\ClassReflection;
24-
use PHPStan\Reflection\ParametersAcceptorSelector;
2524
use PHPStan\Reflection\PropertiesClassReflectionExtension;
2625
use PHPStan\Reflection\PropertyReflection;
27-
use PHPStan\Reflection\ReflectionProvider;
2826
use PHPStan\Type\MixedType;
2927
use PHPStan\Type\ObjectType;
3028
use PHPStan\Type\Type;
31-
use PHPStan\Type\TypeCombinator;
3229

3330
/**
3431
* Types virtual properties on `CodeIgniter\Entity\Entity` subclasses, layering `$dates` and `$casts`
@@ -37,8 +34,7 @@
3734
final class EntityPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension
3835
{
3936
public function __construct(
40-
private readonly ReflectionProvider $reflectionProvider,
41-
private readonly CastTypeResolver $castTypeResolver,
37+
private readonly CastFieldTypeResolver $castFieldTypeResolver,
4238
private readonly SchemaProvider $schemaProvider,
4339
private readonly ModelTableMapProvider $modelTableMapProvider,
4440
private readonly ColumnTypeResolver $columnTypeResolver,
@@ -70,7 +66,7 @@ private function resolveType(ClassReflection $classReflection, string $propertyN
7066
}
7167

7268
if (isset($casts[$column])) {
73-
return $this->resolveCastType($classReflection, $casts[$column]);
69+
return $this->castFieldTypeResolver->resolve($casts[$column], $this->readStringMap($classReflection, 'castHandlers'));
7470
}
7571

7672
if ($schema !== null && ! $this->hasGetter($classReflection, $column)) {
@@ -80,36 +76,6 @@ private function resolveType(ClassReflection $classReflection, string $propertyN
8076
return null;
8177
}
8278

83-
private function resolveCastType(ClassReflection $classReflection, string $cast): Type
84-
{
85-
return $this->castTypeResolver->resolve($cast) ?? $this->resolveCustomHandlerType($classReflection, $cast);
86-
}
87-
88-
private function resolveCustomHandlerType(ClassReflection $classReflection, string $cast): Type
89-
{
90-
$nullable = str_starts_with($cast, '?');
91-
92-
if ($nullable) {
93-
$cast = substr($cast, 1);
94-
}
95-
96-
$handler = $this->readStringMap($classReflection, 'castHandlers')[$this->castName($cast)] ?? null;
97-
98-
if ($handler === null || ! $this->reflectionProvider->hasClass($handler)) {
99-
return new MixedType();
100-
}
101-
102-
$handlerReflection = $this->reflectionProvider->getClass($handler);
103-
104-
if (! $handlerReflection->hasNativeMethod('get')) {
105-
return new MixedType();
106-
}
107-
108-
$type = ParametersAcceptorSelector::combineAcceptors($handlerReflection->getNativeMethod('get')->getVariants())->getReturnType();
109-
110-
return $nullable ? TypeCombinator::addNull($type) : $type;
111-
}
112-
11379
private function lookupColumn(ClassReflection $classReflection, string $column): ?Column
11480
{
11581
$table = $this->modelTableMapProvider->getTableForEntity($classReflection->getName());
@@ -128,15 +94,6 @@ private function hasGetter(ClassReflection $classReflection, string $column): bo
12894
return $classReflection->hasNativeMethod($method) || $classReflection->hasNativeMethod('_' . $method);
12995
}
13096

131-
private function castName(string $cast): string
132-
{
133-
if (preg_match('/\A(.+)\[.+\]\z/', $cast, $matches) === 1) {
134-
return $matches[1];
135-
}
136-
137-
return $cast;
138-
}
139-
14097
private function mapColumn(ClassReflection $classReflection, string $propertyName): string
14198
{
14299
$mapped = $this->readStringMap($classReflection, 'datamap')[$propertyName] ?? '';

src/Type/ModelFetchedReturnTypeHelper.php

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
namespace CodeIgniter\PHPStan\Type;
1515

16-
use CodeIgniter\PHPStan\Database\Schema\CastTypeResolver;
16+
use CodeIgniter\PHPStan\Database\Schema\CastFieldTypeResolver;
1717
use CodeIgniter\PHPStan\Database\Schema\Column;
1818
use CodeIgniter\PHPStan\Database\Schema\ColumnTypeResolver;
1919
use CodeIgniter\PHPStan\Database\SchemaProvider;
@@ -43,7 +43,7 @@ public function __construct(
4343
private readonly ReflectionProvider $reflectionProvider,
4444
private readonly SchemaProvider $schemaProvider,
4545
private readonly ColumnTypeResolver $columnTypeResolver,
46-
private readonly CastTypeResolver $castTypeResolver,
46+
private readonly CastFieldTypeResolver $castFieldTypeResolver,
4747
) {}
4848

4949
public function getFetchedReturnType(ClassReflection $classReflection, ?MethodCall $methodCall, Scope $scope): Type
@@ -94,23 +94,25 @@ private function resolveArrayRowType(ClassReflection $classReflection): Type
9494
return new ArrayType(new StringType(), new MixedType());
9595
}
9696

97-
$casts = $this->readStringMap($classReflection, 'casts');
98-
$builder = ConstantArrayTypeBuilder::createEmpty();
97+
$casts = $this->readStringMap($classReflection, 'casts');
98+
$castHandlers = $this->readStringMap($classReflection, 'castHandlers');
99+
$builder = ConstantArrayTypeBuilder::createEmpty();
99100

100101
foreach ($table->columns as $column) {
101-
$builder->setOffsetValueType(new ConstantStringType($column->name), $this->resolveFieldType($column, $casts));
102+
$builder->setOffsetValueType(new ConstantStringType($column->name), $this->resolveFieldType($column, $casts, $castHandlers));
102103
}
103104

104105
return $builder->getArray();
105106
}
106107

107108
/**
108109
* @param array<string, string> $casts
110+
* @param array<string, string> $castHandlers
109111
*/
110-
private function resolveFieldType(Column $column, array $casts): Type
112+
private function resolveFieldType(Column $column, array $casts, array $castHandlers): Type
111113
{
112114
if (isset($casts[$column->name])) {
113-
return $this->castTypeResolver->resolve($casts[$column->name]) ?? new MixedType();
115+
return $this->castFieldTypeResolver->resolve($casts[$column->name], $castHandlers);
114116
}
115117

116118
return $this->columnTypeResolver->resolve($column);

tests/Fixtures/Models/BlogPostModel.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@
1414
namespace CodeIgniter\PHPStan\Tests\Fixtures\Models;
1515

1616
use CodeIgniter\Model;
17+
use CodeIgniter\PHPStan\Tests\Fixtures\Entity\MoneyCast;
1718

1819
final class BlogPostModel extends Model
1920
{
2021
protected $table = 'blog_posts';
2122
protected array $casts = [
22-
'user_id' => 'string',
23+
'user_id' => 'money',
2324
'title' => 'datetime',
2425
];
26+
protected array $castHandlers = [
27+
'money' => MoneyCast::class,
28+
];
2529
}

tests/data/type-inference/model-return-types.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,5 @@
3131

3232
$posts = new BlogPostModel();
3333

34-
assertType('array{id: int, user_id: string, title: CodeIgniter\I18n\Time}|null', $posts->first());
35-
assertType('list<array{id: int, user_id: string, title: CodeIgniter\I18n\Time}>', $posts->findAll());
34+
assertType('array{id: int, user_id: CodeIgniter\PHPStan\Tests\Fixtures\Entity\Money, title: CodeIgniter\I18n\Time}|null', $posts->first());
35+
assertType('list<array{id: int, user_id: CodeIgniter\PHPStan\Tests\Fixtures\Entity\Money, title: CodeIgniter\I18n\Time}>', $posts->findAll());

0 commit comments

Comments
 (0)