Repository: larastan/larastan
Pull request #2231: Fix compatibility with Laravel 12.3.0
Author: login: AJenbo
Created: 2025-03-19T13:54:09Z | Merged: 2025-03-26T19:19:33Z

Fixes: #1774
Fixes: #2233

Depends on https://github.com/larastan/larastan/pull/2208


Diff:
diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml
index 8b7996c9..26cc70c6 100644
--- a/.github/workflows/static-analysis.yml
+++ b/.github/workflows/static-analysis.yml
@@ -26,7 +26,7 @@ jobs:
       fail-fast: false
       matrix:
         php: ["8.2", "8.3", "8.4"]
-        laravel: ["^11.15.0", "^12.0"]
+        laravel: ["^11.42.2", "^12.0"]
         dependencies:
           - "lowest"
           - "highest"
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 133b4176..6b5d36ee 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -26,7 +26,7 @@ jobs:
       fail-fast: false
       matrix:
         php: ["8.2", "8.3", "8.4"]
-        laravel: ["^11.15.0", "^12.0"]
+        laravel: ["^11.42.2", "^12.0"]
         dependencies:
           - "lowest"
           - "highest"
diff --git a/composer.json b/composer.json
index 33375284..4c1b4f98 100644
--- a/composer.json
+++ b/composer.json
@@ -18,19 +18,19 @@
         "php": "^8.2",
         "ext-json": "*",
         "iamcal/sql-parser": "^0.5.0",
-        "illuminate/console": "^11.41.3 || ^12.0",
-        "illuminate/container": "^11.41.3 || ^12.0",
-        "illuminate/contracts": "^11.41.3 || ^12.0",
-        "illuminate/database": "^11.41.3 || ^12.0",
-        "illuminate/http": "^11.41.3 || ^12.0",
-        "illuminate/pipeline": "^11.41.3 || ^12.0",
-        "illuminate/support": "^11.41.3 || ^12.0",
+        "illuminate/console": "^11.42.2 || ^12.0",
+        "illuminate/container": "^11.42.2 || ^12.0",
+        "illuminate/contracts": "^11.42.2 || ^12.0",
+        "illuminate/database": "^11.42.2 || ^12.0",
+        "illuminate/http": "^11.42.2 || ^12.0",
+        "illuminate/pipeline": "^11.42.2 || ^12.0",
+        "illuminate/support": "^11.42.2 || ^12.0",
         "phpstan/phpstan": "^2.1.8"
     },
     "require-dev": {
         "doctrine/coding-standard": "^12.0",
         "nikic/php-parser": "^5.3",
-        "laravel/framework": "^11.41.3 || ^12.0",
+        "laravel/framework": "^11.42.2 || ^12.0",
         "mockery/mockery": "^1.6",
         "orchestra/canvas": "^v9.1.3 || ^10.0",
         "orchestra/testbench-core": "^9.5.2 || ^10.0",
diff --git a/extension.neon b/extension.neon
index 3c08a49e..5f3a3569 100644
--- a/extension.neon
+++ b/extension.neon
@@ -219,11 +219,6 @@ services:
         tags:
             - phpstan.broker.dynamicMethodReturnTypeExtension
 
-    -
-        class: Larastan\Larastan\ReturnTypes\RelationFindExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
     -
         class: Larastan\Larastan\ReturnTypes\RelationCollectionExtension
         tags:
diff --git a/src/ReturnTypes/RelationFindExtension.php b/src/ReturnTypes/RelationFindExtension.php
deleted file mode 100644
index 40353cfd..00000000
--- a/src/ReturnTypes/RelationFindExtension.php
+++ /dev/null
@@ -1,87 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Larastan\Larastan\ReturnTypes;
-
-use Illuminate\Database\Eloquent\Builder;
-use Illuminate\Database\Eloquent\Collection;
-use Illuminate\Database\Eloquent\Relations\Relation;
-use Illuminate\Database\Query\Builder as QueryBuilder;
-use Illuminate\Support\Str;
-use Larastan\Larastan\Support\CollectionHelper;
-use PhpParser\Node\Expr\MethodCall;
-use PHPStan\Analyser\Scope;
-use PHPStan\Reflection\MethodReflection;
-use PHPStan\Reflection\ReflectionProvider;
-use PHPStan\Type\ArrayType;
-use PHPStan\Type\DynamicMethodReturnTypeExtension;
-use PHPStan\Type\MixedType;
-use PHPStan\Type\ObjectType;
-use PHPStan\Type\Type;
-use PHPStan\Type\TypeCombinator;
-
-use function in_array;
-
-/** @internal */
-final class RelationFindExtension implements DynamicMethodReturnTypeExtension
-{
-    public function __construct(private ReflectionProvider $reflectionProvider, private CollectionHelper $collectionHelper)
-    {
-    }
-
-    public function getClass(): string
-    {
-        return Relation::class;
-    }
-
-    public function isMethodSupported(MethodReflection $methodReflection): bool
-    {
-        if (! Str::startsWith($methodReflection->getName(), 'find')) {
-            return false;
-        }
-
-        $modelType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TRelatedModel');
-
-        if (! $modelType) {
-            return false;
-        }
-
-        if ($modelType->getObjectClassNames() === []) {
-            return false;
-        }
-
-        return $methodReflection->getDeclaringClass()->hasNativeMethod($methodReflection->getName()) ||
-            $this->reflectionProvider->getClass(Builder::class)->hasNativeMethod($methodReflection->getName()) ||
-            $this->reflectionProvider->getClass(QueryBuilder::class)->hasNativeMethod($methodReflection->getName());
-    }
-
-    public function getTypeFromMethodCall(
-        MethodReflection $methodReflection,
-        MethodCall $methodCall,
-        Scope $scope,
-    ): Type|null {
-        $modelType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TRelatedModel');
-        if ($modelType === null) {
-            return null;
-        }
-
-        $modelName = $modelType->getObjectClassNames()[0];
-
-        $argType = $scope->getType($methodCall->getArgs()[0]->value);
-
-        $returnType = $methodReflection->getVariants()[0]->getReturnType();
-
-        if (in_array(Collection::class, $returnType->getReferencedClasses(), true)) {
-            if ($argType->isIterable()->yes()) {
-                return $this->collectionHelper->determineCollectionClass($modelName);
-            }
-
-            $returnType = TypeCombinator::remove($returnType, new ObjectType(Collection::class));
-
-            return TypeCombinator::remove($returnType, new ArrayType(new MixedType(), $modelType));
-        }
-
-        return $returnType;
-    }
-}
diff --git a/stubs/common/BelongsToMany.stub b/stubs/common/BelongsToMany.stub
index c0f3ea38..6b2cea37 100644
--- a/stubs/common/BelongsToMany.stub
+++ b/stubs/common/BelongsToMany.stub
@@ -2,20 +2,53 @@
 
 namespace Illuminate\Database\Eloquent\Relations;
 
+use Closure;
+use Illuminate\Database\Eloquent\Model;
+
 /**
  * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
  * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
+ * @template TPivotModel of \Illuminate\Database\Eloquent\Relations\Pivot = \Illuminate\Database\Eloquent\Relations\Pivot
+ * @template TAccessor of string = 'pivot'
  *
- * @extends \Illuminate\Database\Eloquent\Relations\Relation<TRelatedModel, TDeclaringModel, \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>>
+ * @extends \Illuminate\Database\Eloquent\Relations\Relation<TRelatedModel, TDeclaringModel, \Illuminate\Database\Eloquent\Collection<int, TRelatedModel&object{pivot: TPivotModel}>>
  */
 class BelongsToMany extends Relation
 {
     /**
-     * Find a related model by its primary key or return new instance of the related model.
+     * Specify the custom pivot model to use for the relationship.
+     *
+     * @template TNewPivotModel of \Illuminate\Database\Eloquent\Relations\Pivot
+     *
+     * @param  class-string<TNewPivotModel>  $class
+     * @return $this
+     *
+     * @phpstan-this-out static<TRelatedModel, TDeclaringModel, TNewPivotModel, TAccessor>
+     */
+    public function using($class);
+
+    /**
+     * Specify the custom pivot accessor to use for the relationship.
+     *
+     * @template TNewAccessor of string
+     *
+     * @param  TNewAccessor  $accessor
+     * @return $this
+     *
+     * @phpstan-this-out static<TRelatedModel, TDeclaringModel, TPivotModel, TNewAccessor>
+     */
+    public function as($accessor);
+
+    /**
+     * Find a related model by its primary key or return a new instance of the related model.
      *
      * @param  mixed  $id
      * @param  array<int, (model-property<TRelatedModel>|'*')>|model-property<TRelatedModel>|'*'  $columns
-     * @return ($id is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>) ? \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> : TRelatedModel)
+     * @return (
+     *     $id is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>)
+     *     ? \Illuminate\Database\Eloquent\Collection<int, TRelatedModel&object{pivot: TPivotModel}>
+     *     : TRelatedModel&object{pivot: TPivotModel}
+     * )
      */
     public function findOrNew($id, $columns = ['*']);
 
@@ -24,18 +57,18 @@ class BelongsToMany extends Relation
      *
      * @param  array<model-property<TRelatedModel>, mixed>  $attributes
      * @param  array<model-property<TRelatedModel>, mixed>  $values
-     * @return TRelatedModel
+     * @return TRelatedModel&object{pivot: TPivotModel}
      */
     public function firstOrNew(array $attributes = [], array $values = []);
 
     /**
-     * Get the first related record matching the attributes or create it.
+     * Get the first record matching the attributes. If the record is not found, create it.
      *
      * @param  array<model-property<TRelatedModel>, mixed>  $attributes
      * @param  array<model-property<TRelatedModel>, mixed>  $values
      * @param  array<string, mixed>  $joining
      * @param  bool  $touch
-     * @return TRelatedModel
+     * @return TRelatedModel&object{pivot: TPivotModel}
      */
     public function firstOrCreate(array $attributes = [], array $values = [], array $joining = [], $touch = true);
 
@@ -46,7 +79,7 @@ class BelongsToMany extends Relation
      * @param  array<model-property<TRelatedModel>, mixed>  $values
      * @param  array<string, mixed>  $joining
      * @param  bool  $touch
-     * @return TRelatedModel
+     * @return TRelatedModel&object{pivot: TPivotModel}
      */
     public function createOrFirst(array $attributes, array $values = [], array $joining = [], $touch = true);
 
@@ -57,7 +90,7 @@ class BelongsToMany extends Relation
      * @param  array<model-property<TRelatedModel>, mixed>  $values
      * @param  array<string, mixed>  $joining
      * @param  bool  $touch
-     * @return TRelatedModel
+     * @return TRelatedModel&object{pivot: TPivotModel}
      */
     public function updateOrCreate(array $attributes, array $values = [], array $joining = [], $touch = true);
 
@@ -66,7 +99,11 @@ class BelongsToMany extends Relation
      *
      * @param  mixed  $id
      * @param  array<int, (model-property<TRelatedModel>|'*')>  $columns
-     * @return ($id is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>) ? \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> : TRelatedModel|null)
+     * @return (
+     *     $id is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>)
+     *     ? \Illuminate\Database\Eloquent\Collection<int, TRelatedModel&object{pivot: TPivotModel}>
+     *     : (TRelatedModel&object{pivot: TPivotModel})|null
+     * )
      */
     public function find($id, $columns = ['*']);
 
