Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8a8b1c0

Browse files
committed
Type entity virtual properties from casts and custom handlers
1 parent 657f1a1 commit 8a8b1c0

7 files changed

Lines changed: 345 additions & 0 deletions

File tree

extension.neon

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ services:
4646
schemaProvider:
4747
class: CodeIgniter\PHPStan\Database\SchemaProvider
4848

49+
castTypeResolver:
50+
class: CodeIgniter\PHPStan\Database\Schema\CastTypeResolver
51+
4952
factoriesReturnTypeHelper:
5053
class: CodeIgniter\PHPStan\Helpers\FactoriesReturnTypeHelper
5154
arguments:
@@ -114,6 +117,11 @@ services:
114117
tags:
115118
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
116119

120+
-
121+
class: CodeIgniter\PHPStan\Reflection\EntityPropertiesClassReflectionExtension
122+
tags:
123+
- phpstan.broker.propertiesClassReflectionExtension
124+
117125
-
118126
class: CodeIgniter\PHPStan\Rules\Functions\FactoriesFunctionArgumentTypeRule
119127
arguments:
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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 <[email protected]>
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\Reflection;
15+
16+
use CodeIgniter\Entity\Entity;
17+
use CodeIgniter\PHPStan\Database\Schema\CastTypeResolver;
18+
use PHPStan\Reflection\ClassReflection;
19+
use PHPStan\Reflection\ParametersAcceptorSelector;
20+
use PHPStan\Reflection\PropertiesClassReflectionExtension;
21+
use PHPStan\Reflection\PropertyReflection;
22+
use PHPStan\Reflection\ReflectionProvider;
23+
use PHPStan\Type\MixedType;
24+
use PHPStan\Type\Type;
25+
use PHPStan\Type\TypeCombinator;
26+
27+
/**
28+
* Types virtual properties on `CodeIgniter\Entity\Entity` subclasses from their `$casts` entries,
29+
* reflecting custom `$castHandlers` for casts the framework does not define.
30+
*/
31+
final class EntityPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension
32+
{
33+
public function __construct(
34+
private readonly ReflectionProvider $reflectionProvider,
35+
private readonly CastTypeResolver $castTypeResolver,
36+
) {}
37+
38+
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
39+
{
40+
if (! $classReflection->is(Entity::class)) {
41+
return false;
42+
}
43+
44+
$casts = $this->readStringMap($classReflection, 'casts');
45+
$column = $this->mapColumn($classReflection, $propertyName);
46+
47+
return isset($casts[$column]);
48+
}
49+
50+
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
51+
{
52+
$casts = $this->readStringMap($classReflection, 'casts');
53+
$column = $this->mapColumn($classReflection, $propertyName);
54+
$cast = $casts[$column] ?? '';
55+
56+
return new EntityPropertyReflection($classReflection, $this->resolveCastType($classReflection, $cast));
57+
}
58+
59+
private function resolveCastType(ClassReflection $classReflection, string $cast): Type
60+
{
61+
return $this->castTypeResolver->resolve($cast) ?? $this->resolveCustomHandlerType($classReflection, $cast);
62+
}
63+
64+
private function resolveCustomHandlerType(ClassReflection $classReflection, string $cast): Type
65+
{
66+
$nullable = str_starts_with($cast, '?');
67+
68+
if ($nullable) {
69+
$cast = substr($cast, 1);
70+
}
71+
72+
$handler = $this->readStringMap($classReflection, 'castHandlers')[$this->castName($cast)] ?? null;
73+
74+
if ($handler === null || ! $this->reflectionProvider->hasClass($handler)) {
75+
return new MixedType();
76+
}
77+
78+
$handlerReflection = $this->reflectionProvider->getClass($handler);
79+
80+
if (! $handlerReflection->hasNativeMethod('get')) {
81+
return new MixedType();
82+
}
83+
84+
$type = ParametersAcceptorSelector::combineAcceptors($handlerReflection->getNativeMethod('get')->getVariants())->getReturnType();
85+
86+
return $nullable ? TypeCombinator::addNull($type) : $type;
87+
}
88+
89+
private function castName(string $cast): string
90+
{
91+
if (preg_match('/\A(.+)\[.+\]\z/', $cast, $matches) === 1) {
92+
return $matches[1];
93+
}
94+
95+
return $cast;
96+
}
97+
98+
private function mapColumn(ClassReflection $classReflection, string $propertyName): string
99+
{
100+
$mapped = $this->readStringMap($classReflection, 'datamap')[$propertyName] ?? '';
101+
102+
return $mapped !== '' ? $mapped : $propertyName;
103+
}
104+
105+
/**
106+
* @return array<string, string>
107+
*/
108+
private function readStringMap(ClassReflection $classReflection, string $property): array
109+
{
110+
$value = $classReflection->getNativeReflection()->getDefaultProperties()[$property] ?? [];
111+
112+
if (! is_array($value)) {
113+
return [];
114+
}
115+
116+
$map = [];
117+
118+
foreach ($value as $key => $cast) {
119+
if (is_string($key) && is_string($cast)) {
120+
$map[$key] = $cast;
121+
}
122+
}
123+
124+
return $map;
125+
}
126+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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 <[email protected]>
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\Reflection;
15+
16+
use PHPStan\Reflection\ClassReflection;
17+
use PHPStan\Reflection\PropertyReflection;
18+
use PHPStan\TrinaryLogic;
19+
use PHPStan\Type\MixedType;
20+
use PHPStan\Type\Type;
21+
22+
/**
23+
* Reflection for an entity virtual property whose read type is derived from a `$casts` entry.
24+
*/
25+
final class EntityPropertyReflection implements PropertyReflection
26+
{
27+
public function __construct(
28+
private readonly ClassReflection $declaringClass,
29+
private readonly Type $readableType,
30+
) {}
31+
32+
public function getDeclaringClass(): ClassReflection
33+
{
34+
return $this->declaringClass;
35+
}
36+
37+
public function isStatic(): bool
38+
{
39+
return false;
40+
}
41+
42+
public function isPrivate(): bool
43+
{
44+
return false;
45+
}
46+
47+
public function isPublic(): bool
48+
{
49+
return true;
50+
}
51+
52+
public function getDocComment(): ?string
53+
{
54+
return null;
55+
}
56+
57+
public function getReadableType(): Type
58+
{
59+
return $this->readableType;
60+
}
61+
62+
public function getWritableType(): Type
63+
{
64+
return new MixedType();
65+
}
66+
67+
public function canChangeTypeAfterAssignment(): bool
68+
{
69+
return false;
70+
}
71+
72+
public function isReadable(): bool
73+
{
74+
return true;
75+
}
76+
77+
public function isWritable(): bool
78+
{
79+
return true;
80+
}
81+
82+
public function isDeprecated(): TrinaryLogic
83+
{
84+
return TrinaryLogic::createNo();
85+
}
86+
87+
public function getDeprecatedDescription(): ?string
88+
{
89+
return null;
90+
}
91+
92+
public function isInternal(): TrinaryLogic
93+
{
94+
return TrinaryLogic::createNo();
95+
}
96+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 <[email protected]>
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\Tests\Fixtures\Entity;
15+
16+
use CodeIgniter\Entity\Entity;
17+
18+
final class CastedEntity extends Entity
19+
{
20+
protected $datamap = [
21+
'identifier' => 'id',
22+
];
23+
protected $casts = [
24+
'id' => 'integer',
25+
'is_active' => 'boolean',
26+
'rating' => '?float',
27+
'name' => 'string',
28+
'options' => 'json',
29+
'tags' => 'json-array',
30+
'roles' => 'csv',
31+
'published' => 'datetime',
32+
'balance' => 'money',
33+
'discount' => '?money',
34+
];
35+
protected $castHandlers = [
36+
'money' => MoneyCast::class,
37+
];
38+
}

tests/Fixtures/Entity/Money.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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 <[email protected]>
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\Tests\Fixtures\Entity;
15+
16+
final class Money
17+
{
18+
public function __construct(public readonly int $amount) {}
19+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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 <[email protected]>
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\Tests\Fixtures\Entity;
15+
16+
use CodeIgniter\Entity\Cast\BaseCast;
17+
18+
final class MoneyCast extends BaseCast
19+
{
20+
public static function get($value, array $params = []): Money
21+
{
22+
return new Money((int) $value);
23+
}
24+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 <[email protected]>
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\Tests\Type;
15+
16+
use CodeIgniter\PHPStan\Tests\Fixtures\Entity\CastedEntity;
17+
18+
use function PHPStan\Testing\assertType;
19+
20+
$entity = new CastedEntity();
21+
22+
assertType('int', $entity->id);
23+
assertType('int', $entity->identifier);
24+
assertType('bool', $entity->is_active);
25+
assertType('float|null', $entity->rating);
26+
assertType('string', $entity->name);
27+
assertType('stdClass', $entity->options);
28+
assertType('array', $entity->tags);
29+
assertType('list<string>', $entity->roles);
30+
assertType('CodeIgniter\I18n\Time', $entity->published);
31+
assertType('CodeIgniter\PHPStan\Tests\Fixtures\Entity\Money', $entity->balance);
32+
assertType('CodeIgniter\PHPStan\Tests\Fixtures\Entity\Money|null', $entity->discount);
33+
34+
assertType('array<int|string, mixed>|bool|float|int|object|string|null', $entity->unknown);

0 commit comments

Comments
 (0)