44
55use InvalidArgumentException ;
66use JsonSerializable ;
7- use RuntimeException ;
87use StellarWP \Models \Contracts \Arrayable ;
98use StellarWP \Models \Contracts \Model as ModelInterface ;
109use 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
0 commit comments