@@ -75,7 +112,7 @@ class BelongsToMany extends Relation
      *
      * @param  \Illuminate\Contracts\Support\Arrayable<array-key, mixed>|int[]  $ids
      * @param  array<int, (model-property<TRelatedModel>|'*')>  $columns
-     * @return \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>
+     * @return \Illuminate\Database\Eloquent\Collection<int, TRelatedModel&object{pivot: TPivotModel}>
      */
     public function findMany($ids, $columns = ['*']);
 
@@ -84,17 +121,48 @@ class BelongsToMany extends Relation
      *
      * @param  mixed  $id
      * @param  array<int, (model-property<TRelatedModel>|'*')>  $columns
-     * @return ($id is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>) ? \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> : TRelatedModel)
+     * @return (
+     *     $id is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>)
+     *     ? \Illuminate\Database\Eloquent\Collection<int, TRelatedModel&object{pivot: TPivotModel}>
+     *     : TRelatedModel&object{pivot: TPivotModel}
+     * )
      *
-     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
+     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException<TRelatedModel>
      */
     public function findOrFail($id, $columns = ['*']);
 
+    /**
+     * Find a related model by its primary key or call a callback.
+     *
+     * @template TValue
+     *
+     * @param  mixed  $id
+     * @param  (\Closure(): TValue)|list<string>|string  $columns
+     * @param  (\Closure(): TValue)|null  $callback
+     * @return (
+     *     $id is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>)
+     *     ? \Illuminate\Database\Eloquent\Collection<int, TRelatedModel&object{pivot: TPivotModel}>|TValue
+     *     : (TRelatedModel&object{pivot: TPivotModel})|TValue
+     * )
+     */
+    public function findOr($id, $columns = ['*'], ?Closure $callback = null);
+
+    /**
+     * Add a basic where clause to the query, and return the first result.
+     *
+     * @param  (\Closure(\Illuminate\Database\Eloquent\Builder<TRelatedModel>):mixed)|model-property<TRelatedModel>|array<int|model-property<TRelatedModel>, mixed> $column
+     * @param  mixed  $operator
+     * @param  mixed  $value
+     * @param  string  $boolean
+     * @return (TRelatedModel&object{pivot: TPivotModel})|null
+     */
+    public function firstWhere($column, $operator = null, $value = null, $boolean = 'and');
+
     /**
      * Execute the query and get the first result.
      *
      * @param  array<int, (model-property<TRelatedModel>|'*')>  $columns
-     * @return TRelatedModel|null
+     * @return (TRelatedModel&object{pivot: TPivotModel})|null
      */
     public function first($columns = ['*']);
 
@@ -102,21 +170,22 @@ class BelongsToMany extends Relation
      * Execute the query and get the first result or throw an exception.
      *
      * @param  array<int, (model-property<TRelatedModel>|'*')>  $columns
