diff --git a/lib/Model.php b/lib/Model.php index 8855f43c..fde5b4fa 100644 --- a/lib/Model.php +++ b/lib/Model.php @@ -1515,7 +1515,7 @@ public static function __callStatic(string $method, mixed $args): mixed $method = 'find_by' . substr($method, 17); } - if ('find_by' === substr($method, 0, 7)) { + if (str_starts_with($method, 'find_by')) { $attributes = substr($method, 8); $options['conditions'] = SQLBuilder::create_conditions_from_underscored_string(static::connection(), $attributes, $args, static::$alias_attribute); @@ -1524,7 +1524,7 @@ public static function __callStatic(string $method, mixed $args): mixed } return $ret; - } elseif ('find_all_by' === substr($method, 0, 11)) { + } elseif (str_starts_with($method, 'find_all_by')) { $options['conditions'] = SQLBuilder::create_conditions_from_underscored_string(static::connection(), substr($method, 12), $args, static::$alias_attribute); return static::find('all', $options); diff --git a/lib/PhpStan/FindAllDynamicMethodReturnTypeReflection.php b/lib/PhpStan/FindAllDynamicMethodReturnTypeReflection.php new file mode 100644 index 00000000..c38069e3 --- /dev/null +++ b/lib/PhpStan/FindAllDynamicMethodReturnTypeReflection.php @@ -0,0 +1,41 @@ +getName(); + $pos = strpos($name, 'find_all'); + + return 0 === $pos; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type + { + $class = $methodReflection->getDeclaringClass(); + + return new ArrayType( + new IntegerType(), + new ObjectType($class->getName()) + ); + } +} diff --git a/lib/PhpStan/ModelMethodsClassReflectionExtension.php b/lib/PhpStan/ModelMethodsClassReflectionExtension.php new file mode 100644 index 00000000..8297bc72 --- /dev/null +++ b/lib/PhpStan/ModelMethodsClassReflectionExtension.php @@ -0,0 +1,31 @@ +isSubclassOf(Model::class)) { + if (preg_match('/find_(all_)?by_/', $methodName)) { + return true; + } + + if (str_ends_with($methodName, '_set')) { + return true; + } + } + + return false; + } + + public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection + { + return new ModelStaticMethodReflection($classReflection, $methodName); + } +} diff --git a/lib/PhpStan/ModelParameterReflection.php b/lib/PhpStan/ModelParameterReflection.php new file mode 100644 index 00000000..7ed44f2f --- /dev/null +++ b/lib/PhpStan/ModelParameterReflection.php @@ -0,0 +1,43 @@ +classReflection = $classReflection; + $this->name = $name; + } + + public function isFinal(): TrinaryLogic + { + // TODO: Implement isFinal() method. + return TrinaryLogic::createNo(); + } + + public function isInternal(): TrinaryLogic + { + // TODO: Implement isInternal() method. + return TrinaryLogic::createNo(); + } + + public function getDocComment(): ?string + { + // TODO: Implement getDocComment() method. + return null; + } + + public function isDeprecated(): TrinaryLogic + { + // TODO: Implement isDeprecated() method. + return TrinaryLogic::createNo(); + } + + public function hasSideEffects(): TrinaryLogic + { + // TODO: Implement hasSideEffects() method. + return TrinaryLogic::createMaybe(); + } + + public function getThrowType(): ?\PHPStan\Type\Type + { + // TODO: Implement getThrowType() method. + return null; + } + + public function getDeprecatedDescription(): ?string + { + // TODO: Implement getDeprecatedDescription() method. + return null; + } + + public function getDeclaringClass(): ClassReflection + { + return $this->classReflection; + } + + public function getPrototype(): ClassMemberReflection + { + return $this; + } + + public function isStatic(): bool + { + return true; + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function getName(): string + { + return $this->name; + } + + public function isVariadic(): bool + { + return false; + } + + /** + * @return \PHPStan\Reflection\ParametersAcceptor[] + */ + public function getVariants(): array + { + if (str_starts_with($this->name, 'find_by')) { + $parts = SQLBuilder::underscored_string_to_parts(substr($this->name, 8), 0); + + return [ + new FunctionVariant( + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + array_fill(0, count($parts), new ModelParameterReflection()), + false, + new UnionType([ + new ObjectType($this->classReflection->getDisplayName()), + new NullType() + ]) + ) + ]; + } elseif (str_starts_with($this->name, 'find_all')) { + $parts = SQLBuilder::underscored_string_to_parts(substr($this->name, 9), 0); + + return [ + new FunctionVariant( + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + array_fill(0, count($parts), new ModelParameterReflection()), + false, + new ArrayType( + new IntegerType(), + new ObjectType($this->classReflection->getDisplayName()), + ) + ) + ]; + } elseif (preg_match('/_set$/', $this->name)) { + return [ + new FunctionVariant( + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + [new ModelParameterReflection()], + false, + new ObjectType($this->classReflection->getDisplayName()) + ) + ]; + } elseif (preg_match('/_refresh/', $this->name)) { + return [ + new FunctionVariant( + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + [], + false, + new VoidType() + ) + ]; + } elseif (preg_match('/_dirty/', $this->name)) { + return [ + new FunctionVariant( + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + [], + false, + new VoidType() + ) + ]; + } + + return [ + new FunctionVariant( + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + [], + false, + new ObjectType($this->classReflection->getDisplayName()) + ), + ]; + } +} diff --git a/lib/SQLBuilder.php b/lib/SQLBuilder.php index 61759a90..be5b58fb 100644 --- a/lib/SQLBuilder.php +++ b/lib/SQLBuilder.php @@ -251,6 +251,14 @@ public static function reverse_order(string $order = ''): string return join(',', $parts); } + /** + * @return array + */ + public static function underscored_string_to_parts(string $string, int $flags=PREG_SPLIT_DELIM_CAPTURE): array + { + return preg_split('/(_and_|_or_)/i', $string, -1, $flags); + } + /** * Converts a string like "id_and_name_or_z" into a conditions value like array("id=? AND name=? OR z=?", values, ...). * @@ -267,10 +275,11 @@ public static function create_conditions_from_underscored_string(Connection $con return null; } - $parts = preg_split('/(_and_|_or_)/i', $name, -1, PREG_SPLIT_DELIM_CAPTURE); $num_values = count((array) $values); $conditions = ['']; + $parts = static::underscored_string_to_parts($name); + for ($i = 0, $j = 0, $n = count($parts); $i < $n; $i += 2, ++$j) { if ($i >= 2) { $res = preg_replace(['/_and_/i', '/_or_/i'], [' AND ', ' OR '], $parts[$i - 1]); diff --git a/phpstan.neon.dist b/phpstan.neon.dist index bf5a659d..5dca0cd0 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -13,6 +13,14 @@ services: class: ActiveRecord\PhpStan\FindDynamicMethodReturnTypeReflection tags: - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - + class: ActiveRecord\PhpStan\FindAllDynamicMethodReturnTypeReflection + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - + class: ActiveRecord\PhpStan\ModelMethodsClassReflectionExtension + tags: + - phpstan.broker.methodsClassReflectionExtension includes: - vendor/phpstan/phpstan-phpunit/extension.neon diff --git a/test/phpstan/PhpStanReflectionTests.php b/test/phpstan/DynamicFInd.php similarity index 100% rename from test/phpstan/PhpStanReflectionTests.php rename to test/phpstan/DynamicFInd.php diff --git a/test/phpstan/DynamicFindAll.php b/test/phpstan/DynamicFindAll.php new file mode 100644 index 00000000..808ce4fd --- /dev/null +++ b/test/phpstan/DynamicFindAll.php @@ -0,0 +1,17 @@ +