Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);
Expand Down
41 changes: 41 additions & 0 deletions lib/PhpStan/FindAllDynamicMethodReturnTypeReflection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace ActiveRecord\PhpStan;

namespace ActiveRecord\PhpStan;

use ActiveRecord\Model;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\ArrayType;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\IntegerType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;

class FindAllDynamicMethodReturnTypeReflection implements DynamicStaticMethodReturnTypeExtension
{
public function getClass(): string
{
return Model::class;
}

public function isStaticMethodSupported(MethodReflection $methodReflection): bool
{
$name = $methodReflection->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())
);
}
}
31 changes: 31 additions & 0 deletions lib/PhpStan/ModelMethodsClassReflectionExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace ActiveRecord\PhpStan;

use ActiveRecord\Model;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;

class ModelMethodsClassReflectionExtension implements MethodsClassReflectionExtension
{
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
if ($classReflection->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);
}
}
43 changes: 43 additions & 0 deletions lib/PhpStan/ModelParameterReflection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace ActiveRecord\PhpStan;

use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\PassedByReference;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;

class ModelParameterReflection implements ParameterReflection
{
public function getName(): string
{
return 'name';
}

public function isOptional(): bool
{
return false;
}

public function getDefaultValue(): ?Type
{
return null;
}

public function getType(): Type
{
return new MixedType();
}

public function passedByReference(): PassedByReference
{
return PassedByReference::createNo();
}

public function isVariadic(): bool
{
return false;
}
}
186 changes: 186 additions & 0 deletions lib/PhpStan/ModelStaticMethodReflection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<?php

declare(strict_types=1);

namespace ActiveRecord\PhpStan;

use ActiveRecord\SQLBuilder;
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\IntegerType;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\UnionType;
use PHPStan\Type\VoidType;

class ModelStaticMethodReflection implements MethodReflection
{
private ClassReflection $classReflection;
private string $name;

public function __construct(ClassReflection $classReflection, string $name)
{
$this->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())
),
];
}
}
11 changes: 10 additions & 1 deletion lib/SQLBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,14 @@ public static function reverse_order(string $order = ''): string
return join(',', $parts);
}

/**
* @return array<mixed>
*/
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, ...).
*
Expand All @@ -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]);
Expand Down
8 changes: 8 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
File renamed without changes.
17 changes: 17 additions & 0 deletions test/phpstan/DynamicFindAll.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
/**
* This file is not something we need to execute in tests. It's included
* only as a means to test and aid in the development of dynamic PHPStan
* extensions. If it doesn't emit any errors when you run 'composer stan',
* then everything is working fine.
*
* see lib/PhpStan/FindDynamicAllMethodReturnTypeReflection.php
*/

use test\models\Book;

$book = Book::find_all_by_name('Foo');
assert(is_array($book));

$book = Book::find_all_by_name_and_publisher('Foo', 'Penguin');
assert(is_array($book));
Loading