-     * @return TRelatedModel
+     * @return TRelatedModel&object{pivot: TPivotModel}
      *
      * @throws \Illuminate\Database\Eloquent\ModelNotFoundException<TRelatedModel>
      */
     public function firstOrFail($columns = ['*']);
 
     /**
-     * Create a new instance of the related model.
+     * Execute the query and get the first result or call a callback.
      *
-     * @param  array<model-property<TRelatedModel>, mixed>  $attributes
-     * @param  array<string, mixed>  $joining
-     * @param  bool  $touch
-     * @return TRelatedModel
+     * @template TValue
+     *
+     * @param  (\Closure(): TValue)|list<string>  $columns
+     * @param  (\Closure(): TValue)|null  $callback
+     * @return (TRelatedModel&object{pivot: TPivotModel})|TValue
      */
-    public function create(array $attributes = [], array $joining = [], $touch = true);
+    public function firstOr($columns = ['*'], ?Closure $callback = null);
 
     /**
      * Get a paginator for the "select" statement.
@@ -125,7 +194,7 @@ class BelongsToMany extends Relation
      * @param  array<int, (model-property<TRelatedModel>|'*')>  $columns
      * @param  string  $pageName
      * @param  int|null  $page
-     * @return \Illuminate\Pagination\LengthAwarePaginator<TRelatedModel>
+     * @return \Illuminate\Pagination\LengthAwarePaginator<int, TRelatedModel&object{pivot: TPivotModel}>
      */
     public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null);
 
@@ -136,7 +205,7 @@ class BelongsToMany extends Relation
      * @param  array<int, (model-property<TRelatedModel>|'*')>  $columns
      * @param  string  $pageName
      * @param  int|null  $page
-     * @return \Illuminate\Pagination\Paginator<TRelatedModel>
+     * @return \Illuminate\Contracts\Pagination\Paginator<int, TRelatedModel&object{pivot: TPivotModel}>
      */
     public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null);
 
@@ -147,7 +216,81 @@ class BelongsToMany extends Relation
      * @param  array<int, (model-property<TRelatedModel>|'*')>  $columns
      * @param  string  $cursorName
      * @param  string|null  $cursor
-     * @return \Illuminate\Pagination\CursorPaginator<TRelatedModel>
+     * @return \Illuminate\Contracts\Pagination\CursorPaginator<int, TRelatedModel&object{pivot: TPivotModel}>
      */
     public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null);
+
+    /**
+     * Query lazily, by chunks of the given size.
+     *
+     * @param  int  $chunkSize
+     * @return \Illuminate\Support\LazyCollection<int, TRelatedModel&object{pivot: TPivotModel}>
+     */
+    public function lazy($chunkSize = 1000);
+
+    /**
+     * Query lazily, by chunking the results of a query by comparing IDs.
+     *
+     * @param  int  $chunkSize
+     * @param  string|null  $column
+     * @param  string|null  $alias
+     * @return \Illuminate\Support\LazyCollection<int, TRelatedModel&object{pivot: TPivotModel}>
+     */
+    public function lazyById($chunkSize = 1000, $column = null, $alias = null);
+
+    /**
+     * Query lazily, by chunking the results of a query by comparing IDs in descending order.
+     *
+     * @param  int  $chunkSize
+     * @param  string|null  $column
+     * @param  string|null  $alias
+     * @return \Illuminate\Support\LazyCollection<int, TRelatedModel&object{pivot: TPivotModel}>
+     */
+    public function lazyByIdDesc($chunkSize = 1000, $column = null, $alias = null);
+
+    /**
+     * Get a lazy collection for the given query.
+     *
+     * @return \Illuminate\Support\LazyCollection<int, TRelatedModel&object{pivot: TPivotModel}>
+     */
+    public function cursor();
+
+    /**
+     * Save a new model and attach it to the parent model.
+     *
+     * @param  TRelatedModel  $model
+     * @param  array<model-string<TPivotModel>, mixed>  $pivotAttributes
+     * @param  bool  $touch
+     * @return TRelatedModel&object{pivot: TPivotModel}
+     */
+    public function save(Model $model, array $pivotAttributes = [], $touch = true);
+
+    /**
+     * Save a new model without raising any events and attach it to the parent model.
+     *
+     * @param  TRelatedModel  $model
+     * @param  array<model-string<TPivotModel>, mixed>  $pivotAttributes
+     * @param  bool  $touch
+     * @return TRelatedModel&object{pivot: TPivotModel}
+     */
+    public function saveQuietly(Model $model, array $pivotAttributes = [], $touch = true);
+
+    /**
+     * Create a new instance of the related model.
+     *
+     * @param  array<model-property<TRelatedModel>, mixed>  $attributes
+     * @param  array<string, mixed>  $joining
+     * @param  bool  $touch
+     * @return TRelatedModel&object{pivot: TPivotModel}
+     */
+    public function create(array $attributes = [], array $joining = [], $touch = true);
+
+    /**
+     * Create an array of new instances of the related models.
+     *
+     * @param  iterable<array-key, mixed>  $records
+     * @param  array<array-key, mixed>  $joinings
+     * @return array<int, TRelatedModel&object{pivot: TPivotModel}>
+     */
+    public function createMany(iterable $records, array $joinings = []);
 }
diff --git a/stubs/common/Contracts/Pagination.stub b/stubs/common/Contracts/Pagination.stub
index b37af992..8625c9f5 100644
--- a/stubs/common/Contracts/Pagination.stub
+++ b/stubs/common/Contracts/Pagination.stub
@@ -3,31 +3,24 @@
 namespace Illuminate\Contracts\Pagination;
 
 /**
- * @template TItem
+ * @template TKey of array-key
+ * @template TValue
  */
 interface Paginator
-{
-    /**
-     * @return array<TItem>
-     */
-    public function items(): array;
-}
+{}
 
 /**
- * @template TItem
+ * @template TKey of array-key
+ * @template TValue
  *
- * @extends Paginator<TItem>
+ * @extends Paginator<TKey, TValue>
  */
 interface LengthAwarePaginator extends Paginator
 {}
 
 /**
- * @template TItem
+ * @template TKey of array-key
+ * @template TValue
  */
 interface CursorPaginator
-{
-    /**
-     * @return array<TItem>
-     */
-    public function items(): array;
-}
+{}
diff --git a/stubs/common/EloquentBuilder.stub b/stubs/common/EloquentBuilder.stub
index 290291a3..3ca3ceb1 100644
--- a/stubs/common/EloquentBuilder.stub
+++ b/stubs/common/EloquentBuilder.stub
@@ -315,7 +315,7 @@ class Builder
      * @param  array<int, (model-property<TModel>|'*')>  $columns
      * @param  string  $pageName
      * @param  int|null  $page
-     * @return \Illuminate\Pagination\LengthAwarePaginator<TModel>
+     * @return \Illuminate\Pagination\LengthAwarePaginator<int, TModel>
      *
      * @throws \InvalidArgumentException
      */
@@ -328,7 +328,7 @@ class Builder
      * @param  array<int, (model-property<TModel>|'*')>  $columns
      * @param  string  $pageName
      * @param  int|null  $page
-     * @return \Illuminate\Pagination\Paginator<TModel>
+     * @return \Illuminate\Pagination\Paginator<int, TModel>
      */
     public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null);
 
@@ -339,7 +339,7 @@ class Builder
      * @param  array<int, (model-property<TModel>|'*')>  $columns
      * @param  string  $cursorName
      * @param  \Illuminate\Pagination\Cursor|string|null  $cursor
