Skip to content

Commit c414e0d

Browse files
committed
refactor: strengthens types across files
1 parent 45dc315 commit c414e0d

File tree

7 files changed

+97
-32
lines changed

7 files changed

+97
-32
lines changed

src/Models/Contracts/ModelCrud.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
/**
88
* @since 1.0.0
99
*/
10-
interface ModelCrud {
10+
interface ModelCrud extends ModelBuildsFromData {
1111
/**
1212
* @since 1.0.0
1313
*

src/Models/Model.php

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use InvalidArgumentException;
66
use JsonSerializable;
7-
use RuntimeException;
87
use StellarWP\Models\Contracts\Arrayable;
98
use StellarWP\Models\Contracts\Model as ModelInterface;
109
use StellarWP\Models\ValueObjects\Relationship;
@@ -38,7 +37,7 @@ abstract class Model implements ModelInterface, Arrayable, JsonSerializable {
3837
/**
3938
* Relationships that have already been loaded and don't need to be loaded again.
4039
*
41-
* @var array<string,Model|list<Model>>
40+
* @var array<string,Model|list<Model>|null>
4241
*/
4342
private $cachedRelations = [];
4443

@@ -59,17 +58,20 @@ final public function __construct( array $attributes = [] ) {
5958
*
6059
* @since 2.0.0
6160
*/
62-
protected function afterConstruct() {
63-
// This method is meant to be overridden by the model to perform actions after the model is constructed.
61+
protected function afterConstruct(): void {
6462
return;
6563
}
64+
6665
/**
6766
* Casts the value for the type, used when constructing a model from query data. If the model needs to support
6867
* additional types, especially class types, this method can be overridden.
6968
*
69+
* Note: Type casting is performed at runtime based on property definitions. PHPStan cannot statically verify
70+
* the resulting types, so we suppress type-checking errors for the cast operations.
71+
*
7072
* @since 2.0.0 changed to static
7173
*
72-
* @param string $type
74+
* @param ModelPropertyDefinition $definition The property definition.
7375
* @param mixed $value The query data value to cast, probably a string.
7476
* @param string $property The property being casted.
7577
*
@@ -89,11 +91,12 @@ protected static function castValueForProperty( ModelPropertyDefinition $definit
8991
throw new InvalidArgumentException( "Property '$property' has multiple types: " . implode( ', ', $type ) . ". To support additional types, implement a custom castValueForProperty() method." );
9092
}
9193

94+
// Runtime type casting based on property definition - PHPStan cannot verify this statically
9295
switch ( $type[0] ) {
9396
case 'int':
94-
return (int) $value;
97+
return (int) $value; // @phpstan-ignore-line
9598
case 'string':
96-
return (string) $value;
99+
return (string) $value; // @phpstan-ignore-line
97100
case 'bool':
98101
return (bool) filter_var( $value, FILTER_VALIDATE_BOOLEAN );
99102
case 'array':
@@ -203,10 +206,13 @@ public static function getPropertyDefinition( string $key ): ModelPropertyDefini
203206
* @return array<string,ModelPropertyDefinition>
204207
*/
205208
public static function getPropertyDefinitions(): array {
206-
static $definition = null;
209+
/** @var array<string,ModelPropertyDefinition>|null $cachedDefinitions */
210+
static $cachedDefinitions = null;
207211

208-
if ( $definition === null ) {
212+
if ( $cachedDefinitions === null ) {
209213
$definitions = array_merge( static::$properties, static::properties() );
214+
/** @var array<string,ModelPropertyDefinition> $processedDefinitions */
215+
$processedDefinitions = [];
210216

211217
foreach ( $definitions as $key => $definition ) {
212218
if ( ! is_string( $key ) ) {
@@ -217,13 +223,13 @@ public static function getPropertyDefinitions(): array {
217223
$definition = ModelPropertyDefinition::fromShorthand( $definition );
218224
}
219225

220-
$definitions[ $key ] = $definition->lock();
226+
$processedDefinitions[ $key ] = $definition->lock();
221227
}
222228

223-
$definition = $definitions;
229+
$cachedDefinitions = $processedDefinitions;
224230
}
225231

226-
return $definition;
232+
return $cachedDefinitions;
227233
}
228234

229235
/**
@@ -251,6 +257,36 @@ public function isSet( string $key ): bool {
251257
return $this->propertyCollection->isSet( $key );
252258
}
253259

260+
/**
261+
* Checks if a method exists that returns a ModelQueryBuilder instance.
262+
*
263+
* @since 2.0.0
264+
*
265+
* @param string $method Method name.
266+
*
267+
* @return bool
268+
*/
269+
protected function hasRelationshipMethod( string $method ): bool {
270+
if ( ! method_exists( $this, $method ) ) {
271+
return false;
272+
}
273+
274+
try {
275+
$reflectionMethod = new \ReflectionMethod( $this, $method );
276+
$returnType = $reflectionMethod->getReturnType();
277+
278+
if ( ! $returnType instanceof \ReflectionNamedType ) {
279+
return false;
280+
}
281+
282+
$typeName = $returnType->getName();
283+
284+
return $typeName === ModelQueryBuilder::class || is_subclass_of( $typeName, ModelQueryBuilder::class );
285+
} catch ( \ReflectionException $e ) {
286+
return false;
287+
}
288+
}
289+
254290
/**
255291
* Returns a relationship.
256292
*
@@ -261,7 +297,7 @@ public function isSet( string $key ): bool {
261297
* @return Model|list<Model>|null
262298
*/
263299
protected function getRelationship( string $key ) {
264-
if ( ! is_callable( [ $this, $key ] ) ) {
300+
if ( ! $this->hasRelationshipMethod( $key ) ) {
265301
$exception = Config::getInvalidArgumentException();
266302
throw new $exception( "$key() does not exist." );
267303
}
@@ -270,16 +306,23 @@ protected function getRelationship( string $key ) {
270306
return $this->cachedRelations[ $key ];
271307
}
272308

273-
$relationship = static::$relationships[ $key ];
309+
/** @var ModelQueryBuilder<Model> $queryBuilder */
310+
$queryBuilder = $this->$key();
274311

275-
switch ( $relationship ) {
312+
switch ( static::$relationships[ $key ] ) {
276313
case Relationship::BELONGS_TO:
277314
case Relationship::HAS_ONE:
278-
return $this->cachedRelations[ $key ] = $this->$key()->get();
315+
$result = $queryBuilder->get();
316+
/** @var Model|null $result */
317+
$this->cachedRelations[ $key ] = $result;
318+
return $result;
279319
case Relationship::HAS_MANY:
280320
case Relationship::BELONGS_TO_MANY:
281321
case Relationship::MANY_TO_MANY:
282-
return $this->cachedRelations[ $key ] = $this->$key()->getAll();
322+
$result = $queryBuilder->getAll();
323+
/** @var list<Model>|null $result */
324+
$this->cachedRelations[ $key ] = $result;
325+
return $result;
283326
}
284327

285328
return null;
@@ -413,7 +456,7 @@ public static function fromData($data, $mode = self::BUILD_MODE_IGNORE_EXTRA) {
413456

414457
$initialValues = [];
415458

416-
foreach (static::$properties as $key => $type) {
459+
foreach (static::$properties as $key => $_) {
417460
if ( ! array_key_exists( $key, $data ) ) {
418461
// Skip missing properties when BUILD_MODE_IGNORE_MISSING is set
419462
if ( $mode & self::BUILD_MODE_IGNORE_MISSING ) {
@@ -422,7 +465,6 @@ public static function fromData($data, $mode = self::BUILD_MODE_IGNORE_EXTRA) {
422465
Config::throwInvalidArgumentException( "Property '$key' does not exist." );
423466
}
424467

425-
// Remember not to use $type, as it may be an array that includes the default value. Safer to use getPropertyType().
426468
$initialValues[ $key ] = static::castValueForProperty( static::getPropertyDefinition( $key ), $data[ $key ], $key );
427469
}
428470

src/Models/ModelFactory.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public function make( array $attributes = [] ) {
5959
/**
6060
* @since 1.2.3
6161
*/
62-
public function makeAndResolveTo($property): Closure
62+
public function makeAndResolveTo(string $property): Closure
6363
{
6464
return function() use ($property) {
6565
return is_array($results = $this->make())
@@ -92,7 +92,7 @@ public function create( array $attributes = [] ) {
9292
/**
9393
* @since 1.2.3
9494
*/
95-
public function createAndResolveTo( $property ): Closure {
95+
public function createAndResolveTo( string $property ): Closure {
9696
return function() use ( $property ) {
9797
return is_array( $results = $this->create() )
9898
? array_column( $results, $property )
@@ -106,6 +106,7 @@ public function createAndResolveTo( $property ): Closure {
106106
* @since 1.2.3 Add support for resolving Closures.
107107
* @since 1.0.0
108108
*
109+
* @param array<string,mixed> $attributes
109110
* @return M
110111
*/
111112
protected function makeInstance( array $attributes ) {
@@ -123,6 +124,8 @@ function( $attribute ) {
123124
* Configure the factory.
124125
*
125126
* @since 1.0.0
127+
*
128+
* @return ModelFactory<M>
126129
*/
127130
public function configure() : self {
128131
return $this;
@@ -132,6 +135,8 @@ public function configure() : self {
132135
* Sets the number of models to generate.
133136
*
134137
* @since 1.0.0
138+
*
139+
* @return ModelFactory<M>
135140
*/
136141
public function count( int $count ) : self {
137142
$this->count = $count;

src/Models/ModelProperty.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ public function getKey(): string {
8383
* Get the original value of the property.
8484
*
8585
* @since 2.0.0
86+
*
87+
* @return mixed
8688
*/
8789
public function getOriginalValue() {
8890
return $this->originalValue;
@@ -92,6 +94,8 @@ public function getOriginalValue() {
9294
* Get the value of the property.
9395
*
9496
* @since 2.0.0
97+
*
98+
* @return mixed
9599
*/
96100
public function getValue() {
97101
return $this->value ?? null;
@@ -153,6 +157,8 @@ public function commitChanges(): void {
153157
* Sets the value of the property.
154158
*
155159
* @since 2.0.0
160+
*
161+
* @param mixed $value
156162
*/
157163
public function setValue( $value ): self {
158164
if ( ! $this->definition->isValidValue( $value ) ) {

src/Models/ModelPropertyCollection.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public function count(): int {
6767
*
6868
* @since 2.0.0
6969
*
70-
* @param callable(ModelProperty,string):bool $callback
70+
* @param callable $callback
7171
* @param int $mode The mode as used in array_filter() which determines the arguments passed to the callback.
7272
*/
7373
public function filter( callable $callback, $mode = 0 ): ModelPropertyCollection {
@@ -80,6 +80,7 @@ public function filter( callable $callback, $mode = 0 ): ModelPropertyCollection
8080
* @since 2.0.0
8181
*
8282
* @param array<string,ModelPropertyDefinition> $propertyDefinitions
83+
* @param array<string,mixed> $initialValues
8384
* @return ModelPropertyCollection
8485
*/
8586
public static function fromPropertyDefinitions( array $propertyDefinitions, array $initialValues = [] ): ModelPropertyCollection {
@@ -241,11 +242,14 @@ public function isSet( string $key ): bool {
241242
* @return array<string,TMapValue>
242243
*/
243244
public function map( callable $callback ) {
244-
return $this->reduce( static function( $carry, ModelProperty $property ) use ( $callback ) {
245+
$reducer = static function( array $carry, ModelProperty $property ) use ( $callback ): array {
245246
$carry[ $property->getKey() ] = $callback( $property );
246247

247248
return $carry;
248-
}, [] );
249+
};
250+
251+
/** @var array<string,TMapValue> */
252+
return $this->reduce( $reducer, [] );
249253
}
250254

251255
/**
@@ -255,7 +259,7 @@ public function map( callable $callback ) {
255259
*
256260
* @template TReduceInitial
257261
* @template TReduceResult
258-
* @param callable(TReduceResult,ModelProperty):TReduceResult $callback
262+
* @param callable(TReduceInitial|TReduceResult,ModelProperty):(TReduceInitial|TReduceResult) $callback
259263
* @param TReduceInitial $initial
260264
* @return TReduceResult|TReduceInitial
261265
*/
@@ -288,7 +292,7 @@ public function revertProperty( string $key ): void {
288292
*
289293
* @param array<string,mixed> $values
290294
*/
291-
public function setValues( array $values ) {
295+
public function setValues( array $values ): void {
292296
foreach ( $values as $key => $value ) {
293297
if ( ! $this->has( $key ) ) {
294298
throw new InvalidArgumentException( 'Property ' . $key . ' does not exist.' );
@@ -317,6 +321,8 @@ public function tap( callable $callback ): self {
317321
* Unset a property.
318322
*
319323
* @since 2.0.0
324+
*
325+
* @param string $key
320326
*/
321327
public function unsetProperty( $key ): void {
322328
$this->getOrFail( $key )->unset();

src/Models/ModelPropertyDefinition.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class ModelPropertyDefinition {
2828
*
2929
* @since 2.0.0
3030
*
31-
* @var Closure|null A closure that accepts the value and property instance as parameters and returns the cast value.
31+
* @var Closure A closure that accepts the value and property instance as parameters and returns the cast value.
3232
*/
3333
private Closure $castMethod;
3434

@@ -274,6 +274,7 @@ public function isValidValue( $value ): bool {
274274
case 'double':
275275
return $this->supportsType( 'float' );
276276
case 'object':
277+
/** @var object $value */
277278
if ( $this->supportsType( 'object' ) ) {
278279
return true;
279280
} else {

src/Models/ModelQueryBuilder.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public function __construct( string $modelClass ) {
4040
*
4141
* @param null|string $column
4242
*/
43-
public function count( ?string $column = null ) : int {
43+
public function count( $column = null ) : int {
4444
$column = ( ! $column || $column === '*' ) ? '1' : trim( $column );
4545

4646
if ( '1' === $column ) {
@@ -60,10 +60,11 @@ public function count( ?string $column = null ) : int {
6060
*
6161
* @param string $output
6262
*
63-
* @return M|null
63+
* @return M|array<string,mixed>|object|null
6464
*/
65-
public function get( $output = self::MODEL ): ?Model {
65+
public function get( $output = self::MODEL ) {
6666
if ( $output !== self::MODEL ) {
67+
/** @var array<string,mixed>|object|null */
6768
return parent::get( $output );
6869
}
6970

@@ -73,6 +74,7 @@ public function get( $output = self::MODEL ): ?Model {
7374
return null;
7475
}
7576

77+
/** @var array<string,mixed>|object $row */
7678
return $this->model::fromData( $row );
7779
}
7880

@@ -81,19 +83,22 @@ public function get( $output = self::MODEL ): ?Model {
8183
*
8284
* @since 1.0.0
8385
*
84-
* @return list<M>|null
86+
* @return list<M|array<string,mixed>|object>|null
8587
*/
8688
public function getAll( $output = self::MODEL ) : ?array {
8789
if ( $output !== self::MODEL ) {
90+
/** @var list<array<string,mixed>|object>|null */
8891
return parent::getAll( $output );
8992
}
9093

94+
/** @var list<object> */
9195
$results = DB::get_results( $this->getSQL() );
9296

9397
if ( ! $results ) {
9498
return null;
9599
}
96100

101+
/** @var list<M> */
97102
return array_map( [ $this->model, 'fromData' ], $results );
98103
}
99104
}

0 commit comments

Comments
 (0)