diff --git a/.gitignore b/.gitignore
index 87a995b..30e01fb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,7 +3,6 @@ composer.lock
 composer.phar
 .DS_Store
 Thumbs.db
-tests/.phpunit.cache
 tests/.phpunit.result.cache
 /tests/integration/tmp
 .vagrant
diff --git a/bootstrap.php b/bootstrap.php
deleted file mode 100644
index dcc4d8a..0000000
--- a/bootstrap.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-/*
- * This file is part of Flarum.
- *
- * For detailed copyright and license information, please view the
- * LICENSE file that was distributed with this source code.
- */
-
-if (! defined('LARAVEL_VERSION')) {
-    define('LARAVEL_VERSION', '10.0');
-}
-
-if (! function_exists('database_path')) {
-    function database_path($path = ''): string
-    {
-        return __DIR__."/../../$path";
-    }
-}
-
-$site = (new \Flarum\Testing\integration\Setup\Bootstrapper())->run();
-$site->bootApp();
diff --git a/composer.json b/composer.json
index 226a86c..ead4261 100644
--- a/composer.json
+++ b/composer.json
@@ -1,12 +1,11 @@
 {
     "name": "flarum/phpstan",
     "description": "Flarum PHPStan extension",
-    "minimum-stability": "dev",
+    "minimum-stability": "stable",
     "license": "MIT",
     "require": {
-        "flarum/testing": "^2.0",
-        "phpstan/phpstan": "^1.10.0",
-        "larastan/larastan": "2.9.14"
+        "phpstan/phpstan-php-parser": "^1.0",
+        "phpstan/phpstan": "^1.2"
     },
     "autoload": {
         "psr-4": {
@@ -20,12 +19,7 @@
             ]
         },
         "branch-alias": {
-            "dev-main": "2.x-dev"
+            "dev-master": "1.x-dev"
         }
-    },
-    "repositories": [{
-            "type": "path",
-            "url": "../../*/*"
-        }],
-    "prefer-stable": true
+    }
 }
diff --git a/extension.neon b/extension.neon
index f9ee1d0..7ff7961 100644
--- a/extension.neon
+++ b/extension.neon
@@ -1,45 +1,291 @@
 includes:
-    - larastan-extension.neon
-    - phpstan-baseline.neon
-
+    - vendor/phpstan/phpstan-php-parser/extension.neon
 parameters:
-  stubFiles:
-    - stubs/Illuminate/Contracts/Container/Container.stub
-    - stubs/Illuminate/Queue/ListenerOptions.stub
-    - stubs/Illuminate/Support/ServiceProvider.stub
-    - stubs/Illuminate/Filesystem/Filesystem.stub
-    - stubs/Illuminate/Filesystem/FilesystemManager.stub
-
-    # We're changing the disk return type from Filesystem to Cloud,
-    # rather than hacking every bit of the codebase with a phpdoc @var.
-    - stubs/Illuminate/Contracts/Filesystem/Factory.stub
-    - stubs/Illuminate/Contracts/Filesystem/Cloud.stub
-    - stubs/Illuminate/Contracts/Filesystem/Filesystem.stub
-
-    # We know for a fact the JsonApi object used internally is always the Flarum one.
-    - stubs/Tobyz/JsonApiServer/JsonApi.stub
-    - stubs/Tobyz/JsonApiServer/Context.stub
+    stubFiles:
+        - stubs/Illuminate/Enumerable.stub
+        - stubs/Illuminate/Database/EloquentBuilder.stub
+        - stubs/Illuminate/Collection.stub
+        - stubs/Illuminate/Database/EloquentCollection.stub
+        - stubs/Illuminate/Database/Factory.stub
+        - stubs/Illuminate/Database/Model.stub
+        - stubs/Illuminate/Database/Gate.stub
+        - stubs/Illuminate/Database/Relation.stub
+        - stubs/Illuminate/Database/BelongsTo.stub
+        - stubs/Illuminate/Database/BelongsToMany.stub
+        - stubs/Illuminate/Database/HasOneOrMany.stub
+        - stubs/Illuminate/Database/HasMany.stub
+        - stubs/Illuminate/Database/HasOne.stub
+        - stubs/Illuminate/Database/HasOneThrough.stub
+        - stubs/Illuminate/Database/HasManyThrough.stub
+        - stubs/Illuminate/Database/MorphTo.stub
+        - stubs/Illuminate/Database/MorphToMany.stub
+        - stubs/Illuminate/Database/MorphMany.stub
+        - stubs/Illuminate/Database/MorphOne.stub
+        - stubs/Illuminate/Database/MorphOneOrMany.stub
+        - stubs/Illuminate/HigherOrderProxies.stub
+        - stubs/Illuminate/Database/QueryBuilder.stub
+        - stubs/Illuminate/EnumeratesValues.stub
+        - stubs/Contracts/Support.stub
+    universalObjectCratesClasses:
+        - Illuminate\Http\Request
+    mixinExcludeClasses:
+        - Eloquent
+    earlyTerminatingFunctionCalls:
+        - abort
+        - dd
+    excludePaths:
+        - *.blade.php
+    checkGenericClassInNonGenericObjectType: false
+    checkModelProperties: false
+    databaseMigrationsPath: []
+
+parametersSchema:
+    databaseMigrationsPath: listOf(string())
+    checkModelProperties: bool()
 
 services:
-	-
-		class: Flarum\PHPStan\Relations\ModelRelationsExtension
-		tags:
-			- phpstan.broker.methodsClassReflectionExtension
-			- phpstan.broker.propertiesClassReflectionExtension
-	-
-		class: Flarum\PHPStan\Attributes\ModelDateAttributesExtension
-		tags:
-			- phpstan.broker.propertiesClassReflectionExtension
-	-
-		class: Flarum\PHPStan\Attributes\ModelCastAttributeExtension
-		tags:
-			- phpstan.broker.propertiesClassReflectionExtension
-	-
-		class: Flarum\PHPStan\Extender\FilesProvider
-		arguments:
-			- %paths%
-	-
-		class: Flarum\PHPStan\Extender\Resolver
-		arguments:
-			- @Flarum\PHPStan\Extender\FilesProvider
-			- @defaultAnalysisParser
+        -
+            class: Flarum\PHPStan\Methods\RelationForwardsCallsExtension
+            tags:
+                - phpstan.broker.methodsClassReflectionExtension
+
+        -
+            class: Flarum\PHPStan\Methods\ModelForwardsCallsExtension
+            tags:
+                - phpstan.broker.methodsClassReflectionExtension
+
+        -
+            class: Flarum\PHPStan\Methods\EloquentBuilderForwardsCallsExtension
+            tags:
+                - phpstan.broker.methodsClassReflectionExtension
+
+        -
+            class: Flarum\PHPStan\Methods\HigherOrderTapProxyExtension
+            tags:
+                - phpstan.broker.methodsClassReflectionExtension
+
+        -
+            class: Flarum\PHPStan\Methods\HigherOrderCollectionProxyExtension
+            tags:
+                - phpstan.broker.methodsClassReflectionExtension
+
+        -
+            class: Flarum\PHPStan\Methods\StorageMethodsClassReflectionExtension
+            tags:
+                - phpstan.broker.methodsClassReflectionExtension
+
+        -
+            class: Flarum\PHPStan\Methods\Extension
+            tags:
+                - phpstan.broker.methodsClassReflectionExtension
+        -
+            class: Flarum\PHPStan\Methods\ModelFactoryMethodsClassReflectionExtension
+            tags:
+                - phpstan.broker.methodsClassReflectionExtension
+
+        -
+            class: Flarum\PHPStan\Properties\ModelAccessorExtension
+            tags:
+                - phpstan.broker.propertiesClassReflectionExtension
+
+        -
+            class: Flarum\PHPStan\Properties\ModelPropertyExtension
+            tags:
+                - phpstan.broker.propertiesClassReflectionExtension
+
+        -
+            class: Flarum\PHPStan\Properties\HigherOrderCollectionProxyPropertyExtension
+            tags:
+                - phpstan.broker.propertiesClassReflectionExtension
+
+        -
+            class: Flarum\PHPStan\Types\RelationDynamicMethodReturnTypeExtension
+            tags:
+                - phpstan.broker.dynamicMethodReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\Types\ModelRelationsDynamicMethodReturnTypeExtension
+            tags:
+                - phpstan.broker.dynamicMethodReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\ReturnTypes\HigherOrderTapProxyExtension
+            tags:
+                - phpstan.broker.dynamicMethodReturnTypeExtension
+        -
+            class: Flarum\PHPStan\ReturnTypes\ContainerArrayAccessDynamicMethodReturnTypeExtension
+            tags:
+                - phpstan.broker.dynamicMethodReturnTypeExtension
+            arguments:
+                className: Illuminate\Contracts\Container\Container
+        -
+            class: Flarum\PHPStan\ReturnTypes\ContainerArrayAccessDynamicMethodReturnTypeExtension
+            tags:
+                - phpstan.broker.dynamicMethodReturnTypeExtension
+            arguments:
+                className: Illuminate\Container\Container
+        -
+            class: Flarum\PHPStan\ReturnTypes\ContainerArrayAccessDynamicMethodReturnTypeExtension
+            tags:
+                - phpstan.broker.dynamicMethodReturnTypeExtension
+            arguments:
+                className: Illuminate\Foundation\Application
+        -
+            class: Flarum\PHPStan\ReturnTypes\ContainerArrayAccessDynamicMethodReturnTypeExtension
+            tags:
+                - phpstan.broker.dynamicMethodReturnTypeExtension
+            arguments:
+                className: Illuminate\Contracts\Foundation\Application
+
+        -
+            class: Flarum\PHPStan\Properties\ModelRelationsExtension
+            tags:
+                - phpstan.broker.propertiesClassReflectionExtension
+
+        -
+            class: Flarum\PHPStan\ReturnTypes\ModelFactoryDynamicStaticMethodReturnTypeExtension
+            tags:
+                - phpstan.broker.dynamicStaticMethodReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\ReturnTypes\ModelExtension
+            tags:
+                - phpstan.broker.dynamicStaticMethodReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\ReturnTypes\RequestExtension
+            tags:
+                - phpstan.broker.dynamicMethodReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\ReturnTypes\EloquentBuilderExtension
+            tags:
+                - phpstan.broker.dynamicMethodReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\ReturnTypes\RelationFindExtension
+            tags:
+                - phpstan.broker.dynamicMethodReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\ReturnTypes\RelationCollectionExtension
+            tags:
+                - phpstan.broker.dynamicMethodReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\ReturnTypes\ModelFindExtension
+            tags:
+                - phpstan.broker.dynamicStaticMethodReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\ReturnTypes\BuilderModelFindExtension
+            tags:
+                - phpstan.broker.dynamicMethodReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\ReturnTypes\TestCaseExtension
+            tags:
+                - phpstan.broker.dynamicMethodReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\ReturnTypes\CollectionMakeDynamicStaticMethodReturnTypeExtension
+            tags:
+                - phpstan.broker.dynamicStaticMethodReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\Support\CollectionHelper
+
+        -
+            class: Flarum\PHPStan\ReturnTypes\Helpers\CollectExtension
+            tags:
+                - phpstan.broker.dynamicFunctionReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\ReturnTypes\Helpers\TransExtension
+            tags:
+                - phpstan.broker.dynamicFunctionReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\ReturnTypes\Helpers\ValidatorExtension
+            tags:
+                - phpstan.broker.dynamicFunctionReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\ReturnTypes\CollectionFilterDynamicReturnTypeExtension
+            tags:
+                - phpstan.broker.dynamicMethodReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\Types\AbortIfFunctionTypeSpecifyingExtension
+            tags:
+                - phpstan.typeSpecifier.functionTypeSpecifyingExtension
+            arguments:
+                methodName: 'abort'
+                negate: false
+
+        -
+            class: Flarum\PHPStan\Types\AbortIfFunctionTypeSpecifyingExtension
+            tags:
+                - phpstan.typeSpecifier.functionTypeSpecifyingExtension
+            arguments:
+                methodName: 'abort'
+                negate: true
+
+        -
+            class: Flarum\PHPStan\Types\AbortIfFunctionTypeSpecifyingExtension
+            tags:
+                - phpstan.typeSpecifier.functionTypeSpecifyingExtension
+            arguments:
+                methodName: throw
+                negate: false
+
+        -
+            class: Flarum\PHPStan\Types\AbortIfFunctionTypeSpecifyingExtension
+            tags:
+                - phpstan.typeSpecifier.functionTypeSpecifyingExtension
+            arguments:
+                methodName: throw
+                negate: true
+
+        -
+            class: Flarum\PHPStan\ReturnTypes\Helpers\AppExtension
+            tags:
+                - phpstan.broker.dynamicFunctionReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\ReturnTypes\Helpers\ValueExtension
+            tags:
+                - phpstan.broker.dynamicFunctionReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\ReturnTypes\Helpers\TapExtension
+            tags:
+                - phpstan.broker.dynamicFunctionReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\ReturnTypes\StorageDynamicStaticMethodReturnTypeExtension
+            tags:
+                - phpstan.broker.dynamicStaticMethodReturnTypeExtension
+
+        -
+            class: Flarum\PHPStan\Types\GenericEloquentCollectionTypeNodeResolverExtension
+            tags:
+                - phpstan.phpDoc.typeNodeResolverExtension
+
+        -
+            class: Flarum\PHPStan\Types\ViewStringTypeNodeResolverExtension
+            tags:
+                - phpstan.phpDoc.typeNodeResolverExtension
+        -
+            class: Flarum\PHPStan\Methods\BuilderHelper
+            arguments:
+                checkProperties: %checkModelProperties%
+        -
+            class: Flarum\PHPStan\Properties\MigrationHelper
+            arguments:
+                databaseMigrationPath: %databaseMigrationsPath%
+                parser: @currentPhpVersionSimpleDirectParser
+        -
+            class: Flarum\PHPStan\Types\RelationParserHelper
+            arguments:
+                parser: @currentPhpVersionSimpleDirectParser
diff --git a/larastan-extension.neon b/larastan-extension.neon
deleted file mode 100644
index 55c21e2..0000000
--- a/larastan-extension.neon
+++ /dev/null
@@ -1,559 +0,0 @@
-parameters:
-    universalObjectCratesClasses:
-        - Illuminate\Http\Request
-        - Illuminate\Support\Optional
-    earlyTerminatingFunctionCalls:
-        - abort
-        - dd
-    excludePaths:
-        - *.blade.php
-    mixinExcludeClasses:
-        - Eloquent
-    bootstrapFiles:
-        - bootstrap.php
-    checkOctaneCompatibility: false
-    noEnvCallsOutsideOfConfig: false
-    noModelMake: true
-    noUnnecessaryCollectionCall: true
-    noUnnecessaryCollectionCallOnly: []
-    noUnnecessaryCollectionCallExcept: []
-    squashedMigrationsPath: []
-    databaseMigrationsPath: []
-    disableMigrationScan: false
-    disableSchemaScan: false
-    viewDirectories: []
-    checkModelProperties: false
-    checkPhpDocMissingReturn: false
-    checkUnusedViews: false
-    checkModelAppends: false
-
-parametersSchema:
-    checkOctaneCompatibility: bool()
-    noEnvCallsOutsideOfConfig: bool()
-    noModelMake: bool()
-    noUnnecessaryCollectionCall: bool()
-    noUnnecessaryCollectionCallOnly: listOf(string())
-    noUnnecessaryCollectionCallExcept: listOf(string())
-    databaseMigrationsPath: listOf(string())
-    disableMigrationScan: bool()
-    viewDirectories: listOf(string())
-    squashedMigrationsPath: listOf(string())
-    disableSchemaScan: bool()
-    checkModelProperties: bool()
-    checkUnusedViews: bool()
-    checkModelAppends: bool()
-
-conditionalTags:
-    Larastan\Larastan\Rules\NoEnvCallsOutsideOfConfigRule:
-        phpstan.rules.rule: %noEnvCallsOutsideOfConfig%
-    Larastan\Larastan\Rules\NoModelMakeRule:
-        phpstan.rules.rule: %noModelMake%
-    Larastan\Larastan\Rules\NoUnnecessaryCollectionCallRule:
-        phpstan.rules.rule: %noUnnecessaryCollectionCall%
-    Larastan\Larastan\Rules\OctaneCompatibilityRule:
-        phpstan.rules.rule: %checkOctaneCompatibility%
-    Larastan\Larastan\Rules\UnusedViewsRule:
-        phpstan.rules.rule: %checkUnusedViews%
-    Larastan\Larastan\Rules\ModelAppendsRule:
-        phpstan.rules.rule: %checkModelAppends%
-
-services:
-    -
-        class: Larastan\Larastan\Methods\RelationForwardsCallsExtension
-        tags:
-            - phpstan.broker.methodsClassReflectionExtension
-
-    -
-        class: Larastan\Larastan\Methods\ModelForwardsCallsExtension
-        tags:
-            - phpstan.broker.methodsClassReflectionExtension
-
-    -
-        class: Larastan\Larastan\Methods\EloquentBuilderForwardsCallsExtension
-        tags:
-            - phpstan.broker.methodsClassReflectionExtension
-
-    -
-        class: Larastan\Larastan\Methods\HigherOrderTapProxyExtension
-        tags:
-            - phpstan.broker.methodsClassReflectionExtension
-
-    -
-        class: Larastan\Larastan\Methods\HigherOrderCollectionProxyExtension
-        tags:
-            - phpstan.broker.methodsClassReflectionExtension
-
-    -
-        class: Larastan\Larastan\Methods\StorageMethodsClassReflectionExtension
-        tags:
-            - phpstan.broker.methodsClassReflectionExtension
-
-    -
-        class: Larastan\Larastan\Methods\Extension
-        tags:
-            - phpstan.broker.methodsClassReflectionExtension
-    -
-        class: Larastan\Larastan\Methods\ModelFactoryMethodsClassReflectionExtension
-        tags:
-            - phpstan.broker.methodsClassReflectionExtension
-    -
-        class: Larastan\Larastan\Methods\RedirectResponseMethodsClassReflectionExtension
-        tags:
-            - phpstan.broker.methodsClassReflectionExtension
-    -
-        class: Larastan\Larastan\Methods\MacroMethodsClassReflectionExtension
-        tags:
-            - phpstan.broker.methodsClassReflectionExtension
-    -
-        class: Larastan\Larastan\Methods\ViewWithMethodsClassReflectionExtension
-        tags:
-            - phpstan.broker.methodsClassReflectionExtension
-
-    -
-        class: Larastan\Larastan\Properties\ModelAccessorExtension
-        tags:
-            - phpstan.broker.propertiesClassReflectionExtension
-
-    -
-        class: Larastan\Larastan\Properties\ModelPropertyExtension
-        tags:
-            - phpstan.broker.propertiesClassReflectionExtension
-
-    -
-        class: Larastan\Larastan\Properties\HigherOrderCollectionProxyPropertyExtension
-        tags:
-            - phpstan.broker.propertiesClassReflectionExtension
-
-    -
-        class: Larastan\Larastan\Types\RelationDynamicMethodReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\Types\ModelRelationsDynamicMethodReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\HigherOrderTapProxyExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-    -
-        class: Larastan\Larastan\ReturnTypes\ContainerArrayAccessDynamicMethodReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-        arguments:
-            className: Illuminate\Contracts\Container\Container
-    -
-        class: Larastan\Larastan\ReturnTypes\ContainerArrayAccessDynamicMethodReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-        arguments:
-            className: Illuminate\Container\Container
-    -
-        class: Larastan\Larastan\ReturnTypes\ContainerArrayAccessDynamicMethodReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-        arguments:
-            className: Illuminate\Foundation\Application
-    -
-        class: Larastan\Larastan\ReturnTypes\ContainerArrayAccessDynamicMethodReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-        arguments:
-            className: Illuminate\Contracts\Foundation\Application
-
-    -
-        class: Larastan\Larastan\Properties\ModelRelationsExtension
-        tags:
-            - phpstan.broker.propertiesClassReflectionExtension
-    -
-        class: Larastan\Larastan\ReturnTypes\ModelOnlyDynamicMethodReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\ModelFactoryDynamicStaticMethodReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicStaticMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\ModelDynamicStaticMethodReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicStaticMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\AppMakeDynamicReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicStaticMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\AuthExtension
-        tags:
-            - phpstan.broker.dynamicStaticMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\GuardDynamicStaticMethodReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicStaticMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\AuthManagerExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\DateExtension
-        tags:
-            - phpstan.broker.dynamicStaticMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\GuardExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\RequestFileExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\RequestRouteExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\RequestUserExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\EloquentBuilderExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\RelationCollectionExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\ModelFindExtension
-        tags:
-            - phpstan.broker.dynamicStaticMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\BuilderModelFindExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\TestCaseExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\Support\CollectionHelper
-
-    -
-        class: Larastan\Larastan\ReturnTypes\Helpers\AuthExtension
-        tags:
-            - phpstan.broker.dynamicFunctionReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\Helpers\CollectExtension
-        tags:
-            - phpstan.broker.dynamicFunctionReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\Helpers\NowAndTodayExtension
-        tags:
-            - phpstan.broker.dynamicFunctionReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\Helpers\ResponseExtension
-        tags:
-            - phpstan.broker.dynamicFunctionReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\Helpers\ValidatorExtension
-        tags:
-            - phpstan.broker.dynamicFunctionReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\CollectionFilterRejectDynamicReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-    -
-        class: Larastan\Larastan\ReturnTypes\CollectionWhereNotNullDynamicReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\NewModelQueryDynamicMethodReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\FactoryDynamicMethodReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\Types\AbortIfFunctionTypeSpecifyingExtension
-        tags:
-            - phpstan.typeSpecifier.functionTypeSpecifyingExtension
-        arguments:
-            methodName: 'abort'
-            negate: false
-
-    -
-        class: Larastan\Larastan\Types\AbortIfFunctionTypeSpecifyingExtension
-        tags:
-            - phpstan.typeSpecifier.functionTypeSpecifyingExtension
-        arguments:
-            methodName: 'abort'
-            negate: true
-
-    -
-        class: Larastan\Larastan\Types\AbortIfFunctionTypeSpecifyingExtension
-        tags:
-            - phpstan.typeSpecifier.functionTypeSpecifyingExtension
-        arguments:
-            methodName: throw
-            negate: false
-
-    -
-        class: Larastan\Larastan\Types\AbortIfFunctionTypeSpecifyingExtension
-        tags:
-            - phpstan.typeSpecifier.functionTypeSpecifyingExtension
-        arguments:
-            methodName: throw
-            negate: true
-
-    -
-        class: Larastan\Larastan\ReturnTypes\Helpers\AppExtension
-        tags:
-            - phpstan.broker.dynamicFunctionReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\Helpers\ValueExtension
-        tags:
-            - phpstan.broker.dynamicFunctionReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\Helpers\StrExtension
-        tags:
-            - phpstan.broker.dynamicFunctionReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\Helpers\TapExtension
-        tags:
-            - phpstan.broker.dynamicFunctionReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\StorageDynamicStaticMethodReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicStaticMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\Types\GenericEloquentCollectionTypeNodeResolverExtension
-        tags:
-            - phpstan.phpDoc.typeNodeResolverExtension
-
-    -
-        class: Larastan\Larastan\Types\ViewStringTypeNodeResolverExtension
-        tags:
-            - phpstan.phpDoc.typeNodeResolverExtension
-
-    -
-        class: Larastan\Larastan\Rules\OctaneCompatibilityRule
-
-    -
-        class: Larastan\Larastan\Rules\NoEnvCallsOutsideOfConfigRule
-
-    -
-        class: Larastan\Larastan\Rules\NoModelMakeRule
-
-    -
-        class: Larastan\Larastan\Rules\NoUnnecessaryCollectionCallRule
-        arguments:
-            onlyMethods: %noUnnecessaryCollectionCallOnly%
-            excludeMethods: %noUnnecessaryCollectionCallExcept%
-
-    -
-        class: Larastan\Larastan\Rules\ModelAppendsRule
-
-    -
-        class: Larastan\Larastan\Types\GenericEloquentBuilderTypeNodeResolverExtension
-        tags:
-            - phpstan.phpDoc.typeNodeResolverExtension
-    -
-        class: Larastan\Larastan\ReturnTypes\AppEnvironmentReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\Types\ModelProperty\ModelPropertyTypeNodeResolverExtension
-        tags:
-            - phpstan.phpDoc.typeNodeResolverExtension
-        arguments:
-            active: %checkModelProperties%
-
-    -
-        class: Larastan\Larastan\Types\RelationParserHelper
-        arguments:
-            parser: @currentPhpVersionSimpleDirectParser
-
-    -
-        class: Larastan\Larastan\Properties\MigrationHelper
-        arguments:
-            databaseMigrationPath: %databaseMigrationsPath%
-            disableMigrationScan: %disableMigrationScan%
-            parser: @currentPhpVersionSimpleDirectParser
-            reflectionProvider: @reflectionProvider
-
-    -
-        class: Larastan\Larastan\Properties\SquashedMigrationHelper
-        arguments:
-            schemaPaths: %squashedMigrationsPath%
-            disableSchemaScan: %disableSchemaScan%
-
-    -
-        class: Larastan\Larastan\Properties\ModelCastHelper
-
-    -
-        class: Larastan\Larastan\Properties\ModelPropertyHelper
-
-    -
-        class: Larastan\Larastan\Rules\ModelRuleHelper
-
-    -
-        class: Larastan\Larastan\Methods\BuilderHelper
-        arguments:
-            checkProperties: %checkModelProperties%
-
-    -
-        class: Larastan\Larastan\Rules\RelationExistenceRule
-        tags:
-            - phpstan.rules.rule
-
-    -
-        class: Larastan\Larastan\Rules\CheckDispatchArgumentTypesCompatibleWithClassConstructorRule
-        arguments:
-            dispatchableClass: Illuminate\Foundation\Bus\Dispatchable
-        tags:
-            - phpstan.rules.rule
-    -
-        class: Larastan\Larastan\Rules\CheckDispatchArgumentTypesCompatibleWithClassConstructorRule
-        arguments:
-            dispatchableClass: Illuminate\Foundation\Events\Dispatchable
-        tags:
-            - phpstan.rules.rule
-
-    -
-        class: Larastan\Larastan\Properties\Schema\PhpMyAdminDataTypeToPhpTypeConverter
-
-    -
-        class: Larastan\Larastan\LarastanStubFilesExtension
-        tags:
-            - phpstan.stubFilesExtension
-
-    -
-        class: Larastan\Larastan\Rules\UnusedViewsRule
-
-    -
-        class: Larastan\Larastan\Collectors\UsedViewFunctionCollector
-        tags:
-            - phpstan.collector
-
-    -
-        class: Larastan\Larastan\Collectors\UsedEmailViewCollector
-        tags:
-            - phpstan.collector
-
-    -
-        class: Larastan\Larastan\Collectors\UsedViewMakeCollector
-        tags:
-            - phpstan.collector
-
-    -
-        class: Larastan\Larastan\Collectors\UsedViewFacadeMakeCollector
-        tags:
-            - phpstan.collector
-
-    -
-        class: Larastan\Larastan\Collectors\UsedRouteFacadeViewCollector
-        tags:
-            - phpstan.collector
-    -
-        class: Larastan\Larastan\Collectors\UsedViewInAnotherViewCollector
-        arguments:
-            parser: @currentPhpVersionSimpleDirectParser
-    -
-        class: Larastan\Larastan\Support\ViewFileHelper
-        arguments:
-            viewDirectories: %viewDirectories%
-
-#    -
-#        class: Larastan\Larastan\ReturnTypes\ApplicationMakeDynamicReturnTypeExtension
-#        tags:
-#            - phpstan.broker.dynamicMethodReturnTypeExtension
-#
-#    -
-#        class: Larastan\Larastan\ReturnTypes\ContainerMakeDynamicReturnTypeExtension
-#        tags:
-#            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\ConsoleCommand\ArgumentDynamicReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\ConsoleCommand\HasArgumentDynamicReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\ConsoleCommand\OptionDynamicReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\ConsoleCommand\HasOptionDynamicReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\TranslatorGetReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicMethodReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\TransHelperReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicFunctionReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\DoubleUnderscoreHelperReturnTypeExtension
-        tags:
-            - phpstan.broker.dynamicFunctionReturnTypeExtension
-
-    -
-        class: Larastan\Larastan\ReturnTypes\AppMakeHelper
-
-    -
-        class: Larastan\Larastan\Internal\ConsoleApplicationResolver
-
-    -
-        class: Larastan\Larastan\Internal\ConsoleApplicationHelper
-
-    -
-        class: Larastan\Larastan\Support\HigherOrderCollectionProxyHelper
-
-rules:
-    - Larastan\Larastan\Rules\UselessConstructs\NoUselessWithFunctionCallsRule
-    - Larastan\Larastan\Rules\UselessConstructs\NoUselessValueFunctionCallsRule
-    - Larastan\Larastan\Rules\DeferrableServiceProviderMissingProvidesRule
-    - Larastan\Larastan\Rules\ConsoleCommand\UndefinedArgumentOrOptionRule
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
deleted file mode 100644
index b95ae01..0000000
--- a/phpstan-baseline.neon
+++ /dev/null
@@ -1,54 +0,0 @@
-parameters:
-	reportUnmatchedIgnoredErrors: false
-	ignoreErrors:
-		-
-			identifier: missingType.iterableValue
-		-
-			identifier: missingType.generics
-
-		# Remove this group below with larastan 2.0 (i.e Flarum 2.0)
-		- message: "#Relation '[A-z_-]+' is not found in [A-z\_]+ model.#"
-		  reportUnmatched: false
-		- message: '#^Parameter \#1 \$query of method [A-z_<>\\]+\:\:union\(\) expects [A-z_<> .,|\\]+ given\.$#'
-		  reportUnmatched: false
-		- message: '#^Parameter \#1 \$query of method [A-z_<>\\]+\:\:joinSub\(\) expects [A-z_<> .,|\\]+ given\.$#'
-		  reportUnmatched: false
-
-		#	We ignore this because resolve can either take a class name as the generic return type or just a binding name.
-		- message: "#Template type T of function resolve[\\(\\)]{2} is not referenced in a parameter.#"
-		  reportUnmatched: false
-
-		#	We ignore new static errors because we want extensibility.
-		#	@TODO: needs discussion.
-		- message: "#^Unsafe usage of new static[\\(\\)]{2}.$#"
-		  reportUnmatched: false
-		- message: "#^Static access to instance property .*#"
-		  reportUnmatched: false
-
-		# ConnectionInterface lacks methods that exist in the implementation,
-		# yet we don't want to inject the implementation.
-		- message: '#^Call to an undefined method Illuminate\\Database\\ConnectionInterface\:\:[A-z0-9_]+\(\)\.$#'
-		  reportUnmatched: false
-
-		# By default when a callable parameter is typed with for example 3 parameters,
-		# and the implementation only accepts two of them, PHPStan complains.
-		# At the time of adding this error, there are no configuration values to make it
-		# ignore this error, so we have to ignore it globally.
-		- message: '#^Parameter \#[0-9]+ \$[A-z0-9_]+ of method Flarum\Extend\[A-z0-9_:\\\\(\\)]+ expects \(?callable\([A-z0-9_,|\\: \\(\\)-]+\)\)?, (callable|Closure)\([A-z0-9_,|\\: \\(\\)-]+\) given\.$#'
-		  reportUnmatched: false
-
-		# PHPStan suddenly doesn't recognize callables can be function names?
-		- message: '#^Parameter \#[0-9]+ \$[A-z0-9_]+ of function [A-z0-9_:\\\\(\\)]+ expects \(?callable\([A-z0-9_,|\\: \\(\\)-]+, ''[A-z0-9_:\\\\(\\)]+'' given\.$#'
-		  reportUnmatched: false
-
-		# Not if we're using our own static make method.
-		- message: '#^Called ''Model\:\:make\(\)'' which performs unnecessary work, use ''new Model\(\)''\.$#'
-
-		# This assumes that the phpdoc telling it it's not nullable is correct, that's not the case for internal Laravel typings.
-		- message: '#^Property [A-z0-9-_:$,\\]+ \([A-z]+\) on left side of \?\? is not nullable\.$#'
-
-		# Ignore overridden classes from packages so that it's always easier to keep track of what's being overridden.
-		- message: '#^Method Flarum\\Api\\Serializer\:\:[A-z0-9_]+\(\) has parameter \$[A-z0-9_]+ with no type specified\.$#'
-		- message: '#^Method Flarum\\Api\\Endpoint\\[A-z0-9_]+\:\:[A-z0-9_]+\(\) has parameter \$[A-z0-9_]+ with no type specified\.$#'
-
-		- message: '#^Unable to resolve the template type TRelatedModel in call to method Illuminate\\Database\\Eloquent\\Model\:\:[A-z]+\(\)$#'
diff --git a/src/Attributes/ModelCastAttributeExtension.php b/src/Attributes/ModelCastAttributeExtension.php
deleted file mode 100644
index 49c3c8b..0000000
--- a/src/Attributes/ModelCastAttributeExtension.php
+++ /dev/null
@@ -1,87 +0,0 @@
-<?php
-
-/*
- * This file is part of Flarum.
- *
- * For detailed copyright and license information, please view the
- * LICENSE file that was distributed with this source code.
- */
-
-namespace Flarum\PHPStan\Attributes;
-
-use Carbon\Carbon;
-use Flarum\PHPStan\Extender\MethodCall;
-use Flarum\PHPStan\Extender\Resolver;
-use PHPStan\PhpDoc\TypeStringResolver;
-use PHPStan\Reflection\ClassReflection;
-use PHPStan\Reflection\PropertiesClassReflectionExtension;
-use PHPStan\Reflection\PropertyReflection;
-use PHPStan\Type\NullType;
-use PHPStan\Type\ObjectType;
-use PHPStan\Type\UnionType;
-
-class ModelCastAttributeExtension implements PropertiesClassReflectionExtension
-{
-    /** @var Resolver */
-    private $extendersResolver;
-    /** @var TypeStringResolver */
-    private $typeStringResolver;
-
-    public function __construct(Resolver $extendersResolver, TypeStringResolver $typeStringResolver)
-    {
-        $this->extendersResolver = $extendersResolver;
-        $this->typeStringResolver = $typeStringResolver;
-    }
-
-    public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
-    {
-        return $this->findCastAttributeMethod($classReflection, $propertyName) !== null;
-    }
-
-    public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
-    {
-        return $this->resolveCastAttributeProperty($this->findCastAttributeMethod($classReflection, $propertyName), $classReflection);
-    }
-
-    private function findCastAttributeMethod(ClassReflection $classReflection, string $propertyName): ?MethodCall
-    {
-        foreach ($this->extendersResolver->getExtenders() as $extender) {
-            if (! $extender->isExtender('Model')) {
-                continue;
-            }
-
-            foreach (array_merge([$classReflection->getName()], $classReflection->getParentClassesNames()) as $className) {
-                if ($className === 'Flarum\Database\AbstractModel') {
-                    break;
-                }
-
-                if ($extender->extends($className)) {
-                    if ($methodCalls = $extender->findMethodCalls('cast')) {
-                        foreach ($methodCalls as $methodCall) {
-                            if ($methodCall->arguments[0]->value === $propertyName) {
-                                return $methodCall;
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
-        return null;
-    }
-
-    private function resolveCastAttributeProperty(MethodCall $methodCall, ClassReflection $classReflection): PropertyReflection
-    {
-        $typeName = $methodCall->arguments[1]->value;
-        $type = $this->typeStringResolver->resolve("$typeName|null");
-
-        if (str_contains($typeName, 'date') || $typeName === 'timestamp') {
-            $type = new UnionType([
-                new ObjectType(Carbon::class),
-                new NullType(),
-            ]);
-        }
-
-        return new AttributeProperty($classReflection, $type);
-    }
-}
diff --git a/src/Attributes/ModelDateAttributesExtension.php b/src/Attributes/ModelDateAttributesExtension.php
deleted file mode 100644
index 4699366..0000000
--- a/src/Attributes/ModelDateAttributesExtension.php
+++ /dev/null
@@ -1,76 +0,0 @@
-<?php
-
-/*
- * This file is part of Flarum.
- *
- * For detailed copyright and license information, please view the
- * LICENSE file that was distributed with this source code.
- */
-
-namespace Flarum\PHPStan\Attributes;
-
-use Carbon\Carbon;
-use Flarum\PHPStan\Extender\MethodCall;
-use Flarum\PHPStan\Extender\Resolver;
-use PHPStan\Reflection\ClassReflection;
-use PHPStan\Reflection\PropertiesClassReflectionExtension;
-use PHPStan\Reflection\PropertyReflection;
-use PHPStan\Type\NullType;
-use PHPStan\Type\ObjectType;
-use PHPStan\Type\UnionType;
-
-class ModelDateAttributesExtension implements PropertiesClassReflectionExtension
-{
-    /** @var Resolver */
-    private $extendersResolver;
-
-    public function __construct(Resolver $extendersResolver)
-    {
-        $this->extendersResolver = $extendersResolver;
-    }
-
-    public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
-    {
-        return $this->findDateAttributeMethod($classReflection, $propertyName) !== null;
-    }
-
-    public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
-    {
-        return $this->resolveDateAttributeProperty($this->findDateAttributeMethod($classReflection, $propertyName), $classReflection);
-    }
-
-    private function findDateAttributeMethod(ClassReflection $classReflection, string $propertyName): ?MethodCall
-    {
-        foreach ($this->extendersResolver->getExtenders() as $extender) {
-            if (! $extender->isExtender('Model')) {
-                continue;
-            }
-
-            foreach (array_merge([$classReflection->getName()], $classReflection->getParentClassesNames()) as $className) {
-                if ($className === 'Flarum\Database\AbstractModel') {
-                    break;
-                }
-
-                if ($extender->extends($className)) {
-                    if ($methodCalls = $extender->findMethodCalls('dateAttribute')) {
-                        foreach ($methodCalls as $methodCall) {
-                            if ($methodCall->arguments[0]->value === $propertyName) {
-                                return $methodCall;
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
-        return null;
-    }
-
-    private function resolveDateAttributeProperty(MethodCall $methodCall, ClassReflection $classReflection): PropertyReflection
-    {
-        return new AttributeProperty($classReflection, new UnionType([
-            new ObjectType(Carbon::class),
-            new NullType(),
-        ]));
-    }
-}
diff --git a/src/Concerns/HasContainer.php b/src/Concerns/HasContainer.php
new file mode 100644
index 0000000..00ded5a
--- /dev/null
+++ b/src/Concerns/HasContainer.php
@@ -0,0 +1,65 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Concerns;
+
+use Illuminate\Container\Container;
+use Illuminate\Contracts\Container\BindingResolutionException;
+use Illuminate\Contracts\Container\Container as ContainerContract;
+use Psr\Container\NotFoundExceptionInterface;
+use ReflectionException;
+
+/**
+ * @internal
+ */
+trait HasContainer
+{
+    /**
+     * @var ?\Illuminate\Contracts\Container\Container
+     */
+    protected $container;
+
+    /**
+     * @param  \Illuminate\Contracts\Container\Container  $container
+     * @return void
+     */
+    public function setContainer(ContainerContract $container): void
+    {
+        $this->container = $container;
+    }
+
+    /**
+     * Returns the current broker.
+     *
+     * @return \Illuminate\Contracts\Container\Container
+     */
+    public function getContainer(): ContainerContract
+    {
+        return $this->container ?? Container::getInstance();
+    }
+
+    /**
+     * Resolve the given type from the container.
+     *
+     * @param  string  $abstract
+     * @return mixed
+     */
+    public function resolve(string $abstract)
+    {
+        $concrete = null;
+
+        try {
+            $concrete = $this->getContainer()
+                ->make($abstract);
+        } catch (ReflectionException $exception) {
+            // ..
+        } catch (BindingResolutionException $exception) {
+            // ..
+        } catch (NotFoundExceptionInterface $exception) {
+            // ..
+        }
+
+        return $concrete;
+    }
+}
diff --git a/src/Concerns/LoadsAuthModel.php b/src/Concerns/LoadsAuthModel.php
new file mode 100644
index 0000000..3e1401e
--- /dev/null
+++ b/src/Concerns/LoadsAuthModel.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Concerns;
+
+use Illuminate\Config\Repository as ConfigRepository;
+
+trait LoadsAuthModel
+{
+    /** @phpstan-return class-string|null */
+    private function getAuthModel(ConfigRepository $config, ?string $guard = null): ?string
+    {
+        if (
+            ($guard === null && ! ($guard = $config->get('auth.defaults.guard'))) ||
+            ! ($provider = $config->get('auth.guards.'.$guard.'.provider')) ||
+            ! ($authModel = $config->get('auth.providers.'.$provider.'.model'))
+        ) {
+            return null;
+        }
+
+        return $authModel;
+    }
+}
diff --git a/src/Contracts/Methods/PassableContract.php b/src/Contracts/Methods/PassableContract.php
new file mode 100644
index 0000000..3ff5a9a
--- /dev/null
+++ b/src/Contracts/Methods/PassableContract.php
@@ -0,0 +1,97 @@
+<?php
+/*
+ * This file is part of PhpStorm.
+ *
+ * (c) Nuno Maduro <enunomaduro@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Flarum\PHPStan\Contracts\Methods;
+
+use Illuminate\Contracts\Container\Container as ContainerContract;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\Php\PhpMethodReflectionFactory;
+use PHPStan\Reflection\ReflectionProvider;
+
+/**
+ * @internal
+ */
+interface PassableContract
+{
+    /**
+     * @param  \Illuminate\Contracts\Container\Container  $container
+     * @return void
+     */
+    public function setContainer(ContainerContract $container): void;
+
+    /**
+     * @return \PHPStan\Reflection\ClassReflection
+     */
+    public function getClassReflection(): ClassReflection;
+
+    /**
+     * @param  \PHPStan\Reflection\ClassReflection  $classReflection
+     * @return PassableContract
+     */
+    public function setClassReflection(ClassReflection $classReflection): PassableContract;
+
+    /**
+     * @return string
+     */
+    public function getMethodName(): string;
+
+    /**
+     * @return bool
+     */
+    public function hasFound(): bool;
+
+    /**
+     * @param  string  $class
+     * @return bool
+     */
+    public function searchOn(string $class): bool;
+
+    /**
+     * @return \PHPStan\Reflection\MethodReflection
+     *
+     * @throws \LogicException
+     */
+    public function getMethodReflection(): MethodReflection;
+
+    /**
+     * @param  \PHPStan\Reflection\MethodReflection  $methodReflection
+     */
+    public function setMethodReflection(MethodReflection $methodReflection): void;
+
+    /**
+     * Declares that the provided method can be called statically.
+     *
+     * @param  bool  $staticAllowed
+     * @return void
+     */
+    public function setStaticAllowed(bool $staticAllowed): void;
+
+    /**
+     * Returns whether the method can be called statically.
+     *
+     * @return bool
+     */
+    public function isStaticAllowed(): bool;
+
+    /**
+     * @param  class-string  $class
+     * @param  bool  $staticAllowed
+     * @return bool
+     */
+    public function sendToPipeline(string $class, $staticAllowed = false): bool;
+
+    public function getReflectionProvider(): ReflectionProvider;
+
+    /**
+     * @return \PHPStan\Reflection\Php\PhpMethodReflectionFactory
+     */
+    public function getMethodReflectionFactory(): PhpMethodReflectionFactory;
+}
diff --git a/src/Contracts/Methods/Pipes/PipeContract.php b/src/Contracts/Methods/Pipes/PipeContract.php
new file mode 100644
index 0000000..3685721
--- /dev/null
+++ b/src/Contracts/Methods/Pipes/PipeContract.php
@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Contracts\Methods\Pipes;
+
+use Closure;
+use Flarum\PHPStan\Contracts\Methods\PassableContract;
+
+/**
+ * @internal
+ */
+interface PipeContract
+{
+    /**
+     * @param  \Flarum\PHPStan\Contracts\Methods\PassableContract  $passable
+     * @param  \Closure  $next
+     * @return void
+     */
+    public function handle(PassableContract $passable, Closure $next): void;
+}
diff --git a/src/Contracts/Types/PassableContract.php b/src/Contracts/Types/PassableContract.php
new file mode 100644
index 0000000..bc0cc8f
--- /dev/null
+++ b/src/Contracts/Types/PassableContract.php
@@ -0,0 +1,30 @@
+<?php
+/*
+ * This file is part of PhpStorm.
+ *
+ * (c) Nuno Maduro <enunomaduro@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Flarum\PHPStan\Contracts\Types;
+
+use PHPStan\Type\Type;
+
+/**
+ * @internal
+ */
+interface PassableContract
+{
+    /**
+     * @return \PHPStan\Type\Type
+     */
+    public function getType(): Type;
+
+    /**
+     * @param  \PHPStan\Type\Type  $type
+     * @return void
+     */
+    public function setType(Type $type): void;
+}
diff --git a/src/Contracts/Types/Pipes/PipeContract.php b/src/Contracts/Types/Pipes/PipeContract.php
new file mode 100644
index 0000000..f0b13aa
--- /dev/null
+++ b/src/Contracts/Types/Pipes/PipeContract.php
@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Contracts\Types\Pipes;
+
+use Closure;
+use Flarum\PHPStan\Contracts\Types\PassableContract;
+
+/**
+ * @internal
+ */
+interface PipeContract
+{
+    /**
+     * @param  \Flarum\PHPStan\Contracts\Types\PassableContract  $passable
+     * @param  \Closure  $next
+     * @return void
+     */
+    public function handle(PassableContract $passable, Closure $next): void;
+}
diff --git a/src/Extender/Extender.php b/src/Extender/Extender.php
deleted file mode 100644
index 1ef6ab4..0000000
--- a/src/Extender/Extender.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-
-/*
- * This file is part of Flarum.
- *
- * For detailed copyright and license information, please view the
- * LICENSE file that was distributed with this source code.
- */
-
-namespace Flarum\PHPStan\Extender;
-
-use PhpParser\Node\Expr;
-use PhpParser\Node\Scalar;
-
-class Extender
-{
-    /** @var string */
-    public $qualifiedClassName;
-    /** @var Expr[] */
-    public $constructorArguments;
-    /** @var MethodCall[] */
-    public $methodCalls;
-
-    public function __construct(string $qualifiedClassName, array $constructorArguments = [], array $methodCalls = [])
-    {
-        $this->qualifiedClassName = $qualifiedClassName;
-        $this->constructorArguments = $constructorArguments;
-        $this->methodCalls = $methodCalls;
-    }
-
-    public function isExtender(string $className): bool
-    {
-        return $this->qualifiedClassName === "Flarum\\Extend\\$className";
-    }
-
-    public function extends(...$args): bool
-    {
-        foreach ($this->constructorArguments as $index => $constructorArgument) {
-            $string = null;
-
-            switch ($constructorArgument::class) {
-                case Expr\ClassConstFetch::class:
-                    $string = $constructorArgument->class->toString();
-                    break;
-                case Scalar\String_::class:
-                    $string = $constructorArgument->value;
-                    break;
-                default:
-                    $string = $constructorArgument;
-            }
-
-            if ($string !== $args[$index]) {
-                return false;
-            }
-        }
-
-        return true;
-    }
-
-    /** @return MethodCall[] */
-    public function findMethodCalls(string ...$methods): array
-    {
-        $methodCalls = [];
-
-        foreach ($this->methodCalls as $methodCall) {
-            if (in_array($methodCall->methodName, $methods)) {
-                $methodCalls[] = $methodCall;
-            }
-        }
-
-        return $methodCalls;
-    }
-}
diff --git a/src/Extender/FilesProvider.php b/src/Extender/FilesProvider.php
deleted file mode 100644
index 4b7b4e5..0000000
--- a/src/Extender/FilesProvider.php
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php
-
-/*
- * This file is part of Flarum.
- *
- * For detailed copyright and license information, please view the
- * LICENSE file that was distributed with this source code.
- */
-
-namespace Flarum\PHPStan\Extender;
-
-class FilesProvider
-{
-    /** @var string[] */
-    private $cachedExtenderFiles;
-    /** @var string[] */
-    private $paths;
-
-    public function __construct(array $paths)
-    {
-        $this->paths = $paths;
-    }
-
-    public function getExtenderFiles(): array
-    {
-        if ($this->cachedExtenderFiles === null) {
-            $this->cachedExtenderFiles = $this->findExtenderFiles();
-        }
-
-        return $this->cachedExtenderFiles;
-    }
-
-    private function findExtenderFiles(): array
-    {
-        $extenderFiles = [];
-
-        foreach ($this->paths as $path) {
-            if (str_contains($path, 'extend.php') && file_exists($path)) {
-                $extenderFiles[] = $path;
-            }
-        }
-
-        return $extenderFiles;
-    }
-}
diff --git a/src/Extender/MethodCall.php b/src/Extender/MethodCall.php
deleted file mode 100644
index 0d2f196..0000000
--- a/src/Extender/MethodCall.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-/*
- * This file is part of Flarum.
- *
- * For detailed copyright and license information, please view the
- * LICENSE file that was distributed with this source code.
- */
-
-namespace Flarum\PHPStan\Extender;
-
-class MethodCall
-{
-    /** @var string */
-    public $methodName;
-    /** @var array */
-    public $arguments;
-
-    public function __construct(string $methodName, array $arguments = [])
-    {
-        $this->methodName = $methodName;
-        $this->arguments = $arguments;
-    }
-}
diff --git a/src/Extender/Resolver.php b/src/Extender/Resolver.php
deleted file mode 100644
index 7042883..0000000
--- a/src/Extender/Resolver.php
+++ /dev/null
@@ -1,155 +0,0 @@
-<?php
-
-/*
- * This file is part of Flarum.
- *
- * For detailed copyright and license information, please view the
- * LICENSE file that was distributed with this source code.
- */
-
-namespace Flarum\PHPStan\Extender;
-
-use Flarum\PHPStan\Extender\MethodCall as ExtenderMethodCall;
-use PhpParser\Node\Arg;
-use PhpParser\Node\Expr\Array_;
-use PhpParser\Node\Expr\MethodCall;
-use PhpParser\Node\Expr\New_;
-use PhpParser\Node\Stmt\Namespace_;
-use PhpParser\Node\Stmt\Return_;
-use PHPStan\Parser\Parser;
-use PHPStan\Parser\ParserErrorsException;
-
-class Resolver
-{
-    /** @var Extender[] */
-    private $cachedExtenders = [];
-    /** @var FilesProvider */
-    private $extenderFilesProvider;
-    /** @var Parser */
-    private $parser;
-
-    public function __construct(FilesProvider $extenderFilesProvider, Parser $parser)
-    {
-        $this->extenderFilesProvider = $extenderFilesProvider;
-        $this->parser = $parser;
-    }
-
-    public function getExtenders(): array
-    {
-        if ($this->cachedExtenders) {
-            return $this->cachedExtenders;
-        }
-
-        return $this->cachedExtenders = $this->resolveExtenders();
-    }
-
-    public function getExtendersFor(string $extenderClass, ...$args): array
-    {
-        $extenders = [];
-
-        foreach ($this->getExtenders() as $extender) {
-            if ($extender->isExtender($extenderClass)) {
-                $extenders[] = $extender;
-            }
-        }
-
-        return $extenders;
-    }
-
-    private function resolveExtenders(): array
-    {
-        $extenders = [];
-
-        foreach ($this->extenderFilesProvider->getExtenderFiles() as $extenderFile) {
-            $extenders = array_merge($extenders, $this->resolveExtendersFromFile($extenderFile));
-        }
-
-        return $extenders;
-    }
-
-    /**
-     * Retrieves all extenders from a given `extend.php` file.
-     *
-     * @return Extender[]
-     * @throws ParserErrorsException
-     * @throws \Exception
-     */
-    private function resolveExtendersFromFile($extenderFile): array
-    {
-        /** @var Extender[] $extenders */
-        $extenders = [];
-
-        $statements = $this->parser->parseFile($extenderFile);
-
-        if ($statements[0] instanceof Namespace_) {
-            $statements = $statements[0]->stmts;
-        }
-
-        foreach ($statements as $statement) {
-            if ($statement instanceof Return_) {
-                $expression = $statement->expr;
-
-                if ($expression instanceof Array_) {
-                    foreach ($expression->items as $item) {
-                        if ($item->value instanceof MethodCall) {
-                            // Conditional extenders
-                            if ($item->value->name->toString() === 'whenExtensionEnabled') {
-                                $conditionalExtenders = $item->value->args[1] ?? null;
-
-                                if ($conditionalExtenders->value instanceof Array_) {
-                                    foreach ($conditionalExtenders->value->items as $conditionalExtender) {
-                                        if ($conditionalExtender->value instanceof MethodCall) {
-                                            $extenders[] = $this->resolveExtender($conditionalExtender->value);
-                                        }
-                                    }
-                                }
-                            }
-                            // Normal extenders
-                            else {
-                                $extenders[] = $this->resolveExtender($item->value);
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
-        return $extenders;
-    }
-
-    private function resolveExtenderNew(New_ $var, array $methodCalls = []): Extender
-    {
-        return new Extender($var->class->toString(), array_map(function (Arg $arg) {
-            $arg->value->setAttributes([]);
-
-            return $arg->value;
-        }, $var->args), $methodCalls);
-    }
-
-    private function resolveMethod(MethodCall $var): ExtenderMethodCall
-    {
-        return new ExtenderMethodCall($var->name->toString(), array_map(function (Arg $arg) {
-            $arg->value->setAttributes([]);
-
-            return $arg->value;
-        }, $var->args));
-    }
-
-    private function resolveExtender(MethodCall $value): Extender
-    {
-        $methodStack = [$this->resolveMethod($value)];
-
-        while ($value->var instanceof MethodCall) {
-            $methodStack[] = $this->resolveMethod($value->var);
-            $value = $value->var;
-        }
-
-        $methodStack = array_reverse($methodStack);
-
-        if (! $value->var instanceof New_) {
-            throw new \Exception('Unable to resolve extender for '.$value->var::class);
-        }
-
-        return $this->resolveExtenderNew($value->var, $methodStack);
-    }
-}
diff --git a/src/Methods/BuilderHelper.php b/src/Methods/BuilderHelper.php
new file mode 100644
index 0000000..ec84272
--- /dev/null
+++ b/src/Methods/BuilderHelper.php
@@ -0,0 +1,231 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Methods;
+
+use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
+use Illuminate\Database\Query\Builder as QueryBuilder;
+use Illuminate\Support\Str;
+use Flarum\PHPStan\Reflection\AnnotationScopeMethodParameterReflection;
+use Flarum\PHPStan\Reflection\AnnotationScopeMethodReflection;
+use Flarum\PHPStan\Reflection\DynamicWhereParameterReflection;
+use Flarum\PHPStan\Reflection\EloquentBuilderMethodReflection;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\MissingMethodFromReflectionException;
+use PHPStan\Reflection\ParametersAcceptorSelector;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\ShouldNotHappenException;
+use PHPStan\TrinaryLogic;
+use PHPStan\Type\Generic\GenericObjectType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+use PHPStan\Type\VerbosityLevel;
+
+class BuilderHelper
+{
+    /** @var string[] */
+    public const MODEL_RETRIEVAL_METHODS = ['first', 'find', 'findMany', 'findOrFail', 'firstOrFail', 'sole'];
+
+    /** @var string[] */
+    public const MODEL_CREATION_METHODS = ['make', 'create', 'forceCreate', 'findOrNew', 'firstOrNew', 'updateOrCreate', 'firstOrCreate'];
+
+    /**
+     * The methods that should be returned from query builder.
+     *
+     * @var string[]
+     */
+    public $passthru = [
+        'average', 'avg',
+        'count',
+        'dd', 'dump',
+        'doesntExist', 'exists',
+        'getBindings', 'getConnection', 'getGrammar',
+        'insert', 'insertGetId', 'insertOrIgnore', 'insertUsing',
+        'max', 'min',
+        'raw',
+        'sum',
+        'toSql',
+    ];
+
+    /** @var ReflectionProvider */
+    private $reflectionProvider;
+
+    /** @var bool */
+    private $checkProperties;
+
+    public function __construct(ReflectionProvider $reflectionProvider, bool $checkProperties)
+    {
+        $this->reflectionProvider = $reflectionProvider;
+        $this->checkProperties = $checkProperties;
+    }
+
+    public function dynamicWhere(
+        string $methodName,
+        Type $returnObject
+    ): ?EloquentBuilderMethodReflection {
+        if (! Str::startsWith($methodName, 'where')) {
+            return null;
+        }
+
+        if ($returnObject instanceof GenericObjectType && $this->checkProperties) {
+            $returnClassReflection = $returnObject->getClassReflection();
+
+            if ($returnClassReflection !== null) {
+                $modelType = $returnClassReflection->getActiveTemplateTypeMap()->getType('TModelClass');
+
+                if ($modelType === null) {
+                    $modelType = $returnClassReflection->getActiveTemplateTypeMap()->getType('TRelatedModel');
+                }
+
+                if ($modelType !== null) {
+                    $finder = substr($methodName, 5);
+
+                    $segments = preg_split(
+                        '/(And|Or)(?=[A-Z])/', $finder, -1, PREG_SPLIT_DELIM_CAPTURE
+                    );
+
+                    if ($segments !== false) {
+                        $trinaryLogic = TrinaryLogic::createYes();
+
+                        foreach ($segments as $segment) {
+                            if ($segment !== 'And' && $segment !== 'Or') {
+                                $trinaryLogic = $trinaryLogic->and($modelType->hasProperty(Str::snake($segment)));
+                            }
+                        }
+
+                        if (! $trinaryLogic->yes()) {
+                            return null;
+                        }
+                    }
+                }
+            }
+        }
+
+        $classReflection = $this->reflectionProvider->getClass(QueryBuilder::class);
+
+        $methodReflection = $classReflection->getNativeMethod('dynamicWhere');
+
+        return new EloquentBuilderMethodReflection(
+            $methodName,
+            $classReflection,
+            $methodReflection,
+            [new DynamicWhereParameterReflection],
+            $returnObject,
+            true
+        );
+    }
+
+    /**
+     * This method mimics the `EloquentBuilder::__call` method.
+     * Does not handle the case where $methodName exists in `EloquentBuilder`,
+     * that should be checked by caller before calling this method.
+     *
+     * @param  ClassReflection  $eloquentBuilder  Can be `EloquentBuilder` or a custom builder extending it.
+     * @param  string  $methodName
+     * @param  ClassReflection  $model
+     * @return MethodReflection|null
+     *
+     * @throws MissingMethodFromReflectionException
+     * @throws ShouldNotHappenException
+     */
+    public function searchOnEloquentBuilder(ClassReflection $eloquentBuilder, string $methodName, ClassReflection $model): ?MethodReflection
+    {
+        // Check for local query scopes
+        if (array_key_exists('scope'.ucfirst($methodName), $model->getMethodTags())) {
+            $methodTag = $model->getMethodTags()['scope'.ucfirst($methodName)];
+
+            $parameters = [];
+            foreach ($methodTag->getParameters() as $parameterName => $parameterTag) {
+                $parameters[] = new AnnotationScopeMethodParameterReflection($parameterName, $parameterTag->getType(), $parameterTag->passedByReference(), $parameterTag->isOptional(), $parameterTag->isVariadic(), $parameterTag->getDefaultValue());
+            }
+
+            // We shift the parameters,
+            // because first parameter is the Builder
+            array_shift($parameters);
+
+            return new EloquentBuilderMethodReflection(
+                'scope'.ucfirst($methodName),
+                $model,
+                new AnnotationScopeMethodReflection('scope'.ucfirst($methodName), $model, $methodTag->getReturnType(), $parameters, $methodTag->isStatic(), false),
+                $parameters,
+                $methodTag->getReturnType()
+            );
+        }
+
+        if ($model->hasNativeMethod('scope'.ucfirst($methodName))) {
+            $methodReflection = $model->getNativeMethod('scope'.ucfirst($methodName));
+            $parametersAcceptor = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants());
+
+            $parameters = $parametersAcceptor->getParameters();
+            // We shift the parameters,
+            // because first parameter is the Builder
+            array_shift($parameters);
+
+            $returnType = $parametersAcceptor->getReturnType();
+
+            return new EloquentBuilderMethodReflection(
+                'scope'.ucfirst($methodName),
+                $methodReflection->getDeclaringClass(),
+                $methodReflection,
+                $parameters,
+                $returnType,
+                $parametersAcceptor->isVariadic()
+            );
+        }
+
+        $queryBuilderReflection = $this->reflectionProvider->getClass(QueryBuilder::class);
+
+        if (in_array($methodName, $this->passthru, true)) {
+            return $queryBuilderReflection->getNativeMethod($methodName);
+        }
+
+        if ($queryBuilderReflection->hasNativeMethod($methodName)) {
+            return $queryBuilderReflection->getNativeMethod($methodName);
+        }
+
+        return $this->dynamicWhere($methodName, new GenericObjectType($eloquentBuilder->getName(), [new ObjectType($model->getName())]));
+    }
+
+    /**
+     * @param  string  $modelClassName
+     * @return string
+     *
+     * @throws MissingMethodFromReflectionException
+     * @throws ShouldNotHappenException
+     */
+    public function determineBuilderName(string $modelClassName): string
+    {
+        $method = $this->reflectionProvider->getClass($modelClassName)->getNativeMethod('newEloquentBuilder');
+
+        $returnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType();
+
+        if (in_array(EloquentBuilder::class, $returnType->getReferencedClasses(), true)) {
+            return EloquentBuilder::class;
+        }
+
+        if ($returnType instanceof ObjectType) {
+            return $returnType->getClassName();
+        }
+
+        return $returnType->describe(VerbosityLevel::value());
+    }
+
+    /**
+     * @throws MissingMethodFromReflectionException
+     * @throws ShouldNotHappenException
+     */
+    public function determineCollectionClassName(string $modelClassName): string
+    {
+        $newCollectionMethod = $this->reflectionProvider->getClass($modelClassName)->getNativeMethod('newCollection');
+
+        $returnType = ParametersAcceptorSelector::selectSingle($newCollectionMethod->getVariants())->getReturnType();
+
+        if ($returnType instanceof ObjectType) {
+            return $returnType->getClassName();
+        }
+
+        return $returnType->describe(VerbosityLevel::value());
+    }
+}
diff --git a/src/Methods/EloquentBuilderForwardsCallsExtension.php b/src/Methods/EloquentBuilderForwardsCallsExtension.php
new file mode 100644
index 0000000..b7067e7
--- /dev/null
+++ b/src/Methods/EloquentBuilderForwardsCallsExtension.php
@@ -0,0 +1,152 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Methods;
+
+use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
+use Flarum\PHPStan\Reflection\EloquentBuilderMethodReflection;
+use PHPStan\Analyser\OutOfClassScope;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\MethodsClassReflectionExtension;
+use PHPStan\Reflection\MissingMethodFromReflectionException;
+use PHPStan\Reflection\ParametersAcceptorSelector;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\ShouldNotHappenException;
+use PHPStan\Type\Generic\GenericObjectType;
+use PHPStan\Type\Generic\TemplateMixedType;
+use PHPStan\Type\Generic\TemplateObjectType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+use PHPStan\Type\TypeWithClassName;
+
+final class EloquentBuilderForwardsCallsExtension implements MethodsClassReflectionExtension
+{
+    /** @var array<string, MethodReflection> */
+    private $cache = [];
+
+    /** @var BuilderHelper */
+    private $builderHelper;
+
+    /** @var ReflectionProvider */
+    private $reflectionProvider;
+
+    public function __construct(BuilderHelper $builderHelper, ReflectionProvider $reflectionProvider)
+    {
+        $this->builderHelper = $builderHelper;
+        $this->reflectionProvider = $reflectionProvider;
+    }
+
+    /**
+     * @throws ShouldNotHappenException
+     * @throws MissingMethodFromReflectionException
+     */
+    public function hasMethod(ClassReflection $classReflection, string $methodName): bool
+    {
+        if (array_key_exists($classReflection->getCacheKey().'-'.$methodName, $this->cache)) {
+            return true;
+        }
+
+        $methodReflection = $this->findMethod($classReflection, $methodName);
+
+        if ($methodReflection !== null && $classReflection->isGeneric()) {
+            $this->cache[$classReflection->getCacheKey().'-'.$methodName] = $methodReflection;
+
+            return true;
+        }
+
+        return false;
+    }
+
+    public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
+    {
+        return $this->cache[$classReflection->getCacheKey().'-'.$methodName];
+    }
+
+    /**
+     * @throws MissingMethodFromReflectionException
+     * @throws ShouldNotHappenException
+     */
+    private function findMethod(ClassReflection $classReflection, string $methodName): ?MethodReflection
+    {
+        if ($classReflection->getName() !== EloquentBuilder::class && ! $classReflection->isSubclassOf(EloquentBuilder::class)) {
+            return null;
+        }
+
+        /** @var Type|TemplateMixedType|null $modelType */
+        $modelType = $classReflection->getActiveTemplateTypeMap()->getType('TModelClass');
+
+        // Generic type is not specified
+        if ($modelType === null) {
+            return null;
+        }
+
+        if ($modelType instanceof TemplateObjectType) {
+            $modelType = $modelType->getBound();
+
+            if ($modelType->equals(new ObjectType(Model::class))) {
+                return null;
+            }
+        }
+
+        if ($modelType instanceof TypeWithClassName) {
+            $modelReflection = $modelType->getClassReflection();
+        } else {
+            $modelReflection = $this->reflectionProvider->getClass(Model::class);
+        }
+
+        if ($modelReflection === null) {
+            return null;
+        }
+
+        $ref = $this->builderHelper->searchOnEloquentBuilder($classReflection, $methodName, $modelReflection);
+
+        if ($ref === null) {
+            // Special case for `SoftDeletes` trait
+            if (
+                in_array($methodName, ['withTrashed', 'onlyTrashed', 'withoutTrashed'], true) &&
+                in_array(SoftDeletes::class, array_keys($modelReflection->getTraits(true)))
+            ) {
+                $ref = $this->reflectionProvider->getClass(SoftDeletes::class)->getMethod($methodName, new OutOfClassScope());
+
+                return new EloquentBuilderMethodReflection(
+                    $methodName,
+                    $classReflection,
+                    $ref,
+                    ParametersAcceptorSelector::selectSingle($ref->getVariants())->getParameters(),
+                    new GenericObjectType($classReflection->getName(), [$modelType]),
+                    ParametersAcceptorSelector::selectSingle($ref->getVariants())->isVariadic()
+                );
+            }
+
+            return null;
+        }
+
+        $parametersAcceptor = ParametersAcceptorSelector::selectSingle($ref->getVariants());
+
+        if (in_array($methodName, $this->builderHelper->passthru, true)) {
+            $returnType = $parametersAcceptor->getReturnType();
+
+            return new EloquentBuilderMethodReflection(
+                $methodName, $classReflection,
+                $ref,
+                $parametersAcceptor->getParameters(),
+                $returnType,
+                $parametersAcceptor->isVariadic()
+            );
+        }
+
+        // Returning custom reflection
+        // to ensure return type is always `EloquentBuilder<Model>`
+        return new EloquentBuilderMethodReflection(
+            $methodName, $classReflection,
+            $ref,
+            $parametersAcceptor->getParameters(),
+            new GenericObjectType($classReflection->getName(), [$modelType]),
+            $parametersAcceptor->isVariadic()
+        );
+    }
+}
diff --git a/src/Methods/Extension.php b/src/Methods/Extension.php
new file mode 100644
index 0000000..271a385
--- /dev/null
+++ b/src/Methods/Extension.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Methods;
+
+use Illuminate\Database\Eloquent\Model;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\MethodsClassReflectionExtension;
+use PHPStan\Reflection\Php\PhpMethodReflectionFactory;
+use PHPStan\Reflection\ReflectionProvider;
+
+/**
+ * @internal
+ */
+final class Extension implements MethodsClassReflectionExtension
+{
+    /**
+     * @var Kernel
+     */
+    private $kernel;
+
+    /** @var MethodReflection[] */
+    private $methodReflections = [];
+
+    public function __construct(PhpMethodReflectionFactory $methodReflectionFactory, ReflectionProvider $reflectionProvider, Kernel $kernel = null)
+    {
+        $this->kernel = $kernel ?? new Kernel($methodReflectionFactory, $reflectionProvider);
+    }
+
+    public function hasMethod(ClassReflection $classReflection, string $methodName): bool
+    {
+        if ($classReflection->getName() === Model::class) {
+            return false;
+        }
+
+        if (array_key_exists($methodName.'-'.$classReflection->getName(), $this->methodReflections)) {
+            return true;
+        }
+
+        $passable = $this->kernel->handle($classReflection, $methodName);
+
+        $found = $passable->hasFound();
+
+        if ($found) {
+            $this->methodReflections[$methodName.'-'.$classReflection->getName()] = $passable->getMethodReflection();
+        }
+
+        return $found;
+    }
+
+    public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
+    {
+        return $this->methodReflections[$methodName.'-'.$classReflection->getName()];
+    }
+}
diff --git a/src/Methods/HigherOrderCollectionProxyExtension.php b/src/Methods/HigherOrderCollectionProxyExtension.php
new file mode 100644
index 0000000..cd04ac4
--- /dev/null
+++ b/src/Methods/HigherOrderCollectionProxyExtension.php
@@ -0,0 +1,143 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Methods;
+
+use Flarum\PHPStan\Support\HigherOrderCollectionProxyHelper;
+use PHPStan\Analyser\OutOfClassScope;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\FunctionVariant;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\MethodsClassReflectionExtension;
+use PHPStan\Reflection\ParametersAcceptorSelector;
+use PHPStan\TrinaryLogic;
+use PHPStan\Type;
+
+final class HigherOrderCollectionProxyExtension implements MethodsClassReflectionExtension
+{
+    public function hasMethod(ClassReflection $classReflection, string $methodName): bool
+    {
+        return HigherOrderCollectionProxyHelper::hasPropertyOrMethod($classReflection, $methodName, 'method');
+    }
+
+    public function getMethod(
+        ClassReflection $classReflection,
+        string $methodName
+    ): MethodReflection {
+        $activeTemplateTypeMap = $classReflection->getActiveTemplateTypeMap();
+
+        /** @var Type\Constant\ConstantStringType $methodType */
+        $methodType = $activeTemplateTypeMap->getType('T');
+
+        /** @var Type\ObjectType $valueType */
+        $valueType = $activeTemplateTypeMap->getType('TValue');
+
+        $modelMethodReflection = $valueType->getMethod($methodName, new OutOfClassScope());
+
+        $modelMethodReturnType = ParametersAcceptorSelector::selectSingle($modelMethodReflection->getVariants())->getReturnType();
+
+        $returnType = HigherOrderCollectionProxyHelper::determineReturnType($methodType->getValue(), $valueType, $modelMethodReturnType);
+
+        return new class($classReflection, $methodName, $modelMethodReflection, $returnType) implements MethodReflection
+        {
+            /** @var ClassReflection */
+            private $classReflection;
+
+            /** @var string */
+            private $methodName;
+
+            /** @var MethodReflection */
+            private $modelMethodReflection;
+
+            /** @var Type\Type */
+            private $returnType;
+
+            public function __construct(ClassReflection $classReflection, string $methodName, MethodReflection $modelMethodReflection, Type\Type $returnType)
+            {
+                $this->classReflection = $classReflection;
+                $this->methodName = $methodName;
+                $this->modelMethodReflection = $modelMethodReflection;
+                $this->returnType = $returnType;
+            }
+
+            public function getDeclaringClass(): \PHPStan\Reflection\ClassReflection
+            {
+                return $this->classReflection;
+            }
+
+            public function isStatic(): bool
+            {
+                return false;
+            }
+
+            public function isPrivate(): bool
+            {
+                return false;
+            }
+
+            public function isPublic(): bool
+            {
+                return true;
+            }
+
+            public function getDocComment(): ?string
+            {
+                return null;
+            }
+
+            public function getName(): string
+            {
+                return $this->methodName;
+            }
+
+            public function getPrototype(): \PHPStan\Reflection\ClassMemberReflection
+            {
+                return $this;
+            }
+
+            public function getVariants(): array
+            {
+                return [
+                    new FunctionVariant(
+                        ParametersAcceptorSelector::selectSingle($this->modelMethodReflection->getVariants())->getTemplateTypeMap(),
+                        ParametersAcceptorSelector::selectSingle($this->modelMethodReflection->getVariants())->getResolvedTemplateTypeMap(),
+                        ParametersAcceptorSelector::selectSingle($this->modelMethodReflection->getVariants())->getParameters(),
+                        ParametersAcceptorSelector::selectSingle($this->modelMethodReflection->getVariants())->isVariadic(),
+                        $this->returnType
+                    ),
+                ];
+            }
+
+            public function isDeprecated(): TrinaryLogic
+            {
+                return TrinaryLogic::createNo();
+            }
+
+            public function getDeprecatedDescription(): ?string
+            {
+                return null;
+            }
+
+            public function isFinal(): TrinaryLogic
+            {
+                return TrinaryLogic::createNo();
+            }
+
+            public function isInternal(): TrinaryLogic
+            {
+                return TrinaryLogic::createNo();
+            }
+
+            public function getThrowType(): ?\PHPStan\Type\Type
+            {
+                return null;
+            }
+
+            public function hasSideEffects(): TrinaryLogic
+            {
+                return TrinaryLogic::createMaybe();
+            }
+        };
+    }
+}
diff --git a/src/Methods/HigherOrderTapProxyExtension.php b/src/Methods/HigherOrderTapProxyExtension.php
new file mode 100644
index 0000000..93c6c48
--- /dev/null
+++ b/src/Methods/HigherOrderTapProxyExtension.php
@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Methods;
+
+use Illuminate\Support\HigherOrderTapProxy;
+use PHPStan\Analyser\OutOfClassScope;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\MethodsClassReflectionExtension;
+use PHPStan\Type\ObjectType;
+
+final class HigherOrderTapProxyExtension implements MethodsClassReflectionExtension
+{
+    public function hasMethod(ClassReflection $classReflection, string $methodName): bool
+    {
+        if ($classReflection->getName() !== HigherOrderTapProxy::class) {
+            return false;
+        }
+
+        $templateTypeMap = $classReflection->getActiveTemplateTypeMap();
+
+        $templateType = $templateTypeMap->getType('TClass');
+
+        if (! $templateType instanceof ObjectType) {
+            return false;
+        }
+
+        if ($templateType->getClassReflection() === null) {
+            return false;
+        }
+
+        return $templateType->hasMethod($methodName)->yes();
+    }
+
+    public function getMethod(
+        ClassReflection $classReflection,
+        string $methodName
+    ): MethodReflection {
+        /** @var ObjectType $templateType */
+        $templateType = $classReflection->getActiveTemplateTypeMap()->getType('TClass');
+
+        /** @var ClassReflection $reflection */
+        $reflection = $templateType->getClassReflection();
+
+        return $reflection->getMethod($methodName, new OutOfClassScope());
+    }
+}
diff --git a/src/Methods/Kernel.php b/src/Methods/Kernel.php
new file mode 100644
index 0000000..bc7c0dd
--- /dev/null
+++ b/src/Methods/Kernel.php
@@ -0,0 +1,72 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Methods;
+
+use Illuminate\Pipeline\Pipeline;
+use Flarum\PHPStan\Concerns;
+use Flarum\PHPStan\Contracts\Methods\PassableContract;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\Php\PhpMethodReflectionFactory;
+use PHPStan\Reflection\ReflectionProvider;
+
+/**
+ * @internal
+ */
+final class Kernel
+{
+    use Concerns\HasContainer;
+
+    /**
+     * @var PhpMethodReflectionFactory
+     */
+    private $methodReflectionFactory;
+    /**
+     * @var ReflectionProvider
+     */
+    private $reflectionProvider;
+
+    /**
+     * Kernel constructor.
+     *
+     * @param  PhpMethodReflectionFactory  $methodReflectionFactory
+     */
+    public function __construct(
+        PhpMethodReflectionFactory $methodReflectionFactory,
+        ReflectionProvider $reflectionProvider
+    ) {
+        $this->methodReflectionFactory = $methodReflectionFactory;
+        $this->reflectionProvider = $reflectionProvider;
+    }
+
+    /**
+     * @param  ClassReflection  $classReflection
+     * @param  string  $methodName
+     * @return PassableContract
+     */
+    public function handle(ClassReflection $classReflection, string $methodName): PassableContract
+    {
+        $pipeline = new Pipeline($this->getContainer());
+
+        $passable = new Passable($this->methodReflectionFactory, $this->reflectionProvider, $pipeline, $classReflection, $methodName);
+
+        $pipeline->send($passable)
+            ->through(
+                [
+                    Pipes\SelfClass::class,
+                    Pipes\Macros::class,
+                    Pipes\Contracts::class,
+                    Pipes\Facades::class,
+                    Pipes\Managers::class,
+                    Pipes\Auths::class,
+                ]
+            )
+            ->then(
+                function ($method) {
+                }
+            );
+
+        return $passable;
+    }
+}
diff --git a/src/Methods/Macro.php b/src/Methods/Macro.php
new file mode 100644
index 0000000..e0dca28
--- /dev/null
+++ b/src/Methods/Macro.php
@@ -0,0 +1,259 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Methods;
+
+use function array_map;
+use Closure;
+use ErrorException;
+use Illuminate\Validation\ValidationException;
+use PHPStan\Reflection\ClassMemberReflection;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\FunctionVariant;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\ParameterReflection;
+use PHPStan\Reflection\PassedByReference;
+use PHPStan\TrinaryLogic;
+use PHPStan\Type\Generic\TemplateTypeMap;
+use PHPStan\Type\MixedType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+use PHPStan\Type\TypehintHelper;
+use ReflectionFunction;
+use ReflectionParameter;
+use ReflectionType;
+use stdClass;
+
+final class Macro implements MethodReflection
+{
+    /**
+     * @var ClassReflection
+     */
+    private $classReflection;
+
+    /**
+     * The method name.
+     *
+     * @var string
+     */
+    private $methodName;
+
+    /**
+     * The reflection function.
+     *
+     * @var ReflectionFunction
+     */
+    private $reflectionFunction;
+
+    /**
+     * The parameters.
+     *
+     * @var ReflectionParameter[]
+     */
+    private $parameters;
+
+    /**
+     * The is static.
+     *
+     * @var bool
+     */
+    private $isStatic = false;
+
+    /**
+     * Map of macro methods and thrown exception classes.
+     *
+     * @var string[]
+     */
+    private $methodThrowTypeMap = [
+        'validate' => ValidationException::class,
+        'validateWithBag' => ValidationException::class,
+    ];
+
+    public function __construct(ClassReflection $classReflection, string $methodName, ReflectionFunction $reflectionFunction)
+    {
+        $this->classReflection = $classReflection;
+        $this->methodName = $methodName;
+        $this->reflectionFunction = $reflectionFunction;
+        $this->parameters = $this->reflectionFunction->getParameters();
+
+        if ($this->reflectionFunction->isClosure()) {
+            try {
+                /** @var Closure $closure */
+                $closure = $this->reflectionFunction->getClosure();
+                Closure::bind($closure, new stdClass);
+                // The closure can be bound so it was not explicitly marked as static
+            } catch (ErrorException $e) {
+                // The closure was explicitly marked as static
+                $this->isStatic = true;
+            }
+        }
+    }
+
+    public function getDeclaringClass(): ClassReflection
+    {
+        return $this->classReflection;
+    }
+
+    public function isPrivate(): bool
+    {
+        return false;
+    }
+
+    public function isPublic(): bool
+    {
+        return true;
+    }
+
+    public function isFinal(): TrinaryLogic
+    {
+        return TrinaryLogic::createNo();
+    }
+
+    public function isInternal(): TrinaryLogic
+    {
+        return TrinaryLogic::createNo();
+    }
+
+    public function isStatic(): bool
+    {
+        return $this->isStatic;
+    }
+
+    /**
+     * Set the is static value.
+     *
+     * @param  bool  $isStatic
+     * @return void
+     */
+    public function setIsStatic(bool $isStatic): void
+    {
+        $this->isStatic = $isStatic;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDocComment(): ?string
+    {
+        return $this->reflectionFunction->getDocComment() ?: null;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getName(): string
+    {
+        return $this->methodName;
+    }
+
+    /** @return ParameterReflection[] */
+    public function getParameters(): array
+    {
+        return array_map(function (ReflectionParameter $reflection): ParameterReflection {
+            return new class($reflection) implements ParameterReflection
+            {
+                /**
+                 * @var ReflectionParameter
+                 */
+                private $reflection;
+
+                public function __construct(ReflectionParameter $reflection)
+                {
+                    $this->reflection = $reflection;
+                }
+
+                public function getName(): string
+                {
+                    return $this->reflection->getName();
+                }
+
+                public function isOptional(): bool
+                {
+                    return $this->reflection->isOptional();
+                }
+
+                public function getType(): Type
+                {
+                    $type = $this->reflection->getType();
+
+                    if ($type === null) {
+                        return new MixedType();
+                    }
+
+                    return TypehintHelper::decideTypeFromReflection($this->reflection->getType());
+                }
+
+                public function passedByReference(): PassedByReference
+                {
+                    return PassedByReference::createNo();
+                }
+
+                public function isVariadic(): bool
+                {
+                    return $this->reflection->isVariadic();
+                }
+
+                public function getDefaultValue(): ?Type
+                {
+                    return null;
+                }
+            };
+        }, $this->parameters);
+    }
+
+    /**
+     * Set the parameters value.
+     *
+     * @param  ReflectionParameter[]  $parameters
+     * @return void
+     */
+    public function setParameters(array $parameters): void
+    {
+        $this->parameters = $parameters;
+    }
+
+    public function getReturnType(): ?ReflectionType
+    {
+        return $this->reflectionFunction->getReturnType();
+    }
+
+    public function isDeprecated(): TrinaryLogic
+    {
+        return TrinaryLogic::createFromBoolean($this->reflectionFunction->isDeprecated());
+    }
+
+    public function getPrototype(): ClassMemberReflection
+    {
+        return $this;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function getVariants(): array
+    {
+        return [
+            new FunctionVariant(TemplateTypeMap::createEmpty(), null, $this->getParameters(), $this->reflectionFunction->isVariadic(), TypehintHelper::decideTypeFromReflection($this->getReturnType())),
+        ];
+    }
+
+    public function getDeprecatedDescription(): ?string
+    {
+        return null;
+    }
+
+    public function getThrowType(): ?Type
+    {
+        if (array_key_exists($this->methodName, $this->methodThrowTypeMap)) {
+            return new ObjectType($this->methodThrowTypeMap[$this->methodName]);
+        }
+
+        return null;
+    }
+
+    public function hasSideEffects(): TrinaryLogic
+    {
+        return TrinaryLogic::createMaybe();
+    }
+}
diff --git a/src/Methods/ModelFactoryMethodsClassReflectionExtension.php b/src/Methods/ModelFactoryMethodsClassReflectionExtension.php
new file mode 100644
index 0000000..89d4582
--- /dev/null
+++ b/src/Methods/ModelFactoryMethodsClassReflectionExtension.php
@@ -0,0 +1,160 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Methods;
+
+use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Str;
+use PHPStan\Analyser\OutOfClassScope;
+use PHPStan\Reflection\ClassMemberReflection;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\FunctionVariant;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\MethodsClassReflectionExtension;
+use PHPStan\Reflection\ParametersAcceptorSelector;
+use PHPStan\TrinaryLogic;
+use PHPStan\Type\Generic\TemplateTypeMap;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+
+class ModelFactoryMethodsClassReflectionExtension implements MethodsClassReflectionExtension
+{
+    public function hasMethod(ClassReflection $classReflection, string $methodName): bool
+    {
+        // Class only available on Laravel 8
+        if (! class_exists('\Illuminate\Database\Eloquent\Factories\Factory')) {
+            return false;
+        }
+
+        if (! $classReflection->isSubclassOf(Factory::class)) {
+            return false;
+        }
+
+        if (! Str::startsWith($methodName, ['for', 'has'])) {
+            return false;
+        }
+
+        $relationship = Str::camel(Str::substr($methodName, 3));
+
+        $parent = $classReflection->getParentClass();
+
+        if ($parent === null) {
+            return false;
+        }
+
+        $modelType = $parent->getActiveTemplateTypeMap()->getType('TModel');
+
+        if ($modelType === null) {
+            return false;
+        }
+
+        return $modelType->hasMethod($relationship)->yes();
+    }
+
+    public function getMethod(
+        ClassReflection $classReflection,
+        string $methodName
+    ): MethodReflection {
+        return new class($classReflection, $methodName) implements MethodReflection
+        {
+            /** @var ClassReflection */
+            private $classReflection;
+
+            /** @var string */
+            private $methodName;
+
+            public function __construct(ClassReflection $classReflection, string $methodName)
+            {
+                $this->classReflection = $classReflection;
+                $this->methodName = $methodName;
+            }
+
+            public function getDeclaringClass(): ClassReflection
+            {
+                return $this->classReflection;
+            }
+
+            public function isStatic(): bool
+            {
+                return false;
+            }
+
+            public function isPrivate(): bool
+            {
+                return false;
+            }
+
+            public function isPublic(): bool
+            {
+                return true;
+            }
+
+            public function getDocComment(): ?string
+            {
+                return null;
+            }
+
+            public function getName(): string
+            {
+                return $this->methodName;
+            }
+
+            public function getPrototype(): ClassMemberReflection
+            {
+                return $this;
+            }
+
+            public function getVariants(): array
+            {
+                $returnType = new ObjectType($this->classReflection->getName());
+                $stateParameter = ParametersAcceptorSelector::selectSingle($this->classReflection->getMethod('state', new OutOfClassScope())->getVariants())->getParameters()[0];
+                $countParameter = ParametersAcceptorSelector::selectSingle($this->classReflection->getMethod('count', new OutOfClassScope())->getVariants())->getParameters()[0];
+
+                $variants = [
+                    new FunctionVariant(TemplateTypeMap::createEmpty(), null, [], false, $returnType),
+                ];
+
+                if (Str::startsWith($this->methodName, 'for')) {
+                    $variants[] = new FunctionVariant(TemplateTypeMap::createEmpty(), null, [$stateParameter], false, $returnType);
+                } else {
+                    $variants[] = new FunctionVariant(TemplateTypeMap::createEmpty(), null, [$countParameter], false, $returnType);
+                    $variants[] = new FunctionVariant(TemplateTypeMap::createEmpty(), null, [$stateParameter], false, $returnType);
+                    $variants[] = new FunctionVariant(TemplateTypeMap::createEmpty(), null, [$countParameter, $stateParameter], false, $returnType);
+                }
+
+                return $variants;
+            }
+
+            public function isDeprecated(): TrinaryLogic
+            {
+                return TrinaryLogic::createNo();
+            }
+
+            public function getDeprecatedDescription(): ?string
+            {
+                return null;
+            }
+
+            public function isFinal(): TrinaryLogic
+            {
+                return TrinaryLogic::createNo();
+            }
+
+            public function isInternal(): TrinaryLogic
+            {
+                return TrinaryLogic::createNo();
+            }
+
+            public function getThrowType(): ?Type
+            {
+                return null;
+            }
+
+            public function hasSideEffects(): TrinaryLogic
+            {
+                return TrinaryLogic::createMaybe();
+            }
+        };
+    }
+}
diff --git a/src/Methods/ModelForwardsCallsExtension.php b/src/Methods/ModelForwardsCallsExtension.php
new file mode 100644
index 0000000..84c5540
--- /dev/null
+++ b/src/Methods/ModelForwardsCallsExtension.php
@@ -0,0 +1,212 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Methods;
+
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Model;
+use Flarum\PHPStan\Reflection\EloquentBuilderMethodReflection;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\MethodsClassReflectionExtension;
+use PHPStan\Reflection\MissingMethodFromReflectionException;
+use PHPStan\Reflection\ParametersAcceptorSelector;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\ShouldNotHappenException;
+use PHPStan\TrinaryLogic;
+use PHPStan\Type\Generic\GenericObjectType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+use PHPStan\Type\TypeTraverser;
+use PHPStan\Type\TypeWithClassName;
+
+final class ModelForwardsCallsExtension implements MethodsClassReflectionExtension
+{
+    /** @var BuilderHelper */
+    private $builderHelper;
+
+    /** @var ReflectionProvider */
+    private $reflectionProvider;
+
+    /** @var EloquentBuilderForwardsCallsExtension */
+    private $eloquentBuilderForwardsCallsExtension;
+
+    /** @var array<string, MethodReflection> */
+    private $cache = [];
+
+    public function __construct(BuilderHelper $builderHelper, ReflectionProvider $reflectionProvider, EloquentBuilderForwardsCallsExtension $eloquentBuilderForwardsCallsExtension)
+    {
+        $this->builderHelper = $builderHelper;
+        $this->reflectionProvider = $reflectionProvider;
+        $this->eloquentBuilderForwardsCallsExtension = $eloquentBuilderForwardsCallsExtension;
+    }
+
+    /**
+     * @throws MissingMethodFromReflectionException
+     * @throws ShouldNotHappenException
+     */
+    public function hasMethod(ClassReflection $classReflection, string $methodName): bool
+    {
+        if (array_key_exists($classReflection->getCacheKey().'-'.$methodName, $this->cache)) {
+            return true;
+        }
+
+        $methodReflection = $this->findMethod($classReflection, $methodName);
+
+        if ($methodReflection !== null) {
+            $this->cache[$classReflection->getCacheKey().'-'.$methodName] = $methodReflection;
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * @param  ClassReflection  $classReflection
+     * @param  string  $methodName
+     * @return MethodReflection
+     */
+    public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
+    {
+        return $this->cache[$classReflection->getCacheKey().'-'.$methodName];
+    }
+
+    /**
+     * @throws ShouldNotHappenException
+     * @throws MissingMethodFromReflectionException
+     */
+    private function findMethod(ClassReflection $classReflection, string $methodName): ?MethodReflection
+    {
+        if ($classReflection->getName() !== Model::class && ! $classReflection->isSubclassOf(Model::class)) {
+            return null;
+        }
+
+        $builderName = $this->builderHelper->determineBuilderName($classReflection->getName());
+
+        if (in_array($methodName, ['increment', 'decrement'], true)) {
+            $methodReflection = $classReflection->getNativeMethod($methodName);
+
+            return new class($classReflection, $methodName, $methodReflection) implements MethodReflection
+            {
+                /** @var ClassReflection */
+                private $classReflection;
+
+                /** @var string */
+                private $methodName;
+
+                /** @var MethodReflection */
+                private $methodReflection;
+
+                public function __construct(ClassReflection $classReflection, string $methodName, MethodReflection $methodReflection)
+                {
+                    $this->classReflection = $classReflection;
+                    $this->methodName = $methodName;
+                    $this->methodReflection = $methodReflection;
+                }
+
+                public function getDeclaringClass(): \PHPStan\Reflection\ClassReflection
+                {
+                    return $this->classReflection;
+                }
+
+                public function isStatic(): bool
+                {
+                    return false;
+                }
+
+                public function isPrivate(): bool
+                {
+                    return false;
+                }
+
+                public function isPublic(): bool
+                {
+                    return true;
+                }
+
+                public function getDocComment(): ?string
+                {
+                    return null;
+                }
+
+                public function getName(): string
+                {
+                    return $this->methodName;
+                }
+
+                public function getPrototype(): \PHPStan\Reflection\ClassMemberReflection
+                {
+                    return $this;
+                }
+
+                public function getVariants(): array
+                {
+                    return $this->methodReflection->getVariants();
+                }
+
+                public function isDeprecated(): \PHPStan\TrinaryLogic
+                {
+                    return TrinaryLogic::createNo();
+                }
+
+                public function getDeprecatedDescription(): ?string
+                {
+                    return null;
+                }
+
+                public function isFinal(): \PHPStan\TrinaryLogic
+                {
+                    return TrinaryLogic::createNo();
+                }
+
+                public function isInternal(): \PHPStan\TrinaryLogic
+                {
+                    return TrinaryLogic::createNo();
+                }
+
+                public function getThrowType(): ?\PHPStan\Type\Type
+                {
+                    return null;
+                }
+
+                public function hasSideEffects(): \PHPStan\TrinaryLogic
+                {
+                    return TrinaryLogic::createYes();
+                }
+            };
+        }
+
+        $builderReflection = $this->reflectionProvider->getClass($builderName)->withTypes([new ObjectType($classReflection->getName())]);
+        $genericBuilderAndModelType = new GenericObjectType($builderName, [new ObjectType($classReflection->getName())]);
+
+        if ($builderReflection->hasNativeMethod($methodName)) {
+            $reflection = $builderReflection->getNativeMethod($methodName);
+
+            $parametersAcceptor = ParametersAcceptorSelector::selectSingle($reflection->getVariants());
+
+            $returnType = TypeTraverser::map($parametersAcceptor->getReturnType(), static function (Type $type, callable $traverse) use ($genericBuilderAndModelType) {
+                if ($type instanceof TypeWithClassName && $type->getClassName() === Builder::class) {
+                    return $genericBuilderAndModelType;
+                }
+
+                return $traverse($type);
+            });
+
+            return new EloquentBuilderMethodReflection(
+                $methodName, $classReflection,
+                $reflection,
+                $parametersAcceptor->getParameters(),
+                $returnType,
+                $parametersAcceptor->isVariadic()
+            );
+        }
+
+        if ($this->eloquentBuilderForwardsCallsExtension->hasMethod($builderReflection, $methodName)) {
+            return $this->eloquentBuilderForwardsCallsExtension->getMethod($builderReflection, $methodName);
+        }
+
+        return null;
+    }
+}
diff --git a/src/Methods/ModelTypeHelper.php b/src/Methods/ModelTypeHelper.php
new file mode 100644
index 0000000..4c527f7
--- /dev/null
+++ b/src/Methods/ModelTypeHelper.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Methods;
+
+use Illuminate\Database\Eloquent\Model;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\ObjectWithoutClassType;
+use PHPStan\Type\StaticType;
+use PHPStan\Type\Type;
+use PHPStan\Type\TypeTraverser;
+use PHPStan\Type\TypeWithClassName;
+
+final class ModelTypeHelper
+{
+    public static function replaceStaticTypeWithModel(Type $type, string $modelClass): Type
+    {
+        return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($modelClass): Type {
+            if ($type instanceof ObjectWithoutClassType || $type instanceof StaticType) {
+                return new ObjectType($modelClass);
+            }
+
+            if ($type instanceof TypeWithClassName && $type->getClassName() === Model::class) {
+                return new ObjectType($modelClass);
+            }
+
+            return $traverse($type);
+        });
+    }
+}
diff --git a/src/Methods/Passable.php b/src/Methods/Passable.php
new file mode 100644
index 0000000..735784d
--- /dev/null
+++ b/src/Methods/Passable.php
@@ -0,0 +1,217 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Methods;
+
+use Illuminate\Contracts\Pipeline\Pipeline;
+use LogicException;
+use Mockery;
+use Flarum\PHPStan\Concerns;
+use Flarum\PHPStan\Contracts\Methods\PassableContract;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\Php\PhpMethodReflection;
+use PHPStan\Reflection\Php\PhpMethodReflectionFactory;
+use PHPStan\Reflection\ReflectionProvider;
+
+/**
+ * @internal
+ */
+final class Passable implements PassableContract
+{
+    use Concerns\HasContainer;
+
+    /**
+     * @var \PHPStan\Reflection\Php\PhpMethodReflectionFactory
+     */
+    private $methodReflectionFactory;
+
+    /**
+     * @var ReflectionProvider
+     */
+    private $reflectionProvider;
+
+    /**
+     * @var \Illuminate\Contracts\Pipeline\Pipeline
+     */
+    private $pipeline;
+
+    /**
+     * @var \PHPStan\Reflection\ClassReflection
+     */
+    private $classReflection;
+
+    /**
+     * @var string
+     */
+    private $methodName;
+
+    /**
+     * @var \PHPStan\Reflection\MethodReflection|null
+     */
+    private $methodReflection;
+
+    /**
+     * @var bool
+     */
+    private $staticAllowed = false;
+
+    /**
+     * Method constructor.
+     *
+     * @param  \PHPStan\Reflection\Php\PhpMethodReflectionFactory  $methodReflectionFactory
+     * @param  ReflectionProvider  $reflectionProvider
+     * @param  \Illuminate\Contracts\Pipeline\Pipeline  $pipeline
+     * @param  \PHPStan\Reflection\ClassReflection  $classReflection
+     * @param  string  $methodName
+     */
+    public function __construct(
+        PhpMethodReflectionFactory $methodReflectionFactory,
+        ReflectionProvider $reflectionProvider,
+        Pipeline $pipeline,
+        ClassReflection $classReflection,
+        string $methodName
+    ) {
+        $this->methodReflectionFactory = $methodReflectionFactory;
+        $this->reflectionProvider = $reflectionProvider;
+        $this->pipeline = $pipeline;
+        $this->classReflection = $classReflection;
+        $this->methodName = $methodName;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getClassReflection(): ClassReflection
+    {
+        return $this->classReflection;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setClassReflection(ClassReflection $classReflection): PassableContract
+    {
+        $this->classReflection = $classReflection;
+
+        return $this;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getMethodName(): string
+    {
+        return $this->methodName;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function hasFound(): bool
+    {
+        return $this->methodReflection !== null;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function searchOn(string $class): bool
+    {
+        $classReflection = $this->reflectionProvider->getClass($class);
+
+        $found = $classReflection->hasNativeMethod($this->methodName);
+
+        if ($found) {
+            $this->setMethodReflection($classReflection->getNativeMethod($this->methodName));
+        }
+
+        return $found;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getMethodReflection(): MethodReflection
+    {
+        if ($this->methodReflection === null) {
+            throw new LogicException("MethodReflection doesn't exist");
+        }
+
+        return $this->methodReflection;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setMethodReflection(MethodReflection $methodReflection): void
+    {
+        $this->methodReflection = $methodReflection;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setStaticAllowed(bool $staticAllowed): void
+    {
+        $this->staticAllowed = $staticAllowed;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isStaticAllowed(): bool
+    {
+        return $this->staticAllowed;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function sendToPipeline(string $class, $staticAllowed = false): bool
+    {
+        $classReflection = $this->reflectionProvider->getClass($class);
+
+        $this->setStaticAllowed($this->staticAllowed ?: $staticAllowed);
+
+        $originalClassReflection = $this->classReflection;
+        $this->pipeline->send($this->setClassReflection($classReflection))
+            ->then(
+                function (PassableContract $passable) use ($originalClassReflection) {
+                    if ($passable->hasFound()) {
+                        $this->setMethodReflection($passable->getMethodReflection());
+                        $this->setStaticAllowed($passable->isStaticAllowed());
+                    }
+
+                    $this->setClassReflection($originalClassReflection);
+                }
+            );
+
+        if ($result = $this->hasFound()) {
+            $methodReflection = $this->getMethodReflection();
+            if (get_class($methodReflection) === PhpMethodReflection::class) {
+                $methodReflection = Mockery::mock($methodReflection);
+                $methodReflection->shouldReceive('isStatic')
+                    ->andReturn($this->isStaticAllowed());
+            }
+
+            $this->setMethodReflection($methodReflection);
+        }
+
+        return $result;
+    }
+
+    public function getReflectionProvider(): ReflectionProvider
+    {
+        return $this->reflectionProvider;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getMethodReflectionFactory(): PhpMethodReflectionFactory
+    {
+        return $this->methodReflectionFactory;
+    }
+}
diff --git a/src/Methods/Pipes/Auths.php b/src/Methods/Pipes/Auths.php
new file mode 100644
index 0000000..4dfa86e
--- /dev/null
+++ b/src/Methods/Pipes/Auths.php
@@ -0,0 +1,61 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Methods\Pipes;
+
+use Closure;
+use Illuminate\Contracts\Auth\Access\Authorizable;
+use Illuminate\Contracts\Auth\Authenticatable;
+use Illuminate\Contracts\Auth\CanResetPassword;
+use function in_array;
+use Flarum\PHPStan\Concerns;
+use Flarum\PHPStan\Contracts\Methods\PassableContract;
+use Flarum\PHPStan\Contracts\Methods\Pipes\PipeContract;
+
+/**
+ * @internal
+ */
+final class Auths implements PipeContract
+{
+    use Concerns\HasContainer;
+    use Concerns\LoadsAuthModel;
+
+    /**
+     * @var string[]
+     */
+    private $classes = [
+        Authenticatable::class,
+        CanResetPassword::class,
+        Authorizable::class,
+    ];
+
+    /**
+     * {@inheritdoc}
+     */
+    public function handle(PassableContract $passable, Closure $next): void
+    {
+        $classReflectionName = $passable->getClassReflection()
+            ->getName();
+
+        $found = false;
+
+        $config = $this->resolve('config');
+
+        if ($config !== null && in_array($classReflectionName, $this->classes, true)) {
+            $authModel = $this->getAuthModel($config);
+
+            if ($authModel !== null) {
+                $found = $passable->sendToPipeline($authModel);
+            }
+        } elseif ($classReflectionName === \Illuminate\Contracts\Auth\Factory::class || $classReflectionName === \Illuminate\Auth\AuthManager::class) {
+            $found = $passable->sendToPipeline(
+                \Illuminate\Contracts\Auth\Guard::class
+            );
+        }
+
+        if (! $found) {
+            $next($passable);
+        }
+    }
+}
diff --git a/src/Methods/Pipes/Contracts.php b/src/Methods/Pipes/Contracts.php
new file mode 100644
index 0000000..b985dd1
--- /dev/null
+++ b/src/Methods/Pipes/Contracts.php
@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Methods\Pipes;
+
+use Closure;
+use function get_class;
+use Illuminate\Support\Str;
+use Flarum\PHPStan\Concerns;
+use Flarum\PHPStan\Contracts\Methods\PassableContract;
+use Flarum\PHPStan\Contracts\Methods\Pipes\PipeContract;
+use PHPStan\Reflection\ClassReflection;
+
+/**
+ * @internal
+ */
+final class Contracts implements PipeContract
+{
+    use Concerns\HasContainer;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function handle(PassableContract $passable, Closure $next): void
+    {
+        $found = false;
+
+        foreach ($this->concretes($passable->getClassReflection()) as $concrete) {
+            if ($found = $passable->sendToPipeline($concrete)) {
+                break;
+            }
+        }
+
+        if (! $found) {
+            $next($passable);
+        }
+    }
+
+    /**
+     * @param  \PHPStan\Reflection\ClassReflection  $classReflection
+     * @return class-string[]
+     */
+    private function concretes(ClassReflection $classReflection): array
+    {
+        if ($classReflection->isInterface() && Str::startsWith($classReflection->getName(), 'Illuminate\Contracts')) {
+            $concrete = $this->resolve($classReflection->getName());
+
+            if ($concrete !== null) {
+                $class = get_class($concrete);
+
+                if ($class) {
+                    return [$class];
+                }
+            }
+        }
+
+        return [];
+    }
+}
diff --git a/src/Methods/Pipes/Facades.php b/src/Methods/Pipes/Facades.php
new file mode 100644
index 0000000..826cd12
--- /dev/null
+++ b/src/Methods/Pipes/Facades.php
@@ -0,0 +1,59 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Methods\Pipes;
+
+use Closure;
+use Illuminate\Support\Facades\Facade;
+use Illuminate\Support\Str;
+use Flarum\PHPStan\Contracts\Methods\PassableContract;
+use Flarum\PHPStan\Contracts\Methods\Pipes\PipeContract;
+
+/**
+ * @internal
+ */
+final class Facades implements PipeContract
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function handle(PassableContract $passable, Closure $next): void
+    {
+        $classReflection = $passable->getClassReflection();
+
+        $found = false;
+
+        if ($classReflection->isSubclassOf(Facade::class)) {
+            $facadeClass = $classReflection->getName();
+
+            if ($concrete = $facadeClass::getFacadeRoot()) {
+                $class = get_class($concrete);
+
+                if ($class) {
+                    $found = $passable->sendToPipeline($class, true);
+                }
+            }
+
+            if (! $found && Str::startsWith($passable->getMethodName(), 'assert')) {
+                $fakeFacadeClass = $this->getFake($facadeClass);
+
+                if ($passable->getReflectionProvider()->hasClass($fakeFacadeClass)) {
+                    assert(class_exists($fakeFacadeClass));
+                    $found = $passable->sendToPipeline($fakeFacadeClass, true);
+                }
+            }
+        }
+
+        if (! $found) {
+            $next($passable);
+        }
+    }
+
+    private function getFake(string $facade): string
+    {
+        $shortClassName = substr($facade, strrpos($facade, '\\') + 1);
+
+        return sprintf('\\Illuminate\\Support\\Testing\\Fakes\\%sFake', $shortClassName);
+    }
+}
diff --git a/src/Methods/Pipes/Macros.php b/src/Methods/Pipes/Macros.php
new file mode 100644
index 0000000..5f93eff
--- /dev/null
+++ b/src/Methods/Pipes/Macros.php
@@ -0,0 +1,96 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Methods\Pipes;
+
+use Carbon\Traits\Macro as CarbonMacro;
+use Closure;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Support\Str;
+use Illuminate\Support\Traits\Macroable;
+use Flarum\PHPStan\Concerns;
+use Flarum\PHPStan\Contracts\Methods\PassableContract;
+use Flarum\PHPStan\Contracts\Methods\Pipes\PipeContract;
+use Flarum\PHPStan\Methods\Macro;
+use PHPStan\Reflection\ClassReflection;
+
+/**
+ * @internal
+ */
+final class Macros implements PipeContract
+{
+    use Concerns\HasContainer;
+
+    private function hasIndirectTraitUse(ClassReflection $class, string $traitName): bool
+    {
+        foreach ($class->getTraits() as $trait) {
+            if ($this->hasIndirectTraitUse($trait, $traitName)) {
+                return true;
+            }
+        }
+
+        return $class->hasTraitUse($traitName);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function handle(PassableContract $passable, Closure $next): void
+    {
+        $classReflection = $passable->getClassReflection();
+
+        /** @var class-string $className */
+        $className = null;
+        $found = false;
+        $macroTraitProperty = null;
+
+        if ($classReflection->isInterface() && Str::startsWith($classReflection->getName(), 'Illuminate\Contracts')) {
+            /** @var object|null $concrete */
+            $concrete = $this->resolve($classReflection->getName());
+
+            if ($concrete !== null) {
+                $className = get_class($concrete);
+
+                if ($className && $passable->getReflectionProvider()
+                    ->getClass($className)
+                    ->hasTraitUse(Macroable::class)) {
+                    $macroTraitProperty = 'macros';
+                }
+            }
+        } elseif ($classReflection->hasTraitUse(Macroable::class) || $classReflection->getName() === Builder::class) {
+            $className = $classReflection->getName();
+            $macroTraitProperty = 'macros';
+        } elseif ($this->hasIndirectTraitUse($classReflection, CarbonMacro::class)) {
+            $className = $classReflection->getName();
+            $macroTraitProperty = 'globalMacros';
+        }
+
+        if ($className !== null && $macroTraitProperty) {
+            $classReflection = $passable->getReflectionProvider()->getClass($className);
+            $refObject = new \ReflectionClass($className);
+            $refProperty = $refObject->getProperty($macroTraitProperty);
+            $refProperty->setAccessible(true);
+
+            $found = $className === Builder::class
+                ? $className::hasGlobalMacro($passable->getMethodName())
+                : $className::hasMacro($passable->getMethodName());
+
+            if ($found) {
+                $reflectionFunction = new \ReflectionFunction($refProperty->getValue()[$passable->getMethodName()]);
+
+                $methodReflection = new Macro(
+                    $classReflection, $passable->getMethodName(), $reflectionFunction
+                );
+
+                $methodReflection->setIsStatic(true);
+
+                $passable->setMethodReflection($methodReflection);
+            }
+        }
+
+        if (! $found) {
+            $next($passable);
+        }
+    }
+}
diff --git a/src/Methods/Pipes/Managers.php b/src/Methods/Pipes/Managers.php
new file mode 100644
index 0000000..dfd41b4
--- /dev/null
+++ b/src/Methods/Pipes/Managers.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Methods\Pipes;
+
+use Closure;
+use Illuminate\Support\Manager;
+use InvalidArgumentException;
+use Flarum\PHPStan\Concerns;
+use Flarum\PHPStan\Contracts\Methods\PassableContract;
+use Flarum\PHPStan\Contracts\Methods\Pipes\PipeContract;
+
+/**
+ * @internal
+ */
+final class Managers implements PipeContract
+{
+    use Concerns\HasContainer;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function handle(PassableContract $passable, Closure $next): void
+    {
+        $classReflection = $passable->getClassReflection();
+
+        $found = false;
+
+        if ($classReflection->isSubclassOf(Manager::class)) {
+            $driver = null;
+
+            $concrete = $this->resolve(
+                $classReflection->getName()
+            );
+
+            try {
+                $driver = $concrete->driver();
+            } catch (InvalidArgumentException $exception) {
+                // ..
+            }
+
+            if ($driver !== null) {
+                $class = get_class($driver);
+
+                if ($class) {
+                    $found = $passable->sendToPipeline($class);
+                }
+            }
+        }
+
+        if (! $found) {
+            $next($passable);
+        }
+    }
+}
diff --git a/src/Methods/Pipes/SelfClass.php b/src/Methods/Pipes/SelfClass.php
new file mode 100644
index 0000000..330fda7
--- /dev/null
+++ b/src/Methods/Pipes/SelfClass.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Methods\Pipes;
+
+use Closure;
+use Flarum\PHPStan\Contracts\Methods\PassableContract;
+use Flarum\PHPStan\Contracts\Methods\Pipes\PipeContract;
+
+/**
+ * @internal
+ */
+final class SelfClass implements PipeContract
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function handle(PassableContract $passable, Closure $next): void
+    {
+        $className = $passable->getClassReflection()
+            ->getName();
+
+        if (! $passable->searchOn($className)) {
+            $next($passable);
+        }
+    }
+}
diff --git a/src/Methods/RelationForwardsCallsExtension.php b/src/Methods/RelationForwardsCallsExtension.php
new file mode 100644
index 0000000..9e77259
--- /dev/null
+++ b/src/Methods/RelationForwardsCallsExtension.php
@@ -0,0 +1,139 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Methods;
+
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\Relation;
+use Flarum\PHPStan\Reflection\EloquentBuilderMethodReflection;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\MethodsClassReflectionExtension;
+use PHPStan\Reflection\MissingMethodFromReflectionException;
+use PHPStan\Reflection\ParametersAcceptorSelector;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\ShouldNotHappenException;
+use PHPStan\Type\Generic\GenericObjectType;
+use PHPStan\Type\Generic\TemplateMixedType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+use PHPStan\Type\TypeWithClassName;
+
+final class RelationForwardsCallsExtension implements MethodsClassReflectionExtension
+{
+    /** @var BuilderHelper */
+    private $builderHelper;
+
+    /** @var array<string, MethodReflection> */
+    private $cache = [];
+
+    /** @var ReflectionProvider */
+    private $reflectionProvider;
+
+    /** @var EloquentBuilderForwardsCallsExtension */
+    private $eloquentBuilderForwardsCallsExtension;
+
+    public function __construct(BuilderHelper $builderHelper, ReflectionProvider $reflectionProvider, EloquentBuilderForwardsCallsExtension $eloquentBuilderForwardsCallsExtension)
+    {
+        $this->builderHelper = $builderHelper;
+        $this->reflectionProvider = $reflectionProvider;
+        $this->eloquentBuilderForwardsCallsExtension = $eloquentBuilderForwardsCallsExtension;
+    }
+
+    public function hasMethod(ClassReflection $classReflection, string $methodName): bool
+    {
+        if (array_key_exists($classReflection->getCacheKey().'-'.$methodName, $this->cache)) {
+            return true;
+        }
+
+        $methodReflection = $this->findMethod($classReflection, $methodName);
+
+        if ($methodReflection !== null) {
+            $this->cache[$classReflection->getCacheKey().'-'.$methodName] = $methodReflection;
+
+            return true;
+        }
+
+        return false;
+    }
+
+    public function getMethod(
+        ClassReflection $classReflection,
+        string $methodName
+    ): MethodReflection {
+        return $this->cache[$classReflection->getCacheKey().'-'.$methodName];
+    }
+
+    /**
+     * @throws MissingMethodFromReflectionException
+     * @throws ShouldNotHappenException
+     */
+    private function findMethod(ClassReflection $classReflection, string $methodName): ?MethodReflection
+    {
+        if (! $classReflection->isSubclassOf(Relation::class)) {
+            return null;
+        }
+
+        /** @var Type|TemplateMixedType|null $relatedModel */
+        $relatedModel = $classReflection->getActiveTemplateTypeMap()->getType('TRelatedModel');
+
+        if ($relatedModel === null) {
+            return null;
+        }
+
+        if ($relatedModel instanceof TypeWithClassName) {
+            $modelReflection = $relatedModel->getClassReflection();
+        } else {
+            $modelReflection = $this->reflectionProvider->getClass(Model::class);
+        }
+
+        if ($modelReflection === null) {
+            return null;
+        }
+
+        $builderName = $this->builderHelper->determineBuilderName($modelReflection->getName());
+
+        $builderReflection = $this->reflectionProvider->getClass($builderName)->withTypes([$relatedModel]);
+
+        if ($builderReflection->hasNativeMethod($methodName)) {
+            $reflection = $builderReflection->getNativeMethod($methodName);
+        } elseif ($this->eloquentBuilderForwardsCallsExtension->hasMethod($builderReflection, $methodName)) {
+            $reflection = $this->eloquentBuilderForwardsCallsExtension->getMethod($builderReflection, $methodName);
+        } else {
+            return null;
+        }
+
+        $parametersAcceptor = ParametersAcceptorSelector::selectSingle($reflection->getVariants());
+        $returnType = $parametersAcceptor->getReturnType();
+
+        $types = [$relatedModel];
+
+        // BelongsTo relation needs second generic type
+        if ($classReflection->getName() === BelongsTo::class) {
+            $childType = $classReflection->getActiveTemplateTypeMap()->getType('TChildModel');
+
+            if ($childType !== null) {
+                $types[] = $childType;
+            }
+        }
+
+        if ((new ObjectType(Builder::class))->isSuperTypeOf($returnType)->yes()) {
+            return new EloquentBuilderMethodReflection(
+                $methodName, $classReflection,
+                $reflection, $parametersAcceptor->getParameters(),
+                new GenericObjectType($classReflection->getName(), $types),
+                $parametersAcceptor->isVariadic()
+            );
+        }
+
+        return new EloquentBuilderMethodReflection(
+            $methodName, $classReflection,
+            $reflection, $parametersAcceptor->getParameters(),
+            $returnType,
+            $parametersAcceptor->isVariadic()
+        );
+    }
+}
diff --git a/src/Methods/StorageMethodsClassReflectionExtension.php b/src/Methods/StorageMethodsClassReflectionExtension.php
new file mode 100644
index 0000000..f1a2f2f
--- /dev/null
+++ b/src/Methods/StorageMethodsClassReflectionExtension.php
@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Methods;
+
+use Illuminate\Filesystem\FilesystemAdapter;
+use Illuminate\Filesystem\FilesystemManager;
+use Illuminate\Support\Facades\Storage;
+use Flarum\PHPStan\Reflection\StaticMethodReflection;
+use PHPStan\Analyser\OutOfClassScope;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\MethodsClassReflectionExtension;
+use PHPStan\Reflection\ReflectionProvider;
+
+class StorageMethodsClassReflectionExtension implements MethodsClassReflectionExtension
+{
+    /**
+     * @var ReflectionProvider
+     */
+    private $reflectionProvider;
+
+    public function __construct(ReflectionProvider $reflectionProvider)
+    {
+        $this->reflectionProvider = $reflectionProvider;
+    }
+
+    public function hasMethod(ClassReflection $classReflection, string $methodName): bool
+    {
+        if ($classReflection->getName() !== Storage::class) {
+            return false;
+        }
+
+        if ($this->reflectionProvider->getClass(FilesystemManager::class)->hasMethod($methodName)) {
+            return true;
+        }
+
+        if ($this->reflectionProvider->getClass(FilesystemAdapter::class)->hasMethod($methodName)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function getMethod(
+        ClassReflection $classReflection,
+        string $methodName
+    ): MethodReflection {
+        if ($this->reflectionProvider->getClass(FilesystemManager::class)->hasMethod($methodName)) {
+            return new StaticMethodReflection(
+                $this->reflectionProvider->getClass(FilesystemManager::class)->getMethod($methodName, new OutOfClassScope())
+            );
+        }
+
+        return new StaticMethodReflection(
+            $this->reflectionProvider->getClass(FilesystemAdapter::class)->getMethod($methodName, new OutOfClassScope())
+        );
+    }
+}
diff --git a/src/Properties/HigherOrderCollectionProxyPropertyExtension.php b/src/Properties/HigherOrderCollectionProxyPropertyExtension.php
new file mode 100644
index 0000000..87a7919
--- /dev/null
+++ b/src/Properties/HigherOrderCollectionProxyPropertyExtension.php
@@ -0,0 +1,118 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Properties;
+
+use Flarum\PHPStan\Support\HigherOrderCollectionProxyHelper;
+use PHPStan\Analyser\OutOfClassScope;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\PropertiesClassReflectionExtension;
+use PHPStan\Reflection\PropertyReflection;
+use PHPStan\TrinaryLogic;
+use PHPStan\Type;
+
+final class HigherOrderCollectionProxyPropertyExtension implements PropertiesClassReflectionExtension
+{
+    public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
+    {
+        return HigherOrderCollectionProxyHelper::hasPropertyOrMethod($classReflection, $propertyName, 'property');
+    }
+
+    public function getProperty(
+        ClassReflection $classReflection,
+        string $propertyName
+    ): PropertyReflection {
+        $activeTemplateTypeMap = $classReflection->getActiveTemplateTypeMap();
+
+        /** @var Type\Constant\ConstantStringType $methodType */
+        $methodType = $activeTemplateTypeMap->getType('T');
+
+        /** @var Type\ObjectType $modelType */
+        $modelType = $activeTemplateTypeMap->getType('TValue');
+
+        $propertyType = $modelType->getProperty($propertyName, new OutOfClassScope())->getReadableType();
+
+        $returnType = HigherOrderCollectionProxyHelper::determineReturnType($methodType->getValue(), $modelType, $propertyType);
+
+        return new class($classReflection, $returnType) implements PropertyReflection
+        {
+            /** @var ClassReflection */
+            private $classReflection;
+
+            /** @var Type\Type */
+            private $returnType;
+
+            public function __construct(ClassReflection $classReflection, Type\Type $returnType)
+            {
+                $this->classReflection = $classReflection;
+                $this->returnType = $returnType;
+            }
+
+            public function getDeclaringClass(): \PHPStan\Reflection\ClassReflection
+            {
+                return $this->classReflection;
+            }
+
+            public function isStatic(): bool
+            {
+                return false;
+            }
+
+            public function isPrivate(): bool
+            {
+                return false;
+            }
+
+            public function isPublic(): bool
+            {
+                return true;
+            }
+
+            public function getDocComment(): ?string
+            {
+                return null;
+            }
+
+            public function getReadableType(): Type\Type
+            {
+                return $this->returnType;
+            }
+
+            public function getWritableType(): Type\Type
+            {
+                return $this->returnType;
+            }
+
+            public function canChangeTypeAfterAssignment(): bool
+            {
+                return false;
+            }
+
+            public function isReadable(): bool
+            {
+                return true;
+            }
+
+            public function isWritable(): bool
+            {
+                return false;
+            }
+
+            public function isDeprecated(): \PHPStan\TrinaryLogic
+            {
+                return TrinaryLogic::createNo();
+            }
+
+            public function getDeprecatedDescription(): ?string
+            {
+                return null;
+            }
+
+            public function isInternal(): \PHPStan\TrinaryLogic
+            {
+                return TrinaryLogic::createNo();
+            }
+        };
+    }
+}
diff --git a/src/Properties/MigrationHelper.php b/src/Properties/MigrationHelper.php
new file mode 100644
index 0000000..7a59229
--- /dev/null
+++ b/src/Properties/MigrationHelper.php
@@ -0,0 +1,95 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Properties;
+
+use PHPStan\File\FileHelper;
+use PHPStan\Parser\Parser;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use RegexIterator;
+use SplFileInfo;
+
+class MigrationHelper
+{
+    /** @var Parser */
+    private $parser;
+
+    /** @var string[] */
+    private $databaseMigrationPath;
+
+    /** @var FileHelper */
+    private $fileHelper;
+
+    /**
+     * @param  string[]  $databaseMigrationPath
+     */
+    public function __construct(Parser $parser, array $databaseMigrationPath, FileHelper $fileHelper)
+    {
+        $this->parser = $parser;
+        $this->databaseMigrationPath = $databaseMigrationPath;
+        $this->fileHelper = $fileHelper;
+    }
+
+    /**
+     * @return array<string, SchemaTable>
+     */
+    public function initializeTables(): array
+    {
+        if (empty($this->databaseMigrationPath)) {
+            $this->databaseMigrationPath = [database_path('migrations')];
+        }
+
+        $schemaAggregator = new SchemaAggregator();
+        $filesArray = $this->getMigrationFiles();
+
+        if (empty($filesArray)) {
+            return [];
+        }
+
+        ksort($filesArray);
+
+        $this->requireFiles($filesArray);
+
+        foreach ($filesArray as $file) {
+            $schemaAggregator->addStatements($this->parser->parseFile($file->getPathname()));
+        }
+
+        return $schemaAggregator->tables;
+    }
+
+    /**
+     * @return SplFileInfo[]
+     */
+    private function getMigrationFiles(): array
+    {
+        /** @var SplFileInfo[] $migrationFiles */
+        $migrationFiles = [];
+
+        foreach ($this->databaseMigrationPath as $additionalPath) {
+            $absolutePath = $this->fileHelper->absolutizePath($additionalPath);
+
+            if (is_dir($absolutePath)) {
+                $migrationFiles += iterator_to_array(
+                    new RegexIterator(
+                        new RecursiveIteratorIterator(new RecursiveDirectoryIterator($absolutePath)),
+                        '/\.php$/i'
+                    )
+                );
+            }
+        }
+
+        return $migrationFiles;
+    }
+
+    /**
+     * @param  SplFileInfo[]  $files
+     */
+    private function requireFiles(array $files): void
+    {
+        foreach ($files as $file) {
+            require_once $file;
+        }
+    }
+}
diff --git a/src/Properties/ModelAccessorExtension.php b/src/Properties/ModelAccessorExtension.php
new file mode 100644
index 0000000..a2ee2d4
--- /dev/null
+++ b/src/Properties/ModelAccessorExtension.php
@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Properties;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Str;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\PropertiesClassReflectionExtension;
+use PHPStan\Reflection\PropertyReflection;
+
+/**
+ * @internal
+ */
+final class ModelAccessorExtension implements PropertiesClassReflectionExtension
+{
+    public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
+    {
+        if (! $classReflection->isSubclassOf(Model::class)) {
+            return false;
+        }
+
+        return $classReflection->hasNativeMethod('get'.Str::studly($propertyName).'Attribute');
+    }
+
+    public function getProperty(
+        ClassReflection $classReflection,
+        string $propertyName
+    ): PropertyReflection {
+        $method = $classReflection->getNativeMethod('get'.Str::studly($propertyName).'Attribute');
+
+        return new ModelProperty(
+            $classReflection,
+            $method->getVariants()[0]->getReturnType(),
+            $method->getVariants()[0]->getReturnType()
+        );
+    }
+}
diff --git a/src/Attributes/AttributeProperty.php b/src/Properties/ModelProperty.php
similarity index 62%
rename from src/Attributes/AttributeProperty.php
rename to src/Properties/ModelProperty.php
index d30f1d2..28625a5 100644
--- a/src/Attributes/AttributeProperty.php
+++ b/src/Properties/ModelProperty.php
@@ -1,35 +1,39 @@
 <?php
 
-/*
- * This file is part of Flarum.
- *
- * For detailed copyright and license information, please view the
- * LICENSE file that was distributed with this source code.
- */
+declare(strict_types=1);
 
-namespace Flarum\PHPStan\Attributes;
+namespace Flarum\PHPStan\Properties;
 
 use PHPStan\Reflection\ClassReflection;
 use PHPStan\Reflection\PropertyReflection;
 use PHPStan\TrinaryLogic;
 use PHPStan\Type\Type;
 
-class AttributeProperty implements PropertyReflection
+class ModelProperty implements PropertyReflection
 {
     /** @var ClassReflection */
-    private $classReflection;
+    private $declaringClass;
+
+    /** @var Type */
+    private $readableType;
+
     /** @var Type */
-    private $type;
+    private $writableType;
 
-    public function __construct(ClassReflection $classReflection, Type $type)
+    /** @var bool */
+    private $writeable;
+
+    public function __construct(ClassReflection $declaringClass, Type $readableType, Type $writableType, bool $writeable = true)
     {
-        $this->classReflection = $classReflection;
-        $this->type = $type;
+        $this->declaringClass = $declaringClass;
+        $this->readableType = $readableType;
+        $this->writableType = $writableType;
+        $this->writeable = $writeable;
     }
 
     public function getDeclaringClass(): ClassReflection
     {
-        return $this->classReflection;
+        return $this->declaringClass;
     }
 
     public function isStatic(): bool
@@ -47,34 +51,34 @@ public function isPublic(): bool
         return true;
     }
 
-    public function getDocComment(): ?string
+    public function isReadable(): bool
     {
-        return null;
+        return true;
     }
 
-    public function getReadableType(): Type
+    public function isWritable(): bool
     {
-        return $this->type;
+        return $this->writeable;
     }
 
-    public function getWritableType(): Type
+    public function getDocComment(): ?string
     {
-        return $this->getReadableType();
+        return null;
     }
 
-    public function canChangeTypeAfterAssignment(): bool
+    public function getReadableType(): Type
     {
-        return false;
+        return $this->readableType;
     }
 
-    public function isReadable(): bool
+    public function getWritableType(): Type
     {
-        return true;
+        return $this->writableType;
     }
 
-    public function isWritable(): bool
+    public function canChangeTypeAfterAssignment(): bool
     {
-        return true;
+        return false;
     }
 
     public function isDeprecated(): TrinaryLogic
diff --git a/src/Properties/ModelPropertyExtension.php b/src/Properties/ModelPropertyExtension.php
new file mode 100644
index 0000000..8923b49
--- /dev/null
+++ b/src/Properties/ModelPropertyExtension.php
@@ -0,0 +1,266 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Properties;
+
+use ArrayObject;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Str;
+use Flarum\PHPStan\Reflection\ReflectionHelper;
+use PHPStan\PhpDoc\TypeStringResolver;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\PropertiesClassReflectionExtension;
+use PHPStan\Reflection\PropertyReflection;
+use PHPStan\ShouldNotHappenException;
+use PHPStan\Type\IntegerType;
+
+/**
+ * @internal
+ */
+final class ModelPropertyExtension implements PropertiesClassReflectionExtension
+{
+    /** @var array<string, SchemaTable> */
+    private $tables = [];
+
+    /** @var TypeStringResolver */
+    private $stringResolver;
+
+    /** @var string */
+    private $dateClass;
+
+    /** @var MigrationHelper */
+    private $migrationHelper;
+
+    public function __construct(TypeStringResolver $stringResolver, MigrationHelper $migrationHelper)
+    {
+        $this->stringResolver = $stringResolver;
+        $this->migrationHelper = $migrationHelper;
+    }
+
+    public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
+    {
+        if (! $classReflection->isSubclassOf(Model::class)) {
+            return false;
+        }
+
+        if ($classReflection->isAbstract()) {
+            return false;
+        }
+
+        if ($classReflection->hasNativeMethod('get'.Str::studly($propertyName).'Attribute')) {
+            return false;
+        }
+
+        if (ReflectionHelper::hasPropertyTag($classReflection, $propertyName)) {
+            return false;
+        }
+
+        if (count($this->tables) === 0) {
+            $this->tables = $this->migrationHelper->initializeTables();
+        }
+
+        if ($propertyName === 'id') {
+            return true;
+        }
+
+        $modelName = $classReflection->getNativeReflection()->getName();
+
+        try {
+            $reflect = new \ReflectionClass($modelName);
+
+            /** @var Model $modelInstance */
+            $modelInstance = $reflect->newInstanceWithoutConstructor();
+
+            $tableName = $modelInstance->getTable();
+        } catch (\ReflectionException $e) {
+            return false;
+        }
+
+        if (! array_key_exists($tableName, $this->tables)) {
+            return false;
+        }
+
+        if (! array_key_exists($propertyName, $this->tables[$tableName]->columns)) {
+            return false;
+        }
+
+        $this->castPropertiesType($modelInstance);
+
+        $column = $this->tables[$tableName]->columns[$propertyName];
+
+        [$readableType, $writableType] = $this->getReadableAndWritableTypes($column, $modelInstance);
+
+        $column->readableType = $readableType;
+        $column->writeableType = $writableType;
+
+        $this->tables[$tableName]->columns[$propertyName] = $column;
+
+        return true;
+    }
+
+    public function getProperty(
+        ClassReflection $classReflection,
+        string $propertyName
+    ): PropertyReflection {
+        $modelName = $classReflection->getNativeReflection()->getName();
+
+        try {
+            $reflect = new \ReflectionClass($modelName);
+
+            /** @var Model $modelInstance */
+            $modelInstance = $reflect->newInstanceWithoutConstructor();
+
+            $tableName = $modelInstance->getTable();
+        } catch (\ReflectionException $e) {
+            // `hasProperty` should return false if there was a reflection exception.
+            // so this should never happen
+            throw new ShouldNotHappenException();
+        }
+
+        if (
+            (! array_key_exists($tableName, $this->tables)
+                || ! array_key_exists($propertyName, $this->tables[$tableName]->columns)
+            )
+            && $propertyName === 'id'
+        ) {
+            return new ModelProperty(
+                $classReflection,
+                new IntegerType(),
+                new IntegerType()
+            );
+        }
+
+        $column = $this->tables[$tableName]->columns[$propertyName];
+
+        return new ModelProperty(
+            $classReflection,
+            $this->stringResolver->resolve($column->readableType),
+            $this->stringResolver->resolve($column->writeableType)
+        );
+    }
+
+    private function getDateClass(): string
+    {
+        if (! $this->dateClass) {
+            $this->dateClass = class_exists(\Illuminate\Support\Facades\Date::class)
+                ? '\\'.get_class(\Illuminate\Support\Facades\Date::now())
+                : '\Illuminate\Support\Carbon';
+
+            $this->dateClass .= '|\Carbon\Carbon';
+        }
+
+        return $this->dateClass;
+    }
+
+    /**
+     * @param  SchemaColumn  $column
+     * @param  Model  $modelInstance
+     * @return string[]
+     * @phpstan-return array<int, string>
+     */
+    private function getReadableAndWritableTypes(SchemaColumn $column, Model $modelInstance): array
+    {
+        $readableType = $column->readableType;
+        $writableType = $column->writeableType;
+
+        if (in_array($column->name, $modelInstance->getDates(), true)) {
+            return [$this->getDateClass().($column->nullable ? '|null' : ''), $this->getDateClass().'|string'.($column->nullable ? '|null' : '')];
+        }
+
+        switch ($column->readableType) {
+            case 'string':
+            case 'int':
+            case 'float':
+                $readableType = $writableType = $column->readableType.($column->nullable ? '|null' : '');
+                break;
+
+            case 'boolean':
+            case 'bool':
+                switch ((string) config('database.default')) {
+                    case 'sqlite':
+                    case 'mysql':
+                        $writableType = '0|1|bool';
+                        $readableType = 'bool';
+                        break;
+                    default:
+                        $readableType = $writableType = 'bool';
+                        break;
+                }
+                break;
+            case 'enum':
+                if (! $column->options) {
+                    $readableType = $writableType = 'string';
+                } else {
+                    $readableType = $writableType = '\''.implode('\'|\'', $column->options).'\'';
+                }
+
+                break;
+
+            default:
+                break;
+        }
+
+        return [$readableType, $writableType];
+    }
+
+    private function castPropertiesType(Model $modelInstance): void
+    {
+        $casts = $modelInstance->getCasts();
+        foreach ($casts as $name => $type) {
+            if (! array_key_exists($name, $this->tables[$modelInstance->getTable()]->columns)) {
+                continue;
+            }
+
+            switch ($type) {
+                case 'boolean':
+                case 'bool':
+                    $realType = 'boolean';
+                    break;
+                case 'string':
+                    $realType = 'string';
+                    break;
+                case 'array':
+                case 'json':
+                    $realType = 'array';
+                    break;
+                case 'object':
+                    $realType = 'object';
+                    break;
+                case 'int':
+                case 'integer':
+                case 'timestamp':
+                    $realType = 'integer';
+                    break;
+                case 'real':
+                case 'double':
+                case 'float':
+                    $realType = 'float';
+                    break;
+                case 'date':
+                case 'datetime':
+                    $realType = $this->getDateClass();
+                    break;
+                case 'collection':
+                    $realType = '\Illuminate\Support\Collection';
+                    break;
+                case 'Illuminate\Database\Eloquent\Casts\AsArrayObject':
+                    $realType = ArrayObject::class;
+                    break;
+                case 'Illuminate\Database\Eloquent\Casts\AsCollection':
+                    $realType = '\Illuminate\Support\Collection<mixed, mixed>';
+                    break;
+                default:
+                    $realType = class_exists($type) ? ('\\'.$type) : 'mixed';
+                    break;
+            }
+
+            if ($this->tables[$modelInstance->getTable()]->columns[$name]->nullable) {
+                $realType .= '|null';
+            }
+
+            $this->tables[$modelInstance->getTable()]->columns[$name]->readableType = $realType;
+            $this->tables[$modelInstance->getTable()]->columns[$name]->writeableType = $realType;
+        }
+    }
+}
diff --git a/src/Properties/ModelRelationsExtension.php b/src/Properties/ModelRelationsExtension.php
new file mode 100644
index 0000000..f38e09e
--- /dev/null
+++ b/src/Properties/ModelRelationsExtension.php
@@ -0,0 +1,116 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Properties;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\Relation;
+use Illuminate\Support\Str;
+use Flarum\PHPStan\Concerns;
+use Flarum\PHPStan\Methods\BuilderHelper;
+use Flarum\PHPStan\Reflection\ReflectionHelper;
+use Flarum\PHPStan\Types\RelationParserHelper;
+use PHPStan\Analyser\OutOfClassScope;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\ParametersAcceptorSelector;
+use PHPStan\Reflection\PropertiesClassReflectionExtension;
+use PHPStan\Reflection\PropertyReflection;
+use PHPStan\Type\Generic\GenericObjectType;
+use PHPStan\Type\MixedType;
+use PHPStan\Type\NeverType;
+use PHPStan\Type\NullType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\UnionType;
+
+/**
+ * @internal
+ */
+final class ModelRelationsExtension implements PropertiesClassReflectionExtension
+{
+    use Concerns\HasContainer;
+
+    /** @var RelationParserHelper */
+    private $relationParserHelper;
+
+    /** @var BuilderHelper */
+    private $builderHelper;
+
+    public function __construct(
+        RelationParserHelper $relationParserHelper,
+        BuilderHelper $builderHelper)
+    {
+        $this->relationParserHelper = $relationParserHelper;
+        $this->builderHelper = $builderHelper;
+    }
+
+    public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
+    {
+        if (! $classReflection->isSubclassOf(Model::class)) {
+            return false;
+        }
+
+        if (ReflectionHelper::hasPropertyTag($classReflection, $propertyName)) {
+            return false;
+        }
+
+        $hasNativeMethod = $classReflection->hasNativeMethod($propertyName);
+
+        if (! $hasNativeMethod) {
+            return false;
+        }
+
+        $returnType = ParametersAcceptorSelector::selectSingle($classReflection->getNativeMethod($propertyName)->getVariants())->getReturnType();
+
+        if (! (new ObjectType(Relation::class))->isSuperTypeOf($returnType)->yes()) {
+            return false;
+        }
+
+        return true;
+    }
+
+    public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
+    {
+        $method = $classReflection->getMethod($propertyName, new OutOfClassScope());
+
+        /** @var ObjectType $returnType */
+        $returnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType();
+
+        if ($returnType instanceof GenericObjectType) {
+            /** @var ObjectType $relatedModelType */
+            $relatedModelType = $returnType->getTypes()[0];
+            $relatedModelClassName = $relatedModelType->getClassName();
+        } else {
+            $relatedModelClassName = $this
+                ->relationParserHelper
+                ->findRelatedModelInRelationMethod($method);
+        }
+
+        if ($relatedModelClassName === null) {
+            $relatedModelClassName = Model::class;
+        }
+
+        $relatedModel = new ObjectType($relatedModelClassName);
+        $collectionClass = $this->builderHelper->determineCollectionClassName($relatedModelClassName);
+
+        if (Str::contains($returnType->getClassName(), 'Many')) {
+            return new ModelProperty(
+                $classReflection,
+                new GenericObjectType($collectionClass, [$relatedModel]),
+                new NeverType(), false
+            );
+        }
+
+        if (Str::endsWith($returnType->getClassName(), 'MorphTo')) {
+            return new ModelProperty($classReflection, new UnionType([
+                new ObjectType(Model::class),
+                new MixedType(),
+            ]), new NeverType(), false);
+        }
+
+        return new ModelProperty($classReflection, new UnionType([
+            $relatedModel,
+            new NullType(),
+        ]), new NeverType(), false);
+    }
+}
diff --git a/src/Properties/ReflectionTypeContainer.php b/src/Properties/ReflectionTypeContainer.php
new file mode 100644
index 0000000..c296569
--- /dev/null
+++ b/src/Properties/ReflectionTypeContainer.php
@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Properties;
+
+use ReflectionNamedType;
+
+/**
+ * @internal
+ */
+final class ReflectionTypeContainer extends ReflectionNamedType
+{
+    /**
+     * @var string
+     */
+    private $type;
+
+    /**
+     * ReflectionTypeContainer constructor.
+     *
+     * @param  string  $type
+     */
+    public function __construct(string $type)
+    {
+        $this->type = $type;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function allowsNull(): bool
+    {
+        return false;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isBuiltin(): bool
+    {
+        return false;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function __toString(): string
+    {
+        return $this->getName();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getName(): string
+    {
+        return $this->type;
+    }
+}
diff --git a/src/Properties/SchemaAggregator.php b/src/Properties/SchemaAggregator.php
new file mode 100644
index 0000000..fa3099d
--- /dev/null
+++ b/src/Properties/SchemaAggregator.php
@@ -0,0 +1,432 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Properties;
+
+use Illuminate\Support\Str;
+use PhpParser;
+use PhpParser\NodeFinder;
+
+/**
+ * @see https://github.com/psalm/laravel-psalm-plugin/blob/master/src/SchemaAggregator.php
+ */
+final class SchemaAggregator
+{
+    /** @var array<string, SchemaTable> */
+    public $tables = [];
+
+    /**
+     * @param  array<int, PhpParser\Node\Stmt>  $stmts
+     */
+    public function addStatements(array $stmts): void
+    {
+        $nodeFinder = new NodeFinder();
+
+        /** @var PhpParser\Node\Stmt\Class_[] $classes */
+        $classes = $nodeFinder->findInstanceOf($stmts, PhpParser\Node\Stmt\Class_::class);
+
+        foreach ($classes as $stmt) {
+            $this->addClassStatements($stmt->stmts);
+        }
+    }
+
+    /**
+     * @param  array<int, PhpParser\Node\Stmt>  $stmts
+     */
+    private function addClassStatements(array $stmts): void
+    {
+        foreach ($stmts as $stmt) {
+            if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod
+                && $stmt->name->name !== 'down'
+                && $stmt->stmts
+            ) {
+                $this->addUpMethodStatements($stmt->stmts);
+            }
+        }
+    }
+
+    /**
+     * @param  PhpParser\Node\Stmt[]  $stmts
+     */
+    private function addUpMethodStatements(array $stmts): void
+    {
+        $nodeFinder = new NodeFinder();
+        $methods = $nodeFinder->findInstanceOf($stmts, PhpParser\Node\Stmt\Expression::class);
+
+        foreach ($methods as $stmt) {
+            if ($stmt instanceof PhpParser\Node\Stmt\Expression
+                && $stmt->expr instanceof PhpParser\Node\Expr\StaticCall
+                && ($stmt->expr->class instanceof PhpParser\Node\Name)
+                && $stmt->expr->name instanceof PhpParser\Node\Identifier
+                && ($stmt->expr->class->toCodeString() === '\Illuminate\Support\Facades\Schema' || $stmt->expr->class->toCodeString() === '\Schema')
+            ) {
+                switch ($stmt->expr->name->name) {
+                    case 'create':
+                        $this->alterTable($stmt->expr, true);
+                        break;
+
+                    case 'table':
+                        $this->alterTable($stmt->expr, false);
+                        break;
+
+                    case 'drop':
+                    case 'dropIfExists':
+                        $this->dropTable($stmt->expr);
+                        break;
+
+                    case 'rename':
+                        $this->renameTable($stmt->expr);
+                }
+            }
+        }
+    }
+
+    private function alterTable(PhpParser\Node\Expr\StaticCall $call, bool $creating): void
+    {
+        if (! isset($call->args[0])
+            || ! $call->getArgs()[0]->value instanceof PhpParser\Node\Scalar\String_
+        ) {
+            return;
+        }
+
+        $tableName = $call->getArgs()[0]->value->value;
+
+        if ($creating) {
+            $this->tables[$tableName] = new SchemaTable($tableName);
+        }
+
+        if (! isset($call->args[1])
+            || ! $call->getArgs()[1]->value instanceof PhpParser\Node\Expr\Closure
+            || count($call->getArgs()[1]->value->params) < 1
+            || ($call->getArgs()[1]->value->params[0]->type instanceof PhpParser\Node\Name
+                && $call->getArgs()[1]->value->params[0]->type->toCodeString()
+                !== '\\Illuminate\Database\Schema\Blueprint')
+        ) {
+            return;
+        }
+
+        $updateClosure = $call->getArgs()[1]->value;
+
+        if ($call->getArgs()[1]->value->params[0]->var instanceof PhpParser\Node\Expr\Variable
+            && is_string($call->getArgs()[1]->value->params[0]->var->name)
+        ) {
+            $argName = $call->getArgs()[1]->value->params[0]->var->name;
+
+            $this->processColumnUpdates($tableName, $argName, $updateClosure->stmts);
+        }
+    }
+
+    /**
+     * @param  string  $tableName
+     * @param  string  $argName
+     * @param  PhpParser\Node\Stmt[]  $stmts
+     *
+     * @throws \Exception
+     */
+    private function processColumnUpdates(string $tableName, string $argName, array $stmts): void
+    {
+        if (! isset($this->tables[$tableName])) {
+            return;
+        }
+
+        $table = $this->tables[$tableName];
+
+        foreach ($stmts as $stmt) {
+            if ($stmt instanceof PhpParser\Node\Stmt\Expression
+                && $stmt->expr instanceof PhpParser\Node\Expr\MethodCall
+                && $stmt->expr->name instanceof PhpParser\Node\Identifier
+            ) {
+                $rootVar = $stmt->expr;
+
+                $firstMethodCall = $rootVar;
+
+                $nullable = false;
+
+                while ($rootVar instanceof PhpParser\Node\Expr\MethodCall) {
+                    if ($rootVar->name instanceof PhpParser\Node\Identifier
+                        && $rootVar->name->name === 'nullable'
+                    ) {
+                        $nullable = true;
+                    }
+
+                    $firstMethodCall = $rootVar;
+                    $rootVar = $rootVar->var;
+                }
+
+                if ($rootVar instanceof PhpParser\Node\Expr\Variable
+                    && $rootVar->name === $argName
+                    && $firstMethodCall->name instanceof PhpParser\Node\Identifier
+                ) {
+                    $firstArg = $firstMethodCall->getArgs()[0]->value ?? null;
+                    $secondArg = $firstMethodCall->getArgs()[1]->value ?? null;
+
+                    if ($firstMethodCall->name->name === 'foreignIdFor') {
+                        if ($firstArg instanceof PhpParser\Node\Expr\ClassConstFetch
+                            && $firstArg->class instanceof PhpParser\Node\Name
+                        ) {
+                            $modelClass = $firstArg->class->toCodeString();
+                        } elseif ($firstArg instanceof PhpParser\Node\Scalar\String_) {
+                            $modelClass = $firstArg->value;
+                        } else {
+                            continue;
+                        }
+
+                        $columnName = Str::snake(class_basename($modelClass)).'_id';
+                        if ($secondArg instanceof PhpParser\Node\Scalar\String_) {
+                            $columnName = $secondArg->value;
+                        }
+
+                        $table->setColumn(new SchemaColumn($columnName, 'int', $nullable));
+
+                        continue;
+                    }
+
+                    if (! $firstArg instanceof PhpParser\Node\Scalar\String_) {
+                        if ($firstMethodCall->name->name === 'timestamps'
+                            || $firstMethodCall->name->name === 'timestampsTz'
+                            || $firstMethodCall->name->name === 'nullableTimestamps'
+                            || $firstMethodCall->name->name === 'nullableTimestampsTz'
+                            || $firstMethodCall->name->name === 'rememberToken'
+                        ) {
+                            switch (strtolower($firstMethodCall->name->name)) {
+                                case 'droptimestamps':
+                                case 'droptimestampstz':
+                                    $table->dropColumn('created_at');
+                                    $table->dropColumn('updated_at');
+                                    break;
+
+                                case 'remembertoken':
+                                    $table->setColumn(new SchemaColumn('remember_token', 'string', $nullable));
+                                    break;
+
+                                case 'dropremembertoken':
+                                    $table->dropColumn('remember_token');
+                                    break;
+
+                                case 'timestamps':
+                                case 'timestampstz':
+                                case 'nullabletimestamps':
+                                    $table->setColumn(new SchemaColumn('created_at', 'string', true));
+                                    $table->setColumn(new SchemaColumn('updated_at', 'string', true));
+                                    break;
+                            }
+
+                            continue;
+                        }
+
+                        if ($firstMethodCall->name->name === 'softDeletes'
+                            || $firstMethodCall->name->name === 'softDeletesTz'
+                            || $firstMethodCall->name->name === 'dropSoftDeletes'
+                            || $firstMethodCall->name->name === 'dropSoftDeletesTz'
+                        ) {
+                            $columnName = 'deleted_at';
+                        } else {
+                            continue;
+                        }
+                    } else {
+                        $columnName = $firstArg->value;
+                    }
+
+                    $secondArgArray = null;
+
+                    if ($secondArg instanceof PhpParser\Node\Expr\Array_) {
+                        $secondArgArray = [];
+
+                        foreach ($secondArg->items as $array_item) {
+                            if ($array_item !== null && $array_item->value instanceof PhpParser\Node\Scalar\String_) {
+                                $secondArgArray[] = $array_item->value->value;
+                            }
+                        }
+                    }
+
+                    switch (strtolower($firstMethodCall->name->name)) {
+                        case 'biginteger':
+                        case 'increments':
+                        case 'integer':
+                        case 'integerincrements':
+                        case 'mediumincrements':
+                        case 'mediuminteger':
+                        case 'smallincrements':
+                        case 'smallinteger':
+                        case 'tinyincrements':
+                        case 'tinyinteger':
+                        case 'unsignedbiginteger':
+                        case 'unsignedinteger':
+                        case 'unsignedmediuminteger':
+                        case 'unsignedsmallinteger':
+                        case 'unsignedtinyinteger':
+                        case 'bigincrements':
+                            $table->setColumn(new SchemaColumn($columnName, 'int', $nullable));
+                            break;
+
+                        case 'char':
+                        case 'datetimetz':
+                        case 'date':
+                        case 'datetime':
+                        case 'ipaddress':
+                        case 'json':
+                        case 'jsonb':
+                        case 'linestring':
+                        case 'longtext':
+                        case 'macaddress':
+                        case 'mediumtext':
+                        case 'multilinestring':
+                        case 'string':
+                        case 'text':
+                        case 'time':
+                        case 'timestamp':
+                        case 'uuid':
+                        case 'binary':
+                            $table->setColumn(new SchemaColumn($columnName, 'string', $nullable));
+                            break;
+
+                        case 'boolean':
+                            $table->setColumn(new SchemaColumn($columnName, 'bool', $nullable));
+                            break;
+
+                        case 'geometry':
+                        case 'geometrycollection':
+                        case 'multipoint':
+                        case 'multipolygon':
+                        case 'multipolygonz':
+                        case 'point':
+                        case 'polygon':
+                        case 'computed':
+                            $table->setColumn(new SchemaColumn($columnName, 'mixed', $nullable));
+                            break;
+
+                        case 'double':
+                        case 'float':
+                        case 'unsigneddecimal':
+                        case 'decimal':
+                            $table->setColumn(new SchemaColumn($columnName, 'float', $nullable));
+                            break;
+
+                        case 'after':
+                            if ($secondArg instanceof PhpParser\Node\Expr\Closure
+                                && $secondArg->params[0]->var instanceof PhpParser\Node\Expr\Variable
+                                && ! ($secondArg->params[0]->var->name instanceof PhpParser\Node\Expr)) {
+                                $argName = $secondArg->params[0]->var->name;
+                                $this->processColumnUpdates($tableName, $argName, $secondArg->stmts);
+                            }
+                            break;
+
+                        case 'dropcolumn':
+                        case 'dropifexists':
+                        case 'dropsoftdeletes':
+                        case 'dropsoftdeletestz':
+                        case 'removecolumn':
+                        case 'drop':
+                            $table->dropColumn($columnName);
+                            break;
+
+                        case 'dropforeign':
+                        case 'dropindex':
+                        case 'dropprimary':
+                        case 'dropunique':
+                        case 'foreign':
+                        case 'index':
+                        case 'primary':
+                        case 'renameindex':
+                        case 'spatialIndex':
+                        case 'unique':
+                        case 'dropspatialindex':
+                            break;
+
+                        case 'dropmorphs':
+                            $table->dropColumn($columnName.'_type');
+                            $table->dropColumn($columnName.'_id');
+                            break;
+
+                        case 'enum':
+                            $table->setColumn(new SchemaColumn($columnName, 'enum', $nullable, $secondArgArray));
+                            break;
+
+                        case 'morphs':
+                            $table->setColumn(new SchemaColumn($columnName.'_type', 'string', $nullable));
+                            $table->setColumn(new SchemaColumn($columnName.'_id', 'int', $nullable));
+                            break;
+
+                        case 'nullablemorphs':
+                            $table->setColumn(new SchemaColumn($columnName.'_type', 'string', true));
+                            $table->setColumn(new SchemaColumn($columnName.'_id', 'int', true));
+                            break;
+
+                        case 'nullableuuidmorphs':
+                            $table->setColumn(new SchemaColumn($columnName.'_type', 'string', true));
+                            $table->setColumn(new SchemaColumn($columnName.'_id', 'string', true));
+                            break;
+
+                        case 'rename':
+                        case 'renamecolumn':
+                            if ($secondArg instanceof PhpParser\Node\Scalar\String_) {
+                                $table->renameColumn($columnName, $secondArg->value);
+                            }
+                            break;
+
+                        case 'set':
+                            $table->setColumn(new SchemaColumn($columnName, 'set', $nullable, $secondArgArray));
+                            break;
+
+                        case 'softdeletestz':
+                        case 'timestamptz':
+                        case 'timetz':
+                        case 'year':
+                        case 'softdeletes':
+                            $table->setColumn(new SchemaColumn($columnName, 'string', true));
+                            break;
+
+                        case 'uuidmorphs':
+                            $table->setColumn(new SchemaColumn($columnName.'_type', 'string', $nullable));
+                            $table->setColumn(new SchemaColumn($columnName.'_id', 'string', $nullable));
+                            break;
+
+                        default:
+                            // We know a property exists with a name, we just don't know its type.
+                            $table->setColumn(new SchemaColumn($columnName, 'mixed', $nullable));
+                            break;
+                    }
+                }
+            }
+        }
+    }
+
+    private function dropTable(PhpParser\Node\Expr\StaticCall $call): void
+    {
+        if (! isset($call->args[0])
+            || ! $call->getArgs()[0]->value instanceof PhpParser\Node\Scalar\String_
+        ) {
+            return;
+        }
+
+        $tableName = $call->getArgs()[0]->value->value;
+
+        unset($this->tables[$tableName]);
+    }
+
+    private function renameTable(PhpParser\Node\Expr\StaticCall $call): void
+    {
+        if (! isset($call->args[0], $call->args[1])
+            || ! $call->getArgs()[0]->value instanceof PhpParser\Node\Scalar\String_
+            || ! $call->getArgs()[1]->value instanceof PhpParser\Node\Scalar\String_
+        ) {
+            return;
+        }
+
+        $oldTableName = $call->getArgs()[0]->value->value;
+        $newTableName = $call->getArgs()[1]->value->value;
+
+        if (! isset($this->tables[$oldTableName])) {
+            return;
+        }
+
+        $table = $this->tables[$oldTableName];
+
+        unset($this->tables[$oldTableName]);
+
+        $table->name = $newTableName;
+
+        $this->tables[$newTableName] = $table;
+    }
+}
diff --git a/src/Properties/SchemaColumn.php b/src/Properties/SchemaColumn.php
new file mode 100644
index 0000000..18c5c4c
--- /dev/null
+++ b/src/Properties/SchemaColumn.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Properties;
+
+/**
+ * @see https://github.com/psalm/laravel-psalm-plugin/blob/master/src/SchemaColumn.php
+ */
+final class SchemaColumn
+{
+    /** @var string */
+    public $name;
+
+    /** @var string */
+    public $readableType;
+
+    /** @var string */
+    public $writeableType;
+
+    /** @var bool */
+    public $nullable;
+
+    /** @var ?array<int, string> */
+    public $options;
+
+    /**
+     * @param  string  $name
+     * @param  string  $readableType
+     * @param  bool  $nullable
+     * @param  string[]|null  $options
+     */
+    public function __construct(
+        string $name,
+        string $readableType,
+        bool $nullable = false,
+        ?array $options = null
+    ) {
+        $this->name = $name;
+        $this->readableType = $readableType;
+        $this->writeableType = $readableType;
+        $this->nullable = $nullable;
+        $this->options = $options;
+    }
+}
diff --git a/src/Properties/SchemaTable.php b/src/Properties/SchemaTable.php
new file mode 100644
index 0000000..05631ba
--- /dev/null
+++ b/src/Properties/SchemaTable.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Properties;
+
+/**
+ * @see https://github.com/psalm/laravel-psalm-plugin/blob/master/src/SchemaTable.php
+ */
+final class SchemaTable
+{
+    /** @var string */
+    public $name;
+
+    /** @var SchemaColumn[] */
+    public $columns = [];
+
+    public function __construct(string $name)
+    {
+        $this->name = $name;
+    }
+
+    public function setColumn(SchemaColumn $column): void
+    {
+        $this->columns[$column->name] = $column;
+    }
+
+    public function renameColumn(string $oldName, string $newName): void
+    {
+        if (! isset($this->columns[$oldName])) {
+            return;
+        }
+
+        $oldColumn = $this->columns[$oldName];
+
+        unset($this->columns[$oldName]);
+
+        $oldColumn->name = $newName;
+
+        $this->columns[$newName] = $oldColumn;
+    }
+
+    public function dropColumn(string $columnName): void
+    {
+        unset($this->columns[$columnName]);
+    }
+}
diff --git a/src/Reflection/AnnotationScopeMethodParameterReflection.php b/src/Reflection/AnnotationScopeMethodParameterReflection.php
new file mode 100644
index 0000000..9f3420a
--- /dev/null
+++ b/src/Reflection/AnnotationScopeMethodParameterReflection.php
@@ -0,0 +1,70 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Reflection;
+
+use PHPStan\Reflection\ParameterReflection;
+use PHPStan\Reflection\PassedByReference;
+use PHPStan\Type\Type;
+
+final class AnnotationScopeMethodParameterReflection implements ParameterReflection
+{
+    /** @var string */
+    private $name;
+
+    /** @var Type */
+    private $type;
+
+    /** @var PassedByReference */
+    private $passedByReference;
+
+    /** @var bool */
+    private $isOptional;
+
+    /** @var bool */
+    private $isVariadic;
+
+    /** @var Type|null */
+    private $defaultValue;
+
+    public function __construct(string $name, Type $type, PassedByReference $passedByReference, bool $isOptional, bool $isVariadic, ?Type $defaultValue)
+    {
+        $this->name = $name;
+        $this->type = $type;
+        $this->passedByReference = $passedByReference;
+        $this->isOptional = $isOptional;
+        $this->isVariadic = $isVariadic;
+        $this->defaultValue = $defaultValue;
+    }
+
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    public function isOptional(): bool
+    {
+        return $this->isOptional;
+    }
+
+    public function getType(): Type
+    {
+        return $this->type;
+    }
+
+    public function passedByReference(): PassedByReference
+    {
+        return $this->passedByReference;
+    }
+
+    public function isVariadic(): bool
+    {
+        return $this->isVariadic;
+    }
+
+    public function getDefaultValue(): ?Type
+    {
+        return $this->defaultValue;
+    }
+}
diff --git a/src/Reflection/AnnotationScopeMethodReflection.php b/src/Reflection/AnnotationScopeMethodReflection.php
new file mode 100644
index 0000000..ada3b4f
--- /dev/null
+++ b/src/Reflection/AnnotationScopeMethodReflection.php
@@ -0,0 +1,133 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Reflection;
+
+use PHPStan\Reflection\ClassMemberReflection;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\FunctionVariant;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\ParametersAcceptor;
+use PHPStan\TrinaryLogic;
+use PHPStan\Type\Generic\TemplateTypeMap;
+use PHPStan\Type\Type;
+
+final class AnnotationScopeMethodReflection implements MethodReflection
+{
+    /** @var string */
+    private $name;
+
+    /** @var ClassReflection */
+    private $declaringClass;
+
+    /** @var Type */
+    private $returnType;
+
+    /** @var bool */
+    private $isStatic;
+
+    /** @var AnnotationScopeMethodParameterReflection[] */
+    private $parameters;
+
+    /** @var bool */
+    private $isVariadic;
+
+    /** @var FunctionVariant[]|null */
+    private $variants = null;
+
+    /**
+     * @param  string  $name
+     * @param  ClassReflection  $declaringClass
+     * @param  Type  $returnType
+     * @param  AnnotationScopeMethodParameterReflection[]  $parameters
+     * @param  bool  $isStatic
+     * @param  bool  $isVariadic
+     */
+    public function __construct(string $name, ClassReflection $declaringClass, Type $returnType, array $parameters, bool $isStatic, bool $isVariadic)
+    {
+        $this->name = $name;
+        $this->declaringClass = $declaringClass;
+        $this->returnType = $returnType;
+        $this->parameters = $parameters;
+        $this->isStatic = $isStatic;
+        $this->isVariadic = $isVariadic;
+    }
+
+    public function getDeclaringClass(): ClassReflection
+    {
+        return $this->declaringClass;
+    }
+
+    public function getPrototype(): ClassMemberReflection
+    {
+        return $this;
+    }
+
+    public function isStatic(): bool
+    {
+        return $this->isStatic;
+    }
+
+    public function isPrivate(): bool
+    {
+        return false;
+    }
+
+    public function isPublic(): bool
+    {
+        return true;
+    }
+
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    /**
+     * @return ParametersAcceptor[]
+     */
+    public function getVariants(): array
+    {
+        if ($this->variants === null) {
+            $this->variants = [new FunctionVariant(TemplateTypeMap::createEmpty(), null, $this->parameters, $this->isVariadic, $this->returnType)];
+        }
+
+        return $this->variants;
+    }
+
+    public function isDeprecated(): TrinaryLogic
+    {
+        return TrinaryLogic::createNo();
+    }
+
+    public function getDeprecatedDescription(): ?string
+    {
+        return null;
+    }
+
+    public function isFinal(): TrinaryLogic
+    {
+        return TrinaryLogic::createNo();
+    }
+
+    public function isInternal(): TrinaryLogic
+    {
+        return TrinaryLogic::createNo();
+    }
+
+    public function getThrowType(): ?Type
+    {
+        return null;
+    }
+
+    public function hasSideEffects(): TrinaryLogic
+    {
+        return TrinaryLogic::createMaybe();
+    }
+
+    public function getDocComment(): ?string
+    {
+        return null;
+    }
+}
diff --git a/src/Reflection/DynamicWhereParameterReflection.php b/src/Reflection/DynamicWhereParameterReflection.php
new file mode 100644
index 0000000..cb8cfd1
--- /dev/null
+++ b/src/Reflection/DynamicWhereParameterReflection.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Reflection;
+
+use PHPStan\Reflection\ParameterReflection;
+use PHPStan\Reflection\PassedByReference;
+use PHPStan\Type\MixedType;
+use PHPStan\Type\Type;
+
+class DynamicWhereParameterReflection implements ParameterReflection
+{
+    public function getName(): string
+    {
+        return 'dynamic-where-parameter';
+    }
+
+    public function isOptional(): bool
+    {
+        return true;
+    }
+
+    public function getType(): Type
+    {
+        return new MixedType();
+    }
+
+    public function passedByReference(): PassedByReference
+    {
+        return PassedByReference::createNo();
+    }
+
+    public function isVariadic(): bool
+    {
+        return false;
+    }
+
+    public function getDefaultValue(): ?Type
+    {
+        return null;
+    }
+}
diff --git a/src/Reflection/EloquentBuilderMethodReflection.php b/src/Reflection/EloquentBuilderMethodReflection.php
new file mode 100644
index 0000000..3179eb6
--- /dev/null
+++ b/src/Reflection/EloquentBuilderMethodReflection.php
@@ -0,0 +1,154 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Reflection;
+
+use Illuminate\Database\Eloquent\Builder;
+use PHPStan\Reflection\ClassMemberReflection;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\FunctionVariant;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\ParameterReflection;
+use PHPStan\TrinaryLogic;
+use PHPStan\Type\Generic\TemplateTypeMap;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+
+final class EloquentBuilderMethodReflection implements MethodReflection
+{
+    /**
+     * @var string
+     */
+    private $methodName;
+
+    /**
+     * @var ClassReflection
+     */
+    private $classReflection;
+
+    /** @var MethodReflection */
+    private $originalMethodReflection;
+
+    /**
+     * @var ParameterReflection[]
+     */
+    private $parameters;
+
+    /**
+     * @var Type
+     */
+    private $returnType;
+
+    /**
+     * @var bool
+     */
+    private $isVariadic;
+
+    /**
+     * @param  string  $methodName
+     * @param  ClassReflection  $classReflection
+     * @param  MethodReflection  $originalMethodReflection
+     * @param  ParameterReflection[]  $parameters
+     * @param  Type|null  $returnType
+     * @param  bool  $isVariadic
+     */
+    public function __construct(string $methodName, ClassReflection $classReflection, MethodReflection $originalMethodReflection, array $parameters, ?Type $returnType = null, bool $isVariadic = false)
+    {
+        $this->methodName = $methodName;
+        $this->classReflection = $classReflection;
+        $this->originalMethodReflection = $originalMethodReflection;
+        $this->parameters = $parameters;
+        $this->returnType = $returnType ?? new ObjectType(Builder::class);
+        $this->isVariadic = $isVariadic;
+    }
+
+    public function getDeclaringClass(): ClassReflection
+    {
+        return $this->classReflection;
+    }
+
+    public function isStatic(): bool
+    {
+        return true;
+    }
+
+    public function isPrivate(): bool
+    {
+        return false;
+    }
+
+    public function isPublic(): bool
+    {
+        return true;
+    }
+
+    public function getName(): string
+    {
+        return $this->methodName;
+    }
+
+    public function getPrototype(): ClassMemberReflection
+    {
+        return $this;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getVariants(): array
+    {
+        return [
+            new FunctionVariant(
+                TemplateTypeMap::createEmpty(),
+                null,
+                $this->parameters,
+                $this->isVariadic,
+                $this->returnType
+            ),
+        ];
+    }
+
+    public function getDocComment(): ?string
+    {
+        return null;
+    }
+
+    public function isDeprecated(): TrinaryLogic
+    {
+        return TrinaryLogic::createNo();
+    }
+
+    public function getDeprecatedDescription(): ?string
+    {
+        return null;
+    }
+
+    public function isFinal(): TrinaryLogic
+    {
+        return TrinaryLogic::createNo();
+    }
+
+    public function isInternal(): TrinaryLogic
+    {
+        return TrinaryLogic::createNo();
+    }
+
+    public function getThrowType(): ?Type
+    {
+        return null;
+    }
+
+    public function hasSideEffects(): TrinaryLogic
+    {
+        return TrinaryLogic::createMaybe();
+    }
+
+    /**
+     * @return MethodReflection
+     */
+    public function getOriginalMethodReflection(): MethodReflection
+    {
+        return $this->originalMethodReflection;
+    }
+}
diff --git a/src/Relations/RelationMethod.php b/src/Reflection/ModelScopeMethodReflection.php
similarity index 53%
rename from src/Relations/RelationMethod.php
rename to src/Reflection/ModelScopeMethodReflection.php
index cbe4884..769a40a 100644
--- a/src/Relations/RelationMethod.php
+++ b/src/Reflection/ModelScopeMethodReflection.php
@@ -1,36 +1,40 @@
 <?php
 
-/*
- * This file is part of Flarum.
- *
- * For detailed copyright and license information, please view the
- * LICENSE file that was distributed with this source code.
- */
+declare(strict_types=1);
 
-namespace Flarum\PHPStan\Relations;
+namespace Flarum\PHPStan\Reflection;
 
-use Flarum\PHPStan\Extender\MethodCall;
 use PHPStan\Reflection\ClassMemberReflection;
 use PHPStan\Reflection\ClassReflection;
 use PHPStan\Reflection\FunctionVariant;
 use PHPStan\Reflection\MethodReflection;
 use PHPStan\TrinaryLogic;
-use PHPStan\Type\Generic\GenericObjectType;
 use PHPStan\Type\Generic\TemplateTypeMap;
 use PHPStan\Type\ObjectType;
 use PHPStan\Type\Type;
 
-class RelationMethod implements MethodReflection
+final class ModelScopeMethodReflection implements MethodReflection
 {
-    /** @var MethodCall */
-    private $methodCall;
-    /** @var ClassReflection */
+    /**
+     * @var string
+     */
+    private $methodName;
+
+    /**
+     * @var ClassReflection
+     */
     private $classReflection;
 
-    public function __construct(MethodCall $methodCall, ClassReflection $classReflection)
+    /**
+     * @var ClassReflection
+     */
+    private $relation;
+
+    public function __construct(string $methodName, ClassReflection $classReflection, ClassReflection $relation)
     {
-        $this->methodCall = $methodCall;
+        $this->methodName = $methodName;
         $this->classReflection = $classReflection;
+        $this->relation = $relation;
     }
 
     public function getDeclaringClass(): ClassReflection
@@ -53,14 +57,9 @@ public function isPublic(): bool
         return true;
     }
 
-    public function getDocComment(): ?string
-    {
-        return null;
-    }
-
     public function getName(): string
     {
-        return $this->methodCall->arguments[0]->value;
+        return $this->methodName;
     }
 
     public function getPrototype(): ClassMemberReflection
@@ -68,38 +67,27 @@ public function getPrototype(): ClassMemberReflection
         return $this;
     }
 
+    /**
+     * {@inheritdoc}
+     */
     public function getVariants(): array
     {
-        $returnType = 'Illuminate\Database\Eloquent\Relations\Relation';
-
-        switch ($this->methodCall->methodName) {
-            case 'belongsTo':
-                $returnType = 'Illuminate\Database\Eloquent\Relations\BelongsTo';
-                break;
-            case 'belongsToMany':
-                $returnType = 'Illuminate\Database\Eloquent\Relations\BelongsToMany';
-                break;
-            case 'hasMany':
-                $returnType = 'Illuminate\Database\Eloquent\Relations\HasMany';
-                break;
-            case 'hasOne':
-                $returnType = 'Illuminate\Database\Eloquent\Relations\HasOne';
-                break;
-        }
-
-        $relationTarget = $this->methodCall->arguments[1]->class->toString();
-
         return [
             new FunctionVariant(
                 TemplateTypeMap::createEmpty(),
                 null,
                 [],
                 false,
-                new GenericObjectType($returnType, [new ObjectType($relationTarget)])
+                new ObjectType($this->relation->getName())
             ),
         ];
     }
 
+    public function getDocComment(): ?string
+    {
+        return null;
+    }
+
     public function isDeprecated(): TrinaryLogic
     {
         return TrinaryLogic::createNo();
@@ -127,6 +115,6 @@ public function getThrowType(): ?Type
 
     public function hasSideEffects(): TrinaryLogic
     {
-        return TrinaryLogic::createNo();
+        return TrinaryLogic::createMaybe();
     }
 }
diff --git a/src/Reflection/ReflectionHelper.php b/src/Reflection/ReflectionHelper.php
new file mode 100644
index 0000000..2442d8f
--- /dev/null
+++ b/src/Reflection/ReflectionHelper.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Reflection;
+
+use PHPStan\Reflection\ClassReflection;
+
+final class ReflectionHelper
+{
+    /**
+     * Does the given class or any of its ancestors have an `@property*` annotation with the given name?
+     */
+    public static function hasPropertyTag(ClassReflection $classReflection, string $propertyName): bool
+    {
+        if (array_key_exists($propertyName, $classReflection->getPropertyTags())) {
+            return true;
+        }
+
+        foreach ($classReflection->getAncestors() as $ancestor) {
+            if (array_key_exists($propertyName, $ancestor->getPropertyTags())) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/src/Reflection/StaticMethodReflection.php b/src/Reflection/StaticMethodReflection.php
new file mode 100644
index 0000000..bce18ec
--- /dev/null
+++ b/src/Reflection/StaticMethodReflection.php
@@ -0,0 +1,92 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Reflection;
+
+use PHPStan\Reflection\ClassMemberReflection;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\TrinaryLogic;
+use PHPStan\Type\Type;
+
+class StaticMethodReflection implements MethodReflection
+{
+    /** @var MethodReflection */
+    private $methodReflection;
+
+    public function __construct(MethodReflection $methodReflection)
+    {
+        $this->methodReflection = $methodReflection;
+    }
+
+    public function getDeclaringClass(): ClassReflection
+    {
+        return $this->methodReflection->getDeclaringClass();
+    }
+
+    public function isStatic(): bool
+    {
+        return true;
+    }
+
+    public function isPrivate(): bool
+    {
+        return $this->methodReflection->isPrivate();
+    }
+
+    public function isPublic(): bool
+    {
+        return $this->methodReflection->isPublic();
+    }
+
+    public function getDocComment(): ?string
+    {
+        return $this->methodReflection->getDocComment();
+    }
+
+    public function getName(): string
+    {
+        return $this->methodReflection->getName();
+    }
+
+    public function getPrototype(): ClassMemberReflection
+    {
+        return $this->methodReflection->getPrototype();
+    }
+
+    public function getVariants(): array
+    {
+        return $this->methodReflection->getVariants();
+    }
+
+    public function isDeprecated(): TrinaryLogic
+    {
+        return $this->methodReflection->isDeprecated();
+    }
+
+    public function getDeprecatedDescription(): ?string
+    {
+        return $this->methodReflection->getDeprecatedDescription();
+    }
+
+    public function isFinal(): TrinaryLogic
+    {
+        return $this->methodReflection->isFinal();
+    }
+
+    public function isInternal(): TrinaryLogic
+    {
+        return $this->methodReflection->isInternal();
+    }
+
+    public function getThrowType(): ?Type
+    {
+        return $this->methodReflection->getThrowType();
+    }
+
+    public function hasSideEffects(): TrinaryLogic
+    {
+        return $this->methodReflection->hasSideEffects();
+    }
+}
diff --git a/src/Relations/ModelRelationsExtension.php b/src/Relations/ModelRelationsExtension.php
deleted file mode 100644
index 737c383..0000000
--- a/src/Relations/ModelRelationsExtension.php
+++ /dev/null
@@ -1,86 +0,0 @@
-<?php
-
-/*
- * This file is part of Flarum.
- *
- * For detailed copyright and license information, please view the
- * LICENSE file that was distributed with this source code.
- */
-
-namespace Flarum\PHPStan\Relations;
-
-use Flarum\PHPStan\Extender\MethodCall;
-use Flarum\PHPStan\Extender\Resolver;
-use PHPStan\Reflection\ClassReflection;
-use PHPStan\Reflection\MethodReflection;
-use PHPStan\Reflection\MethodsClassReflectionExtension;
-use PHPStan\Reflection\PropertiesClassReflectionExtension;
-use PHPStan\Reflection\PropertyReflection;
-
-class ModelRelationsExtension implements MethodsClassReflectionExtension, PropertiesClassReflectionExtension
-{
-    /** @var Resolver */
-    private $extendersResolver;
-
-    public function __construct(Resolver $extendersResolver)
-    {
-        $this->extendersResolver = $extendersResolver;
-    }
-
-    public function hasMethod(ClassReflection $classReflection, string $methodName): bool
-    {
-        return $this->findRelationMethod($classReflection, $methodName) !== null;
-    }
-
-    public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
-    {
-        return $this->resolveRelationMethod($this->findRelationMethod($classReflection, $methodName), $classReflection);
-    }
-
-    public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
-    {
-        return $this->findRelationMethod($classReflection, $propertyName) !== null;
-    }
-
-    public function getProperty(ClassReflection $classReflection, string $propertyName): \PHPStan\Reflection\PropertyReflection
-    {
-        return $this->resolveRelationProperty($this->findRelationMethod($classReflection, $propertyName), $classReflection);
-    }
-
-    private function findRelationMethod(ClassReflection $classReflection, string $methodName): ?MethodCall
-    {
-        foreach ($this->extendersResolver->getExtenders() as $extender) {
-            if (! $extender->isExtender('Model')) {
-                continue;
-            }
-
-            foreach (array_merge([$classReflection->getName()], $classReflection->getParentClassesNames()) as $className) {
-                if ($className === 'Flarum\Database\AbstractModel') {
-                    break;
-                }
-
-                if ($extender->extends($className)) {
-                    if ($methodCalls = $extender->findMethodCalls('belongsTo', 'belongsToMany', 'hasMany', 'hasOne')) {
-                        foreach ($methodCalls as $methodCall) {
-                            if ($methodCall->arguments[0]->value === $methodName) {
-                                return $methodCall;
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
-        return null;
-    }
-
-    private function resolveRelationMethod(MethodCall $methodCall, ClassReflection $classReflection): MethodReflection
-    {
-        return new RelationMethod($methodCall, $classReflection);
-    }
-
-    private function resolveRelationProperty(MethodCall $methodCall, ClassReflection $classReflection): PropertyReflection
-    {
-        return new RelationProperty($methodCall, $classReflection);
-    }
-}
diff --git a/src/Relations/RelationProperty.php b/src/Relations/RelationProperty.php
deleted file mode 100644
index a572451..0000000
--- a/src/Relations/RelationProperty.php
+++ /dev/null
@@ -1,107 +0,0 @@
-<?php
-
-/*
- * This file is part of Flarum.
- *
- * For detailed copyright and license information, please view the
- * LICENSE file that was distributed with this source code.
- */
-
-namespace Flarum\PHPStan\Relations;
-
-use Exception;
-use Flarum\PHPStan\Extender\MethodCall;
-use Illuminate\Database\Eloquent\Collection;
-use PHPStan\Reflection\ClassReflection;
-use PHPStan\Reflection\PropertyReflection;
-use PHPStan\TrinaryLogic;
-use PHPStan\Type\Generic\GenericObjectType;
-use PHPStan\Type\IntegerType;
-use PHPStan\Type\ObjectType;
-use PHPStan\Type\Type;
-
-class RelationProperty implements PropertyReflection
-{
-    /** @var MethodCall */
-    private $methodCall;
-    /** @var ClassReflection */
-    private $classReflection;
-
-    public function __construct(MethodCall $methodCall, ClassReflection $classReflection)
-    {
-        $this->methodCall = $methodCall;
-        $this->classReflection = $classReflection;
-    }
-
-    public function getDeclaringClass(): ClassReflection
-    {
-        return $this->classReflection;
-    }
-
-    public function isStatic(): bool
-    {
-        return false;
-    }
-
-    public function isPrivate(): bool
-    {
-        return false;
-    }
-
-    public function isPublic(): bool
-    {
-        return true;
-    }
-
-    public function getDocComment(): ?string
-    {
-        return null;
-    }
-
-    public function getReadableType(): Type
-    {
-        return match ($this->methodCall->methodName) {
-            'hasMany', 'belongsToMany' => new GenericObjectType(Collection::class, [
-                new IntegerType(),
-                new ObjectType($this->methodCall->arguments[1]->class->toString())
-            ]),
-            'hasOne', 'belongsTo' => new ObjectType($this->methodCall->arguments[1]->class->toString()),
-            default => throw new Exception('Unknown relationship type for relation: '.$this->methodCall->methodName),
-        };
-    }
-
-    public function getWritableType(): Type
-    {
-        return $this->getReadableType();
-    }
-
-    public function canChangeTypeAfterAssignment(): bool
-    {
-        return false;
-    }
-
-    public function isReadable(): bool
-    {
-        return true;
-    }
-
-    public function isWritable(): bool
-    {
-        return true;
-    }
-
-    public function isDeprecated(): TrinaryLogic
-    {
-        return TrinaryLogic::createNo();
-    }
-
-    public function getDeprecatedDescription(): ?string
-    {
-        return null;
-    }
-
-    public function isInternal(): TrinaryLogic
-    {
-        return TrinaryLogic::createNo();
-    }
-}
diff --git a/src/ReturnTypes/BuilderModelFindExtension.php b/src/ReturnTypes/BuilderModelFindExtension.php
new file mode 100644
index 0000000..b1903b4
--- /dev/null
+++ b/src/ReturnTypes/BuilderModelFindExtension.php
@@ -0,0 +1,112 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\ReturnTypes;
+
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Database\Query\Builder as QueryBuilder;
+use Illuminate\Support\Str;
+use Flarum\PHPStan\Methods\BuilderHelper;
+use Flarum\PHPStan\Methods\ModelTypeHelper;
+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\Generic\GenericObjectType;
+use PHPStan\Type\MixedType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+use PHPStan\Type\TypeCombinator;
+
+/**
+ * @internal
+ */
+final class BuilderModelFindExtension implements DynamicMethodReturnTypeExtension
+{
+    /** @var BuilderHelper */
+    private $builderHelper;
+
+    /** @var ReflectionProvider */
+    private $reflectionProvider;
+
+    public function __construct(ReflectionProvider $reflectionProvider, BuilderHelper $builderHelper)
+    {
+        $this->builderHelper = $builderHelper;
+        $this->reflectionProvider = $reflectionProvider;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getClass(): string
+    {
+        return Builder::class;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isMethodSupported(MethodReflection $methodReflection): bool
+    {
+        $methodName = $methodReflection->getName();
+
+        if (! Str::startsWith($methodName, 'find')) {
+            return false;
+        }
+
+        $model = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TModelClass');
+
+        if ($model === null || ! $model instanceof ObjectType) {
+            return false;
+        }
+
+        if (! $this->reflectionProvider->getClass(Builder::class)->hasNativeMethod($methodName) &&
+            ! $this->reflectionProvider->getClass(QueryBuilder::class)->hasNativeMethod($methodName)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getTypeFromMethodCall(
+        MethodReflection $methodReflection,
+        MethodCall $methodCall,
+        Scope $scope
+    ): Type {
+        /** @var ObjectType $model */
+        $model = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TModelClass');
+        $returnType = $methodReflection->getVariants()[0]->getReturnType();
+        $argType = $scope->getType($methodCall->getArgs()[0]->value);
+
+        $returnType = ModelTypeHelper::replaceStaticTypeWithModel($returnType, $model->getClassName());
+
+        if ($argType->isIterable()->yes()) {
+            if (in_array(Collection::class, $returnType->getReferencedClasses(), true)) {
+                $collectionClassName = $this->builderHelper->determineCollectionClassName($model->getClassName());
+
+                return new GenericObjectType($collectionClassName, [$model]);
+            }
+
+            return TypeCombinator::remove($returnType, $model);
+        }
+
+        if ($argType instanceof MixedType) {
+            return $returnType;
+        }
+
+        return TypeCombinator::remove(
+            TypeCombinator::remove(
+                $returnType,
+                new ArrayType(new MixedType(), $model)
+            ),
+            new ObjectType(Collection::class)
+        );
+    }
+}
diff --git a/src/ReturnTypes/CollectionFilterDynamicReturnTypeExtension.php b/src/ReturnTypes/CollectionFilterDynamicReturnTypeExtension.php
new file mode 100644
index 0000000..269ccca
--- /dev/null
+++ b/src/ReturnTypes/CollectionFilterDynamicReturnTypeExtension.php
@@ -0,0 +1,112 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\ReturnTypes;
+
+use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Support\Enumerable;
+use PhpParser\Node\Expr\ArrowFunction;
+use PhpParser\Node\Expr\Closure;
+use PhpParser\Node\Expr\MethodCall;
+use PhpParser\Node\Expr\Variable;
+use PhpParser\Node\Stmt\Return_;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\ParametersAcceptorSelector;
+use PHPStan\Type\Constant\ConstantArrayType;
+use PHPStan\Type\Constant\ConstantBooleanType;
+use PHPStan\Type\Constant\ConstantFloatType;
+use PHPStan\Type\Constant\ConstantIntegerType;
+use PHPStan\Type\Constant\ConstantStringType;
+use PHPStan\Type\DynamicMethodReturnTypeExtension;
+use PHPStan\Type\Generic\GenericObjectType;
+use PHPStan\Type\NullType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+use PHPStan\Type\TypeCombinator;
+use PHPStan\Type\UnionType;
+
+class CollectionFilterDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
+{
+    public function getClass(): string
+    {
+        return Enumerable::class;
+    }
+
+    public function isMethodSupported(MethodReflection $methodReflection): bool
+    {
+        return $methodReflection->getName() === 'filter';
+    }
+
+    public function getTypeFromMethodCall(
+        MethodReflection $methodReflection,
+        MethodCall $methodCall,
+        Scope $scope
+    ): Type {
+        $calledOnType = $scope->getType($methodCall->var);
+
+        if (! $calledOnType instanceof \PHPStan\Type\Generic\GenericObjectType) {
+            return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
+        }
+
+        $keyType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TKey');
+        $valueType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TValue');
+
+        if ($keyType === null || $valueType === null) {
+            return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
+        }
+
+        if (count($methodCall->getArgs()) < 1) {
+            $falseyTypes = $this->getFalseyTypes();
+
+            $nonFalseyTypes = TypeCombinator::remove($valueType, $falseyTypes);
+
+            if ((new ObjectType(Collection::class))->isSuperTypeOf($calledOnType)->yes()) {
+                return new GenericObjectType($calledOnType->getClassName(), [$nonFalseyTypes]);
+            }
+
+            return new GenericObjectType($calledOnType->getClassName(), [$keyType, $nonFalseyTypes]);
+        }
+
+        $callbackArg = $methodCall->getArgs()[0]->value;
+
+        $var = null;
+        $expr = null;
+
+        if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) {
+            $statement = $callbackArg->stmts[0];
+            if ($statement instanceof Return_ && $statement->expr !== null) {
+                $var = $callbackArg->params[0]->var;
+                $expr = $statement->expr;
+            }
+        } elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) {
+            $var = $callbackArg->params[0]->var;
+            $expr = $callbackArg->expr;
+        }
+
+        if ($var !== null && $expr !== null) {
+            if (! $var instanceof Variable || ! is_string($var->name)) {
+                throw new \PHPStan\ShouldNotHappenException();
+            }
+
+            $itemVariableName = $var->name;
+
+            // @phpstan-ignore-next-line
+            $scope = $scope->assignVariable($itemVariableName, $valueType);
+            $scope = $scope->filterByTruthyValue($expr);
+            $valueType = $scope->getVariableType($itemVariableName);
+        }
+
+        if ((new ObjectType(Collection::class))->isSuperTypeOf($calledOnType)->yes()) {
+            return new GenericObjectType($calledOnType->getClassName(), [$valueType]);
+        }
+
+        return new GenericObjectType($calledOnType->getClassName(), [$keyType, $valueType]);
+    }
+
+    private function getFalseyTypes(): UnionType
+    {
+        return new UnionType([new NullType(), new ConstantBooleanType(false), new ConstantIntegerType(0), new ConstantFloatType(0.0), new ConstantStringType(''), new ConstantStringType('0'), new ConstantArrayType([], [])]);
+    }
+}
diff --git a/src/ReturnTypes/CollectionMakeDynamicStaticMethodReturnTypeExtension.php b/src/ReturnTypes/CollectionMakeDynamicStaticMethodReturnTypeExtension.php
new file mode 100644
index 0000000..ecb3ed9
--- /dev/null
+++ b/src/ReturnTypes/CollectionMakeDynamicStaticMethodReturnTypeExtension.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\ReturnTypes;
+
+use Illuminate\Support\Collection;
+use Flarum\PHPStan\Support\CollectionHelper;
+use PhpParser\Node\Expr\StaticCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\ParametersAcceptorSelector;
+use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
+use PHPStan\Type\Type;
+
+class CollectionMakeDynamicStaticMethodReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
+{
+    /**
+     * @var CollectionHelper
+     */
+    private $collectionHelper;
+
+    public function __construct(CollectionHelper $collectionHelper)
+    {
+        $this->collectionHelper = $collectionHelper;
+    }
+
+    public function getClass(): string
+    {
+        return Collection::class;
+    }
+
+    public function isStaticMethodSupported(MethodReflection $methodReflection): bool
+    {
+        return $methodReflection->getName() === 'make';
+    }
+
+    public function getTypeFromStaticMethodCall(
+        MethodReflection $methodReflection,
+        StaticCall $methodCall,
+        Scope $scope
+    ): Type {
+        if (count($methodCall->getArgs()) < 1) {
+            return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
+        }
+
+        $valueType = $scope->getType($methodCall->getArgs()[0]->value);
+
+        return $this->collectionHelper->determineGenericCollectionTypeFromType($valueType);
+    }
+}
diff --git a/src/ReturnTypes/ContainerArrayAccessDynamicMethodReturnTypeExtension.php b/src/ReturnTypes/ContainerArrayAccessDynamicMethodReturnTypeExtension.php
new file mode 100644
index 0000000..eabee76
--- /dev/null
+++ b/src/ReturnTypes/ContainerArrayAccessDynamicMethodReturnTypeExtension.php
@@ -0,0 +1,73 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\ReturnTypes;
+
+use Flarum\PHPStan\Concerns\HasContainer;
+use PhpParser\Node\Expr\MethodCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\ParametersAcceptorSelector;
+use PHPStan\Type\Constant\ConstantStringType;
+use PHPStan\Type\DynamicMethodReturnTypeExtension;
+use PHPStan\Type\ErrorType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+
+class ContainerArrayAccessDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
+{
+    use HasContainer;
+
+    /**
+     * @var string
+     */
+    private $className;
+
+    public function __construct(string $className)
+    {
+        $this->className = $className;
+    }
+
+    public function getClass(): string
+    {
+        return $this->className;
+    }
+
+    public function isMethodSupported(MethodReflection $methodReflection): bool
+    {
+        return $methodReflection->getName() === 'offsetGet';
+    }
+
+    public function getTypeFromMethodCall(
+        MethodReflection $methodReflection,
+        MethodCall $methodCall,
+        Scope $scope
+    ): Type {
+        $args = $methodCall->getArgs();
+
+        if (count($args) === 0) {
+            return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
+        }
+
+        $argType = $scope->getType($args[0]->value);
+
+        if (! $argType instanceof ConstantStringType) {
+            return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
+        }
+
+        $resolvedValue = $this->resolve($argType->getValue());
+
+        if ($resolvedValue === null) {
+            return new ErrorType();
+        }
+
+        if (is_object($resolvedValue)) {
+            $class = get_class($resolvedValue);
+
+            return new ObjectType($class);
+        }
+
+        return $scope->getTypeFromValue($resolvedValue);
+    }
+}
diff --git a/src/ReturnTypes/EloquentBuilderExtension.php b/src/ReturnTypes/EloquentBuilderExtension.php
new file mode 100644
index 0000000..02b9be0
--- /dev/null
+++ b/src/ReturnTypes/EloquentBuilderExtension.php
@@ -0,0 +1,86 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\ReturnTypes;
+
+use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
+use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Support\Str;
+use Flarum\PHPStan\Methods\BuilderHelper;
+use PhpParser\Node\Expr\MethodCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\ParametersAcceptorSelector;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\Type\DynamicMethodReturnTypeExtension;
+use PHPStan\Type\Generic\GenericObjectType;
+use PHPStan\Type\Generic\TemplateMixedType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+
+final class EloquentBuilderExtension implements DynamicMethodReturnTypeExtension
+{
+    /** @var BuilderHelper */
+    private $builderHelper;
+
+    /** @var ReflectionProvider */
+    private $reflectionProvider;
+
+    public function __construct(ReflectionProvider $reflectionProvider, BuilderHelper $builderHelper)
+    {
+        $this->builderHelper = $builderHelper;
+        $this->reflectionProvider = $reflectionProvider;
+    }
+
+    public function getClass(): string
+    {
+        return EloquentBuilder::class;
+    }
+
+    public function isMethodSupported(MethodReflection $methodReflection): bool
+    {
+        $builderReflection = $this->reflectionProvider->getClass(EloquentBuilder::class);
+
+        // Don't handle dynamic wheres
+        if (Str::startsWith($methodReflection->getName(), 'where') &&
+            ! $builderReflection->hasNativeMethod($methodReflection->getName())
+        ) {
+            return false;
+        }
+
+        if (Str::startsWith($methodReflection->getName(), 'find') &&
+            $builderReflection->hasNativeMethod($methodReflection->getName())
+        ) {
+            return false;
+        }
+
+        $templateTypeMap = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap();
+
+        if (! $templateTypeMap->getType('TModelClass') instanceof ObjectType) {
+            return false;
+        }
+
+        return $builderReflection->hasNativeMethod($methodReflection->getName());
+    }
+
+    public function getTypeFromMethodCall(
+        MethodReflection $methodReflection,
+        MethodCall $methodCall,
+        Scope $scope
+    ): Type {
+        $returnType = ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType();
+        $templateTypeMap = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap();
+
+        /** @var Type|ObjectType|TemplateMixedType $modelType */
+        $modelType = $templateTypeMap->getType('TModelClass');
+
+        if ($modelType instanceof ObjectType && in_array(Collection::class, $returnType->getReferencedClasses(), true)) {
+            $collectionClassName = $this->builderHelper->determineCollectionClassName($modelType->getClassName());
+
+            return new GenericObjectType($collectionClassName, [$modelType]);
+        }
+
+        return $returnType;
+    }
+}
diff --git a/src/ReturnTypes/Helpers/AppExtension.php b/src/ReturnTypes/Helpers/AppExtension.php
new file mode 100644
index 0000000..5960e73
--- /dev/null
+++ b/src/ReturnTypes/Helpers/AppExtension.php
@@ -0,0 +1,65 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\ReturnTypes\Helpers;
+
+use Illuminate\Foundation\Application;
+use Flarum\PHPStan\Concerns\HasContainer;
+use PhpParser\Node\Expr;
+use PhpParser\Node\Expr\ClassConstFetch;
+use PhpParser\Node\Expr\FuncCall;
+use PhpParser\Node\Name\FullyQualified;
+use PhpParser\Node\Scalar\String_;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\FunctionReflection;
+use PHPStan\Type\DynamicFunctionReturnTypeExtension;
+use PHPStan\Type\ErrorType;
+use PHPStan\Type\NeverType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+use Throwable;
+
+class AppExtension implements DynamicFunctionReturnTypeExtension
+{
+    use HasContainer;
+
+    public function isFunctionSupported(FunctionReflection $functionReflection): bool
+    {
+        return $functionReflection->getName() === 'app' || $functionReflection->getName() === 'resolve';
+    }
+
+    public function getTypeFromFunctionCall(
+        FunctionReflection $functionReflection,
+        FuncCall $functionCall,
+        Scope $scope
+    ): Type {
+        if (count($functionCall->getArgs()) === 0) {
+            return new ObjectType(Application::class);
+        }
+
+        /** @var Expr $expr */
+        $expr = $functionCall->getArgs()[0]->value;
+
+        if ($expr instanceof String_) {
+            try {
+                /** @var object|null $resolved */
+                $resolved = $this->resolve($expr->value);
+
+                if ($resolved === null) {
+                    return new ErrorType();
+                }
+
+                return new ObjectType(get_class($resolved));
+            } catch (Throwable $exception) {
+                return new ErrorType();
+            }
+        }
+
+        if ($expr instanceof ClassConstFetch && $expr->class instanceof FullyQualified) {
+            return new ObjectType($expr->class->toString());
+        }
+
+        return new NeverType();
+    }
+}
diff --git a/src/ReturnTypes/Helpers/CollectExtension.php b/src/ReturnTypes/Helpers/CollectExtension.php
new file mode 100644
index 0000000..d5c7398
--- /dev/null
+++ b/src/ReturnTypes/Helpers/CollectExtension.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\ReturnTypes\Helpers;
+
+use Flarum\PHPStan\Support\CollectionHelper;
+use PhpParser\Node\Expr\FuncCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\FunctionReflection;
+use PHPStan\Reflection\ParametersAcceptorSelector;
+use PHPStan\Type\DynamicFunctionReturnTypeExtension;
+use PHPStan\Type\Type;
+
+final class CollectExtension implements DynamicFunctionReturnTypeExtension
+{
+    /**
+     * @var CollectionHelper
+     */
+    private $collectionHelper;
+
+    public function __construct(CollectionHelper $collectionHelper)
+    {
+        $this->collectionHelper = $collectionHelper;
+    }
+
+    public function isFunctionSupported(FunctionReflection $functionReflection): bool
+    {
+        return $functionReflection->getName() === 'collect';
+    }
+
+    public function getTypeFromFunctionCall(
+        FunctionReflection $functionReflection,
+        FuncCall $functionCall,
+        Scope $scope
+    ): Type {
+        if (count($functionCall->getArgs()) < 1) {
+            return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
+        }
+
+        $valueType = $scope->getType($functionCall->getArgs()[0]->value);
+
+        return $this->collectionHelper->determineGenericCollectionTypeFromType($valueType);
+    }
+}
diff --git a/src/ReturnTypes/Helpers/TapExtension.php b/src/ReturnTypes/Helpers/TapExtension.php
new file mode 100644
index 0000000..74d2362
--- /dev/null
+++ b/src/ReturnTypes/Helpers/TapExtension.php
@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\ReturnTypes\Helpers;
+
+use Illuminate\Support\HigherOrderTapProxy;
+use PhpParser\Node\Expr\FuncCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\FunctionReflection;
+use PHPStan\Type\DynamicFunctionReturnTypeExtension;
+use PHPStan\Type\Generic\GenericObjectType;
+use PHPStan\Type\NeverType;
+use PHPStan\Type\ThisType;
+use PHPStan\Type\Type;
+
+class TapExtension implements DynamicFunctionReturnTypeExtension
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function isFunctionSupported(FunctionReflection $functionReflection): bool
+    {
+        return $functionReflection->getName() === 'tap';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getTypeFromFunctionCall(
+        FunctionReflection $functionReflection,
+        FuncCall $functionCall,
+        Scope $scope
+    ): Type {
+        if (count($functionCall->getArgs()) === 1) {
+            $type = $scope->getType($functionCall->getArgs()[0]->value);
+
+            return new GenericObjectType(HigherOrderTapProxy::class, [
+                $type instanceof ThisType ? $type->getStaticObjectType() : $type,
+            ]);
+        }
+
+        if (count($functionCall->getArgs()) === 2) {
+            return $scope->getType($functionCall->getArgs()[0]->value);
+        }
+
+        return new NeverType();
+    }
+}
diff --git a/src/ReturnTypes/Helpers/TransExtension.php b/src/ReturnTypes/Helpers/TransExtension.php
new file mode 100644
index 0000000..310e3e6
--- /dev/null
+++ b/src/ReturnTypes/Helpers/TransExtension.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\ReturnTypes\Helpers;
+
+use PhpParser\Node\Expr\FuncCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\FunctionReflection;
+use PHPStan\Type\DynamicFunctionReturnTypeExtension;
+use PHPStan\Type\MixedType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+
+class TransExtension implements DynamicFunctionReturnTypeExtension
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function isFunctionSupported(FunctionReflection $functionReflection): bool
+    {
+        return $functionReflection->getName() === 'trans';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getTypeFromFunctionCall(
+        FunctionReflection $functionReflection,
+        FuncCall $functionCall,
+        Scope $scope
+    ): Type {
+        // No path provided, so it returns a Translator instance
+        if (count($functionCall->getArgs()) === 0) {
+            return new ObjectType(\Illuminate\Contracts\Translation\Translator::class);
+        }
+
+        return new MixedType();
+    }
+}
diff --git a/src/ReturnTypes/Helpers/ValidatorExtension.php b/src/ReturnTypes/Helpers/ValidatorExtension.php
new file mode 100644
index 0000000..26630c7
--- /dev/null
+++ b/src/ReturnTypes/Helpers/ValidatorExtension.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\ReturnTypes\Helpers;
+
+use function count;
+use PhpParser\Node\Expr\FuncCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\FunctionReflection;
+use PHPStan\Type\DynamicFunctionReturnTypeExtension;
+use PHPStan\Type\IntersectionType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+
+/**
+ * @internal
+ */
+final class ValidatorExtension implements DynamicFunctionReturnTypeExtension
+{
+    public function isFunctionSupported(FunctionReflection $functionReflection): bool
+    {
+        return $functionReflection->getName() === 'validator';
+    }
+
+    public function getTypeFromFunctionCall(
+        FunctionReflection $functionReflection,
+        FuncCall $functionCall,
+        Scope $scope
+    ): Type {
+        if (count($functionCall->getArgs()) === 0) {
+            return new ObjectType(\Illuminate\Contracts\Validation\Factory::class);
+        }
+
+        return new IntersectionType([
+            new ObjectType(\Illuminate\Validation\Validator::class),
+            new ObjectType(\Illuminate\Contracts\Validation\Validator::class),
+        ]);
+    }
+}
diff --git a/src/ReturnTypes/Helpers/ValueExtension.php b/src/ReturnTypes/Helpers/ValueExtension.php
new file mode 100644
index 0000000..0766391
--- /dev/null
+++ b/src/ReturnTypes/Helpers/ValueExtension.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\ReturnTypes\Helpers;
+
+use function count;
+use PhpParser\Node\Expr\Closure;
+use PhpParser\Node\Expr\FuncCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\FunctionReflection;
+use PHPStan\Reflection\ParametersAcceptorSelector;
+use PHPStan\Type\DynamicFunctionReturnTypeExtension;
+use PHPStan\Type\NeverType;
+use PHPStan\Type\Type;
+
+/**
+ * @internal
+ */
+final class ValueExtension implements DynamicFunctionReturnTypeExtension
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function isFunctionSupported(FunctionReflection $functionReflection): bool
+    {
+        return $functionReflection->getName() === 'value';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getTypeFromFunctionCall(
+        FunctionReflection $functionReflection,
+        FuncCall $functionCall,
+        Scope $scope
+    ): Type {
+        if (count($functionCall->getArgs()) === 0) {
+            return new NeverType();
+        }
+
+        $arg = $functionCall->getArgs()[0]->value;
+        if ($arg instanceof Closure) {
+            $callbackType = $scope->getType($arg);
+            $callbackReturnType = ParametersAcceptorSelector::selectFromArgs(
+                $scope,
+                $functionCall->getArgs(),
+                $callbackType->getCallableParametersAcceptors($scope)
+            )->getReturnType();
+
+            return $callbackReturnType;
+        }
+
+        return $scope->getType($arg);
+    }
+}
diff --git a/src/ReturnTypes/HigherOrderTapProxyExtension.php b/src/ReturnTypes/HigherOrderTapProxyExtension.php
new file mode 100644
index 0000000..e90f1e0
--- /dev/null
+++ b/src/ReturnTypes/HigherOrderTapProxyExtension.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\ReturnTypes;
+
+use Illuminate\Support\HigherOrderTapProxy;
+use PhpParser\Node\Expr\MethodCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Type\DynamicMethodReturnTypeExtension;
+use PHPStan\Type\Generic\GenericObjectType;
+use PHPStan\Type\MixedType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+
+/**
+ * @internal
+ */
+final class HigherOrderTapProxyExtension implements DynamicMethodReturnTypeExtension
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getClass(): string
+    {
+        return HigherOrderTapProxy::class;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isMethodSupported(MethodReflection $methodReflection): bool
+    {
+        return true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getTypeFromMethodCall(
+        MethodReflection $methodReflection,
+        MethodCall $methodCall,
+        Scope $scope
+    ): Type {
+        $type = $scope->getType($methodCall->var);
+        if ($type instanceof GenericObjectType) {
+            $types = $type->getTypes();
+            if (count($types) === 1 && $types[0] instanceof ObjectType) {
+                return $types[0];
+            }
+        }
+
+        return new MixedType();
+    }
+}
diff --git a/src/ReturnTypes/ModelExtension.php b/src/ReturnTypes/ModelExtension.php
new file mode 100644
index 0000000..f039ab2
--- /dev/null
+++ b/src/ReturnTypes/ModelExtension.php
@@ -0,0 +1,104 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\ReturnTypes;
+
+use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
+use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Query\Builder as QueryBuilder;
+use Flarum\PHPStan\Methods\BuilderHelper;
+use PhpParser\Node\Expr\StaticCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\ParametersAcceptorSelector;
+use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
+use PHPStan\Type\Generic\GenericObjectType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+
+/**
+ * @internal
+ */
+final class ModelExtension implements DynamicStaticMethodReturnTypeExtension
+{
+    /** @var BuilderHelper */
+    private $builderHelper;
+
+    /**
+     * @param  BuilderHelper  $builderHelper
+     */
+    public function __construct(BuilderHelper $builderHelper)
+    {
+        $this->builderHelper = $builderHelper;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getClass(): string
+    {
+        return Model::class;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isStaticMethodSupported(MethodReflection $methodReflection): bool
+    {
+        $name = $methodReflection->getName();
+        if ($name === '__construct') {
+            return false;
+        }
+
+        if (in_array($name, ['get', 'hydrate', 'fromQuery'], true)) {
+            return true;
+        }
+
+        if (! $methodReflection->getDeclaringClass()->hasNativeMethod($name)) {
+            return false;
+        }
+
+        $method = $methodReflection->getDeclaringClass()->getNativeMethod($methodReflection->getName());
+
+        $returnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType();
+
+        return (count(array_intersect([EloquentBuilder::class, QueryBuilder::class, Collection::class], $returnType->getReferencedClasses()))) > 0;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getTypeFromStaticMethodCall(
+        MethodReflection $methodReflection,
+        StaticCall $methodCall,
+        Scope $scope
+    ): Type {
+        $method = $methodReflection->getDeclaringClass()
+            ->getMethod($methodReflection->getName(), $scope);
+
+        $returnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType();
+
+        if ((count(array_intersect([EloquentBuilder::class, QueryBuilder::class], $returnType->getReferencedClasses())) > 0)
+            && $methodCall->class instanceof \PhpParser\Node\Name
+        ) {
+            $returnType = new GenericObjectType(
+                $this->builderHelper->determineBuilderName($scope->resolveName($methodCall->class)),
+                [new ObjectType($scope->resolveName($methodCall->class))]
+            );
+        }
+
+        if (
+            $methodCall->class instanceof \PhpParser\Node\Name
+            && in_array(Collection::class, $returnType->getReferencedClasses(), true)
+            && in_array($methodReflection->getName(), ['get', 'hydrate', 'fromQuery', 'all', 'findMany'], true)
+        ) {
+            $collectionClassName = $this->builderHelper->determineCollectionClassName($scope->resolveName($methodCall->class));
+
+            return new GenericObjectType($collectionClassName, [new ObjectType($scope->resolveName($methodCall->class))]);
+        }
+
+        return $returnType;
+    }
+}
diff --git a/src/ReturnTypes/ModelFactoryDynamicStaticMethodReturnTypeExtension.php b/src/ReturnTypes/ModelFactoryDynamicStaticMethodReturnTypeExtension.php
new file mode 100644
index 0000000..a16440e
--- /dev/null
+++ b/src/ReturnTypes/ModelFactoryDynamicStaticMethodReturnTypeExtension.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\ReturnTypes;
+
+use Illuminate\Database\Eloquent\Model;
+use PhpParser\Node\Expr\StaticCall;
+use PhpParser\Node\Name;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
+use PHPStan\Type\ErrorType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+
+final class ModelFactoryDynamicStaticMethodReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
+{
+    public function getClass(): string
+    {
+        return Model::class;
+    }
+
+    public function isStaticMethodSupported(MethodReflection $methodReflection): bool
+    {
+        if ($methodReflection->getName() !== 'factory') {
+            return false;
+        }
+
+        // Class only available on Laravel 8
+        if (! class_exists('\Illuminate\Database\Eloquent\Factories\Factory')) {
+            return false;
+        }
+
+        return true;
+    }
+
+    public function getTypeFromStaticMethodCall(
+        MethodReflection $methodReflection,
+        StaticCall $methodCall,
+        Scope $scope
+    ): Type {
+        $class = $methodCall->class;
+
+        if (! $class instanceof Name) {
+            return new ErrorType();
+        }
+
+        $modelName = basename(str_replace('\\', '/', $class->toCodeString()));
+
+        if (! class_exists('Database\\Factories\\'.$modelName.'Factory')) {
+            return new ErrorType();
+        }
+
+        return new ObjectType('Database\\Factories\\'.$modelName.'Factory');
+    }
+}
diff --git a/src/ReturnTypes/ModelFindExtension.php b/src/ReturnTypes/ModelFindExtension.php
new file mode 100644
index 0000000..a50a8b3
--- /dev/null
+++ b/src/ReturnTypes/ModelFindExtension.php
@@ -0,0 +1,108 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\ReturnTypes;
+
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Query\Builder as QueryBuilder;
+use Illuminate\Support\Str;
+use Flarum\PHPStan\Methods\BuilderHelper;
+use PhpParser\Node\Expr\StaticCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\Type\ArrayType;
+use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
+use PHPStan\Type\ErrorType;
+use PHPStan\Type\Generic\GenericObjectType;
+use PHPStan\Type\MixedType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+use PHPStan\Type\TypeCombinator;
+
+/**
+ * @internal
+ */
+final class ModelFindExtension implements DynamicStaticMethodReturnTypeExtension
+{
+    /** @var BuilderHelper */
+    private $builderHelper;
+
+    /** @var ReflectionProvider */
+    private $reflectionProvider;
+
+    public function __construct(ReflectionProvider $reflectionProvider, BuilderHelper $builderHelper)
+    {
+        $this->builderHelper = $builderHelper;
+        $this->reflectionProvider = $reflectionProvider;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getClass(): string
+    {
+        return Model::class;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isStaticMethodSupported(MethodReflection $methodReflection): bool
+    {
+        $methodName = $methodReflection->getName();
+
+        if (! Str::startsWith($methodName, 'find')) {
+            return false;
+        }
+
+        if (! $this->reflectionProvider->getClass(Builder::class)->hasNativeMethod($methodName) &&
+            ! $this->reflectionProvider->getClass(QueryBuilder::class)->hasNativeMethod($methodName)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getTypeFromStaticMethodCall(
+        MethodReflection $methodReflection,
+        StaticCall $methodCall,
+        Scope $scope
+    ): Type {
+        if (count($methodCall->getArgs()) < 1) {
+            return new ErrorType();
+        }
+
+        $modelName = $methodReflection->getDeclaringClass()->getName();
+        $returnType = $methodReflection->getVariants()[0]->getReturnType();
+        $argType = $scope->getType($methodCall->getArgs()[0]->value);
+
+        if ($argType->isIterable()->yes()) {
+            if (in_array(Collection::class, $returnType->getReferencedClasses(), true)) {
+                $collectionClassName = $this->builderHelper->determineCollectionClassName($modelName);
+
+                return new GenericObjectType($collectionClassName, [new ObjectType($modelName)]);
+            }
+
+            return TypeCombinator::remove($returnType, new ObjectType($modelName));
+        }
+
+        if ($argType instanceof MixedType) {
+            return $returnType;
+        }
+
+        return TypeCombinator::remove(
+            TypeCombinator::remove(
+                $returnType,
+                new ArrayType(new MixedType(), new ObjectType($modelName))
+            ),
+            new ObjectType(Collection::class)
+        );
+    }
+}
diff --git a/src/ReturnTypes/RelationCollectionExtension.php b/src/ReturnTypes/RelationCollectionExtension.php
new file mode 100644
index 0000000..9a5b8b2
--- /dev/null
+++ b/src/ReturnTypes/RelationCollectionExtension.php
@@ -0,0 +1,86 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\ReturnTypes;
+
+use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Database\Eloquent\Relations\Relation;
+use Illuminate\Support\Str;
+use Flarum\PHPStan\Methods\BuilderHelper;
+use PhpParser\Node\Expr\MethodCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\ParametersAcceptorSelector;
+use PHPStan\Type\DynamicMethodReturnTypeExtension;
+use PHPStan\Type\Generic\GenericObjectType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+
+/**
+ * @internal
+ */
+final class RelationCollectionExtension implements DynamicMethodReturnTypeExtension
+{
+    /** @var BuilderHelper */
+    private $builderHelper;
+
+    public function __construct(BuilderHelper $builderHelper)
+    {
+        $this->builderHelper = $builderHelper;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getClass(): string
+    {
+        return Relation::class;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isMethodSupported(MethodReflection $methodReflection): bool
+    {
+        if (Str::startsWith($methodReflection->getName(), 'find')) {
+            return false;
+        }
+
+        $modelType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TRelatedModel');
+
+        if (! $modelType instanceof ObjectType) {
+            return false;
+        }
+
+        $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
+
+        if (! in_array(Collection::class, $returnType->getReferencedClasses(), true)) {
+            return false;
+        }
+
+        return $methodReflection->getDeclaringClass()->hasNativeMethod($methodReflection->getName());
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getTypeFromMethodCall(
+        MethodReflection $methodReflection,
+        MethodCall $methodCall,
+        Scope $scope
+    ): Type {
+        /** @var ObjectType $modelType */
+        $modelType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TRelatedModel');
+
+        $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
+
+        if (in_array(Collection::class, $returnType->getReferencedClasses(), true)) {
+            $collectionClassName = $this->builderHelper->determineCollectionClassName($modelType->getClassname());
+
+            return new GenericObjectType($collectionClassName, [$modelType]);
+        }
+
+        return $returnType;
+    }
+}
diff --git a/src/ReturnTypes/RelationFindExtension.php b/src/ReturnTypes/RelationFindExtension.php
new file mode 100644
index 0000000..63ea62e
--- /dev/null
+++ b/src/ReturnTypes/RelationFindExtension.php
@@ -0,0 +1,99 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\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 Flarum\PHPStan\Methods\BuilderHelper;
+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\Generic\GenericObjectType;
+use PHPStan\Type\MixedType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+use PHPStan\Type\TypeCombinator;
+
+/**
+ * @internal
+ */
+final class RelationFindExtension implements DynamicMethodReturnTypeExtension
+{
+    /** @var BuilderHelper */
+    private $builderHelper;
+
+    /** @var ReflectionProvider */
+    private $reflectionProvider;
+
+    public function __construct(ReflectionProvider $reflectionProvider, BuilderHelper $builderHelper)
+    {
+        $this->builderHelper = $builderHelper;
+        $this->reflectionProvider = $reflectionProvider;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getClass(): string
+    {
+        return Relation::class;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isMethodSupported(MethodReflection $methodReflection): bool
+    {
+        if (! Str::startsWith($methodReflection->getName(), 'find')) {
+            return false;
+        }
+
+        $modelType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TRelatedModel');
+
+        if (! $modelType instanceof ObjectType) {
+            return false;
+        }
+
+        return $methodReflection->getDeclaringClass()->hasNativeMethod($methodReflection->getName()) ||
+            $this->reflectionProvider->getClass(Builder::class)->hasNativeMethod($methodReflection->getName()) ||
+            $this->reflectionProvider->getClass(QueryBuilder::class)->hasNativeMethod($methodReflection->getName());
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getTypeFromMethodCall(
+        MethodReflection $methodReflection,
+        MethodCall $methodCall,
+        Scope $scope
+    ): Type {
+        /** @var ObjectType $modelType */
+        $modelType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TRelatedModel');
+
+        $argType = $scope->getType($methodCall->getArgs()[0]->value);
+
+        $returnType = $methodReflection->getVariants()[0]->getReturnType();
+
+        if (in_array(Collection::class, $returnType->getReferencedClasses(), true)) {
+            if ($argType->isIterable()->yes()) {
+                $collectionClassName = $this->builderHelper->determineCollectionClassName($modelType->getClassname());
+
+                return new GenericObjectType($collectionClassName, [$modelType]);
+            }
+
+            $returnType = TypeCombinator::remove($returnType, new ObjectType(Collection::class));
+
+            return TypeCombinator::remove($returnType, new ArrayType(new MixedType(), $modelType));
+        }
+
+        return $returnType;
+    }
+}
diff --git a/src/ReturnTypes/RequestExtension.php b/src/ReturnTypes/RequestExtension.php
new file mode 100644
index 0000000..4515818
--- /dev/null
+++ b/src/ReturnTypes/RequestExtension.php
@@ -0,0 +1,61 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\ReturnTypes;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\UploadedFile;
+use PhpParser\Node\Expr\MethodCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Type\ArrayType;
+use PHPStan\Type\DynamicMethodReturnTypeExtension;
+use PHPStan\Type\IntegerType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+use PHPStan\Type\TypeCombinator;
+
+/**
+ * @internal
+ */
+final class RequestExtension implements DynamicMethodReturnTypeExtension
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getClass(): string
+    {
+        return Request::class;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isMethodSupported(MethodReflection $methodReflection): bool
+    {
+        return $methodReflection->getName() === 'file';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getTypeFromMethodCall(
+        MethodReflection $methodReflection,
+        MethodCall $methodCall,
+        Scope $scope
+    ): Type {
+        $uploadedFileType = new ObjectType(UploadedFile::class);
+        $uploadedFileArrayType = new ArrayType(new IntegerType(), $uploadedFileType);
+
+        if (count($methodCall->getArgs()) === 0) {
+            return new ArrayType(new IntegerType(), $uploadedFileType);
+        }
+
+        if (count($methodCall->getArgs()) === 1) {
+            return TypeCombinator::union($uploadedFileArrayType, TypeCombinator::addNull($uploadedFileType));
+        }
+
+        return TypeCombinator::union(TypeCombinator::union($uploadedFileArrayType, $uploadedFileType), $scope->getType($methodCall->getArgs()[1]->value));
+    }
+}
diff --git a/src/ReturnTypes/StorageDynamicStaticMethodReturnTypeExtension.php b/src/ReturnTypes/StorageDynamicStaticMethodReturnTypeExtension.php
new file mode 100644
index 0000000..1568511
--- /dev/null
+++ b/src/ReturnTypes/StorageDynamicStaticMethodReturnTypeExtension.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\ReturnTypes;
+
+use Illuminate\Filesystem\FilesystemAdapter;
+use Illuminate\Support\Facades\Storage;
+use Flarum\PHPStan\Concerns\HasContainer;
+use PhpParser\Node\Expr\StaticCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+
+class StorageDynamicStaticMethodReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
+{
+    use HasContainer;
+
+    public function getClass(): string
+    {
+        return Storage::class;
+    }
+
+    public function isStaticMethodSupported(MethodReflection $methodReflection): bool
+    {
+        return $methodReflection->getName() === 'disk';
+    }
+
+    public function getTypeFromStaticMethodCall(
+        MethodReflection $methodReflection,
+        StaticCall $methodCall,
+        Scope $scope
+    ): Type {
+        return new ObjectType(FilesystemAdapter::class);
+    }
+}
diff --git a/src/ReturnTypes/TestCaseExtension.php b/src/ReturnTypes/TestCaseExtension.php
new file mode 100644
index 0000000..908fd2c
--- /dev/null
+++ b/src/ReturnTypes/TestCaseExtension.php
@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\ReturnTypes;
+
+use Illuminate\Foundation\Testing\TestCase;
+use PhpParser\Node\Expr\MethodCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Type\Constant\ConstantStringType;
+use PHPStan\Type\DynamicMethodReturnTypeExtension;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+use PHPStan\Type\TypeCombinator;
+
+/**
+ * @internal
+ */
+final class TestCaseExtension implements DynamicMethodReturnTypeExtension
+{
+    public function getClass(): string
+    {
+        return TestCase::class;
+    }
+
+    public function isMethodSupported(MethodReflection $methodReflection): bool
+    {
+        return in_array($methodReflection->getName(), [
+            'mock',
+            'partialMock',
+            'spy',
+        ], true);
+    }
+
+    public function getTypeFromMethodCall(
+        MethodReflection $methodReflection,
+        MethodCall $methodCall,
+        Scope $scope
+    ): Type {
+        $defaultReturnType = new ObjectType('Mockery\\MockInterface');
+
+        $classType = $scope->getType($methodCall->getArgs()[0]->value);
+
+        if (! $classType instanceof ConstantStringType) {
+            return $defaultReturnType;
+        }
+
+        $objectType = new ObjectType($classType->getValue());
+
+        return TypeCombinator::intersect($defaultReturnType, $objectType);
+    }
+}
diff --git a/src/Support/CollectionHelper.php b/src/Support/CollectionHelper.php
new file mode 100644
index 0000000..8fd6fce
--- /dev/null
+++ b/src/Support/CollectionHelper.php
@@ -0,0 +1,99 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Support;
+
+use Illuminate\Database\Eloquent\Collection as EloquentCollection;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Enumerable;
+use Iterator;
+use IteratorAggregate;
+use PHPStan\Type\GeneralizePrecision;
+use PHPStan\Type\Generic\GenericObjectType;
+use PHPStan\Type\IntegerType;
+use PHPStan\Type\MixedType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\StringType;
+use PHPStan\Type\Type;
+use PHPStan\Type\TypeCombinator;
+use PHPStan\Type\TypeUtils;
+use PHPStan\Type\TypeWithClassName;
+use Traversable;
+
+final class CollectionHelper
+{
+    public function determineGenericCollectionTypeFromType(Type $type): GenericObjectType
+    {
+        $keyType = TypeCombinator::union(new IntegerType(), new StringType());
+
+        if ($type instanceof TypeWithClassName) {
+            if ((new ObjectType(Enumerable::class))->isSuperTypeOf($type)->yes()) {
+                return $this->getTypeFromEloquentCollection($type);
+            }
+
+            if (
+                (new ObjectType(Traversable::class))->isSuperTypeOf($type)->yes() ||
+                (new ObjectType(IteratorAggregate::class))->isSuperTypeOf($type)->yes() ||
+                (new ObjectType(Iterator::class))->isSuperTypeOf($type)->yes()
+            ) {
+                return $this->getTypeFromIterator($type);
+            }
+        }
+
+        if (! $type->isArray()->yes()) {
+            return new GenericObjectType(Collection::class, [$type->toArray()->getIterableKeyType(), $type->toArray()->getIterableValueType()]);
+        }
+
+        if ($type->isIterableAtLeastOnce()->no()) {
+            return new GenericObjectType(Collection::class, [$keyType, new MixedType()]);
+        }
+
+        return new GenericObjectType(Collection::class, [
+            TypeUtils::generalizeType($type->getIterableKeyType(), GeneralizePrecision::lessSpecific()),
+            TypeUtils::generalizeType($type->getIterableValueType(), GeneralizePrecision::lessSpecific()),
+        ]);
+    }
+
+    private function getTypeFromEloquentCollection(TypeWithClassName $valueType): GenericObjectType
+    {
+        $keyType = TypeCombinator::union(new IntegerType(), new StringType());
+
+        $classReflection = $valueType->getClassReflection();
+
+        if ($classReflection === null) {
+            return new GenericObjectType(Collection::class, [$keyType, new MixedType()]);
+        }
+
+        $innerValueType = $classReflection->getActiveTemplateTypeMap()->getType('TValue');
+
+        if ($classReflection->getName() === EloquentCollection::class || $classReflection->isSubclassOf(EloquentCollection::class)) {
+            $keyType = new IntegerType();
+        }
+
+        if ($innerValueType !== null) {
+            return new GenericObjectType(Collection::class, [$keyType, $innerValueType]);
+        }
+
+        return new GenericObjectType(Collection::class, [$keyType, new MixedType()]);
+    }
+
+    private function getTypeFromIterator(TypeWithClassName $valueType): GenericObjectType
+    {
+        $keyType = TypeCombinator::union(new IntegerType(), new StringType());
+
+        $classReflection = $valueType->getClassReflection();
+
+        if ($classReflection === null) {
+            return new GenericObjectType(Collection::class, [$keyType, new MixedType()]);
+        }
+
+        $templateTypes = array_values($classReflection->getActiveTemplateTypeMap()->getTypes());
+
+        if (count($templateTypes) === 1) {
+            return new GenericObjectType(Collection::class, [$keyType, $templateTypes[0]]);
+        }
+
+        return new GenericObjectType(Collection::class, $templateTypes);
+    }
+}
diff --git a/src/Support/HigherOrderCollectionProxyHelper.php b/src/Support/HigherOrderCollectionProxyHelper.php
new file mode 100644
index 0000000..da48276
--- /dev/null
+++ b/src/Support/HigherOrderCollectionProxyHelper.php
@@ -0,0 +1,134 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Support;
+
+use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Collection as SupportCollection;
+use Illuminate\Support\HigherOrderCollectionProxy;
+use PHPStan\Reflection\ClassReflection;
+use PHPStan\Type;
+
+class HigherOrderCollectionProxyHelper
+{
+    /**
+     * @phpstan-param 'method'|'property' $propertyOrMethod
+     */
+    public static function hasPropertyOrMethod(ClassReflection $classReflection, string $name, string $propertyOrMethod): bool
+    {
+        if ($classReflection->getName() !== HigherOrderCollectionProxy::class) {
+            return false;
+        }
+
+        $activeTemplateTypeMap = $classReflection->getActiveTemplateTypeMap();
+
+        if ($activeTemplateTypeMap->count() !== 2) {
+            return false;
+        }
+
+        $methodType = $activeTemplateTypeMap->getType('T');
+        $valueType = $activeTemplateTypeMap->getType('TValue');
+
+        if (($methodType === null) || ($valueType === null)) {
+            return false;
+        }
+
+        if (! $methodType instanceof Type\Constant\ConstantStringType) {
+            return false;
+        }
+
+        if (! $valueType->canCallMethods()->yes()) {
+            return false;
+        }
+
+        if ($propertyOrMethod === 'method') {
+            return $valueType->hasMethod($name)->yes();
+        }
+
+        return $valueType->hasProperty($name)->yes();
+    }
+
+    public static function determineReturnType(string $name, Type\Type $valueType, Type\Type $methodOrPropertyReturnType): Type\Type
+    {
+        if ((new Type\ObjectType(Model::class))->isSuperTypeOf($valueType)->yes()) {
+            $collectionType = Collection::class;
+            $types = [$valueType];
+        } else {
+            $collectionType = SupportCollection::class;
+            $types = [new Type\IntegerType(), $valueType];
+        }
+        switch ($name) {
+            case 'average':
+            case 'avg':
+                $returnType = new Type\FloatType();
+                break;
+            case 'contains':
+            case 'every':
+            case 'some':
+                $returnType = new Type\BooleanType();
+                break;
+            case 'each':
+            case 'filter':
+            case 'reject':
+            case 'skipUntil':
+            case 'skipWhile':
+            case 'sortBy':
+            case 'sortByDesc':
+            case 'takeUntil':
+            case 'takeWhile':
+            case 'unique':
+                $returnType = new Type\Generic\GenericObjectType($collectionType, $types);
+                break;
+            case 'keyBy':
+                if ($collectionType === SupportCollection::class) {
+                    $returnType = new Type\Generic\GenericObjectType($collectionType, [$methodOrPropertyReturnType, $valueType]);
+                } else {
+                    $returnType = new Type\Generic\GenericObjectType($collectionType, $types);
+                }
+                break;
+            case 'first':
+                $returnType = Type\TypeCombinator::addNull($valueType);
+                break;
+            case 'flatMap':
+                $returnType = new Type\Generic\GenericObjectType(SupportCollection::class, [new Type\IntegerType(), new Type\MixedType()]);
+                break;
+            case 'groupBy':
+            case 'partition':
+                $innerTypes = [
+                    new Type\Generic\GenericObjectType($collectionType, $types),
+                ];
+
+                if ($collectionType === SupportCollection::class) {
+                    array_unshift($innerTypes, new Type\IntegerType());
+                }
+
+                $returnType = new Type\Generic\GenericObjectType($collectionType, $innerTypes);
+                break;
+            case 'map':
+                $returnType = new Type\Generic\GenericObjectType(SupportCollection::class, [
+                    new Type\IntegerType(),
+                    $methodOrPropertyReturnType,
+                ]);
+                break;
+            case 'max':
+            case 'min':
+                $returnType = $methodOrPropertyReturnType;
+                break;
+            case 'sum':
+                if ($methodOrPropertyReturnType->accepts(new Type\IntegerType(), true)->yes()) {
+                    $returnType = new Type\IntegerType();
+                } else {
+                    $returnType = new Type\ErrorType();
+                }
+
+                break;
+            default:
+                $returnType = new Type\ErrorType();
+                break;
+        }
+
+        return $returnType;
+    }
+}
diff --git a/src/Types/AbortIfFunctionTypeSpecifyingExtension.php b/src/Types/AbortIfFunctionTypeSpecifyingExtension.php
new file mode 100644
index 0000000..ec16776
--- /dev/null
+++ b/src/Types/AbortIfFunctionTypeSpecifyingExtension.php
@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Types;
+
+use PhpParser\Node\Expr\FuncCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Analyser\SpecifiedTypes;
+use PHPStan\Analyser\TypeSpecifier;
+use PHPStan\Analyser\TypeSpecifierAwareExtension;
+use PHPStan\Analyser\TypeSpecifierContext;
+use PHPStan\Reflection\FunctionReflection;
+use PHPStan\Type\FunctionTypeSpecifyingExtension;
+
+final class AbortIfFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
+{
+    /** @var TypeSpecifier */
+    private $typeSpecifier;
+
+    /** @var bool */
+    protected $negate;
+
+    /** @var string */
+    protected $methodName;
+
+    public function __construct(bool $negate, string $methodName)
+    {
+        $this->negate = $negate;
+        $this->methodName = $methodName.'_'.($negate === false ? 'if' : 'unless');
+    }
+
+    public function isFunctionSupported(
+        FunctionReflection $functionReflection,
+        FuncCall $node,
+        TypeSpecifierContext $context
+    ): bool {
+        return $functionReflection->getName() === $this->methodName && $context->null();
+    }
+
+    public function specifyTypes(
+        FunctionReflection $functionReflection,
+        FuncCall $node,
+        Scope $scope,
+        TypeSpecifierContext $context
+    ): SpecifiedTypes {
+        if (count($node->args) < 2) {
+            return new SpecifiedTypes();
+        }
+
+        $context = $this->negate === false ? TypeSpecifierContext::createFalsey() : TypeSpecifierContext::createTruthy();
+
+        return $this->typeSpecifier->specifyTypesInCondition($scope, $node->getArgs()[0]->value, $context);
+    }
+
+    public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
+    {
+        $this->typeSpecifier = $typeSpecifier;
+    }
+}
diff --git a/src/Types/GenericEloquentBuilderTypeNodeResolverExtension.php b/src/Types/GenericEloquentBuilderTypeNodeResolverExtension.php
new file mode 100644
index 0000000..155926e
--- /dev/null
+++ b/src/Types/GenericEloquentBuilderTypeNodeResolverExtension.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Types;
+
+use function count;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Model;
+use PHPStan\Analyser\NameScope;
+use PHPStan\PhpDoc\TypeNodeResolverExtension;
+use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
+use PHPStan\PhpDocParser\Ast\Type\TypeNode;
+use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
+use PHPStan\Type\Generic\GenericObjectType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+
+class GenericEloquentBuilderTypeNodeResolverExtension implements TypeNodeResolverExtension
+{
+    public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type
+    {
+        if (! $typeNode instanceof UnionTypeNode || count($typeNode->types) !== 2) {
+            return null;
+        }
+
+        $modelTypeNode = null;
+        $builderTypeNode = null;
+        foreach ($typeNode->types as $innerTypeNode) {
+            if ($innerTypeNode instanceof IdentifierTypeNode
+                && is_subclass_of($nameScope->resolveStringName($innerTypeNode->name), Model::class)
+            ) {
+                $modelTypeNode = $innerTypeNode;
+                continue;
+            }
+
+            if (
+                $innerTypeNode instanceof IdentifierTypeNode
+                && ($nameScope->resolveStringName($innerTypeNode->name) === Builder::class || is_subclass_of($nameScope->resolveStringName($innerTypeNode->name), Builder::class))
+            ) {
+                $builderTypeNode = $innerTypeNode;
+            }
+        }
+
+        if ($modelTypeNode === null || $builderTypeNode === null) {
+            return null;
+        }
+
+        $builderTypeName = $nameScope->resolveStringName($builderTypeNode->name);
+        $modelTypeName = $nameScope->resolveStringName($modelTypeNode->name);
+
+        return new GenericObjectType($builderTypeName, [
+            new ObjectType($modelTypeName),
+        ]);
+    }
+}
diff --git a/src/Types/GenericEloquentCollectionTypeNodeResolverExtension.php b/src/Types/GenericEloquentCollectionTypeNodeResolverExtension.php
new file mode 100644
index 0000000..538db67
--- /dev/null
+++ b/src/Types/GenericEloquentCollectionTypeNodeResolverExtension.php
@@ -0,0 +1,84 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Types;
+
+use function count;
+use Illuminate\Database\Eloquent\Collection;
+use PHPStan\Analyser\NameScope;
+use PHPStan\PhpDoc\TypeNodeResolver;
+use PHPStan\PhpDoc\TypeNodeResolverExtension;
+use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
+use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
+use PHPStan\PhpDocParser\Ast\Type\TypeNode;
+use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
+use PHPStan\Type\Generic\GenericObjectType;
+use PHPStan\Type\Type;
+
+/**
+ * @see https://github.com/nunomaduro/larastan/issues/476
+ * @see https://gist.github.com/ondrejmirtes/56af016d0595788d5400b8dfb6520adc
+ *
+ * This extension interprets docblocks like:
+ *
+ * \Illuminate\Database\Eloquent\Collection|\App\Account[] $accounts
+ *
+ * and transforms them into:
+ *
+ * \Illuminate\Database\Eloquent\Collection<\App\Account> $accounts
+ *
+ * Now IDE's can benefit from auto-completion, and we can benefit from the correct type passed to the generic collection
+ */
+class GenericEloquentCollectionTypeNodeResolverExtension implements TypeNodeResolverExtension
+{
+    /**
+     * @var TypeNodeResolver
+     */
+    private $typeNodeResolver;
+
+    public function __construct(TypeNodeResolver $typeNodeResolver)
+    {
+        $this->typeNodeResolver = $typeNodeResolver;
+    }
+
+    public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type
+    {
+        if (! $typeNode instanceof UnionTypeNode || count($typeNode->types) !== 2) {
+            return null;
+        }
+
+        $arrayTypeNode = null;
+        $identifierTypeNode = null;
+        foreach ($typeNode->types as $innerTypeNode) {
+            if ($innerTypeNode instanceof ArrayTypeNode) {
+                $arrayTypeNode = $innerTypeNode;
+                continue;
+            }
+
+            if ($innerTypeNode instanceof IdentifierTypeNode) {
+                $identifierTypeNode = $innerTypeNode;
+            }
+        }
+
+        if ($arrayTypeNode === null || $identifierTypeNode === null) {
+            return null;
+        }
+
+        $identifierTypeName = $nameScope->resolveStringName($identifierTypeNode->name);
+        if ($identifierTypeName !== Collection::class) {
+            return null;
+        }
+
+        $innerArrayTypeNode = $arrayTypeNode->type;
+        if (! $innerArrayTypeNode instanceof IdentifierTypeNode) {
+            return null;
+        }
+
+        $resolvedInnerArrayType = $this->typeNodeResolver->resolve($innerArrayTypeNode, $nameScope);
+
+        return new GenericObjectType($identifierTypeName, [
+            $resolvedInnerArrayType,
+        ]);
+    }
+}
diff --git a/src/Types/ModelProperty/GenericModelPropertyType.php b/src/Types/ModelProperty/GenericModelPropertyType.php
new file mode 100644
index 0000000..2cb9530
--- /dev/null
+++ b/src/Types/ModelProperty/GenericModelPropertyType.php
@@ -0,0 +1,119 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Types\ModelProperty;
+
+use PHPStan\TrinaryLogic;
+use PHPStan\Type\ClassStringType;
+use PHPStan\Type\CompoundType;
+use PHPStan\Type\Constant\ConstantStringType;
+use PHPStan\Type\Generic\TemplateType;
+use PHPStan\Type\Generic\TemplateTypeMap;
+use PHPStan\Type\Generic\TemplateTypeVariance;
+use PHPStan\Type\IntersectionType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\ObjectWithoutClassType;
+use PHPStan\Type\Type;
+use PHPStan\Type\TypeCombinator;
+use PHPStan\Type\UnionType;
+
+class GenericModelPropertyType extends ModelPropertyType
+{
+    /** @var Type */
+    private $type;
+
+    public function __construct(Type $type)
+    {
+        parent::__construct();
+
+        $this->type = $type;
+    }
+
+    public function getReferencedClasses(): array
+    {
+        return $this->getGenericType()->getReferencedClasses();
+    }
+
+    public function getGenericType(): Type
+    {
+        return $this->type;
+    }
+
+    public function isSuperTypeOf(Type $type): TrinaryLogic
+    {
+        if ($type instanceof ConstantStringType) {
+            return $this->getGenericType()->hasProperty($type->getValue());
+        }
+
+        if ($type instanceof self) {
+            return TrinaryLogic::createYes();
+        }
+
+        if ($type instanceof parent) {
+            return TrinaryLogic::createMaybe();
+        }
+
+        if ($type instanceof CompoundType) {
+            return $type->isSubTypeOf($this);
+        }
+
+        return TrinaryLogic::createNo();
+    }
+
+    public function traverse(callable $cb): Type
+    {
+        $newType = $cb($this->getGenericType());
+
+        if ($newType === $this->getGenericType()) {
+            return $this;
+        }
+
+        return new self($newType);
+    }
+
+    public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
+    {
+        if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
+            return $receivedType->inferTemplateTypesOn($this);
+        }
+
+        if ($receivedType instanceof ConstantStringType) {
+            $typeToInfer = new ObjectType($receivedType->getValue());
+        } elseif ($receivedType instanceof self) {
+            $typeToInfer = $receivedType->type;
+        } elseif ($receivedType instanceof ClassStringType) {
+            $typeToInfer = $this->getGenericType();
+
+            if ($typeToInfer instanceof TemplateType) {
+                $typeToInfer = $typeToInfer->getBound();
+            }
+
+            $typeToInfer = TypeCombinator::intersect($typeToInfer, new ObjectWithoutClassType());
+        } else {
+            return TemplateTypeMap::createEmpty();
+        }
+
+        if (! $this->getGenericType()->isSuperTypeOf($typeToInfer)->no()) {
+            return $this->getGenericType()->inferTemplateTypes($typeToInfer);
+        }
+
+        return TemplateTypeMap::createEmpty();
+    }
+
+    public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
+    {
+        $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant());
+
+        return $this->getGenericType()->getReferencedTemplateTypes($variance);
+    }
+
+    /**
+     * @param  mixed[]  $properties
+     * @return Type
+     */
+    public static function __set_state(array $properties): Type
+    {
+        return new self($properties['type']);
+    }
+}
diff --git a/src/Types/ModelProperty/ModelPropertyType.php b/src/Types/ModelProperty/ModelPropertyType.php
new file mode 100644
index 0000000..a6b9731
--- /dev/null
+++ b/src/Types/ModelProperty/ModelPropertyType.php
@@ -0,0 +1,20 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Types\ModelProperty;
+
+use PHPStan\Type\StringType;
+use PHPStan\Type\Type;
+
+class ModelPropertyType extends StringType
+{
+    /**
+     * @param  mixed[]  $properties
+     * @return Type
+     */
+    public static function __set_state(array $properties): Type
+    {
+        return new self();
+    }
+}
diff --git a/src/Types/ModelProperty/ModelPropertyTypeNodeResolverExtension.php b/src/Types/ModelProperty/ModelPropertyTypeNodeResolverExtension.php
new file mode 100644
index 0000000..ed46f7e
--- /dev/null
+++ b/src/Types/ModelProperty/ModelPropertyTypeNodeResolverExtension.php
@@ -0,0 +1,67 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Types\ModelProperty;
+
+use Illuminate\Database\Eloquent\Model;
+use PHPStan\Analyser\NameScope;
+use PHPStan\PhpDoc\TypeNodeResolver;
+use PHPStan\PhpDoc\TypeNodeResolverExtension;
+use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
+use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
+use PHPStan\PhpDocParser\Ast\Type\TypeNode;
+use PHPStan\Type\ErrorType;
+use PHPStan\Type\NeverType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\StringType;
+use PHPStan\Type\Type;
+
+/**
+ * Ensures a 'model-property' type in PHPDoc is recognised to be of type ModelPropertyType.
+ */
+class ModelPropertyTypeNodeResolverExtension implements TypeNodeResolverExtension
+{
+    /** @var bool */
+    protected $active;
+
+    /** @var TypeNodeResolver */
+    protected $baseResolver;
+
+    public function __construct(TypeNodeResolver $baseResolver, bool $active)
+    {
+        $this->baseResolver = $baseResolver;
+        $this->active = $active;
+    }
+
+    public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type
+    {
+        if ($typeNode instanceof IdentifierTypeNode && $typeNode->name === 'model-property') {
+            return $this->active ? new ModelPropertyType() : new StringType();
+        }
+
+        if ($typeNode instanceof GenericTypeNode && $typeNode->type->name === 'model-property') {
+            if (! $this->active) {
+                return new StringType();
+            }
+
+            if (count($typeNode->genericTypes) !== 1) {
+                return new ErrorType();
+            }
+
+            $genericType = $this->baseResolver->resolve($typeNode->genericTypes[0], $nameScope);
+
+            if ((new ObjectType(Model::class))->isSuperTypeOf($genericType)->no()) {
+                return new ErrorType();
+            }
+
+            if ($genericType instanceof NeverType) {
+                return new ErrorType();
+            }
+
+            return new GenericModelPropertyType($genericType);
+        }
+
+        return null;
+    }
+}
diff --git a/src/Types/ModelRelationsDynamicMethodReturnTypeExtension.php b/src/Types/ModelRelationsDynamicMethodReturnTypeExtension.php
new file mode 100644
index 0000000..cec3276
--- /dev/null
+++ b/src/Types/ModelRelationsDynamicMethodReturnTypeExtension.php
@@ -0,0 +1,108 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Types;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\Relation;
+use Flarum\PHPStan\Concerns\HasContainer;
+use PhpParser\Node\Expr\MethodCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\ParametersAcceptorSelector;
+use PHPStan\ShouldNotHappenException;
+use PHPStan\Type\DynamicMethodReturnTypeExtension;
+use PHPStan\Type\Generic\GenericObjectType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+
+class ModelRelationsDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
+{
+    use HasContainer;
+
+    /** @var RelationParserHelper */
+    private $relationParserHelper;
+
+    public function __construct(RelationParserHelper $relationParserHelper)
+    {
+        $this->relationParserHelper = $relationParserHelper;
+    }
+
+    public function getClass(): string
+    {
+        return Model::class;
+    }
+
+    public function isMethodSupported(MethodReflection $methodReflection): bool
+    {
+        $variants = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants());
+
+        $returnType = $variants->getReturnType();
+
+        if (! $returnType instanceof ObjectType) {
+            return false;
+        }
+
+        if (! (new ObjectType(Relation::class))->isSuperTypeOf($returnType)->yes()) {
+            return false;
+        }
+
+        if (! $methodReflection->getDeclaringClass()->hasNativeMethod($methodReflection->getName())) {
+            return false;
+        }
+
+        if (count($variants->getParameters()) !== 0) {
+            return false;
+        }
+
+        if (in_array($methodReflection->getName(), [
+            'hasOne', 'hasOneThrough', 'morphOne',
+            'belongsTo', 'morphTo',
+            'hasMany', 'hasManyThrough', 'morphMany',
+            'belongsToMany', 'morphToMany', 'morphedByMany',
+        ], true)) {
+            return false;
+        }
+
+        $relatedModel = $this
+            ->relationParserHelper
+            ->findRelatedModelInRelationMethod($methodReflection);
+
+        return $relatedModel !== null;
+    }
+
+    /**
+     * @param  MethodReflection  $methodReflection
+     * @param  MethodCall  $methodCall
+     * @param  Scope  $scope
+     * @return Type
+     *
+     * @throws ShouldNotHappenException
+     */
+    public function getTypeFromMethodCall(
+        MethodReflection $methodReflection,
+        MethodCall $methodCall,
+        Scope $scope
+    ): Type {
+        /** @var ObjectType $returnType */
+        $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
+
+        /** @var string $relatedModelClassName */
+        $relatedModelClassName = $this
+            ->relationParserHelper
+            ->findRelatedModelInRelationMethod($methodReflection);
+
+        $classReflection = $methodReflection->getDeclaringClass();
+
+        if ($returnType->isInstanceOf(BelongsTo::class)->yes()) {
+            return new GenericObjectType($returnType->getClassName(), [
+                new ObjectType($relatedModelClassName),
+                new ObjectType($classReflection->getName()),
+            ]);
+        }
+
+        return new GenericObjectType($returnType->getClassName(), [new ObjectType($relatedModelClassName)]);
+    }
+}
diff --git a/src/Types/Passable.php b/src/Types/Passable.php
new file mode 100644
index 0000000..b980733
--- /dev/null
+++ b/src/Types/Passable.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Types;
+
+use Flarum\PHPStan\Contracts\Types\PassableContract;
+use PHPStan\Type\Type;
+
+/**
+ * @internal
+ */
+final class Passable implements PassableContract
+{
+    /**
+     * @var \PHPStan\Type\Type
+     */
+    private $type;
+
+    /**
+     * Passable constructor.
+     *
+     * @param  \PHPStan\Type\Type  $type
+     */
+    public function __construct(Type $type)
+    {
+        $this->type = $type;
+    }
+
+    /**
+     * @return \PHPStan\Type\Type
+     */
+    public function getType(): Type
+    {
+        return $this->type;
+    }
+
+    /**
+     * @param  \PHPStan\Type\Type  $type
+     */
+    public function setType(Type $type): void
+    {
+        $this->type = $type;
+    }
+}
diff --git a/src/Types/RelationDynamicMethodReturnTypeExtension.php b/src/Types/RelationDynamicMethodReturnTypeExtension.php
new file mode 100644
index 0000000..c41c619
--- /dev/null
+++ b/src/Types/RelationDynamicMethodReturnTypeExtension.php
@@ -0,0 +1,65 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Types;
+
+use Illuminate\Database\Eloquent\Model;
+use PhpParser\Node\Expr\MethodCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\FunctionVariant;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\ParametersAcceptorSelector;
+use PHPStan\ShouldNotHappenException;
+use PHPStan\Type\Constant\ConstantStringType;
+use PHPStan\Type\DynamicMethodReturnTypeExtension;
+use PHPStan\Type\Generic\GenericObjectType;
+use PHPStan\Type\ObjectType;
+use PHPStan\Type\Type;
+
+class RelationDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
+{
+    public function getClass(): string
+    {
+        return Model::class;
+    }
+
+    public function isMethodSupported(MethodReflection $methodReflection): bool
+    {
+        return in_array($methodReflection->getName(), [
+            'hasOne', 'hasOneThrough', 'morphOne',
+            'belongsTo', 'morphTo',
+            'hasMany', 'hasManyThrough', 'morphMany',
+            'belongsToMany', 'morphToMany', 'morphedByMany',
+        ], true);
+    }
+
+    /**
+     * @throws ShouldNotHappenException
+     */
+    public function getTypeFromMethodCall(
+        MethodReflection $methodReflection,
+        MethodCall $methodCall,
+        Scope $scope
+    ): Type {
+        /** @var FunctionVariant $functionVariant */
+        $functionVariant = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants());
+        $returnType = $functionVariant->getReturnType();
+
+        if (count($methodCall->getArgs()) === 0) {
+            return $returnType;
+        }
+
+        $argType = $scope->getType($methodCall->getArgs()[0]->value);
+
+        if (! $argType instanceof ConstantStringType) {
+            return $returnType;
+        }
+
+        if (! $returnType instanceof ObjectType) {
+            return $returnType;
+        }
+
+        return new GenericObjectType($returnType->getClassName(), [new ObjectType($argType->getValue())]);
+    }
+}
diff --git a/src/Types/RelationParserHelper.php b/src/Types/RelationParserHelper.php
new file mode 100644
index 0000000..8e69107
--- /dev/null
+++ b/src/Types/RelationParserHelper.php
@@ -0,0 +1,132 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Types;
+
+use PhpParser\Node;
+use PhpParser\Node\Expr\MethodCall;
+use PhpParser\NodeFinder;
+use PHPStan\Analyser\ScopeContext;
+use PHPStan\Analyser\ScopeFactory;
+use PHPStan\Parser\Parser;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\Type\Constant\ConstantStringType;
+use PHPStan\Type\Generic\GenericClassStringType;
+use PHPStan\Type\Generic\TemplateTypeMap;
+use PHPStan\Type\ObjectType;
+
+class RelationParserHelper
+{
+    /** @var Parser */
+    private $parser;
+
+    /** @var ScopeFactory */
+    private $scopeFactory;
+
+    /** @var ReflectionProvider */
+    private $reflectionProvider;
+
+    public function __construct(Parser $parser, ScopeFactory $scopeFactory, ReflectionProvider $reflectionProvider)
+    {
+        $this->parser = $parser;
+        $this->scopeFactory = $scopeFactory;
+        $this->reflectionProvider = $reflectionProvider;
+    }
+
+    public function findRelatedModelInRelationMethod(
+        MethodReflection $methodReflection
+    ): ?string {
+        $fileName = $methodReflection
+            ->getDeclaringClass()
+            ->getNativeReflection()
+            ->getMethod($methodReflection->getName())
+            ->getFileName();
+
+        if ($fileName === false) {
+            return null;
+        }
+
+        $fileStmts = $this->parser->parseFile($fileName);
+
+        /** @var Node\Stmt\ClassMethod|null $relationMethod */
+        $relationMethod = $this->findMethod($methodReflection->getName(), $fileStmts);
+
+        if ($relationMethod === null) {
+            return null;
+        }
+
+        /** @var Node\Stmt\Return_|null $returnStmt */
+        $returnStmt = $this->findReturn($relationMethod);
+
+        if ($returnStmt === null || ! $returnStmt->expr instanceof MethodCall) {
+            return null;
+        }
+
+        $methodCall = $returnStmt->expr;
+
+        while ($methodCall->var instanceof MethodCall) {
+            $methodCall = $methodCall->var;
+        }
+
+        if (count($methodCall->getArgs()) < 1) {
+            return null;
+        }
+
+        $scope = $this->scopeFactory->create(
+            ScopeContext::create($fileName),
+            false,
+            [],
+            $methodReflection
+        );
+
+        $methodScope = $scope
+            ->enterClass($methodReflection->getDeclaringClass())
+            ->enterClassMethod($relationMethod, TemplateTypeMap::createEmpty(), [], null, null, null, false, false, false);
+
+        $argType = $methodScope->getType($methodCall->getArgs()[0]->value);
+        $returnClass = null;
+
+        if ($argType instanceof ConstantStringType) {
+            $returnClass = $argType->getValue();
+        }
+
+        if ($argType instanceof GenericClassStringType) {
+            $modelType = $argType->getGenericType();
+
+            if (! $modelType instanceof ObjectType) {
+                return null;
+            }
+
+            $returnClass = $modelType->getClassName();
+        }
+
+        if ($returnClass === null) {
+            return null;
+        }
+
+        return $this->reflectionProvider->hasClass($returnClass) ? $returnClass : null;
+    }
+
+    /**
+     * @param  string  $method
+     * @param  mixed  $statements
+     * @return Node|null
+     */
+    private function findMethod(string $method, $statements): ?Node
+    {
+        return (new NodeFinder)->findFirst($statements, static function (Node $node) use ($method) {
+            return $node instanceof Node\Stmt\ClassMethod
+                && $node->name->toString() === $method;
+        });
+    }
+
+    private function findReturn(Node\Stmt\ClassMethod $relationMethod): ?Node
+    {
+        /** @var Node[] $statements */
+        $statements = $relationMethod->stmts;
+
+        return (new NodeFinder)->findFirstInstanceOf($statements, Node\Stmt\Return_::class);
+    }
+}
diff --git a/src/Types/ViewStringType.php b/src/Types/ViewStringType.php
new file mode 100644
index 0000000..985d6c1
--- /dev/null
+++ b/src/Types/ViewStringType.php
@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Types;
+
+use PHPStan\TrinaryLogic;
+use PHPStan\Type\CompoundType;
+use PHPStan\Type\Constant\ConstantStringType;
+use PHPStan\Type\StringType;
+use PHPStan\Type\Type;
+
+/**
+ * The custom 'view-string' type class. It's a subset of the string type. Every string that passes the
+ * view()->exists($string) test is a valid view-string type.
+ */
+class ViewStringType extends StringType
+{
+    public function describe(\PHPStan\Type\VerbosityLevel $level): string
+    {
+        return 'view-string';
+    }
+
+    public function accepts(Type $type, bool $strictTypes): TrinaryLogic
+    {
+        if ($type instanceof CompoundType) {
+            return $type->isAcceptedBy($this, $strictTypes);
+        }
+
+        if ($type instanceof ConstantStringType) {
+            /** @var \Illuminate\View\Factory $view */
+            $view = view();
+
+            return TrinaryLogic::createFromBoolean($view->exists($type->getValue()));
+        }
+
+        if ($type instanceof self) {
+            return TrinaryLogic::createYes();
+        }
+
+        if ($type instanceof StringType) {
+            return TrinaryLogic::createMaybe();
+        }
+
+        return TrinaryLogic::createNo();
+    }
+
+    public function isSuperTypeOf(Type $type): TrinaryLogic
+    {
+        if ($type instanceof ConstantStringType) {
+            /** @var \Illuminate\View\Factory $view */
+            $view = view();
+
+            return TrinaryLogic::createFromBoolean($view->exists($type->getValue()));
+        }
+
+        if ($type instanceof self) {
+            return TrinaryLogic::createYes();
+        }
+
+        if ($type instanceof parent) {
+            return TrinaryLogic::createMaybe();
+        }
+
+        if ($type instanceof CompoundType) {
+            return $type->isSubTypeOf($this);
+        }
+
+        return TrinaryLogic::createNo();
+    }
+
+    /**
+     * @param  mixed[]  $properties
+     * @return Type
+     */
+    public static function __set_state(array $properties): Type
+    {
+        return new self();
+    }
+}
diff --git a/src/Types/ViewStringTypeNodeResolverExtension.php b/src/Types/ViewStringTypeNodeResolverExtension.php
new file mode 100644
index 0000000..8883e6d
--- /dev/null
+++ b/src/Types/ViewStringTypeNodeResolverExtension.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Flarum\PHPStan\Types;
+
+use PHPStan\Analyser\NameScope;
+use PHPStan\PhpDoc\TypeNodeResolverExtension;
+use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
+use PHPStan\PhpDocParser\Ast\Type\TypeNode;
+use PHPStan\Type\Type;
+
+/**
+ * Ensures a 'view-string' type in PHPDoc is recognised to be of type ViewStringType.
+ */
+class ViewStringTypeNodeResolverExtension implements TypeNodeResolverExtension
+{
+    public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type
+    {
+        if ($typeNode instanceof IdentifierTypeNode && $typeNode->__toString() === 'view-string') {
+            return new ViewStringType();
+        }
+
+        return null;
+    }
+}
diff --git a/stubs/Contracts/Container.stub b/stubs/Contracts/Container.stub
new file mode 100644
index 0000000..78ed9c6
--- /dev/null
+++ b/stubs/Contracts/Container.stub
@@ -0,0 +1,16 @@
+<?php
+
+namespace Illuminate\Contracts\Container;
+
+/** @extends \ArrayAccess<string, mixed> */
+interface Container extends \ArrayAccess
+{
+
+}
+
+namespace Illuminate\Contracts\Foundation;
+
+interface Application extends \Illuminate\Contracts\Container\Container
+{
+
+}
diff --git a/stubs/Contracts/Pagination.stub b/stubs/Contracts/Pagination.stub
new file mode 100644
index 0000000..f23beab
--- /dev/null
+++ b/stubs/Contracts/Pagination.stub
@@ -0,0 +1,17 @@
+<?php
+
+namespace Illuminate\Contracts\Pagination;
+
+/**
+ * @mixin \Illuminate\Support\Collection
+ * @mixin \Illuminate\Pagination\Paginator
+ */
+interface Paginator
+{}
+
+/**
+ * @mixin \Illuminate\Support\Collection
+ * @mixin \Illuminate\Pagination\LengthAwarePaginator
+ */
+interface LengthAwarePaginator extends Paginator
+{}
diff --git a/stubs/Contracts/Support.stub b/stubs/Contracts/Support.stub
new file mode 100644
index 0000000..900be08
--- /dev/null
+++ b/stubs/Contracts/Support.stub
@@ -0,0 +1,19 @@
+<?php
+
+namespace Illuminate\Contracts\Support;
+
+interface Htmlable
+{}
+
+interface Arrayable
+{
+    /**
+     * Get the instance as an array.
+     *
+     * @return array<mixed>
+     */
+    public function toArray();
+}
+
+interface Jsonable
+{}
diff --git a/stubs/Flarum/User.stub b/stubs/Flarum/User.stub
new file mode 100644
index 0000000..4ce9aec
--- /dev/null
+++ b/stubs/Flarum/User.stub
@@ -0,0 +1,31 @@
+<?php
+namespace Flarum\User {
+  trait AccountActivationMailerTrait
+  {
+    /**
+     * @param User $user
+     * @param string $email
+     * @return EmailToken
+     */
+    protected function generateToken(\Flarum\User\User $user, $email)
+    {
+    }
+    /**
+     * Get the data that should be made available to email templates.
+     *
+     * @param User $user
+     * @param EmailToken $token
+     * @return array
+     */
+    protected function getEmailData(\Flarum\User\User $user, \Flarum\User\EmailToken $token)
+    {
+    }
+    /**
+     * @param User $user
+     * @param array $data
+     */
+    protected function sendConfirmationEmail(\Flarum\User\User $user, $data)
+    {
+    }
+  }
+}
diff --git a/stubs/Illuminate/Collection.stub b/stubs/Illuminate/Collection.stub
new file mode 100644
index 0000000..8d69c78
--- /dev/null
+++ b/stubs/Illuminate/Collection.stub
@@ -0,0 +1,148 @@
+<?php
+
+namespace Illuminate\Support;
+
+/**
+ * @template TKey
+ * @template TValue
+ * @implements \ArrayAccess<TKey, TValue>
+ * @implements Enumerable<TKey, TValue>
+ */
+class Collection implements \ArrayAccess, Enumerable
+{
+    /**
+     * @param  callable|null  $callback
+     * @param  mixed  $default
+     * @return TValue|null
+     */
+    public function first(callable $callback = null, $default = null){}
+
+    /**
+     * @param  callable|null  $callback
+     * @param  mixed  $default
+     * @return TValue|null
+     */
+    public function last(callable $callback = null, $default = null){}
+
+
+    /**
+     * @param  mixed  $key
+     * @param  mixed  $default
+     * @return TValue|null
+     */
+    public function get($key, $default = null) {}
+
+    /**
+     * @return TValue|null
+     */
+    public function pop() {}
+
+    /**
+     * @param  mixed  $key
+     * @param  mixed  $default
+     * @return TValue|null
+     */
+    public function pull($key, $default = null) {}
+
+    /**
+     * @param mixed $value
+     * @param bool  $strict
+     * @return TKey|false
+     */
+    public function search($value, $strict = false) {}
+
+    /**
+     * @return TValue|null
+     */
+    public function shift() {}
+
+    /**
+     * @param callable(TValue, TKey): (void|bool) $callable
+     * @return static<TValue>
+     */
+    public function each($callable) {}
+
+    /**
+     * @template TReturn
+     * @param callable(TValue, TKey): TReturn $callable
+     * @return static<TKey, TReturn>
+     */
+    public function map($callable) {}
+
+    /**
+     * Run a grouping map over the items.
+     *
+     * The callback should return an associative array with a single key/value pair.
+     *
+     * @template TMapToGroupsKey of array-key
+     * @template TMapToGroupsValue
+     *
+     * @param  callable(TValue, TKey): array<TMapToGroupsKey, TMapToGroupsValue>  $callback
+     * @return static<TMapToGroupsKey, static<int, TMapToGroupsValue>>
+     */
+    public function mapToGroups(callable $callback) {}
+
+    /**
+     * @param array<mixed>|string|callable(TValue, TKey): mixed $groupBy
+     * @param bool $preserveKeys
+     * @return static<array-key, static<TKey, TValue>>
+     */
+    public function groupBy($groupBy, $preserveKeys = false);
+
+    /**
+     * @template TClass
+     * @param class-string<TClass> $class
+     * @return static<int, TClass>
+     */
+    public function mapInto($class);
+
+    /**
+     * @template TReturn
+     * @param callable(TValue, TKey): (array<TReturn>|\Illuminate\Support\Enumerable<array-key, TReturn>) $callback
+     * @return static<TKey, TReturn>
+     */
+    public function flatMap(callable $callback) {}
+
+    /**
+     * @template TReturn
+     * @param callable(TValue ...$values): TReturn $callback
+     * @return static<int, TReturn>
+     */
+    public function mapSpread(callable $callback) {}
+
+    /**
+     * @param int $number
+     * @param null|callable(int, int): mixed $callback
+     * @return static<mixed>
+     */
+    public static function times($number, callable $callback = null) {}
+
+    /**
+     * @param string|array<mixed> $value
+     * @param string|null $key
+     * @return static<int, mixed>
+     */
+    public function pluck($value, $key = null) {}
+
+    /**
+     * @return TValue
+     */
+    public function pop() {}
+
+    /**
+     * Push one or more items onto the end of the collection.
+     *
+     * @param TValue ...$values
+     * @return static
+     */
+    public function push(...$values) {}
+
+    /**
+     * Put an item in the collection by key.
+     *
+     * @param TKey $key
+     * @param TValue $value
+     * @return static
+     */
+    public function put($key, $value) {}
+}
diff --git a/stubs/Illuminate/Contracts/Container/Container.stub b/stubs/Illuminate/Contracts/Container/Container.stub
deleted file mode 100644
index 6e07ef1..0000000
--- a/stubs/Illuminate/Contracts/Container/Container.stub
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php
-
-namespace Illuminate\Contracts\Container;
-
-interface Container
-{
-}
diff --git a/stubs/Illuminate/Contracts/Filesystem/Cloud.stub b/stubs/Illuminate/Contracts/Filesystem/Cloud.stub
deleted file mode 100644
index 37e286c..0000000
--- a/stubs/Illuminate/Contracts/Filesystem/Cloud.stub
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php
-
-namespace Illuminate\Contracts\Filesystem;
-
-interface Cloud extends Filesystem
-{
-}
diff --git a/stubs/Illuminate/Contracts/Filesystem/Factory.stub b/stubs/Illuminate/Contracts/Filesystem/Factory.stub
deleted file mode 100644
index fe87edc..0000000
--- a/stubs/Illuminate/Contracts/Filesystem/Factory.stub
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-namespace Illuminate\Contracts\Filesystem;
-
-interface Factory
-{
-    /**
-     * Get a filesystem implementation.
-     *
-     * @param  string|null  $name
-     * @return Cloud
-     */
-    public function disk($name = null);
-}
diff --git a/stubs/Illuminate/Contracts/Filesystem/Filesystem.stub b/stubs/Illuminate/Contracts/Filesystem/Filesystem.stub
deleted file mode 100644
index ea14b17..0000000
--- a/stubs/Illuminate/Contracts/Filesystem/Filesystem.stub
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php
-
-namespace Illuminate\Contracts\Filesystem;
-
-interface Filesystem
-{
-}
diff --git a/stubs/Illuminate/Database/BelongsTo.stub b/stubs/Illuminate/Database/BelongsTo.stub
new file mode 100644
index 0000000..a5d4740
--- /dev/null
+++ b/stubs/Illuminate/Database/BelongsTo.stub
@@ -0,0 +1,27 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+/**
+ * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
+ * @template TChildModel of \Illuminate\Database\Eloquent\Model
+ * @extends Relation<TRelatedModel>
+ */
+class BelongsTo extends Relation
+{
+    /** @phpstan-return TChildModel */
+    public function associate();
+
+    /** @phpstan-return TChildModel */
+    public function dissociate();
+
+    /** @phpstan-return TChildModel */
+    public function getChild();
+
+    /**
+     * Get the results of the relationship.
+     *
+     * @phpstan-return ?TRelatedModel
+     */
+    public function getResults();
+}
diff --git a/stubs/Illuminate/Database/BelongsToMany.stub b/stubs/Illuminate/Database/BelongsToMany.stub
new file mode 100644
index 0000000..22484d0
--- /dev/null
+++ b/stubs/Illuminate/Database/BelongsToMany.stub
@@ -0,0 +1,112 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+/**
+ * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
+ * @extends Relation<TRelatedModel>
+ */
+class BelongsToMany extends Relation
+{
+    /**
+     * Find a related model by its primary key or return new instance of the related model.
+     *
+     * @param  mixed  $id
+     * @param  array<int, mixed>  $columns
+     * @return \Illuminate\Support\Collection<int, TRelatedModel>|TRelatedModel
+     */
+    public function findOrNew($id, $columns = ['*']);
+
+    /**
+     * Get the first related model record matching the attributes or instantiate it.
+     *
+     * @param  array<string, mixed>  $attributes
+     * @return TRelatedModel
+     */
+    public function firstOrNew(array $attributes);
+
+    /**
+     * Get the first related record matching the attributes or create it.
+     *
+     * @param  array<string, mixed>  $attributes
+     * @param  array<mixed>  $joining
+     * @param  bool  $touch
+     * @return TRelatedModel
+     */
+    public function firstOrCreate(array $attributes, array $joining = [], $touch = true);
+
+    /**
+     * Create or update a related record matching the attributes, and fill it with values.
+     *
+     * @param  array<string, mixed>  $attributes
+     * @param  array<mixed>  $values
+     * @param  array<mixed>  $joining
+     * @param  bool  $touch
+     * @return TRelatedModel
+     */
+    public function updateOrCreate(array $attributes, array $values = [], array $joining = [], $touch = true);
+
+    /**
+     * Find a related model by its primary key.
+     *
+     * @param  mixed  $id
+     * @param  array<int, mixed>  $columns
+     * @return TRelatedModel|\Illuminate\Database\Eloquent\Collection<TRelatedModel>|null
+     */
+    public function find($id, $columns = ['*']);
+
+    /**
+     * Find multiple related models by their primary keys.
+     *
+     * @param  \Illuminate\Contracts\Support\Arrayable|int[]  $ids
+     * @param  array<int, mixed>  $columns
+     * @return \Illuminate\Database\Eloquent\Collection<TRelatedModel>
+     */
+    public function findMany($ids, $columns = ['*']);
+
+    /**
+     * Find a related model by its primary key or throw an exception.
+     *
+     * @param  mixed  $id
+     * @param  array<int, mixed>  $columns
+     * @return TRelatedModel|\Illuminate\Database\Eloquent\Collection<TRelatedModel>
+     *
+     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
+     */
+    public function findOrFail($id, $columns = ['*']);
+
+    /**
+     * Execute the query and get the first result.
+     *
+     * @param  array<int, mixed>  $columns
+     * @return TRelatedModel|null
+     */
+    public function first($columns = ['*']);
+
+    /**
+     * Execute the query and get the first result or throw an exception.
+     *
+     * @param  array<int, mixed>  $columns
+     * @return TRelatedModel
+     *
+     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
+     */
+    public function firstOrFail($columns = ['*']);
+
+    /**
+     * Create a new instance of the related model.
+     *
+     * @param  array<model-property<TRelatedModel>, mixed>  $attributes
+     * @param  mixed[]  $joining
+     * @param  bool  $touch
+     * @return TRelatedModel
+     */
+    public function create(array $attributes = [], array $joining = [], $touch = true);
+
+    /**
+     * Get the results of the relationship.
+     *
+     * @phpstan-return \Illuminate\Database\Eloquent\Collection<TRelatedModel>
+     */
+    public function getResults();
+}
diff --git a/stubs/Illuminate/Database/EloquentBuilder.stub b/stubs/Illuminate/Database/EloquentBuilder.stub
new file mode 100644
index 0000000..039dd8e
--- /dev/null
+++ b/stubs/Illuminate/Database/EloquentBuilder.stub
@@ -0,0 +1,454 @@
+<?php
+
+namespace Illuminate\Database\Eloquent;
+
+/**
+ * @template TModelClass of Model
+ * @property-read static<TModelClass> $orWhere
+ */
+class Builder
+{
+  /**
+   * Create and return an un-saved model instance.
+   *
+   * @phpstan-param array<model-property<TModelClass>, mixed> $attributes
+   * @phpstan-return TModelClass
+   */
+  public function make(array $attributes = []);
+
+  /**
+   * Register a new global scope.
+   *
+   * @param  string  $identifier
+   * @param  \Illuminate\Database\Eloquent\Scope|\Closure  $scope
+   * @return static<TModelClass>
+   */
+  public function withGlobalScope($identifier, $scope);
+
+  /**
+   * Remove a registered global scope.
+   *
+   * @param  \Illuminate\Database\Eloquent\Scope|string  $scope
+   * @return static<TModelClass>
+   */
+  public function withoutGlobalScope($scope);
+
+  /** @phpstan-return TModelClass */
+  public function getModel();
+
+  /**
+   * @phpstan-param array<model-property<TModelClass>, mixed> $attributes
+   * @phpstan-return TModelClass
+   */
+  public function create(array $attributes = []);
+
+  /**
+   * Create a collection of models from plain arrays.
+   *
+   * @param  array<mixed>  $items
+   * @phpstan-return \Illuminate\Database\Eloquent\Collection<TModelClass>
+   */
+  public function hydrate(array $items);
+
+  /**
+   * Create a collection of models from a raw query.
+   *
+   * @param  string  $query
+   * @param  array<mixed>  $bindings
+   * @phpstan-return \Illuminate\Database\Eloquent\Collection<TModelClass>
+   */
+  public function fromQuery($query, $bindings = []);
+
+  /**
+   * Find a model by its primary key.
+   *
+   * @param  mixed  $id
+   * @param  array<int, (model-property<TModelClass>|'*')>|model-property<TModelClass>|'*'  $columns
+   * @phpstan-return TModelClass|\Illuminate\Database\Eloquent\Collection<TModelClass>|null
+   */
+  public function find($id, $columns = ['*']);
+
+  /**
+   * Find multiple models by their primary keys.
+   *
+   * @param  \Illuminate\Contracts\Support\Arrayable|array<mixed>  $ids
+   * @param  array<int, (model-property<TModelClass>|'*')>|model-property<TModelClass>|'*'  $columns
+   * @phpstan-return \Illuminate\Database\Eloquent\Collection<TModelClass>
+   */
+  public function findMany($ids, $columns = ['*']);
+
+  /**
+   * Find a model by its primary key or throw an exception.
+   *
+   * @param  mixed  $id
+   * @param  array<int, (model-property<TModelClass>|'*')>|model-property<TModelClass>|'*'  $columns
+   * @phpstan-return TModelClass|\Illuminate\Database\Eloquent\Collection<TModelClass>
+   *
+   * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
+   */
+  public function findOrFail($id, $columns = ['*']);
+
+  /**
+   * Find a model by its primary key or return fresh model instance.
+   *
+   * @param  mixed  $id
+   * @param  array<int, (model-property<TModelClass>|'*')>|model-property<TModelClass>|'*'  $columns
+   * @phpstan-return TModelClass
+   */
+  public function findOrNew($id, $columns = ['*']);
+
+  /**
+   * Execute the query and get the first result.
+   *
+   * @param  array<model-property<TModelClass>|int, mixed>|string  $columns
+   * @return TModelClass|null
+   */
+  public function first($columns = ['*']);
+
+  /**
+   * Get the first record matching the attributes or instantiate it.
+   *
+   * @param  array<model-property<TModelClass>, mixed>  $attributes
+   * @param  array<model-property<TModelClass>, mixed>  $values
+   * @phpstan-return TModelClass
+   */
+  public function firstOrNew(array $attributes = [], array $values = []);
+
+  /**
+   * Get the first record matching the attributes or create it.
+   *
+   * @param  array<model-property<TModelClass>, mixed>  $attributes
+   * @param  array<model-property<TModelClass>, mixed>  $values
+   * @phpstan-return TModelClass
+   */
+  public function firstOrCreate(array $attributes, array $values = []);
+
+  /**
+   * Create or update a record matching the attributes, and fill it with values.
+   *
+   * @param  array<model-property<TModelClass>, mixed>  $attributes
+   * @param  array<model-property<TModelClass>, mixed>  $values
+   * @phpstan-return TModelClass
+   */
+  public function updateOrCreate(array $attributes, array $values = []);
+
+  /**
+   * @param  array<model-property<TModelClass>, mixed>  $attributes
+   * @phpstan-return TModelClass
+   */
+  public function forceCreate(array $attributes);
+
+  /**
+   * @param  array<model-property<TModelClass>, mixed>  $values
+   * @return int
+   */
+  public function update(array $values);
+
+  /**
+   * Execute the query and get the first result or throw an exception.
+   *
+   * @param  array<int, (model-property<TModelClass>|'*')>|model-property<TModelClass>|'*'  $columns
+   * @phpstan-return TModelClass
+   *
+   * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
+   */
+  public function firstOrFail($columns = ['*']);
+
+  /**
+   * Execute the query and get the first result if it's the sole matching record.
+   *
+   * @param  array<int, (model-property<TModelClass>|'*')>|model-property<TModelClass>|'*'  $columns
+   * @phpstan-return TModelClass
+   */
+  public function sole($columns = ['*']);
+
+  /**
+   * Execute the query and get the first result or call a callback.
+   *
+   * @param  \Closure|array<int, (model-property<TModelClass>|'*')>  $columns
+   * @param  \Closure|null  $callback
+   * @phpstan-return TModelClass|mixed
+   */
+  public function firstOr($columns = ['*'], \Closure $callback = null);
+
+  /**
+   * Add a basic where clause to the query.
+   *
+   * @param  \Closure|model-property<TModelClass>|array<model-property<TModelClass>|int, mixed>|\Illuminate\Database\Query\Expression  $column
+   * @param  mixed  $operator
+   * @param  mixed  $value
+   * @param  string  $boolean
+   * @return static
+   */
+  public function where($column, $operator = null, $value = null, $boolean = 'and');
+
+  /**
+   * Add an "or where" clause to the query.
+   *
+   * @param  \Closure|model-property<TModelClass>|array<model-property<TModelClass>|int, mixed>|\Illuminate\Database\Query\Expression  $column
+   * @param  mixed  $operator
+   * @param  mixed  $value
+   * @return static
+   */
+  public function orWhere($column, $operator = null, $value = null);
+
+  /**
+   * Add a relationship count / exists condition to the query.
+   *
+   * @template TRelatedModel of Model
+   * @param  \Illuminate\Database\Eloquent\Relations\Relation<TRelatedModel>|string  $relation
+   * @param  string  $operator
+   * @param  int  $count
+   * @param  string  $boolean
+   * @param  \Closure|null  $callback
+   * @return static
+   *
+   * @throws \RuntimeException
+   */
+  public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', \Closure $callback = null);
+
+  /**
+   * Add a relationship count / exists condition to the query with an "or".
+   *
+   * @param  string  $relation
+   * @param  string  $operator
+   * @param  int  $count
+   * @return static
+   */
+  public function orHas($relation, $operator = '>=', $count = 1);
+
+  /**
+   * Add a relationship count / exists condition to the query.
+   *
+   * @param  string  $relation
+   * @param  string  $boolean
+   * @param  \Closure|null  $callback
+   * @return static
+   */
+  public function doesntHave($relation, $boolean = 'and', \Closure $callback = null);
+
+  /**
+   * Add a relationship count / exists condition to the query with an "or".
+   *
+   * @param  string  $relation
+   * @return static
+   */
+  public function orDoesntHave($relation);
+
+  /**
+   * Add a relationship count / exists condition to the query with where clauses.
+   *
+   * @param  string  $relation
+   * @param  \Closure|null  $callback
+   * @param  string  $operator
+   * @param  int  $count
+   * @return static
+   */
+  public function whereHas($relation, \Closure $callback = null, $operator = '>=', $count = 1);
+
+  /**
+   * Add a relationship count / exists condition to the query with where clauses and an "or".
+   *
+   * @param  string  $relation
+   * @param  \Closure|null  $callback
+   * @param  string  $operator
+   * @param  int  $count
+   * @return static
+   */
+  public function orWhereHas($relation, \Closure $callback = null, $operator = '>=', $count = 1);
+
+  /**
+   * Add a polymorphic relationship count / exists condition to the query.
+   *
+   * @template TRelatedModel of Model
+   * @template TChildModel of Model
+   * @param  \Illuminate\Database\Eloquent\Relations\MorphTo<TRelatedModel, TChildModel>|string  $relation
+   * @param  string|array<string>  $types
+   * @param  string  $operator
+   * @param  int  $count
+   * @param  string  $boolean
+   * @param  \Closure|null  $callback
+   * @return static
+   */
+  public function hasMorph($relation, $types, $operator = '>=', $count = 1, $boolean = 'and', \Closure $callback = null);
+
+  /**
+   * Add a polymorphic relationship count / exists condition to the query with an "or".
+   *
+   * @template TRelatedModel of Model
+   * @template TChildModel of Model
+   * @param  \Illuminate\Database\Eloquent\Relations\MorphTo<TRelatedModel, TChildModel>|string  $relation
+   * @param  string|array<string>  $types
+   * @param  string  $operator
+   * @param  int  $count
+   * @return static
+   */
+  public function orHasMorph($relation, $types, $operator = '>=', $count = 1);
+
+  /**
+   * Add a polymorphic relationship count / exists condition to the query.
+   *
+   * @template TRelatedModel of Model
+   * @template TChildModel of Model
+   * @param  \Illuminate\Database\Eloquent\Relations\MorphTo<TRelatedModel, TChildModel>|string  $relation
+   * @param  string|array<string>  $types
+   * @param  string  $boolean
+   * @param  \Closure|null  $callback
+   * @return static
+   */
+  public function doesntHaveMorph($relation, $types, $boolean = 'and', \Closure $callback = null);
+
+  /**
+   * Add a polymorphic relationship count / exists condition to the query with an "or".
+   *
+   * @template TRelatedModel of Model
+   * @template TChildModel of Model
+   * @param  \Illuminate\Database\Eloquent\Relations\MorphTo<TRelatedModel, TChildModel>|string  $relation
+   * @param  string|array<string>  $types
+   * @return static
+   */
+  public function orDoesntHaveMorph($relation, $types);
+
+  /**
+   * Add a polymorphic relationship count / exists condition to the query with where clauses.
+   *
+   * @template TRelatedModel of Model
+   * @template TChildModel of Model
+   * @param  \Illuminate\Database\Eloquent\Relations\MorphTo<TRelatedModel, TChildModel>|string  $relation
+   * @param  string|array<string>  $types
+   * @param  \Closure|null  $callback
+   * @param  string  $operator
+   * @param  int  $count
+   * @return static
+   */
+  public function whereHasMorph($relation, $types, \Closure $callback = null, $operator = '>=', $count = 1);
+
+  /**
+   * Add a polymorphic relationship count / exists condition to the query with where clauses and an "or".
+   *
+   * @template TRelatedModel of Model
+   * @template TChildModel of Model
+   * @param  \Illuminate\Database\Eloquent\Relations\MorphTo<TRelatedModel, TChildModel>|string  $relation
+   * @param  string|array<string>  $types
+   * @param  \Closure|null  $callback
+   * @param  string  $operator
+   * @param  int  $count
+   * @return static
+   */
+  public function orWhereHasMorph($relation, $types, \Closure $callback = null, $operator = '>=', $count = 1);
+
+  /**
+   * Add a polymorphic relationship count / exists condition to the query with where clauses.
+   *
+   * @template TRelatedModel of Model
+   * @template TChildModel of Model
+   * @param  \Illuminate\Database\Eloquent\Relations\MorphTo<TRelatedModel, TChildModel>|string  $relation
+   * @param  string|array<string>  $types
+   * @param  \Closure|null  $callback
+   * @return static
+   */
+  public function whereDoesntHaveMorph($relation, $types, \Closure $callback = null);
+
+  /**
+   * Add a polymorphic relationship count / exists condition to the query with where clauses and an "or".
+   *
+   * @template TRelatedModel of Model
+   * @template TChildModel of Model
+   * @param  \Illuminate\Database\Eloquent\Relations\MorphTo<TRelatedModel, TChildModel>|string  $relation
+   * @param  string|array<string>  $types
+   * @param  \Closure|null  $callback
+   * @return static
+   */
+  public function orWhereDoesntHaveMorph($relation, $types, \Closure $callback = null);
+
+  /**
+   * Merge the where constraints from another query to the current query.
+   *
+   * @param  \Illuminate\Database\Eloquent\Builder<TModelClass>  $from
+   * @return static
+   */
+  public function mergeConstraintsFrom(\Illuminate\Database\Eloquent\Builder $from);
+
+  /**
+   * Add a relationship count / exists condition to the query with where clauses and an "or".
+   *
+   * @param  string  $relation
+   * @param  \Closure|null  $callback
+   * @return static
+   */
+  public function orWhereDoesntHave($relation, \Closure $callback = null);
+
+  /**
+   * Add a relationship count / exists condition to the query with where clauses.
+   *
+   * @param  string  $relation
+   * @param  \Closure|null  $callback
+   * @return static
+   */
+  public function whereDoesntHave($relation, \Closure $callback = null);
+
+  /**
+   * Add a basic where clause to the query, and return the first result.
+   *
+   * @param  \Closure|model-property<TModelClass>|array<model-property<TModelClass>|int, mixed>|\Illuminate\Database\Query\Expression  $column
+   * @param  mixed  $operator
+   * @param  mixed  $value
+   * @param  string  $boolean
+   * @phpstan-return TModelClass|null
+   */
+  public function firstWhere($column, $operator = null, $value = null, $boolean = 'and');
+
+  /**
+   * Execute the query as a "select" statement.
+   *
+   * @param  array<int, (model-property<TModelClass>|'*')>|model-property<TModelClass>|'*'  $columns
+   * @phpstan-return \Illuminate\Database\Eloquent\Collection<TModelClass>
+   */
+  public function get($columns = ['*']);
+
+  /**
+   * Get the hydrated models without eager loading.
+   *
+   * @param  array<int, (model-property<TModelClass>|'*')>|model-property<TModelClass>|'*'  $columns
+   * @phpstan-return TModelClass[]
+   */
+  public function getModels($columns = ['*']);
+
+  /**
+   * Get a single column's value from the first result of a query.
+   *
+   * @param  model-property<TModelClass>|\Illuminate\Database\Query\Expression  $column
+   * @return mixed
+   */
+  public function value($column);
+
+  /**
+   * Apply the callback's query changes if the given "value" is true.
+   *
+   * @param mixed  $value
+   * @param callable($this, mixed): (void|Builder<TModelClass>) $callback
+   * @param callable($this, mixed): (null|Builder<TModelClass>)|null  $default
+   * @return mixed|$this
+   */
+  public function when($value, $callback, $default = null);
+
+  /**
+   * Apply the callback's query changes if the given "value" is false.
+   *
+   * @param  mixed  $value
+   * @param  callable($this, mixed): (void|Builder<TModelClass>)  $callback
+   * @param  callable($this, mixed): (null|Builder<TModelClass>)|null  $default
+   * @return mixed|$this
+   */
+  public function unless($value, $callback, $default = null);
+}
+
+class Scope {}
+
+/**
+ * @method static \Illuminate\Database\Eloquent\Builder<static> withTrashed(bool $withTrashed = true)
+ * @method static \Illuminate\Database\Eloquent\Builder<static> onlyTrashed()
+ * @method static \Illuminate\Database\Eloquent\Builder<static> withoutTrashed()
+ * @method static bool restore()
+ */
+trait SoftDeletes {}
diff --git a/stubs/Illuminate/Database/EloquentCollection.stub b/stubs/Illuminate/Database/EloquentCollection.stub
new file mode 100644
index 0000000..02da6d6
--- /dev/null
+++ b/stubs/Illuminate/Database/EloquentCollection.stub
@@ -0,0 +1,43 @@
+<?php
+
+namespace Illuminate\Database\Eloquent;
+
+use Illuminate\Support\Traits\EnumeratesValues;
+
+/**
+ * @template TValue
+ * @extends \Illuminate\Support\Collection<int, TValue>
+ */
+class Collection extends \Illuminate\Support\Collection
+{
+
+    /** @phpstan-use EnumeratesValues<TValue> */
+    use EnumeratesValues;
+
+    /**
+     * @param  mixed  $key
+     * @param  mixed  $default
+     * @phpstan-return TValue|null
+     */
+    public function find($key, $default = null) {}
+
+    /**
+     * @template TReturn
+     * @param callable(TValue, int): TReturn $callable
+     * @return static<TReturn>|\Illuminate\Support\Collection<int, TReturn>
+     */
+    public function map($callable) {}
+
+    /**
+     * @param callable(TValue, int): mixed $callback
+     * @return \Illuminate\Support\Collection<mixed, mixed>
+     */
+    public function flatMap(callable $callback) {}
+
+    /**
+     * @template TReturn
+     * @param callable(TValue ...$values): TReturn $callback
+     * @return static<TReturn>|\Illuminate\Support\Collection<int, TReturn>
+     */
+    public function mapSpread(callable $callback) {}
+}
diff --git a/stubs/Illuminate/Database/Factory.stub b/stubs/Illuminate/Database/Factory.stub
new file mode 100644
index 0000000..dc6add1
--- /dev/null
+++ b/stubs/Illuminate/Database/Factory.stub
@@ -0,0 +1,81 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Factories;
+
+/**
+ * @template TModel of \Illuminate\Database\Eloquent\Model
+ */
+class Factory
+{
+    /**
+     * The name of the factory's corresponding model.
+     *
+     * @var class-string<TModel>
+     */
+    protected $model;
+
+    /**
+     * Get a new factory instance for the given attributes.
+     *
+     * @param  callable|array<model-property<TModel>, mixed>  $attributes
+     * @return static
+     */
+    public static function new($attributes = []) {}
+
+    /**
+     * Create a single model and persist it to the database.
+     *
+     * @param  array<model-property<TModel>, mixed> $attributes
+     * @return TModel
+     */
+    public function createOne($attributes = []) {}
+
+    /**
+     * Create a collection of models and persist them to the database.
+     *
+     * @param  iterable<callable|array<model-property<TModel>, mixed>>  $records
+     * @return \Illuminate\Database\Eloquent\Collection<TModel>
+     */
+    public function createMany(iterable $records) {}
+
+    /**
+     * Create a collection of models and persist them to the database.
+     *
+     * @param  array<model-property<TModel>, mixed>  $attributes
+     * @param  \Illuminate\Database\Eloquent\Model|null  $parent
+     * @return \Illuminate\Database\Eloquent\Collection<TModel>|TModel
+     */
+    public function create($attributes = [], ?\Illuminate\Database\Eloquent\Model $parent = null) {}
+
+    /**
+     * Make a single instance of the model.
+     *
+     * @param  callable|array<model-property<TModel>, mixed>  $attributes
+     * @return TModel
+     */
+    public function makeOne($attributes = []) {}
+
+    /**
+     * Create a collection of models.
+     *
+     * @param  array<model-property<TModel>, mixed>  $attributes
+     * @param  \Illuminate\Database\Eloquent\Model|null  $parent
+     * @return \Illuminate\Database\Eloquent\Collection<TModel>|TModel
+     */
+    public function make($attributes = [], ?\Illuminate\Database\Eloquent\Model $parent = null) {}
+
+    /**
+     * Make an instance of the model with the given attributes.
+     *
+     * @param  \Illuminate\Database\Eloquent\Model|null  $parent
+     * @return TModel
+     */
+    protected function makeInstance(?\Illuminate\Database\Eloquent\Model $parent) {}
+
+    /**
+     * Define the model's default state.
+     *
+     * @return array<model-property<TModel>, mixed>
+     */
+    abstract public function definition();
+}
diff --git a/stubs/Illuminate/Database/Gate.stub b/stubs/Illuminate/Database/Gate.stub
new file mode 100644
index 0000000..ed57dd6
--- /dev/null
+++ b/stubs/Illuminate/Database/Gate.stub
@@ -0,0 +1,31 @@
+<?php
+
+namespace Illuminate\Contracts\Auth\Access
+    {
+
+    interface Gate
+    {
+        /**
+         * Get a guard instance for the given user.
+         *
+         * @param  \Illuminate\Contracts\Auth\Authenticatable|mixed  $user
+         * @return \Illuminate\Contracts\Auth\Access\Gate
+         */
+        public function forUser($user);
+    }
+}
+
+namespace Illuminate\Auth\Access
+{
+
+    class Gate implements \Illuminate\Contracts\Auth\Access\Gate
+    {
+        /**
+         * Get a guard instance for the given user.
+         *
+         * @param  \Illuminate\Contracts\Auth\Authenticatable|mixed  $user
+         * @return \Illuminate\Contracts\Auth\Access\Gate
+         */
+        public function forUser($user);
+    }
+}
diff --git a/stubs/Illuminate/Database/HasMany.stub b/stubs/Illuminate/Database/HasMany.stub
new file mode 100644
index 0000000..4da995d
--- /dev/null
+++ b/stubs/Illuminate/Database/HasMany.stub
@@ -0,0 +1,17 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+/**
+ * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
+ * @extends HasOneOrMany<TRelatedModel>
+ */
+class HasMany extends HasOneOrMany
+{
+    /**
+     * Get the results of the relationship.
+     *
+     * @phpstan-return \Illuminate\Database\Eloquent\Collection<TRelatedModel>
+     */
+    public function getResults();
+}
diff --git a/stubs/Illuminate/Database/HasManyThrough.stub b/stubs/Illuminate/Database/HasManyThrough.stub
new file mode 100644
index 0000000..e4537e0
--- /dev/null
+++ b/stubs/Illuminate/Database/HasManyThrough.stub
@@ -0,0 +1,17 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+/**
+ * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
+ * @extends Relation<TRelatedModel>
+ */
+class HasManyThrough extends Relation
+{
+    /**
+     * Get the results of the relationship.
+     *
+     * @phpstan-return \Illuminate\Database\Eloquent\Collection<TRelatedModel>
+     */
+    public function getResults();
+}
diff --git a/stubs/Illuminate/Database/HasOne.stub b/stubs/Illuminate/Database/HasOne.stub
new file mode 100644
index 0000000..336dd2d
--- /dev/null
+++ b/stubs/Illuminate/Database/HasOne.stub
@@ -0,0 +1,17 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+/**
+ * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
+ * @extends HasOneOrMany<TRelatedModel>
+ */
+class HasOne extends HasOneOrMany
+{
+    /**
+     * Get the results of the relationship.
+     *
+     * @phpstan-return ?TRelatedModel
+     */
+    public function getResults();
+}
diff --git a/stubs/Illuminate/Database/HasOneOrMany.stub b/stubs/Illuminate/Database/HasOneOrMany.stub
new file mode 100644
index 0000000..75c04a1
--- /dev/null
+++ b/stubs/Illuminate/Database/HasOneOrMany.stub
@@ -0,0 +1,86 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+/**
+ * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
+ * @extends Relation<TRelatedModel>
+ */
+abstract class HasOneOrMany extends Relation
+{
+    /**
+     * Create a new has one or many relationship instance.
+     *
+     * @param  \Illuminate\Database\Eloquent\Builder<TRelatedModel>  $query
+     * @param  TRelatedModel  $parent
+     * @param  non-empty-string  $foreignKey
+     * @param  non-empty-string  $localKey
+     * @return void
+     */
+    public function __construct(\Illuminate\Database\Eloquent\Builder $query, \Illuminate\Database\Eloquent\Model $parent, $foreignKey, $localKey);
+
+    /**
+     * @param array<model-property<TRelatedModel>, mixed> $attributes
+     * @phpstan-return TRelatedModel
+     */
+    public function make(array $attributes = []);
+
+    /**
+     * Find a model by its primary key or return new instance of the related model.
+     *
+     * @param  mixed  $id
+     * @param  array<int, mixed>  $columns
+     * @return \Illuminate\Support\Collection<int, TRelatedModel>|TRelatedModel
+     */
+    public function findOrNew($id, $columns = ['*']);
+
+    /**
+     * Get the first related model record matching the attributes or instantiate it.
+     *
+     * @param  array<model-property<TRelatedModel>, mixed>  $attributes
+     * @param  array<mixed, mixed>  $values
+     * @return TRelatedModel
+     */
+    public function firstOrNew(array $attributes, array $values = []);
+
+    /**
+     * Get the first related record matching the attributes or create it.
+     *
+     * @param  array<model-property<TRelatedModel>, mixed>  $attributes
+     * @param  array<mixed, mixed>  $values
+     * @return TRelatedModel
+     */
+    public function firstOrCreate(array $attributes, array $values = []);
+
+    /**
+     * Create or update a related record matching the attributes, and fill it with values.
+     *
+     * @param  array<model-property<TRelatedModel>, mixed>  $attributes
+     * @param  array<array-key, mixed>  $values
+     * @return TRelatedModel
+     */
+    public function updateOrCreate(array $attributes, array $values = []);
+
+    /**
+     * Attach a model instance to the parent model.
+     *
+     * @param  \Illuminate\Database\Eloquent\Model  $model
+     * @return TRelatedModel|false
+     */
+    public function save(\Illuminate\Database\Eloquent\Model $model);
+
+    /**
+     * @phpstan-param array<model-property<TRelatedModel>, mixed> $attributes
+     *
+     * @phpstan-return TRelatedModel
+     */
+    public function create(array $attributes = []);
+
+    /**
+     * Create a Collection of new instances of the related model.
+     *
+     * @param  iterable<mixed>  $records
+     * @return \Illuminate\Database\Eloquent\Collection<TRelatedModel>
+     */
+    public function createMany(iterable $records);
+}
diff --git a/stubs/Illuminate/Database/HasOneThrough.stub b/stubs/Illuminate/Database/HasOneThrough.stub
new file mode 100644
index 0000000..222878f
--- /dev/null
+++ b/stubs/Illuminate/Database/HasOneThrough.stub
@@ -0,0 +1,24 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+/**
+ * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
+ * @extends HasManyThrough<TRelatedModel>
+ */
+class HasOneThrough extends HasManyThrough
+{
+    /**
+     * @param array<model-property<TRelatedModel>, mixed> $attributes
+     *
+     * @phpstan-return TRelatedModel
+     */
+    public function create(array $attributes = []);
+
+    /**
+     * Get the results of the relationship.
+     *
+     * @phpstan-return \Illuminate\Database\Eloquent\Collection<TRelatedModel>
+     */
+    public function getResults();
+}
diff --git a/stubs/Illuminate/Database/Model.stub b/stubs/Illuminate/Database/Model.stub
new file mode 100644
index 0000000..a71b3a0
--- /dev/null
+++ b/stubs/Illuminate/Database/Model.stub
@@ -0,0 +1,28 @@
+<?php
+
+namespace Illuminate\Database\Eloquent;
+
+/**
+ * @implements \ArrayAccess<string, mixed>
+ */
+abstract class Model implements \JsonSerializable, \ArrayAccess
+{
+    /**
+     * Update the model in the database.
+     *
+     * @param  array<model-property<static>, mixed>  $attributes
+     * @param  array<int|string, mixed>  $options
+     * @return bool
+     */
+    public function update(array $attributes = [], array $options = []);
+
+    /**
+     * Begin querying a model with eager loading.
+     *
+     * @param  non-empty-string|array<int|string, (\Closure)|string>  $relations
+     * @return \Illuminate\Database\Eloquent\Builder<static>
+     */
+    public static function with($relations);
+}
+
+class ModelNotFoundException extends \RuntimeException {}
diff --git a/stubs/Illuminate/Database/MorphMany.stub b/stubs/Illuminate/Database/MorphMany.stub
new file mode 100644
index 0000000..4543315
--- /dev/null
+++ b/stubs/Illuminate/Database/MorphMany.stub
@@ -0,0 +1,24 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+/**
+ * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
+ * @extends MorphOneOrMany<TRelatedModel>
+ */
+class MorphMany extends MorphOneOrMany
+{
+    /**
+     * @param array<model-property<TRelatedModel>, mixed> $attributes
+     *
+     * @phpstan-return TRelatedModel
+     */
+    public function create(array $attributes = []);
+
+    /**
+     * Get the results of the relationship.
+     *
+     * @phpstan-return \Illuminate\Database\Eloquent\Collection<TRelatedModel>
+     */
+    public function getResults();
+}
diff --git a/stubs/Illuminate/Database/MorphOne.stub b/stubs/Illuminate/Database/MorphOne.stub
new file mode 100644
index 0000000..bf5e437
--- /dev/null
+++ b/stubs/Illuminate/Database/MorphOne.stub
@@ -0,0 +1,17 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+/**
+ * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
+ * @extends MorphOneOrMany<TRelatedModel>
+ */
+class MorphOne extends MorphOneOrMany
+{
+    /**
+     * Get the results of the relationship.
+     *
+     * @phpstan-return ?TRelatedModel
+     */
+    public function getResults();
+}
diff --git a/stubs/Illuminate/Database/MorphOneOrMany.stub b/stubs/Illuminate/Database/MorphOneOrMany.stub
new file mode 100644
index 0000000..c3b775b
--- /dev/null
+++ b/stubs/Illuminate/Database/MorphOneOrMany.stub
@@ -0,0 +1,17 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+/**
+ * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
+ * @extends HasOneOrMany<TRelatedModel>
+ */
+abstract class MorphOneOrMany extends HasOneOrMany
+{
+    /**
+     * @param array<model-property<TRelatedModel>, mixed> $attributes
+     *
+     * @phpstan-return TRelatedModel
+     */
+    public function create(array $attributes = []);
+}
diff --git a/stubs/Illuminate/Database/MorphTo.stub b/stubs/Illuminate/Database/MorphTo.stub
new file mode 100644
index 0000000..2904af2
--- /dev/null
+++ b/stubs/Illuminate/Database/MorphTo.stub
@@ -0,0 +1,11 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+/**
+ * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
+ * @template TChildModel of \Illuminate\Database\Eloquent\Model
+ * @extends BelongsTo<TRelatedModel, TChildModel>
+ */
+class MorphTo extends BelongsTo
+{}
diff --git a/stubs/Illuminate/Database/MorphToMany.stub b/stubs/Illuminate/Database/MorphToMany.stub
new file mode 100644
index 0000000..65d0ec3
--- /dev/null
+++ b/stubs/Illuminate/Database/MorphToMany.stub
@@ -0,0 +1,11 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+/**
+ * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
+ * @extends BelongsToMany<TRelatedModel>
+ */
+class MorphToMany extends BelongsToMany
+{
+}
diff --git a/stubs/Illuminate/Database/QueryBuilder.stub b/stubs/Illuminate/Database/QueryBuilder.stub
new file mode 100644
index 0000000..5641034
--- /dev/null
+++ b/stubs/Illuminate/Database/QueryBuilder.stub
@@ -0,0 +1,1440 @@
+<?php
+namespace Illuminate\Database\Query;
+
+class Expression {}
+
+class Builder
+{
+    /**
+     * Set the columns to be selected.
+     *
+     * @param  array<mixed>|mixed  $columns
+     * @return $this
+     */
+    public function select($columns = ['*'])
+    {}
+
+    /**
+     * Add a subselect expression to the query.
+     *
+     * @param  \Closure|$this|non-empty-string $query
+     * @param  non-empty-string  $as
+     * @return $this
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function selectSub($query, $as)
+    {}
+
+    /**
+     * Add a new "raw" select expression to the query.
+     *
+     * @param  non-empty-string  $expression
+     * @param  array<mixed>  $bindings
+     * @return $this
+     */
+    public function selectRaw($expression, array $bindings = [])
+    {}
+
+    /**
+     * Makes "from" fetch from a subquery.
+     *
+     * @param  \Closure|\Illuminate\Database\Query\Builder|non-empty-string $query
+     * @param  non-empty-string  $as
+     * @return $this
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function fromSub($query, $as)
+    {}
+
+    /**
+     * Add a raw from clause to the query.
+     *
+     * @param  non-empty-string  $expression
+     * @param  mixed  $bindings
+     * @return $this
+     */
+    public function fromRaw($expression, $bindings = [])
+    {}
+
+    /**
+     * Creates a subquery and parse it.
+     *
+     * @param  \Closure|\Illuminate\Database\Query\Builder|non-empty-string $query
+     * @return array<mixed>
+     */
+    protected function createSub($query)
+    {}
+
+    /**
+     * Parse the subquery into SQL and bindings.
+     *
+     * @param  mixed  $query
+     * @return array<mixed>
+     *
+     * @throws \InvalidArgumentException
+     */
+    protected function parseSub($query)
+    {}
+
+    /**
+     * Add a new select column to the query.
+     *
+     * @param  array|mixed  $column
+     * @return $this
+     */
+    public function addSelect($column)
+    {}
+
+    /**
+     * Force the query to only return distinct results.
+     *
+     * @return $this
+     */
+    public function distinct()
+    {}
+
+    /**
+     * Set the table which the query is targeting.
+     *
+     * @param  \Closure|\Illuminate\Database\Query\Builder|non-empty-string $table
+     * @param  non-empty-string|null  $as
+     * @return $this
+     */
+    public function from($table, $as = null)
+    {}
+
+    /**
+     * Add a join clause to the query.
+     *
+     * @param  non-empty-string  $table
+     * @param  \Closure|non-empty-string $first
+     * @param  non-empty-string|null  $operator
+     * @param  non-empty-string|null  $second
+     * @param  non-empty-string  $type
+     * @param  bool  $where
+     * @return $this
+     */
+    public function join($table, $first, $operator = null, $second = null, $type = 'inner', $where = false)
+    {}
+
+    /**
+     * Add a "join where" clause to the query.
+     *
+     * @param  non-empty-string  $table
+     * @param  \Closure|non-empty-string $first
+     * @param  non-empty-string  $operator
+     * @param  non-empty-string  $second
+     * @param  non-empty-string  $type
+     * @return $this
+     */
+    public function joinWhere($table, $first, $operator, $second, $type = 'inner')
+    {}
+
+    /**
+     * Add a subquery join clause to the query.
+     *
+     * @param  \Closure|\Illuminate\Database\Query\Builder|non-empty-string $query
+     * @param  non-empty-string  $as
+     * @param  \Closure|non-empty-string $first
+     * @param  non-empty-string|null  $operator
+     * @param  non-empty-string|null  $second
+     * @param  non-empty-string  $type
+     * @param  bool  $where
+     * @return $this
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function joinSub($query, $as, $first, $operator = null, $second = null, $type = 'inner', $where = false)
+    {}
+
+    /**
+     * Add a left join to the query.
+     *
+     * @param  non-empty-string  $table
+     * @param  \Closure|non-empty-string $first
+     * @param  non-empty-string|null  $operator
+     * @param  non-empty-string|null  $second
+     * @return $this
+     */
+    public function leftJoin($table, $first, $operator = null, $second = null)
+    {}
+
+    /**
+     * Add a "join where" clause to the query.
+     *
+     * @param  non-empty-string  $table
+     * @param  \Closure|non-empty-string $first
+     * @param  non-empty-string  $operator
+     * @param  non-empty-string  $second
+     * @return $this
+     */
+    public function leftJoinWhere($table, $first, $operator, $second)
+    {}
+
+    /**
+     * Add a subquery left join to the query.
+     *
+     * @param  \Closure|\Illuminate\Database\Query\Builder|non-empty-string $query
+     * @param  non-empty-string  $as
+     * @param  \Closure|non-empty-string $first
+     * @param  non-empty-string|null  $operator
+     * @param  non-empty-string|null  $second
+     * @return $this
+     */
+    public function leftJoinSub($query, $as, $first, $operator = null, $second = null)
+    {}
+
+    /**
+     * Add a right join to the query.
+     *
+     * @param  non-empty-string  $table
+     * @param  \Closure|non-empty-string $first
+     * @param  non-empty-string|null  $operator
+     * @param  non-empty-string|null  $second
+     * @return $this
+     */
+    public function rightJoin($table, $first, $operator = null, $second = null)
+    {}
+
+    /**
+     * Add a "right join where" clause to the query.
+     *
+     * @param  non-empty-string  $table
+     * @param  \Closure|non-empty-string $first
+     * @param  non-empty-string  $operator
+     * @param  non-empty-string  $second
+     * @return $this
+     */
+    public function rightJoinWhere($table, $first, $operator, $second)
+    {}
+
+    /**
+     * Add a subquery right join to the query.
+     *
+     * @param  \Closure|\Illuminate\Database\Query\Builder|non-empty-string $query
+     * @param  non-empty-string  $as
+     * @param  \Closure|non-empty-string $first
+     * @param  non-empty-string|null  $operator
+     * @param  non-empty-string|null  $second
+     * @return $this
+     */
+    public function rightJoinSub($query, $as, $first, $operator = null, $second = null)
+    {}
+
+    /**
+     * Add a "cross join" clause to the query.
+     *
+     * @param  non-empty-string  $table
+     * @param  \Closure|string|null  $first
+     * @param  non-empty-string|null  $operator
+     * @param  non-empty-string|null  $second
+     * @return $this
+     */
+    public function crossJoin($table, $first = null, $operator = null, $second = null)
+    {}
+
+    /**
+     * Add a basic where clause to the query.
+     *
+     * @param  \Closure|string|array<string|int, mixed>  $column
+     * @param  mixed  $operator
+     * @param  mixed  $value
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    public function where($column, $operator = null, $value = null, $boolean = 'and')
+    {}
+
+    /**
+     * Add an array of where clauses to the query.
+     *
+     * @param  array<string|int, mixed>  $column
+     * @param  non-empty-string  $boolean
+     * @param  non-empty-string  $method
+     * @return $this
+     */
+    protected function addArrayOfWheres($column, $boolean, $method = 'where')
+    {}
+
+    /**
+     * Prepare the value and operator for a where clause.
+     *
+     * @param  non-empty-string  $value
+     * @param  non-empty-string  $operator
+     * @param  bool  $useDefault
+     * @return array<mixed>
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function prepareValueAndOperator($value, $operator, $useDefault = false)
+    {}
+
+    /**
+     * Determine if the given operator and value combination is legal.
+     *
+     * Prevents using Null values with invalid operators.
+     *
+     * @param  non-empty-string  $operator
+     * @param  mixed  $value
+     * @return bool
+     */
+    protected function invalidOperatorAndValue($operator, $value)
+    {}
+
+    /**
+     * Determine if the given operator is supported.
+     *
+     * @param  non-empty-string  $operator
+     * @return bool
+     */
+    protected function invalidOperator($operator)
+    {}
+
+    /**
+     * Add an "or where" clause to the query.
+     *
+     * @param  \Closure|array<int, mixed>  $column
+     * @param  mixed  $operator
+     * @param  mixed  $value
+     * @return $this
+     */
+    public function orWhere($column, $operator = null, $value = null)
+    {}
+
+    /**
+     * Add a "where" clause comparing two columns to the query.
+     *
+     * @param  array<int, mixed>  $first
+     * @param  non-empty-string|null  $operator
+     * @param  non-empty-string|null  $second
+     * @param  non-empty-string|null  $boolean
+     * @return $this
+     */
+    public function whereColumn($first, $operator = null, $second = null, $boolean = 'and')
+    {}
+
+    /**
+     * Add an "or where" clause comparing two columns to the query.
+     *
+     * @param  non-empty-string|array<mixed>  $first
+     * @param  non-empty-string|null  $operator
+     * @param  non-empty-string|null  $second
+     * @return $this
+     */
+    public function orWhereColumn($first, $operator = null, $second = null)
+    {}
+
+    /**
+     * Add a raw where clause to the query.
+     *
+     * @param  non-empty-string  $sql
+     * @param  mixed  $bindings
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    public function whereRaw($sql, $bindings = [], $boolean = 'and')
+    {}
+
+    /**
+     * Add a raw or where clause to the query.
+     *
+     * @param  non-empty-string  $sql
+     * @param  mixed  $bindings
+     * @return $this
+     */
+    public function orWhereRaw($sql, $bindings = [])
+    {}
+
+    /**
+     * Add a "where in" clause to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  mixed  $values
+     * @param  non-empty-string  $boolean
+     * @param  bool  $not
+     * @return $this
+     */
+    public function whereIn($column, $values, $boolean = 'and', $not = false)
+    {}
+
+    /**
+     * Add an "or where in" clause to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  mixed  $values
+     * @return $this
+     */
+    public function orWhereIn($column, $values)
+    {}
+
+    /**
+     * Add a "where not in" clause to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  mixed  $values
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    public function whereNotIn($column, $values, $boolean = 'and')
+    {}
+
+    /**
+     * Add an "or where not in" clause to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  mixed  $values
+     * @return $this
+     */
+    public function orWhereNotIn($column, $values)
+    {}
+
+    /**
+     * Add a "where in raw" clause for integer values to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  \Illuminate\Contracts\Support\Arrayable|array<int|string, mixed>  $values
+     * @param  non-empty-string  $boolean
+     * @param  bool  $not
+     * @return $this
+     */
+    public function whereIntegerInRaw($column, $values, $boolean = 'and', $not = false)
+    {}
+
+    /**
+     * Add an "or where in raw" clause for integer values to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  \Illuminate\Contracts\Support\Arrayable|array<int|string, mixed>  $values
+     * @return $this
+     */
+    public function orWhereIntegerInRaw($column, $values)
+    {}
+
+    /**
+     * Add a "where not in raw" clause for integer values to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  \Illuminate\Contracts\Support\Arrayable|array<string, mixed>  $values
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    public function whereIntegerNotInRaw($column, $values, $boolean = 'and')
+    {}
+
+    /**
+     * Add an "or where not in raw" clause for integer values to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  \Illuminate\Contracts\Support\Arrayable|array<string, mixed>  $values
+     * @return $this
+     */
+    public function orWhereIntegerNotInRaw($column, $values)
+    {}
+
+    /**
+     * Add a "where null" clause to the query.
+     *
+     * @param  non-empty-string|array<string>  $columns
+     * @param  non-empty-string  $boolean
+     * @param  bool  $not
+     * @return $this
+     */
+    public function whereNull($columns, $boolean = 'and', $not = false)
+    {}
+
+    /**
+     * Add an "or where null" clause to the query.
+     *
+     * @param  non-empty-string  $column
+     * @return $this
+     */
+    public function orWhereNull($column)
+    {}
+
+    /**
+     * Add a "where not null" clause to the query.
+     *
+     * @param  non-empty-string|array<string>  $columns
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    public function whereNotNull($columns, $boolean = 'and')
+    {}
+
+    /**
+     * Add a where between statement to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  array<string|int, mixed>  $values
+     * @param  non-empty-string  $boolean
+     * @param  bool  $not
+     * @return $this
+     */
+    public function whereBetween($column, array $values, $boolean = 'and', $not = false)
+    {}
+
+    /**
+     * Add an or where between statement to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  array<string|int, mixed>  $values
+     * @return $this
+     */
+    public function orWhereBetween($column, array $values)
+    {}
+
+    /**
+     * Add a where not between statement to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  array<string|int, mixed>  $values
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    public function whereNotBetween($column, array $values, $boolean = 'and')
+    {}
+
+    /**
+     * Add an or where not between statement to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  array<string|int, mixed>  $values
+     * @return $this
+     */
+    public function orWhereNotBetween($column, array $values)
+    {}
+
+    /**
+     * Add an "or where not null" clause to the query.
+     *
+     * @param  non-empty-string  $column
+     * @return $this
+     */
+    public function orWhereNotNull($column)
+    {}
+
+    /**
+     * Add a "where date" statement to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  non-empty-string  $operator
+     * @param  \DateTimeInterface|string|null  $value
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    public function whereDate($column, $operator, $value = null, $boolean = 'and')
+    {}
+
+    /**
+     * Add an "or where date" statement to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  non-empty-string  $operator
+     * @param  \DateTimeInterface|string|null  $value
+     * @return $this
+     */
+    public function orWhereDate($column, $operator, $value = null)
+    {}
+
+    /**
+     * Add a "where time" statement to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  non-empty-string  $operator
+     * @param  \DateTimeInterface|string|null  $value
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    public function whereTime($column, $operator, $value = null, $boolean = 'and')
+    {}
+
+    /**
+     * Add an "or where time" statement to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  non-empty-string  $operator
+     * @param  \DateTimeInterface|string|null  $value
+     * @return $this
+     */
+    public function orWhereTime($column, $operator, $value = null)
+    {}
+
+    /**
+     * Add a "where day" statement to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  non-empty-string  $operator
+     * @param  \DateTimeInterface|string|null  $value
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    public function whereDay($column, $operator, $value = null, $boolean = 'and')
+    {}
+
+    /**
+     * Add an "or where day" statement to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  non-empty-string  $operator
+     * @param  \DateTimeInterface|string|null  $value
+     * @return $this
+     */
+    public function orWhereDay($column, $operator, $value = null)
+    {}
+
+    /**
+     * Add a "where month" statement to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  non-empty-string  $operator
+     * @param  \DateTimeInterface|string|null  $value
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    public function whereMonth($column, $operator, $value = null, $boolean = 'and')
+    {}
+
+    /**
+     * Add an "or where month" statement to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  non-empty-string  $operator
+     * @param  \DateTimeInterface|string|null  $value
+     * @return $this
+     */
+    public function orWhereMonth($column, $operator, $value = null)
+    {}
+
+    /**
+     * Add a "where year" statement to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  non-empty-string  $operator
+     * @param  \DateTimeInterface|string|int|null  $value
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    public function whereYear($column, $operator, $value = null, $boolean = 'and')
+    {}
+
+    /**
+     * Add an "or where year" statement to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  non-empty-string  $operator
+     * @param  \DateTimeInterface|string|int|null  $value
+     * @return $this
+     */
+    public function orWhereYear($column, $operator, $value = null)
+    {}
+
+    /**
+     * Add a date based (year, month, day, time) statement to the query.
+     *
+     * @param  non-empty-string  $type
+     * @param  non-empty-string  $column
+     * @param  non-empty-string  $operator
+     * @param  mixed  $value
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    protected function addDateBasedWhere($type, $column, $operator, $value, $boolean = 'and')
+    {}
+
+    /**
+     * Add a nested where statement to the query.
+     *
+     * @param  \Closure  $callback
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    public function whereNested(\Closure $callback, $boolean = 'and')
+    {}
+
+    /**
+     * Create a new query instance for nested where condition.
+     *
+     * @return $this
+     */
+    public function forNestedWhere()
+    {}
+
+    /**
+     * Add another query builder as a nested where to the query builder.
+     *
+     * @param  $this  $query
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    public function addNestedWhereQuery($query, $boolean = 'and')
+    {}
+
+    /**
+     * Add a full sub-select to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  non-empty-string  $operator
+     * @param  \Closure  $callback
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    protected function whereSub($column, $operator, \Closure $callback, $boolean)
+    {}
+
+    /**
+     * Add an exists clause to the query.
+     *
+     * @param  \Closure  $callback
+     * @param  non-empty-string  $boolean
+     * @param  bool  $not
+     * @return $this
+     */
+    public function whereExists(\Closure $callback, $boolean = 'and', $not = false)
+    {}
+
+    /**
+     * Add an or exists clause to the query.
+     *
+     * @param  \Closure  $callback
+     * @param  bool  $not
+     * @return $this
+     */
+    public function orWhereExists(\Closure $callback, $not = false)
+    {}
+
+    /**
+     * Add a where not exists clause to the query.
+     *
+     * @param  \Closure  $callback
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    public function whereNotExists(\Closure $callback, $boolean = 'and')
+    {}
+
+    /**
+     * Add a where not exists clause to the query.
+     *
+     * @param  \Closure  $callback
+     * @return $this
+     */
+    public function orWhereNotExists(\Closure $callback)
+    {}
+
+    /**
+     * Add an exists clause to the query.
+     *
+     * @param  \Illuminate\Database\Query\Builder  $query
+     * @param  non-empty-string  $boolean
+     * @param  bool  $not
+     * @return $this
+     */
+    public function addWhereExistsQuery(self $query, $boolean = 'and', $not = false)
+    {}
+
+    /**
+     * Adds a where condition using row values.
+     *
+     * @param  array<string>  $columns
+     * @param  non-empty-string  $operator
+     * @param  array<string|int, mixed>  $values
+     * @param  non-empty-string  $boolean
+     * @return $this
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function whereRowValues($columns, $operator, $values, $boolean = 'and')
+    {}
+
+    /**
+     * Adds a or where condition using row values.
+     *
+     * @param  array<string>  $columns
+     * @param  non-empty-string  $operator
+     * @param  array<string|int, mixed>  $values
+     * @return $this
+     */
+    public function orWhereRowValues($columns, $operator, $values)
+    {}
+
+    /**
+     * Add a "where JSON contains" clause to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  mixed  $value
+     * @param  non-empty-string  $boolean
+     * @param  bool  $not
+     * @return $this
+     */
+    public function whereJsonContains($column, $value, $boolean = 'and', $not = false)
+    {}
+
+    /**
+     * Add a "or where JSON contains" clause to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  mixed  $value
+     * @return $this
+     */
+    public function orWhereJsonContains($column, $value)
+    {}
+
+    /**
+     * Add a "where JSON not contains" clause to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  mixed  $value
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    public function whereJsonDoesntContain($column, $value, $boolean = 'and')
+    {}
+
+    /**
+     * Add a "or where JSON not contains" clause to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  mixed  $value
+     * @return $this
+     */
+    public function orWhereJsonDoesntContain($column, $value)
+    {}
+
+    /**
+     * Add a "where JSON length" clause to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  mixed  $operator
+     * @param  mixed  $value
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    public function whereJsonLength($column, $operator, $value = null, $boolean = 'and')
+    {}
+
+    /**
+     * Add a "or where JSON length" clause to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  mixed  $operator
+     * @param  mixed  $value
+     * @return $this
+     */
+    public function orWhereJsonLength($column, $operator, $value = null)
+    {}
+
+    /**
+     * Handles dynamic "where" clauses to the query.
+     *
+     * @param  non-empty-string  $method
+     * @param  array<mixed>  $parameters
+     * @return $this
+     */
+    public function dynamicWhere($method, $parameters)
+    {}
+
+    /**
+     * Add a single dynamic where clause statement to the query.
+     *
+     * @param  non-empty-string  $segment
+     * @param  non-empty-string  $connector
+     * @param  array<mixed>  $parameters
+     * @param  int  $index
+     * @return void
+     */
+    protected function addDynamic($segment, $connector, $parameters, $index)
+    {}
+
+    /**
+     * Add a "group by" clause to the query.
+     *
+     * @param  array<mixed>|non-empty-string ...$groups
+     * @return $this
+     */
+    public function groupBy(...$groups)
+    {}
+
+    /**
+     * Add a raw groupBy clause to the query.
+     *
+     * @param  non-empty-string  $sql
+     * @param  array<mixed>  $bindings
+     * @return $this
+     */
+    public function groupByRaw($sql, array $bindings = [])
+    {}
+
+    /**
+     * Add a "having" clause to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  non-empty-string|null  $operator
+     * @param  non-empty-string|null  $value
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    public function having($column, $operator = null, $value = null, $boolean = 'and')
+    {}
+
+    /**
+     * Add a "or having" clause to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  non-empty-string|null  $operator
+     * @param  non-empty-string|null  $value
+     * @return $this
+     */
+    public function orHaving($column, $operator = null, $value = null)
+    {}
+
+    /**
+     * Add a "having between " clause to the query.
+     *
+     * @param  non-empty-string  $column
+     * @param  array<string, mixed>  $values
+     * @param  non-empty-string  $boolean
+     * @param  bool  $not
+     * @return $this
+     */
+    public function havingBetween($column, array $values, $boolean = 'and', $not = false)
+    {}
+
+    /**
+     * Add a raw having clause to the query.
+     *
+     * @param  non-empty-string  $sql
+     * @param  array<mixed>  $bindings
+     * @param  non-empty-string  $boolean
+     * @return $this
+     */
+    public function havingRaw($sql, array $bindings = [], $boolean = 'and')
+    {}
+
+    /**
+     * Add a raw or having clause to the query.
+     *
+     * @param  non-empty-string  $sql
+     * @param  array<mixed>  $bindings
+     * @return $this
+     */
+    public function orHavingRaw($sql, array $bindings = [])
+    {}
+
+    /**
+     * Add an "order by" clause to the query.
+     *
+     * @param  \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Query\Expression|non-empty-string $column
+     * @param  non-empty-string  $direction
+     * @return $this
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function orderBy($column, $direction = 'asc')
+    {}
+
+    /**
+     * Add a descending "order by" clause to the query.
+     *
+     * @param  non-empty-string  $column
+     * @return $this
+     */
+    public function orderByDesc($column)
+    {}
+
+    /**
+     * Add an "order by" clause for a timestamp to the query.
+     *
+     * @param  non-empty-string|\Illuminate\Database\Query\Expression  $column
+     * @return $this
+     */
+    public function latest($column = 'created_at')
+    {}
+
+    /**
+     * Add an "order by" clause for a timestamp to the query.
+     *
+     * @param  non-empty-string|\Illuminate\Database\Query\Expression  $column
+     * @return $this
+     */
+    public function oldest($column = 'created_at')
+    {}
+
+    /**
+     * Put the query's results in random order.
+     *
+     * @param  non-empty-string  $seed
+     * @return $this
+     */
+    public function inRandomOrder($seed = '')
+    {}
+
+    /**
+     * Add a raw "order by" clause to the query.
+     *
+     * @param  non-empty-string  $sql
+     * @param  array<mixed>  $bindings
+     * @return $this
+     */
+    public function orderByRaw($sql, $bindings = [])
+    {}
+
+    /**
+     * Alias to set the "offset" value of the query.
+     *
+     * @param  int  $value
+     * @return $this
+     */
+    public function skip($value)
+    {}
+
+    /**
+     * Set the "offset" value of the query.
+     *
+     * @param  int  $value
+     * @return $this
+     */
+    public function offset($value)
+    {}
+
+    /**
+     * Alias to set the "limit" value of the query.
+     *
+     * @param  int  $value
+     * @return $this
+     */
+    public function take($value)
+    {}
+
+    /**
+     * Set the "limit" value of the query.
+     *
+     * @param  int  $value
+     * @return $this
+     */
+    public function limit($value)
+    {}
+
+    /**
+     * Set the limit and offset for a given page.
+     *
+     * @param  int  $page
+     * @param  int  $perPage
+     * @return $this
+     */
+    public function forPage($page, $perPage = 15)
+    {}
+
+    /**
+     * Constrain the query to the previous "page" of results before a given ID.
+     *
+     * @param  int  $perPage
+     * @param  int|null  $lastId
+     * @param  non-empty-string  $column
+     * @return $this
+     */
+    public function forPageBeforeId($perPage = 15, $lastId = 0, $column = 'id')
+    {}
+
+    /**
+     * Constrain the query to the next "page" of results after a given ID.
+     *
+     * @param  int  $perPage
+     * @param  int|null  $lastId
+     * @param  non-empty-string  $column
+     * @return $this
+     */
+    public function forPageAfterId($perPage = 15, $lastId = 0, $column = 'id')
+    {}
+
+    /**
+     * Remove all existing orders and optionally add a new order.
+     *
+     * @param  non-empty-string|null  $column
+     * @param  non-empty-string  $direction
+     * @return $this
+     */
+    public function reorder($column = null, $direction = 'asc')
+    {}
+
+    /**
+     * Get an array with all orders with a given column removed.
+     *
+     * @param  non-empty-string  $column
+     * @return array<mixed>
+     */
+    protected function removeExistingOrdersFor($column)
+    {}
+
+    /**
+     * Add a union statement to the query.
+     *
+     * @param  \Illuminate\Database\Query\Builder|\Closure  $query
+     * @param  bool  $all
+     * @return $this
+     */
+    public function union($query, $all = false)
+    {}
+
+    /**
+     * Add a union all statement to the query.
+     *
+     * @param  \Illuminate\Database\Query\Builder|\Closure  $query
+     * @return $this
+     */
+    public function unionAll($query)
+    {}
+
+    /**
+     * Lock the selected rows in the table.
+     *
+     * @param  non-empty-string|bool  $value
+     * @return $this
+     */
+    public function lock($value = true)
+    {}
+
+    /**
+     * Lock the selected rows in the table for updating.
+     *
+     * @return static
+     */
+    public function lockForUpdate()
+    {}
+
+    /**
+     * Share lock the selected rows in the table.
+     *
+     * @return $this
+     */
+    public function sharedLock()
+    {}
+
+    /**
+     * Execute a query for a single record by ID.
+     *
+     * @param  int|non-empty-string $id
+     * @param  array<string>  $columns
+     * @return mixed|static
+     */
+    public function find($id, $columns = ['*'])
+    {}
+
+    /**
+     * Throw an exception if the query doesn't have an orderBy clause.
+     *
+     * @return void
+     *
+     * @throws \RuntimeException
+     */
+    protected function enforceOrderBy()
+    {}
+
+    /**
+     * Get an array with the values of a given column.
+     *
+     * @param  non-empty-string|\Illuminate\Database\Query\Expression  $column
+     * @param  non-empty-string|null  $key
+     * @return \Illuminate\Support\Collection<string, mixed>
+     */
+    public function pluck($column, $key = null)
+    {}
+
+    /**
+     * Concatenate values of a given column as a string.
+     *
+     * @param  non-empty-string  $column
+     * @param  non-empty-string  $glue
+     * @return string
+     */
+    public function implode($column, $glue = '')
+    {}
+
+    /**
+     * Determine if any rows exist for the current query.
+     *
+     * @return bool
+     */
+    public function exists()
+    {}
+
+    /**
+     * Determine if no rows exist for the current query.
+     *
+     * @return bool
+     */
+    public function doesntExist()
+    {}
+
+    /**
+     * Execute the given callback if no rows exist for the current query.
+     *
+     * @param  \Closure  $callback
+     * @return mixed
+     */
+    public function existsOr(\Closure $callback)
+    {}
+
+    /**
+     * Execute the given callback if rows exist for the current query.
+     *
+     * @param  \Closure  $callback
+     * @return mixed
+     */
+    public function doesntExistOr(\Closure $callback)
+    {}
+
+    /**
+     * Retrieve the "count" result of the query.
+     *
+     * @param  non-empty-string  $columns
+     * @return int
+     */
+    public function count($columns = '*')
+    {}
+
+    /**
+     * Retrieve the minimum value of a given column.
+     *
+     * @param  non-empty-string  $column
+     * @return mixed
+     */
+    public function min($column)
+    {}
+
+    /**
+     * Retrieve the maximum value of a given column.
+     *
+     * @param  non-empty-string  $column
+     * @return mixed
+     */
+    public function max($column)
+    {}
+
+    /**
+     * Retrieve the sum of the values of a given column.
+     *
+     * @param  non-empty-string  $column
+     * @return mixed
+     */
+    public function sum($column)
+    {}
+
+    /**
+     * Retrieve the average of the values of a given column.
+     *
+     * @param  non-empty-string  $column
+     * @return mixed
+     */
+    public function avg($column)
+    {}
+
+    /**
+     * Alias for the "avg" method.
+     *
+     * @param  non-empty-string  $column
+     * @return mixed
+     */
+    public function average($column)
+    {}
+
+    /**
+     * Execute an aggregate function on the database.
+     *
+     * @param  non-empty-string  $function
+     * @param  array<string>  $columns
+     * @return mixed
+     */
+    public function aggregate($function, $columns = ['*'])
+    {}
+
+    /**
+     * Execute a numeric aggregate function on the database.
+     *
+     * @param  non-empty-string  $function
+     * @param  array<string>  $columns
+     * @return float|int
+     */
+    public function numericAggregate($function, $columns = ['*'])
+    {}
+
+    /**
+     * Set the aggregate property without running the query.
+     *
+     * @param  non-empty-string  $function
+     * @param  array<string>  $columns
+     * @return $this
+     */
+    protected function setAggregate($function, $columns)
+    {}
+
+    /**
+     * Execute the given callback while selecting the given columns.
+     *
+     * After running the callback, the columns are reset to the original value.
+     *
+     * @param  array<string>  $columns
+     * @param  callable  $callback
+     * @return mixed
+     */
+    protected function onceWithColumns($columns, $callback)
+    {}
+
+    /**
+     * Insert a new record into the database.
+     *
+     * @param  array<mixed>  $values
+     * @return bool
+     */
+    public function insert(array $values)
+    {}
+
+    /**
+     * Insert a new record into the database while ignoring errors.
+     *
+     * @param  array<mixed>  $values
+     * @return int
+     */
+    public function insertOrIgnore(array $values)
+    {}
+
+    /**
+     * Insert a new record and get the value of the primary key.
+     *
+     * @param  array<string, mixed>  $values
+     * @param  non-empty-string|null  $sequence
+     * @return int
+     */
+    public function insertGetId(array $values, $sequence = null)
+    {}
+
+    /**
+     * Insert new records into the table using a subquery.
+     *
+     * @param  array<string>  $columns
+     * @param  \Closure|\Illuminate\Database\Query\Builder|non-empty-string $query
+     * @return int
+     */
+    public function insertUsing(array $columns, $query)
+    {}
+
+    /**
+     * Update a record in the database.
+     *
+     * @param  array<string, mixed>  $values
+     * @return int
+     */
+    public function update(array $values)
+    {}
+
+    /**
+     * Insert or update a record matching the attributes, and fill it with values.
+     *
+     * @param  array<string, mixed>  $attributes
+     * @param  array<string, mixed>  $values
+     * @return bool
+     */
+    public function updateOrInsert(array $attributes, array $values = [])
+    {}
+
+    /**
+     * Increment a column's value by a given amount.
+     *
+     * @param  non-empty-string|\Illuminate\Database\Query\Expression  $column
+     * @param  float|int  $amount
+     * @param  array<string, mixed>  $extra
+     * @return int
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function increment($column, $amount = 1, array $extra = [])
+    {}
+
+    /**
+     * Decrement a column's value by a given amount.
+     *
+     * @param  non-empty-string|\Illuminate\Database\Query\Expression  $column
+     * @param  float|int  $amount
+     * @param  array<string, mixed>  $extra
+     * @return int
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function decrement($column, $amount = 1, array $extra = [])
+    {}
+
+    /**
+     * Delete a record from the database.
+     *
+     * @param  mixed  $id
+     * @return int
+     */
+    public function delete($id = null)
+    {}
+
+    /**
+     * Run a truncate statement on the table.
+     *
+     * @return void
+     */
+    public function truncate()
+    {}
+
+    /**
+     * Get a new instance of the query builder.
+     *
+     * @return $this
+     */
+    public function newQuery()
+    {}
+
+    /**
+     * Get the current query value bindings in a flattened array.
+     *
+     * @return array<mixed>
+     */
+    public function getBindings()
+    {}
+
+    /**
+     * Get the raw array of bindings.
+     *
+     * @return array<mixed>
+     */
+    public function getRawBindings()
+    {}
+
+    /**
+     * Set the bindings on the query builder.
+     *
+     * @param  array<mixed>  $bindings
+     * @param  non-empty-string  $type
+     * @return $this
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function setBindings(array $bindings, $type = 'where')
+    {}
+
+    /**
+     * Add a binding to the query.
+     *
+     * @param  mixed  $value
+     * @param  non-empty-string  $type
+     * @return $this
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function addBinding($value, $type = 'where')
+    {}
+
+    /**
+     * Merge an array of bindings into our bindings.
+     *
+     * @param  \Illuminate\Database\Query\Builder  $query
+     * @return $this
+     */
+    public function mergeBindings(self $query)
+    {}
+}
diff --git a/stubs/Illuminate/Database/Relation.stub b/stubs/Illuminate/Database/Relation.stub
new file mode 100644
index 0000000..dff9063
--- /dev/null
+++ b/stubs/Illuminate/Database/Relation.stub
@@ -0,0 +1,24 @@
+<?php
+
+namespace Illuminate\Database\Eloquent\Relations;
+
+/**
+ * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
+ */
+class Relation
+{
+    /**
+     * Execute the query as a "select" statement.
+     *
+     * @param array<int, string> $columns
+     * @phpstan-return \Illuminate\Database\Eloquent\Collection<TRelatedModel>
+     */
+    public function get($columns = ['*']);
+
+
+    /**
+     * @param array<model-property<TRelatedModel>, mixed> $attributes
+     * @phpstan-return TRelatedModel
+     */
+    public function make(array $attributes = []);
+}
diff --git a/stubs/Illuminate/Enumerable.stub b/stubs/Illuminate/Enumerable.stub
new file mode 100644
index 0000000..7fd2c1e
--- /dev/null
+++ b/stubs/Illuminate/Enumerable.stub
@@ -0,0 +1,58 @@
+<?php
+
+namespace Illuminate\Support;
+
+/**
+ * @template TKey
+ * @template TValue
+ *
+ * @extends \IteratorAggregate<TKey, TValue>
+ */
+interface Enumerable extends \Countable, \IteratorAggregate, \JsonSerializable
+{
+    /**
+     * @param string|callable(TValue, TKey): bool $key
+     * @param mixed $operator
+     * @param mixed $value
+     * @return static<static>
+     */
+    public function partition($key, $operator = null, $value = null);
+
+    /**
+     * @param string|callable(TValue, TKey): mixed $keyBy
+     * @return static<TValue>
+     */
+    public function keyBy($keyBy);
+
+    /**
+     * @param callable(TValue, TKey): array<mixed> $callback
+     * @return static<mixed>
+     */
+    public function mapWithKeys(callable $callback);
+
+    /**
+     * @param callable(TValue, TKey): array<mixed> $callback
+     * @return static<array<mixed>>
+     */
+    public function mapToDictionary(callable $callback);
+
+    /**
+     * @param string|callable(TValue, TKey): bool $key
+     * @param mixed $operator
+     * @param mixed $value
+     * @return bool
+     */
+    public function every($key, $operator = null, $value = null);
+
+    /**
+     * @param  int  $size
+     * @return static<static>
+     */
+    public function chunk($size);
+
+    /**
+     * @param callable(static): void $callable
+     * @return static
+     */
+    public function tap($callable);
+}
diff --git a/stubs/Illuminate/EnumeratesValues.stub b/stubs/Illuminate/EnumeratesValues.stub
new file mode 100644
index 0000000..6cc58f8
--- /dev/null
+++ b/stubs/Illuminate/EnumeratesValues.stub
@@ -0,0 +1,36 @@
+<?php
+
+namespace Illuminate\Support\Traits;
+
+use Illuminate\Support\HigherOrderCollectionProxy;
+
+/**
+ * @template TValue
+ * @property-read HigherOrderCollectionProxy<'average', TValue> $average
+ * @property-read HigherOrderCollectionProxy<'avg', TValue> $avg
+ * @property-read HigherOrderCollectionProxy<'contains', TValue> $contains
+ * @property-read HigherOrderCollectionProxy<'each', TValue> $each
+ * @property-read HigherOrderCollectionProxy<'every', TValue> $every
+ * @property-read HigherOrderCollectionProxy<'filter', TValue> $filter
+ * @property-read HigherOrderCollectionProxy<'first', TValue> $first
+ * @property-read HigherOrderCollectionProxy<'flatMap', TValue> $flatMap
+ * @property-read HigherOrderCollectionProxy<'groupBy', TValue> $groupBy
+ * @property-read HigherOrderCollectionProxy<'keyBy', TValue> $keyBy
+ * @property-read HigherOrderCollectionProxy<'map', TValue> $map
+ * @property-read HigherOrderCollectionProxy<'max', TValue> $max
+ * @property-read HigherOrderCollectionProxy<'min', TValue> $min
+ * @property-read HigherOrderCollectionProxy<'partition', TValue> $partition
+ * @property-read HigherOrderCollectionProxy<'reject', TValue> $reject
+ * @property-read HigherOrderCollectionProxy<'some', TValue> $some
+ * @property-read HigherOrderCollectionProxy<'sortBy', TValue> $sortBy
+ * @property-read HigherOrderCollectionProxy<'sortByDesc', TValue> $sortByDesc
+ * @property-read HigherOrderCollectionProxy<'skipUntil', TValue> $skipUntil
+ * @property-read HigherOrderCollectionProxy<'skipWhile', TValue> $skipWhile
+ * @property-read HigherOrderCollectionProxy<'sum', TValue> $sum
+ * @property-read HigherOrderCollectionProxy<'takeUntil', TValue> $takeUntil
+ * @property-read HigherOrderCollectionProxy<'takeWhile', TValue> $takeWhile
+ * @property-read HigherOrderCollectionProxy<'unique', TValue> $unique
+ * @property-read HigherOrderCollectionProxy<'until', TValue> $until
+ */
+trait EnumeratesValues
+{}
diff --git a/stubs/Illuminate/Facades.stub b/stubs/Illuminate/Facades.stub
new file mode 100644
index 0000000..30f2089
--- /dev/null
+++ b/stubs/Illuminate/Facades.stub
@@ -0,0 +1,30 @@
+<?php
+
+namespace Illuminate\Support\Facades;
+
+abstract class Facade {}
+
+/**
+ * @mixin \Redis
+ * @mixin \Illuminate\Redis\RedisManager
+ * @mixin \Illuminate\Contracts\Redis\Factory
+ */
+class Redis {}
+
+/**
+ * @mixin \Illuminate\Database\DatabaseManager
+ * @mixin \Illuminate\Database\Connection
+ * @mixin \Illuminate\Database\ConnectionInterface
+ */
+class DB extends Facade {}
+
+/**
+ * @mixin \Illuminate\Queue\QueueManager
+ * @mixin \Illuminate\Queue\Queue
+ */
+class Queue extends Facade {}
+
+/**
+ * @mixin \Illuminate\Log\Logger
+ */
+class Log extends Facade {}
diff --git a/stubs/Illuminate/Filesystem/Filesystem.stub b/stubs/Illuminate/Filesystem/Filesystem.stub
deleted file mode 100644
index e2719a1..0000000
--- a/stubs/Illuminate/Filesystem/Filesystem.stub
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-namespace Illuminate\Filesystem;
-
-class Filesystem
-{
-    /**
-    * Find path names matching a given pattern.
-    *
-    * @param  string  $pattern
-    * @param  int  $flags
-    * @return mixed[]|false
-    */
-    public function glob($pattern, $flags = 0);
-}
diff --git a/stubs/Illuminate/Filesystem/FilesystemManager.stub b/stubs/Illuminate/Filesystem/FilesystemManager.stub
deleted file mode 100644
index a149072..0000000
--- a/stubs/Illuminate/Filesystem/FilesystemManager.stub
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-namespace Illuminate\Filesystem;
-
-class FilesystemManager
-{
-    /**
-     * Create a new filesystem manager instance.
-     *
-     * @param  \Illuminate\Contracts\Container\Container  $app
-     * @return void
-     */
-    public function __construct($app)
-    {
-        $this->app = $app;
-    }
-}
diff --git a/stubs/Illuminate/Helpers.stub b/stubs/Illuminate/Helpers.stub
new file mode 100644
index 0000000..f0fa8bd
--- /dev/null
+++ b/stubs/Illuminate/Helpers.stub
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @template TValue
+ * @template TDefault
+ *
+ * @param callable(): TValue $callback
+ * @param TDefault|(callable(\Throwable): TDefault) $rescue
+ * @param bool $report
+ * @return TValue|TDefault
+ */
+function rescue(callable $callback, $rescue = null, $report = true)
+{
+}
+
+/**
+ * @template TValue
+ * @param int $times
+ * @param callable(int): TValue $callback
+ * @param int $sleep
+ * @param null|callable(\Exception): bool $when
+ * @phpstan-return TValue
+ *
+ * @throws \Exception
+ */
+function retry($times, callable $callback, $sleep = 0, $when = null)
+{
+}
+
+/**
+ * @template TValue
+ * @param TValue $value
+ * @param null|callable(TValue): void $callback
+ * @return mixed
+ */
+function tap($value, $callback = null)
+{
+}
+
+/**
+ * @param  view-string|null  $view
+ * @param  \Illuminate\Contracts\Support\Arrayable|array<string, mixed>  $data
+ * @param  array<string, mixed>  $mergeData
+ * @return mixed
+ */
+function view($view = null, $data = [], $mergeData = [])
+{
+}
diff --git a/stubs/Illuminate/HigherOrderProxies.stub b/stubs/Illuminate/HigherOrderProxies.stub
new file mode 100644
index 0000000..b3cf0f3
--- /dev/null
+++ b/stubs/Illuminate/HigherOrderProxies.stub
@@ -0,0 +1,24 @@
+<?php
+
+namespace Illuminate\Support;
+
+/**
+ * @template TClass
+ * @property TClass $target
+ */
+class HigherOrderTapProxy
+{
+    /**
+     * @param TClass $target
+     */
+    public function __construct($target)
+    {
+    }
+}
+
+/**
+ * @template T
+ * @template TValue
+ */
+class HigherOrderCollectionProxy
+{}
diff --git a/stubs/Illuminate/Logger.stub b/stubs/Illuminate/Logger.stub
new file mode 100644
index 0000000..9df3e50
--- /dev/null
+++ b/stubs/Illuminate/Logger.stub
@@ -0,0 +1,14 @@
+<?php
+
+namespace Psr\Log {
+    interface LoggerInterface {}
+}
+
+namespace Illuminate\Log {
+
+    /**
+     * @mixin \Illuminate\Log\LogManager
+     * @mixin \Monolog\Logger
+     */
+    class Logger implements \Psr\Log\LoggerInterface {}
+}
diff --git a/stubs/Illuminate/Mailable.stub b/stubs/Illuminate/Mailable.stub
new file mode 100644
index 0000000..e9a04f4
--- /dev/null
+++ b/stubs/Illuminate/Mailable.stub
@@ -0,0 +1,22 @@
+<?php
+
+namespace Illuminate\Mail;
+
+class Mailable
+{
+    /**
+     * @param view-string $view
+     * @param  array<string, mixed>  $data
+     * @return $this
+     */
+    public function markdown($view, array $data = [])
+    {}
+
+    /**
+     * @param  view-string  $view
+     * @param  array<string, mixed>  $data
+     * @return $this
+     */
+    public function view($view, array $data = [])
+    {}
+}
diff --git a/stubs/Illuminate/Pagination.stub b/stubs/Illuminate/Pagination.stub
new file mode 100644
index 0000000..8c57cd0
--- /dev/null
+++ b/stubs/Illuminate/Pagination.stub
@@ -0,0 +1,23 @@
+<?php
+
+namespace Illuminate\Pagination;
+
+/**
+ * @mixin \Illuminate\Support\Collection
+ */
+abstract class AbstractPaginator implements \Illuminate\Contracts\Support\Htmlable
+{}
+
+/**
+ * @implements \ArrayAccess<mixed, mixed>
+ * @implements \IteratorAggregate<mixed, mixed>
+ */
+class Paginator extends AbstractPaginator implements \Illuminate\Contracts\Support\Arrayable, \ArrayAccess, \Countable, \IteratorAggregate, \Illuminate\Contracts\Support\Jsonable, \JsonSerializable, \Illuminate\Contracts\Pagination\Paginator
+{}
+
+/**
+ * @implements \ArrayAccess<mixed, mixed>
+ * @implements \IteratorAggregate<mixed, mixed>
+ */
+class LengthAwarePaginator extends AbstractPaginator implements \Illuminate\Contracts\Support\Arrayable, \ArrayAccess, \Countable, \IteratorAggregate, \Illuminate\Contracts\Support\Jsonable, \JsonSerializable, \Illuminate\Contracts\Pagination\LengthAwarePaginator
+{}
diff --git a/stubs/Illuminate/Queue/ListenerOptions.stub b/stubs/Illuminate/Queue/ListenerOptions.stub
deleted file mode 100644
index c77b760..0000000
--- a/stubs/Illuminate/Queue/ListenerOptions.stub
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php
-
-namespace Illuminate\Queue;
-
-class ListenerOptions
-{
-    /**
-     * The environment the worker should run in.
-     *
-     * @var string|null
-     */
-    public $environment;
-}
diff --git a/stubs/Illuminate/Support/ServiceProvider.stub b/stubs/Illuminate/Support/ServiceProvider.stub
deleted file mode 100644
index ee3ec41..0000000
--- a/stubs/Illuminate/Support/ServiceProvider.stub
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-namespace Illuminate\Support;
-
-use Illuminate\Contracts\Container\Container;
-
-abstract class ServiceProvider
-{
-    /**
-     * The application instance.
-     *
-     * @var Container
-     */
-    protected $app;
-
-    /**
-     * Create a new service provider instance.
-     *
-     * @param  Container  $app
-     * @return void
-     */
-    public function __construct($app);
-}
diff --git a/stubs/Tobyz/JsonApiServer/Context.stub b/stubs/Tobyz/JsonApiServer/Context.stub
deleted file mode 100644
index a50fab5..0000000
--- a/stubs/Tobyz/JsonApiServer/Context.stub
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
-
-namespace Tobyz\JsonApiServer;
-
-/**
- * @mixin \Flarum\Api\Context
- */
-class Context
-{
-
-}
diff --git a/stubs/Tobyz/JsonApiServer/JsonApi.stub b/stubs/Tobyz/JsonApiServer/JsonApi.stub
deleted file mode 100644
index c2d37c6..0000000
--- a/stubs/Tobyz/JsonApiServer/JsonApi.stub
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
-
-namespace Tobyz\JsonApiServer;
-
-/**
- * @mixin \Flarum\Api\JsonApi
- */
-class JsonApi
-{
-
-}