-     * @return \Illuminate\Pagination\CursorPaginator<TModel>
+     * @return \Illuminate\Pagination\CursorPaginator<int, TModel>
      */
      public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null);
 
diff --git a/stubs/common/HasOneOrManyThrough.stub b/stubs/common/HasOneOrManyThrough.stub
index 20f81090..969c1c20 100644
--- a/stubs/common/HasOneOrManyThrough.stub
+++ b/stubs/common/HasOneOrManyThrough.stub
@@ -19,7 +19,7 @@ abstract class HasOneOrManyThrough extends Relation
      * @param  array<int, (model-property<TRelatedModel>|'*')>  $columns
      * @param  string  $pageName
      * @param  int|null  $page
-     * @return \Illuminate\Pagination\LengthAwarePaginator<TRelatedModel>
+     * @return \Illuminate\Pagination\LengthAwarePaginator<int, TRelatedModel>
      */
     public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null);
 
@@ -30,7 +30,7 @@ abstract class HasOneOrManyThrough extends Relation
      * @param  array<int, (model-property<TRelatedModel>|'*')>  $columns
      * @param  string  $pageName
      * @param  int|null  $page
-     * @return \Illuminate\Pagination\Paginator<TRelatedModel>
+     * @return \Illuminate\Pagination\Paginator<int, TRelatedModel>
      */
     public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null);
 
@@ -41,7 +41,7 @@ abstract class HasOneOrManyThrough extends Relation
      * @param  array<int, (model-property<TRelatedModel>|'*')>  $columns
      * @param  string  $cursorName
      * @param  string|null  $cursor
-     * @return \Illuminate\Pagination\CursorPaginator<TRelatedModel>
+     * @return \Illuminate\Pagination\CursorPaginator<int, TRelatedModel>
      */
     public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null);
 
diff --git a/stubs/common/MorphToMany.stub b/stubs/common/MorphToMany.stub
new file mode 100644
index 00000000..6a08384d
--- /dev/null
+++ b/stubs/common/MorphToMany.stub
@@ -0,0 +1,16 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
+ * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
+ * @template TPivotModel of \Illuminate\Database\Eloquent\Relations\Pivot = \Illuminate\Database\Eloquent\Relations\MorphPivot
+ * @template TAccessor of string = 'pivot'
+ *
+ * @extends \Illuminate\Database\Eloquent\Relations\BelongsToMany<TRelatedModel, TDeclaringModel, TPivotModel, TAccessor>
+ */
+class MorphToMany extends BelongsToMany {}
+
diff --git a/stubs/common/Pagination.stub b/stubs/common/Pagination.stub
index a8ffac65..1dfd231e 100644
--- a/stubs/common/Pagination.stub
+++ b/stubs/common/Pagination.stub
@@ -3,114 +3,86 @@
 namespace Illuminate\Pagination;
 
 /**
+ * @template TKey of array-key
  * @template TValue
  *
- * @mixin \Illuminate\Support\Collection<array-key, TValue>
+ * @mixin \Illuminate\Support\Collection<TKey, TValue>
  */
-abstract class AbstractPaginator implements \Illuminate\Contracts\Support\Htmlable
+abstract class AbstractPaginator implements \Illuminate\Contracts\Support\Htmlable, \Stringable
 {
     /**
-     * @return array<TValue>
-     */
-    public function items(): array;
-
-    /**
-     * @return \Illuminate\Support\Collection<array-key, TValue>
-     */
-    public function getCollection(): \Illuminate\Support\Collection;
-
-    /**
-     * @return \ArrayIterator<array-key, TValue>
-     */
-    public function getIterator(): \Traversable;
-
-    public function offsetExists(mixed $offset): bool;
-
-    /**
+     * @param  TKey  $key
      * @return TValue|null
      */
-    public function offsetGet(mixed $offset): mixed;
-
-    /**
-     * @param TValue $value
-     */
-    public function offsetSet(mixed $offset, $value): void;
-
-    public function offsetUnset(mixed $offset): void;
+    public function offsetGet($key): mixed;
 }
 
 /**
+ * @template TKey of array-key
  * @template TValue
  *
- * @implements \ArrayAccess<array-key, TValue>
- * @implements \IteratorAggregate<array-key, TValue>
- * @implements \Illuminate\Contracts\Support\Arrayable<array-key, TValue>
- * @implements \Illuminate\Contracts\Pagination\Paginator<TValue>
+ * @implements \ArrayAccess<TKey, TValue>
+ * @implements \IteratorAggregate<TKey, TValue>
+ * @implements \Illuminate\Contracts\Support\Arrayable<TKey, TValue>
+ * @implements \Illuminate\Contracts\Pagination\Paginator<TKey, TValue>
  *
- * @extends AbstractPaginator<TValue>
+ * @extends AbstractPaginator<TKey, TValue>
  */
 class Paginator extends AbstractPaginator implements \Illuminate\Contracts\Support\Arrayable, \ArrayAccess, \Countable, \IteratorAggregate, \Illuminate\Contracts\Support\Jsonable, \JsonSerializable, \Illuminate\Contracts\Pagination\Paginator
 {}
 
 /**
+ * @template TKey of array-key
  * @template TValue
  *
- * @implements \ArrayAccess<array-key, TValue>
- * @implements \IteratorAggregate<array-key, TValue>
- * @implements \Illuminate\Contracts\Support\Arrayable<array-key, TValue>
- * @implements \Illuminate\Contracts\Pagination\LengthAwarePaginator<TValue>
+ * @implements \ArrayAccess<TKey, TValue>
+ * @implements \IteratorAggregate<TKey, TValue>
+ * @implements \Illuminate\Contracts\Support\Arrayable<TKey, TValue>
+ * @implements \Illuminate\Contracts\Pagination\LengthAwarePaginator<TKey, TValue>
  *
- * @extends AbstractPaginator<TValue>
+ * @extends AbstractPaginator<TKey, TValue>
  */
 class LengthAwarePaginator extends AbstractPaginator implements \Illuminate\Contracts\Support\Arrayable, \ArrayAccess, \Countable, \IteratorAggregate, \Illuminate\Contracts\Support\Jsonable, \JsonSerializable, \Illuminate\Contracts\Pagination\LengthAwarePaginator
 {}
 
 /**
+ * @template TKey of array-key
  * @template TValue
  *
- * @mixin \Illuminate\Support\Collection<array-key, TValue>
+ * @mixin \Illuminate\Support\Collection<TKey, TValue>
  */
