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
8 changes: 3 additions & 5 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
# should be fixed at the source, or suppressed inline for genuine static analysis
# limitations.

includes:
- src/database/extension.neon

parameters:
tmpDir: .cache/phpstan
level: 5
Expand Down Expand Up @@ -85,11 +88,6 @@ parameters:
- identifier: method.childParameterType
path: src/database/*

# Relation @mixin forwarding - Relation methods returning $this forward to Builder methods
# PHPStan sees Builder return type but the actual return is $this (the Relation)
- message: '#should return \$this\(Hypervel\\Database\\Eloquent\\Relations\\.+\) but returns Hypervel\\Database\\(Query|Eloquent)\\Builder#'
path: src/database/*

# BelongsToMany pivot intersection type - PHPDoc uses object{pivot: ...}&TRelatedModel
# to document that models get a pivot property attached, but PHPStan can't track dynamic attachment
- message: '#object\{pivot:#'
Expand Down
3 changes: 3 additions & 0 deletions phpstan.types.neon.dist
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
includes:
- src/database/extension.neon

parameters:
level: max
tmpDir: .cache/phpstan-types
Expand Down
8 changes: 7 additions & 1 deletion src/database/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
"hypervel/support": "^0.4"
},
"require-dev": {
"fakerphp/faker": "^1.24"
"fakerphp/faker": "^1.24",
"phpstan/phpstan": "^2.0"
},
"suggest": {
"fakerphp/faker": "Required to use Eloquent model factories (^1.24)."
Expand All @@ -76,6 +77,11 @@
"Hypervel\\Database\\DatabaseServiceProvider"
]
},
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-main": "0.4-dev"
}
Expand Down
15 changes: 15 additions & 0 deletions src/database/extension.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
services:
-
class: Hypervel\Database\PHPStan\ModelScopeMethodResolver
-
class: Hypervel\Database\PHPStan\ForwardedFluentMethodExtension
tags:
- phpstan.broker.methodsClassReflectionExtension
-
class: Hypervel\Database\PHPStan\ForwardedModelMethodExtension
tags:
- phpstan.broker.methodsClassReflectionExtension
-
class: Hypervel\Database\PHPStan\NamedScopeMethodExtension
tags:
- phpstan.broker.methodsClassReflectionExtension
7 changes: 7 additions & 0 deletions src/database/src/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,13 @@ public function afterQuery(Closure $callback): static

/**
* Invoke the "after query" modification callbacks.
*
* A callback that replaces the collection type owns the resulting type change.
*
* @template TCollection of BaseCollection
*
* @param TCollection $result
* @return TCollection
*/
public function applyAfterQueryCallbacks(BaseCollection $result): BaseCollection
{
Expand Down
49 changes: 46 additions & 3 deletions src/database/src/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,13 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt
*/
protected static array $scopeMethodAttributes = [];

/**
* Cache of whether methods are callable legacy local scopes.
*
* @var array<string, bool>
*/
protected static array $legacyScopeMethods = [];

/**
* Cache of soft deletable models.
*
Expand Down Expand Up @@ -547,7 +554,7 @@ public static function clearBootedModels(): void
static::$classDeclaredAttributes = [];
static::$classPropertyDeclarers = [];
static::$guardConfigurations = [];
static::$scopeMethodAttributes = [];
self::flushScopeCaches();

static::$globalScopes = [];
}
Expand Down Expand Up @@ -1975,7 +1982,7 @@ public function newPivot(self $parent, array $attributes, string $table, bool $e
*/
public function hasNamedScope(string $scope): bool
{
return method_exists($this, 'scope' . ucfirst($scope))
return static::isLegacyScopeMethod($scope)
|| static::isScopeMethodWithAttribute($scope);
}

Expand Down Expand Up @@ -2016,6 +2023,33 @@ protected static function isScopeMethodWithAttribute(string $method): bool
&& $reflection->getAttributes(LocalScope::class) !== [];
}

/**
* Determine if the given method is a callable legacy local scope.
*/
protected static function isLegacyScopeMethod(string $scope): bool
{
$key = static::class . "\0" . strtolower($scope);

if (array_key_exists($key, static::$legacyScopeMethods)) {
return static::$legacyScopeMethods[$key];
}

$method = 'scope' . ucfirst($scope);

// Query-derived dynamic scope names can be arbitrary. Do not retain misses
// for nonexistent methods in a long-running worker.
if (! method_exists(static::class, $method)) {
return false;
}

$reflection = new ReflectionMethod(static::class, $method);
$declaredName = $reflection->getName();

return static::$legacyScopeMethods[$key] = strlen($declaredName) > 5
&& ! $reflection->isPrivate()
&& ! ctype_lower($declaredName[5]);
}

/**
* Convert the model instance to an array.
*/
Expand Down Expand Up @@ -2652,6 +2686,15 @@ public static function flushGuardableColumns(): void
static::$guardableColumns = [];
}

/**
* Flush local scope caches.
*/
private static function flushScopeCaches(): void
{
static::$scopeMethodAttributes = [];
static::$legacyScopeMethods = [];
}

/**
* Flush all static state.
*/
Expand All @@ -2668,7 +2711,7 @@ public static function flushState(): void
static::$classDeclaredAttributes = [];
static::$classPropertyDeclarers = [];
static::$guardConfigurations = [];
static::$scopeMethodAttributes = [];
self::flushScopeCaches();
static::$modelsShouldPreventLazyLoading = false;
static::$modelsShouldAutomaticallyEagerLoadRelationships = false;
static::$lazyLoadingViolationCallback = null;
Expand Down
5 changes: 5 additions & 0 deletions src/database/src/Eloquent/Relations/BelongsToMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,11 @@ public function getResults()
: $this->related->newCollection();
}

/**
* Execute the query as a "select" statement.
*
* @return \Hypervel\Database\Eloquent\Collection<int, object{pivot: TPivotModel}&TRelatedModel>
*/
public function get(array $columns = ['*']): BaseCollection
{
// First we'll add the proper select columns onto the query so it is run with
Expand Down
5 changes: 5 additions & 0 deletions src/database/src/Eloquent/Relations/HasOneOrManyThrough.php
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,11 @@ public function findOr(mixed $id, Closure|array|string $columns = ['*'], ?Closur
return $callback();
}

/**
* Execute the query as a "select" statement.
*
* @return \Hypervel\Database\Eloquent\Collection<int, TRelatedModel>
*/
public function get(array $columns = ['*']): BaseCollection
{
$builder = $this->prepareQueryBuilder($columns);
Expand Down
2 changes: 1 addition & 1 deletion src/database/src/Eloquent/Relations/Relation.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ public function sole(array|string $columns = ['*']): Model
/**
* Execute the query as a "select" statement.
*
* @return \Hypervel\Support\Collection<int, TRelatedModel>
* @return \Hypervel\Database\Eloquent\Collection<int, TRelatedModel>
*/
public function get(array $columns = ['*']): BaseCollection
{
Expand Down
Loading