-abstract class AbstractCursorPaginator implements \Illuminate\Contracts\Support\Htmlable
+abstract class AbstractCursorPaginator implements \Illuminate\Contracts\Support\Htmlable, \Stringable
 {
-    /**
-     * @return array<TValue>
-     */
-    public function items(): array;
-
-    /**
-     * @return \Illuminate\Support\Collection<array-key, TValue>
-     */
-    public function getCollection(): \Illuminate\Support\Collection;
+    /** @return \Illuminate\Support\Collection<TKey, TValue> */
+    public function getCollection();
 
     /**
-     * @return \ArrayIterator<array-key, TValue>
+     * @template TSetKey of array-key
+     * @template TSetValue
+     *
+     * @param  \Illuminate\Support\Collection<TSetKey, TSetValue>  $collection
+     * @phpstan-this-out static<TSetKey, TSetValue>
+     * @return $this
      */
-    public function getIterator(): \Traversable;
-
-    public function offsetExists(mixed $offset): bool;
+    public function setCollection(\Illuminate\Support\Collection $collection);
 
     /**
+     * @param  TKey  $key
      * @return TValue|null
      */
-    public function offsetGet(mixed $offset): mixed;
-
-    /**
-     * @param TValue $value
-     */
-    public function offsetSet(mixed $offset, $value): void;
-
-    public function offsetUnset(mixed $offset): void;
+    public function offsetGet($key): mixed;
 }
 
 /**
+ * @template TKey of array-key
  * @template TValue
  *
- * @implements \ArrayAccess<array-key, TValue>
- * @implements \IteratorAggregate<array-key, TValue>
- * @implements \Illuminate\Contracts\Support\Arrayable<array-key, TValue>
- * @implements \Illuminate\Contracts\Pagination\CursorPaginator<TValue>
+ * @implements \ArrayAccess<TKey, TValue>
+ * @implements \IteratorAggregate<TKey, TValue>
+ * @implements \Illuminate\Contracts\Support\Arrayable<TKey, TValue>
+ * @implements \Illuminate\Contracts\Pagination\CursorPaginator<TKey, TValue>
  *
- * @extends AbstractCursorPaginator<TValue>
+ * @extends AbstractCursorPaginator<TKey, TValue>
  */
 class CursorPaginator extends AbstractCursorPaginator implements \Illuminate\Contracts\Support\Arrayable, \ArrayAccess, \Countable, \IteratorAggregate, \Illuminate\Contracts\Support\Jsonable, \JsonSerializable, \Illuminate\Contracts\Pagination\CursorPaginator
 {}
diff --git a/stubs/common/Pivot.stub b/stubs/common/Pivot.stub
new file mode 100644
index 00000000..9b231ca1
--- /dev/null
+++ b/stubs/common/Pivot.stub
@@ -0,0 +1,8 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+use Illuminate\Database\Eloquent\Model;
+
+class Pivot extends Model {}
+class MorphPivot extends Pivot {}
diff --git a/tests/Type/data/bug-2111.php b/tests/Type/data/bug-2111.php
index 004a4874..162ee7dd 100644
--- a/tests/Type/data/bug-2111.php
+++ b/tests/Type/data/bug-2111.php
@@ -13,8 +13,8 @@ class User extends Model
     public function teams(): BelongsToMany
     {
         $relation = $this->belongsToMany(Team::class);
-        assertType('Illuminate\Database\Eloquent\Relations\BelongsToMany<Bug2111\Team, $this(Bug2111\User)>', $relation);
-        assertType('Illuminate\Database\Eloquent\Relations\BelongsToMany<Bug2111\Team, $this(Bug2111\User)>', $relation->latest());
+        assertType('Illuminate\Database\Eloquent\Relations\BelongsToMany<Bug2111\Team, $this(Bug2111\User), Illuminate\Database\Eloquent\Relations\Pivot, \'pivot\'>', $relation);
+        assertType('Illuminate\Database\Eloquent\Relations\BelongsToMany<Bug2111\Team, $this(Bug2111\User), Illuminate\Database\Eloquent\Relations\Pivot, \'pivot\'>', $relation->latest());
 
         return $relation;
     }
@@ -22,7 +22,7 @@ public function teams(): BelongsToMany
     public function paidTeams(): BelongsToMany
     {
         $relation = $this->teams()->whereHas('activeSubscriptions');
-        assertType('Illuminate\Database\Eloquent\Relations\BelongsToMany<Bug2111\Team, $this(Bug2111\User)>', $relation);
+        assertType('Illuminate\Database\Eloquent\Relations\BelongsToMany<Bug2111\Team, $this(Bug2111\User), Illuminate\Database\Eloquent\Relations\Pivot, \'pivot\'>', $relation);
 
         return $relation;
     }
diff --git a/tests/Type/data/custom-eloquent-collection.php b/tests/Type/data/custom-eloquent-collection.php
index 9e1d687d..22973e44 100644
--- a/tests/Type/data/custom-eloquent-collection.php
+++ b/tests/Type/data/custom-eloquent-collection.php
@@ -104,11 +104,11 @@ function test(): void
     assertType('App\RoleCollection<int, App\Role>', (new User)->roles()->find([1, 2]));
     assertType('Illuminate\Database\Eloquent\Collection<int, App\User>', (new Role)->users()->find([1, 2]));
 
-    assertType('App\Role|null', (new User)->roles()->find(1));
+    assertType('(App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot})|null', (new User)->roles()->find(1));
     assertType('App\RoleCollection<int, App\Role>', (new User)->roles()->findOrFail([1, 2]));
 
     assertType('Illuminate\Database\Eloquent\Collection<int, App\User>', (new Role)->users()->findOrFail([1, 2]));
-    assertType('App\Role', (new User)->roles()->findOrFail(1));
+    assertType('App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot}', (new User)->roles()->findOrFail(1));
 
     assertType('App\RoleCollection<int, App\Role>', (new User)->roles()->findMany([1, 2]));
     assertType('Illuminate\Database\Eloquent\Collection<int, App\User>', (new Role)->users()->findMany([1, 2]));
diff --git a/tests/Type/data/eloquent-builder-l11-42.php b/tests/Type/data/eloquent-builder-l11-42.php
index 7695d37e..80199df8 100644
--- a/tests/Type/data/eloquent-builder-l11-42.php
+++ b/tests/Type/data/eloquent-builder-l11-42.php
@@ -320,8 +320,8 @@ function test(
         'posts.id'
     ));
 
-    assertType('Illuminate\Pagination\LengthAwarePaginator<App\User>', User::query()->paginate());
-    assertType('array<App\User>', User::query()->paginate()->items());
+    assertType('Illuminate\Pagination\LengthAwarePaginator<int, App\User>', User::query()->paginate());
+    assertType('array<int, App\User>', User::query()->paginate()->items());
 
     User::chunk(1000, fn ($collection) => assertType('Illuminate\Database\Eloquent\Collection<int, App\User>', $collection));
     User::chunkById(1000, fn ($collection) => assertType('Illuminate\Database\Eloquent\Collection<int, App\User>', $collection));
diff --git a/tests/Type/data/eloquent-builder.php b/tests/Type/data/eloquent-builder.php
index 3ddb7e20..12033bd2 100644
--- a/tests/Type/data/eloquent-builder.php
+++ b/tests/Type/data/eloquent-builder.php
@@ -320,8 +320,8 @@ function test(
         'posts.id'
     ));
 
-    assertType('Illuminate\Pagination\LengthAwarePaginator<App\User>', User::query()->paginate());
-    assertType('array<App\User>', User::query()->paginate()->items());
+    assertType('Illuminate\Pagination\LengthAwarePaginator<int, App\User>', User::query()->paginate());
+    assertType('array<int, App\User>', User::query()->paginate()->items());
 
     User::chunk(1000, fn ($collection) => assertType('Illuminate\Database\Eloquent\Collection<int, App\User>', $collection));
     User::chunkById(1000, fn ($collection) => assertType('Illuminate\Database\Eloquent\Collection<int, App\User>', $collection));
diff --git a/tests/Type/data/model-relations.php b/tests/Type/data/model-relations.php
index 1cd4c5e6..56819d20 100644
--- a/tests/Type/data/model-relations.php
+++ b/tests/Type/data/model-relations.php
@@ -31,14 +31,14 @@ function test(
 ): void {
     assertType('App\Account', $appUser->accounts()->firstOrCreate([]));
     assertType('App\Account', $appUser->accounts()->createOrFirst([]));
-    assertType(Post::class, $appUser->posts()->create());
+    assertType('App\Post&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot}', $appUser->posts()->create());
     assertType('App\Account', $appUser->accounts()->create());
     assertType('App\Account', $appUser->syncableRelation()->create());
     assertType('int', $appUser->accounts()->increment('id'));
     assertType('int', $appUser->accounts()->decrement('id'));
     assertType('int', $appUser->accounts()->increment('id', 5));
     assertType('int', $appUser->accounts()->decrement('id', 5));
-    assertType('Illuminate\Pagination\LengthAwarePaginator<App\Account>', $appUser->accounts()->paginate(5));
+    assertType('Illuminate\Pagination\LengthAwarePaginator<int, App\Account>', $appUser->accounts()->paginate(5));
     assertType(
         'Illuminate\Database\Eloquent\Relations\MorphTo<Illuminate\Database\Eloquent\Model, App\Address>',
         $address->addressable()->where('name', 'bar')
@@ -59,14 +59,14 @@ function test(
     assertType('App\RoleCollection<int, App\Role>', $appUser->roles()->findOrFail([1]));
     assertType('42|App\RoleCollection<int, App\Role>', $appUser->roles()->findOr([1], fn () => 42));
     assertType('42|App\RoleCollection<int, App\Role>', $appUser->roles()->findOr([1], callback: fn () => 42));
-    assertType('App\Role', $appUser->roles()->findOrNew(1));
-    assertType('App\Role', $appUser->roles()->findOrFail(1));
-    assertType('App\Role|null', $appUser->roles()->find(1));
+    assertType('App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot}', $appUser->roles()->findOrNew(1));
+    assertType('App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot}', $appUser->roles()->findOrFail(1));
+    assertType('(App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot})|null', $appUser->roles()->find(1));
 //    assertType('App\Role|int', $appUser->roles()->findOr(1, fn () => 42));
 //    assertType('App\Role|int', $appUser->roles()->findOr(1, callback: fn () => 42));
-    assertType('App\Role|null', $appUser->roles()->first());
-    assertType('42|App\Role', $appUser->roles()->firstOr(fn () => 42));
-    assertType('42|App\Role', $appUser->roles()->firstOr(callback: fn () => 42));
+    assertType('(App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot})|null', $appUser->roles()->first());
+    assertType('42|(App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot})', $appUser->roles()->firstOr(fn () => 42));
+    assertType('42|(App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot})', $appUser->roles()->firstOr(callback: fn () => 42));
     assertType('Illuminate\Database\Eloquent\Relations\HasMany<App\Account, App\User>', (new User())->accounts()->where('name', 'bar'));
     assertType('Illuminate\Database\Eloquent\Relations\HasMany<App\Account, App\User>', (new User())->accounts()->whereIn('id', [1, 2, 3]));
     assertType('Illuminate\Database\Eloquent\Relations\HasMany<App\Account, App\User>', (new User())->accounts()->whereActive(true));
@@ -114,7 +114,7 @@ function test(
     assertType('Illuminate\Database\Eloquent\Builder<App\User>', $users->getQuery());
     assertType('App\User', $users->make());
     assertType('Illuminate\Database\Eloquent\Relations\BelongsTo<App\Group, App\Account|App\User>', $union->group());
-    assertType('Illuminate\Database\Eloquent\Relations\BelongsToMany<App\Post, App\Account|App\User>', $union->posts());
+    assertType('Illuminate\Database\Eloquent\Relations\BelongsToMany<App\Post, App\Account|App\User, Illuminate\Database\Eloquent\Relations\Pivot, \'pivot\'>', $union->posts());
 
     assertType('App\Account', $appUser->accounts()->sole());
     assertType('App\Group', $appUser->group()->sole());
@@ -146,7 +146,7 @@ function test(
     assertType('App\Account|false', $user->accountsCamel()->save(new Account()));
     assertType('App\Account|false', $user->accountsCamel()->saveQuietly(new Account()));
 
-    assertType('Illuminate\Database\Eloquent\Relations\BelongsToMany<App\Role, App\User>', $user->roles());
+    assertType('Illuminate\Database\Eloquent\Relations\BelongsToMany<App\Role, App\User, Illuminate\Database\Eloquent\Relations\Pivot, \'pivot\'>', $user->roles());
     assertType('App\RoleCollection<int, App\Role>', $user->roles()->getResults());
     assertType('App\RoleCollection<int, App\Role>', $user->roles);
     assertType('App\RoleCollection<int, App\Role>', $user->roles()->find([1]));
@@ -155,35 +155,35 @@ function test(
     assertType('App\RoleCollection<int, App\Role>', $user->roles()->findOrFail([1]));
     assertType('42|App\RoleCollection<int, App\Role>', $user->roles()->findOr([1], fn () => 42));
     assertType('42|App\RoleCollection<int, App\Role>', $user->roles()->findOr([1], callback: fn () => 42));
-    assertType('App\Role', $user->roles()->findOrNew(1));
-    assertType('App\Role', $user->roles()->findOrFail(1));
-    assertType('App\Role|null', $user->roles()->find(1));
+    assertType('App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot}', $user->roles()->findOrNew(1));
+    assertType('App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot}', $user->roles()->findOrFail(1));
+    assertType('(App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot})|null', $user->roles()->find(1));
 //    assertType('int|App\Role', $user->roles()->findOr(1, fn () => 42));
 //    assertType('int|App\Role', $user->roles()->findOr(1, callback: fn () => 42));
-    assertType('App\Role|null', $user->roles()->first());
-    assertType('42|App\Role', $user->roles()->firstOr(fn () => 42));
-    assertType('42|App\Role', $user->roles()->firstOr(callback: fn () => 42));
-    assertType('App\Role|null', $user->roles()->firstWhere('foo'));
-    assertType('App\Role', $user->roles()->firstOrNew());
-    assertType('App\Role', $user->roles()->firstOrFail());
-    assertType('App\Role', $user->roles()->firstOrCreate());
-    assertType('App\Role', $user->roles()->create());
-    assertType('App\Role', $user->roles()->createOrFirst());
-    assertType('App\Role', $user->roles()->updateOrCreate([]));
-    assertType('App\Role', $user->roles()->save(new Role()));
-    assertType('App\Role', $user->roles()->saveQuietly(new Role()));
+    assertType('(App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot})|null', $user->roles()->first());
+    assertType('42|(App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot})', $user->roles()->firstOr(fn () => 42));
+    assertType('42|(App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot})', $user->roles()->firstOr(callback: fn () => 42));
+    assertType('(App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot})|null', $user->roles()->firstWhere('foo'));
+    assertType('App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot}', $user->roles()->firstOrNew());
+    assertType('App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot}', $user->roles()->firstOrFail());
+    assertType('App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot}', $user->roles()->firstOrCreate());
+    assertType('App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot}', $user->roles()->create());
+    assertType('App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot}', $user->roles()->createOrFirst());
+    assertType('App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot}', $user->roles()->updateOrCreate([]));
+    assertType('App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot}', $user->roles()->save(new Role()));
+    assertType('App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot}', $user->roles()->saveQuietly(new Role()));
     $roles = $user->roles()->getResults();
     assertType('App\RoleCollection<int, App\Role>', $user->roles()->saveMany($roles));
     assertType('array<int, App\Role>', $user->roles()->saveMany($roles->all()));
     assertType('App\RoleCollection<int, App\Role>', $user->roles()->saveManyQuietly($roles));
     assertType('array<int, App\Role>', $user->roles()->saveManyQuietly($roles->all()));
-    assertType('array<int, App\Role>', $user->roles()->createMany($roles));
+    assertType('array<int, App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot}>', $user->roles()->createMany($roles));
     assertType('array{attached: array, detached: array, updated: array}', $user->roles()->sync($roles));
     assertType('array{attached: array, detached: array, updated: array}', $user->roles()->syncWithoutDetaching($roles));
     assertType('array{attached: array, detached: array, updated: array}', $user->roles()->syncWithPivotValues($roles, []));
-    assertType('Illuminate\Support\LazyCollection<int, App\Role>', $user->roles()->lazy());
-    assertType('Illuminate\Support\LazyCollection<int, App\Role>', $user->roles()->lazyById());
-    assertType('Illuminate\Support\LazyCollection<int, App\Role>', $user->roles()->cursor());
+    assertType('Illuminate\Support\LazyCollection<int, App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot}>', $user->roles()->lazy());
+    assertType('Illuminate\Support\LazyCollection<int, App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot}>', $user->roles()->lazyById());
+    assertType('Illuminate\Support\LazyCollection<int, App\Role&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot}>', $user->roles()->cursor());
 
     assertType('Illuminate\Database\Eloquent\Relations\HasOneThrough<App\Car, App\Mechanic, App\User>', $user->car());
     assertType('App\Car|null', $user->car()->getResults());
@@ -235,7 +235,7 @@ function test(
     assertType('App\Comment', $comment->commentable()->associate(new Post()));
     assertType('App\Comment', $comment->commentable()->dissociate());
 
-    assertType('Illuminate\Database\Eloquent\Relations\MorphToMany<App\Tag, App\Post>', $post->tags());
+    assertType('Illuminate\Database\Eloquent\Relations\MorphToMany<App\Tag, App\Post, Illuminate\Database\Eloquent\Relations\MorphPivot, \'pivot\'>', $post->tags());
     assertType('Illuminate\Database\Eloquent\Collection<int, App\Tag>', $post->tags()->getResults());
     assertType('Illuminate\Database\Eloquent\Collection<int, App\Tag>', $post->tags);
 
diff --git a/tests/Type/data/paginator-extension.php b/tests/Type/data/paginator-extension.php
index d45be49e..4bc3c18e 100644
--- a/tests/Type/data/paginator-extension.php
+++ b/tests/Type/data/paginator-extension.php
@@ -10,26 +10,26 @@
 
 function test(): void
 {
-    assertType('Illuminate\Pagination\LengthAwarePaginator<App\User>', User::paginate());
-    assertType('array<App\User>', User::paginate()->all());
-    assertType('array<App\User>', User::paginate()->items());
+    assertType('Illuminate\Pagination\LengthAwarePaginator<int, App\User>', User::paginate());
+    assertType('array<int, App\User>', User::paginate()->all());
+    assertType('array<int, App\User>', User::paginate()->items());
     assertType('App\User|null', User::paginate()[0]);
 
-    assertType('Illuminate\Pagination\Paginator<App\User>', User::simplePaginate());
-    assertType('array<App\User>', User::simplePaginate()->all());
-    assertType('array<App\User>', User::simplePaginate()->items());
+    assertType('Illuminate\Pagination\Paginator<int, App\User>', User::simplePaginate());
+    assertType('array<int, App\User>', User::simplePaginate()->all());
+    assertType('array<int, App\User>', User::simplePaginate()->items());
     assertType('App\User|null', User::simplePaginate()[0]);
 
-    assertType('Illuminate\Pagination\CursorPaginator<App\User>', User::cursorPaginate());
-    assertType('array<App\User>', User::cursorPaginate()->all());
-    assertType('array<App\User>', User::cursorPaginate()->items());
+    assertType('Illuminate\Pagination\CursorPaginator<int, App\User>', User::cursorPaginate());
+    assertType('array<int, App\User>', User::cursorPaginate()->all());
+    assertType('array<int, App\User>', User::cursorPaginate()->items());
     assertType('App\User|null', User::cursorPaginate()[0]);
 
-    assertType('ArrayIterator<(int|string), App\User>', User::query()->paginate()->getIterator());
+    assertType('ArrayIterator<int, App\User>', User::query()->paginate()->getIterator());
 
     // HasMany
-    assertType('Illuminate\Pagination\LengthAwarePaginator<App\Account>', (new User())->accounts()->paginate());
+    assertType('Illuminate\Pagination\LengthAwarePaginator<int, App\Account>', (new User())->accounts()->paginate());
 
     // BelongsToMany
-    assertType('Illuminate\Pagination\LengthAwarePaginator<App\Post>', (new User())->posts()->paginate());
+    assertType('Illuminate\Pagination\LengthAwarePaginator<int, App\Post&object{pivot: Illuminate\Database\Eloquent\Relations\Pivot}>', (new User())->posts()->paginate());
 }


Reviews:
user: login: calebdw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T02:57:46Z
user: login: AJenbo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T03:26:32Z
user: login: calebdw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T03:28:07Z
user: login: AJenbo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T03:30:31Z
user: login: calebdw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T03:39:22Z
user: login: calebdw
body: Looks good to me!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T01:50:32Z

Review comments:
diff_hunk: @@ -0,0 +1,7 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+class Pivot
+{
+}
path: stubs/common/Pivot.stub
user: login: calebdw
body: can you please update the MorphToMany stub in accordance with the framework?


you'll need to import the Model
```suggestion
class Pivot extends Model {}
class MorphPivot extends Pivot {}
```
created_at: 2025-03-20T02:57:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,7 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+class Pivot
+{
+}
path: stubs/common/Pivot.stub
user: login: AJenbo
body: There isn't a MorphToMany stub so it should already be using the one from the framework.
created_at: 2025-03-20T03:26:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,7 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+class Pivot
+{
+}
path: stubs/common/Pivot.stub
user: login: calebdw
body: Ah, good deal 👌
created_at: 2025-03-20T03:28:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,7 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+class Pivot
+{
+}
path: stubs/common/Pivot.stub
user: login: AJenbo
body: Oh, you want it so that it applies even before they release the next version, got it.

created_at: 2025-03-20T03:30:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,7 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+class Pivot
+{
+}
path: stubs/common/Pivot.stub
user: login: calebdw
body: It should be fine since the BelongsToMany uses defaults now, the MorphToMany will just become smarter when the release happens.


I hate maintaining duplicate stubs lol
created_at: 2025-03-20T03:39:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: szepeviktor
created_at: 2025-03-19T14:01:55Z
body: Thank you.
author_association: COLLABORATOR
user: login: calebdw
created_at: 2025-03-19T14:03:25Z
body: Please mark this as draft until I finish my framework fixes
author_association: CONTRIBUTOR
user: login: AJenbo
created_at: 2025-03-19T14:05:18Z
body: Looks like I also have to adjust some tests, thanks for running the CI @szepeviktor
author_association: CONTRIBUTOR
user: login: browner12
created_at: 2025-03-19T14:31:20Z
body: thanks for handling this guys!
author_association: NONE
user: login: AJenbo
created_at: 2025-03-19T16:01:29Z
body: Test should now pass
author_association: CONTRIBUTOR
user: login: calebdw
created_at: 2025-03-19T20:05:16Z
body: @AJenbo, can please you update given the new changes upstream?
author_association: CONTRIBUTOR
user: login: AJenbo
created_at: 2025-03-20T00:27:20Z
body: Done, also added the Pivot stub so the type check also pass now.
author_association: CONTRIBUTOR
user: login: calebdw
created_at: 2025-03-20T04:34:45Z
body: @AJenbo, I've already fixed the paginator stubs here: https://github.com/larastan/larastan/pull/2208
author_association: CONTRIBUTOR
user: login: AJenbo
created_at: 2025-03-20T04:37:22Z
body: @calebdw I have had to update several stubs because of various dependencies but I have to take a break now. 

Pagination.stub (I need to rebase on your PR).

QueryBuilder.stub, Model.stub, EloquentBuilder.stub are currently mostly just copies of the original classes and needs to be stripped down to become stubs and recover the overwrites from the old stubs. If you have time to help with this I would appreciate it.
author_association: CONTRIBUTOR
user: login: calebdw
created_at: 2025-03-20T04:41:39Z
body: > QueryBuilder.stub, Model.stub, EloquentBuilder.stub are currently mostly just copies of the original classes and needs to be stripped down to become stubs and recover the overwrites from the old stubs. If you have time to help with this I would appreciate it.

I think you're going to far down a rabbit hole---we're not trying to duplicate all the methods from all the files. We only care to override specific methods as needed (most of the model/builder overrides is just to add the `model-property<...>` type, but I've been thinking of some ways that we use an extension for this instead of overridding all the methods).

I've also been trying to push more and more of the stub overrides into the framework so we don't need to keep maintaining them.
author_association: CONTRIBUTOR
user: login: calebdw
created_at: 2025-03-20T04:43:21Z
body: All that should be needed for this PR are the BelongsToMany changes (and possibly the ManyToMany changes) from my upstream PR, but this will need to be based on by paginator pr
author_association: CONTRIBUTOR
user: login: AJenbo
created_at: 2025-03-20T04:49:42Z
body: I rebased on your pagination PR and that resolved the PHPStan issues, there are still several failing unit tests that needs to be adjusted in case you have time to help.

Tests: 1417, Assertions: 1708, Failures: 23, PHPUnit Deprecations: 39.

They mostly look pretty simple but I won't have time for the next 18h
author_association: CONTRIBUTOR
user: login: AJenbo
created_at: 2025-03-20T04:56:17Z
body: > I think you're going to far down a rabbit hole---we're not trying to duplicate all the methods from all the files.

I was syncing all the overwritten methods but didn't have time to remove all the unnecessary stuff yet, that why the commit was had "draft:" in the name :) I think there might be a good chunk of outdated stuff in the stubs actually.

> I've also been trying to push more and more of the stub overrides into the framework so we don't need to keep maintaining them.

<3
author_association: CONTRIBUTOR
user: login: AJenbo
created_at: 2025-03-22T01:00:47Z
body: @calebdw I brought over your adjustments as best I could since the two trees are a bit diverged, can you please take a look to see if there are anything missing.

@szepeviktor please run the CI tests, I ran both locally so I feel pretty good about it passing this time :)
author_association: CONTRIBUTOR
user: login: Lukmauu
created_at: 2025-03-22T02:22:42Z
body: Is there a solution for this?
```
/**
   * @return BelongsToMany<Task, $this>
   */
  public function tasks(): BelongsToMany
  {
      return $this->belongsToMany(Task::class, event_task_user_table_name())
          ->using(EventTaskUser::class)
          ->withPivot(['is_opted_in', 'created_at'])
          ->wherePivot('is_opted_in', true)
          ->distinct();
  }
```

What should I do fix the pivot error? I did not fully understand why Laravel did this change, but now, I do not know how to fix:

```
Method App\Models\User::tasks() should return
Illuminate\Database\Eloquent\Relations\BelongsToMany<App\Models\Task,
$this(App\Models\User)> but returns
Illuminate\Database\Eloquent\Relations\BelongsToMany<App\Models\Task,
$this(App\Models\User), Illuminate\Database\Eloquent\Relations\Pivot>.
```

I try to the Pivot at the end after $this but to no avail. 
author_association: NONE
user: login: calebdw
created_at: 2025-03-22T02:27:21Z
body: You are supposed to specify the Pivot model, the fix in the framework has not been released yet.

This PR will fix #1774 

```php
  /**
   * @return BelongsToMany<Task, $this, EventTaskUser>
   */
  public function tasks(): BelongsToMany
  {
      return $this->belongsToMany(Task::class, event_task_user_table_name())
          ->using(EventTaskUser::class)
          ->withPivot(['is_opted_in', 'created_at'])
          ->wherePivot('is_opted_in', true)
          ->distinct();
  }
```
author_association: CONTRIBUTOR
user: login: Lukmauu
created_at: 2025-03-22T02:29:26Z
body: Thank you @calebdw I will make the needed changes and just way tight for the next Laravel merge. Thank you very much.
author_association: NONE
user: login: calebdw
created_at: 2025-03-22T02:31:07Z
body: Note that you will likely still see warnings until this is merged as Larastan overrides the framework classes with stubs
author_association: CONTRIBUTOR
user: login: miken32
created_at: 2025-03-26T18:52:33Z
body: Any update on when this will be put in a release? Tests are all failing after an update to Laravel 12.3
author_association: NONE
user: login: calebdw
created_at: 2025-03-26T19:06:24Z
body: @miken32, that's up to @canvural, however, you can immediately start using my published [fork](https://github.com/calebdw/larastan) if you like.
author_association: CONTRIBUTOR
user: login: szepeviktor
created_at: 2025-03-26T19:18:59Z
body: I think I cannot stop your happiness.
https://youtu.be/1hKkAM-ADDU
Risking a revert ...

author_association: COLLABORATOR
user: login: AJenbo
created_at: 2025-03-26T22:57:32Z
body: Hopefully there will also be a release soon so people will get the update :)
author_association: CONTRIBUTOR
user: login: EPGDigital-MW
created_at: 2025-04-01T08:24:18Z
body: Laravel changes are now released, we tried implementing the Pivot models but still got errors so reverted back to using ignore.

Hopefully a larastan release can be done soon, thanks for the work on this.
author_association: NONE
user: login: AJenbo
created_at: 2025-04-12T20:41:27Z
body: fyi larastan was updated last week
author_association: CONTRIBUTOR