Repository: psalm/psalm-plugin-laravel (php)

Issue:
Support Psalm 6
# Problem
When running Psalm 6 with this plugin, the scanning unexpectedly halts with the following error:
```
   Exception 

  ErrorException Undefined property: PhpParser\Node\Name::$parts
Emitted in /var/www/html/vendor/psalm/plugin-laravel/src/Handlers/Auth/GuardHandler.php:114
Stack trace in the forked worker:
#0 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php(255): Illuminate\Foundation\Bootstrap\HandleExceptions->handleError()
#1 /var/www/html/vendor/psalm/plugin-laravel/src/Handlers/Auth/GuardHandler.php(114): Illuminate\Foundation\Bootstrap\HandleExceptions->Illuminate\Foundation\Bootstrap\{closure}()
#2 /var/www/html/vendor/psalm/plugin-laravel/src/Handlers/Auth/GuardHandler.php(65): Psalm\LaravelPlugin\Handlers\Auth\GuardHandler::findGuardNameInCallChain()
#3 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php(101): Psalm\LaravelPlugin\Handlers\Auth\GuardHandler::getMethodReturnType()
#4 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php(121): Psalm\Internal\Provider\MethodReturnTypeProvider->getReturnType()
#5 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php(265): Psalm\Internal\Analyzer\Statements\Expression\Call\Method\MethodCallReturnTypeFetcher::fetch()
#6 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php(484): Psalm\Internal\Analyzer\Statements\Expression\Call\Method\ExistingAtomicMethodCallAnalyzer::analyze()
#7 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php(197): Psalm\Internal\Analyzer\Statements\Expression\Call\Method\AtomicMethodCallAnalyzer::analyze()
#8 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php(213): Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze()
#9 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php(92): Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::handleExpression()
#10 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php(56): Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::analyze()
#11 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php(267): Psalm\Internal\Analyzer\Statements\Expression\Fetch\InstancePropertyFetchAnalyzer::analyze()
#12 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php(92): Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::handleExpression()
#13 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php(245): Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::analyze()
#14 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php(499): Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer::analyze()
#15 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php(205): Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::analyzeAssignment()
#16 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php(92): Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::handleExpression()
#17 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php(555): Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::analyze()
#18 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php(191): Psalm\Internal\Analyzer\StatementsAnalyzer::analyzeStatement()
#19 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php(508): Psalm\Internal\Analyzer\StatementsAnalyzer->analyze()
#20 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassAnalyzer.php(1823): Psalm\Internal\Analyzer\FunctionLikeAnalyzer->analyze()
#21 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassAnalyzer.php(447): Psalm\Internal\Analyzer\ClassAnalyzer->analyzeClassMethod()
#22 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FileAnalyzer.php(197): Psalm\Internal\Analyzer\ClassAnalyzer->analyze()
#23 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(1583): Psalm\Internal\Analyzer\FileAnalyzer->analyze()
#24 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Fork/Pool.php(183): Psalm\Internal\Codebase\Analyzer->analysisWorker()
#25 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(364): Psalm\Internal\Fork\Pool->__construct()
#26 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(264): Psalm\Internal\Codebase\Analyzer->doAnalysis()
#27 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php(530): Psalm\Internal\Codebase\Analyzer->analyzeFiles()
#28 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Cli/Psalm.php(383): Psalm\Internal\Analyzer\ProjectAnalyzer->check()
#29 /var/www/html/vendor/vimeo/psalm/psalm(9): Psalm\Internal\Cli\Psalm::run()
#30 /var/www/html/vendor/bin/psalm(119): include('...')
#31 {main}

  at vendor/vimeo/psalm/src/Psalm/Internal/Fork/Pool.php:377
    373▕                                 ($this->task_done_closure)($message->data);
    374▕                             }
    375▕                         } elseif ($message instanceof ForkProcessErrorMessage) {
    376▕                             $this->killAllChildren();
  ➜ 377▕                             throw new Exception($message->message);
    378▕                         } else {
    379▕                             $this->killAllChildren();
    380▕                             throw new Exception('Child should return ForkMessage - response type=' . gettype($message));
    381▕                         }

```

## Impacted Versions
```
barryvdh/laravel-ide-helper                    v3.1.0
kitloong/laravel-migrations-generator          v7.0.5
laravel/framework                              v10.48.23
laravel/octane                                 v2.5.9
laravel/pint                                   v1.18.1
laravel/prompts                                v0.1.25
laravel/sail                                   v1.37.1
laravel/sanctum                                v3.3.3
laravel/serializable-closure                   v1.3.5
laravel/telescope                              v5.2.4
laravel/tinker                                 v2.10.0
laravel/vapor-cli                              v1.65.1
laravel/vapor-core                             v2.37.1
laravel/vapor-ui                               v1.7.4
pestphp/pest-plugin-laravel                    v2.4.0
psalm/plugin-laravel                           v2.11.1
reliese/laravel                                v1.3.5
spatie/laravel-data                            3.12.0
spatie/laravel-ignition                        2.8.0
spatie/laravel-medialibrary                    10.15.0
spatie/laravel-model-states                    2.7.2
spatie/laravel-package-tools                   1.16.5
spatie/laravel-queueable-action                2.15.0
thedoctor0/laravel-factory-generator           1.4.3
vimeo/psalm                                    6.0.0
```


Pull request:
Dump dependencies: drop Laravel 10 and PHP 8.1 support
Drop Laravel 10 to focus on limited resources for better Laravel 11 support.

Closes #387

Patch:
diff --git a/composer.json b/composer.json
index 9d68558..eb85f45 100644
--- a/composer.json
+++ b/composer.json
@@ -11,33 +11,33 @@
     ],
     "homepage": "https://github.com/psalm/psalm-plugin-laravel",
     "require": {
-        "php": "^8.1",
+        "php": "^8.2",
         "ext-simplexml": "*",
-        "barryvdh/laravel-ide-helper": "^2.13 || ^3.0",
-        "illuminate/config": "^10.48 || ^11.0",
-        "illuminate/container": "^10.48 || ^11.0",
-        "illuminate/contracts": "^10.48 || ^11.0",
-        "illuminate/database": "^10.48 || ^11.0",
-        "illuminate/events": "^10.48 || ^11.0",
-        "illuminate/http": "^10.48 || ^11.0",
-        "illuminate/routing": "^10.48 || ^11.0",
-        "illuminate/support": "^10.48 || ^11.0",
-        "illuminate/view": "^10.48 || ^11.0",
-        "nikic/php-parser": "^4.18 || ^5.0",
-        "orchestra/testbench-core": "^8.22 || ^9.0",
-        "symfony/console": "^6.0 || ^7.0",
-        "symfony/finder": "^6.0 || ^7.0",
-        "vimeo/psalm": "^5.20|^6"
+        "barryvdh/laravel-ide-helper": "~3.5.4",
+        "illuminate/config": "^11.35",
+        "illuminate/container": "^11.35",
+        "illuminate/contracts": "^11.35",
+        "illuminate/database": "^11.35",
+        "illuminate/events": "^11.35",
+        "illuminate/http": "^11.35",
+        "illuminate/routing": "^11.35",
+        "illuminate/support": "^11.35",
+        "illuminate/view": "^11.35",
+        "nikic/php-parser": "^5.0",
+        "orchestra/testbench-core": "^9.9",
+        "symfony/console": "^7.1",
+        "symfony/finder": "^7.1",
+        "vimeo/psalm": "^5.26 || ^6.0"
     },
     "require-dev": {
-        "laravel/framework": "^10.48 || ^11.0",
-        "phpunit/phpunit": "^10.5 || ^11.0",
+        "laravel/framework": "^11.35",
+        "phpunit/phpunit": "^10.5 || ^11.5",
         "phpyh/psalm-tester": "^0.1.0",
         "ramsey/collection": "^1.3",
-        "rector/rector": "^1.0",
-        "slevomat/coding-standard": "^8.8",
-        "squizlabs/php_codesniffer": "*",
-        "symfony/http-foundation": "^6.0 || ^7.0"
+        "rector/rector": "^2.0",
+        "slevomat/coding-standard": "^8.15",
+        "squizlabs/php_codesniffer": "^3.11",
+        "symfony/http-foundation": "^7.1"
     },
     "minimum-stability": "dev",
     "prefer-stable": true,
@@ -68,14 +68,16 @@
         "lint-fix": "phpcbf -n",
         "psalm": "psalm --find-dead-code --find-unused-psalm-suppress --long-progress",
         "psalm-set-baseline": "psalm --set-baseline=psalm-baseline.xml",
+        "rector": "./vendor/bin/rector",
+        "rector:dry": "./vendor/bin/rector --dry-run",
         "test": [
             "@lint",
             "@psalm",
             "@test:unit",
             "@test:type"
         ],
+        "test:app": "./tests/Application/laravel-test.sh",
         "test:type": "phpunit --testsuite=type",
-        "test:unit": "phpunit --testsuite=unit",
-        "rector": "./vendor/bin/rector --dry-run"
+        "test:unit": "phpunit --testsuite=unit"
     }
 }
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 16945ba..6bb5e14 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,25 +1,25 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
-    bootstrap="vendor/autoload.php"
-    cacheDirectory=".phpunit.cache"
-    executionOrder="depends,defects"
-    beStrictAboutCoverageMetadata="true"
-    beStrictAboutOutputDuringTests="true"
-    failOnRisky="true"
-    failOnWarning="true">
-  <testsuites>
-    <testsuite name="unit">
-      <directory>tests/Unit</directory>
-    </testsuite>
-    <testsuite name="type">
-      <directory>tests/Type</directory>
-    </testsuite>
-  </testsuites>
+         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
+         bootstrap="vendor/autoload.php"
+         cacheDirectory=".phpunit.cache"
+         executionOrder="depends,defects"
+         beStrictAboutCoverageMetadata="true"
+         beStrictAboutOutputDuringTests="true"
+         failOnRisky="true"
+         failOnWarning="true">
+    <testsuites>
+        <testsuite name="unit">
+            <directory>tests/Unit</directory>
+        </testsuite>
+        <testsuite name="type">
+            <directory>tests/Type</directory>
+        </testsuite>
+    </testsuites>
 
     <source restrictDeprecations="true" restrictNotices="true" restrictWarnings="true">
-    <include>
-      <directory>src</directory>
-    </include>
-  </source>
+        <include>
+            <directory>src</directory>
+        </include>
+    </source>
 </phpunit>
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index 6a3abd5..650e3a4 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -1,18 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<files psalm-version="5.15.0@5c774aca4746caf3d239d9c8cadb9f882ca29352">
-  <file src="src/Handlers/Application/ContainerHandler.php">
-    <MixedArgument>
-      <code>$abstract</code>
-    </MixedArgument>
-  </file>
+<files psalm-version="6.6.0@07e9a53713232e924da9acb4a6e47507461cd411">
   <file src="src/Handlers/Auth/AuthConfigAnalyzer.php">
     <MixedArgument>
       <code><![CDATA[$this->config->get('auth.guards')]]></code>
     </MixedArgument>
-    <MixedInferredReturnType>
-      <code>?string</code>
-      <code><![CDATA[class-string<\Illuminate\Contracts\Auth\Authenticatable>|null]]></code>
-    </MixedInferredReturnType>
     <MixedReturnStatement>
       <code><![CDATA[$this->config->get("auth.providers.{$provider}.model", null)]]></code>
       <code><![CDATA[$this->config->get('auth.defaults.guard')]]></code>
@@ -20,14 +11,9 @@
   </file>
   <file src="src/Handlers/Auth/AuthHandler.php">
     <TypeDoesNotContainType>
-      <code>null</code>
+      <code><![CDATA[null]]></code>
     </TypeDoesNotContainType>
   </file>
-  <file src="src/Handlers/Auth/GuardHandler.php">
-    <DeprecatedProperty>
-      <code><![CDATA[$previous_call->name->parts]]></code>
-    </DeprecatedProperty>
-  </file>
   <file src="src/Handlers/Eloquent/ModelPropertyAccessorHandler.php">
     <ArgumentTypeCoercion>
       <code><![CDATA[$event->getFqClasslikeName()]]></code>
@@ -35,29 +21,39 @@
       <code><![CDATA[$event->getFqClasslikeName()]]></code>
     </ArgumentTypeCoercion>
     <LessSpecificImplementedReturnType>
-      <code>array</code>
-      <code>array</code>
-      <code>array</code>
+      <code><![CDATA[array]]></code>
+      <code><![CDATA[array]]></code>
+      <code><![CDATA[array]]></code>
     </LessSpecificImplementedReturnType>
   </file>
-  <file src="src/Handlers/Eloquent/Schema/SchemaAggregator.php">
-    <DeprecatedProperty>
-      <code><![CDATA[$first_argument_of_nullable->value->name->parts]]></code>
-    </DeprecatedProperty>
-  </file>
   <file src="src/Handlers/Helpers/PathHandler.php">
     <InternalMethod>
-      <code>new TLiteralString($result)</code>
+      <code><![CDATA[new TLiteralString($result)]]></code>
     </InternalMethod>
   </file>
+  <file src="src/Plugin.php">
+    <RiskyTruthyFalsyComparison>
+      <code><![CDATA[glob(dirname(__DIR__) . '/stubs/' . $majorVersion . '/**/*.stubphp')]]></code>
+      <code><![CDATA[glob(dirname(__DIR__) . '/stubs/' . $majorVersion . '/*.stubphp')]]></code>
+      <code><![CDATA[glob(dirname(__DIR__) . '/stubs/common/TaintAnalysis/Http/*.stubphp')]]></code>
+    </RiskyTruthyFalsyComparison>
+  </file>
   <file src="src/Providers/ApplicationProvider.php">
-     <InternalMethod>
-       <code><![CDATA[createApplication]]></code>
-     </InternalMethod>
+    <InternalMethod>
+      <code><![CDATA[createApplication]]></code>
+    </InternalMethod>
+    <MixedArgument>
+      <code><![CDATA[$app['events']]]></code>
+      <code><![CDATA[$app['events']]]></code>
+    </MixedArgument>
+    <MixedMethodCall>
+      <code><![CDATA[addNamespace]]></code>
+      <code><![CDATA[bootstrap]]></code>
+    </MixedMethodCall>
   </file>
   <file src="src/Util/ContainerResolver.php">
     <InternalMethod>
-      <code>new TLiteralString($concrete)</code>
+      <code><![CDATA[new TLiteralString($concrete)]]></code>
     </InternalMethod>
   </file>
 </files>
diff --git a/psalm.xml b/psalm.xml
index 45b1c6e..6f4b2b3 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -2,7 +2,7 @@
 <psalm
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns="https://getpsalm.org/schema/config"
-    xsi:schemaLocation="https://getpsalm.org/schema/config config.xsd"
+    xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
     errorLevel="1"
     errorBaseline="psalm-baseline.xml"
     findUnusedBaselineEntry="false"
@@ -19,5 +19,6 @@
 
     <issueHandlers>
         <LessSpecificReturnType errorLevel="info" />
+        <ClassMustBeFinal errorLevel="info" />
     </issueHandlers>
 </psalm>
diff --git a/rector.php b/rector.php
index ed2ea39..ce4b8ae 100644
--- a/rector.php
+++ b/rector.php
@@ -2,7 +2,6 @@
 
 use Rector\CodeQuality\Rector\FunctionLike\SimplifyUselessVariableRector;
 use Rector\CodingStyle\Rector\Assign\SplitDoubleAssignRector;
-use Rector\CodingStyle\Rector\Closure\StaticClosureRector;
 use Rector\CodingStyle\Rector\Encapsed\EncapsedStringsToSprintfRector;
 use Rector\CodingStyle\Rector\If_\NullableCompareToNullRector;
 use Rector\Config\RectorConfig;
@@ -11,24 +10,19 @@ use Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector;
 use Rector\Php81\Rector\Array_\FirstClassCallableRector;
 use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector;
 use Rector\Php81\Rector\Property\ReadOnlyPropertyRector;
-use Rector\Strict\Rector\BooleanNot\BooleanInBooleanNotRuleFixerRector;
-use Rector\Strict\Rector\If_\BooleanInIfConditionRuleFixerRector;
 
 return RectorConfig::configure()
     ->withPaths(['src', 'tests'])
-    ->withPhpSets(php81: true)
+    ->withPhpSets(php82: true)
     ->withPreparedSets(deadCode: true, codingStyle: true, typeDeclarations: true)
     ->withSkip([
         ReadOnlyPropertyRector::class,
         ClosureToArrowFunctionRector::class,
         FirstClassCallableRector::class,
         NullToStrictStringFuncCallArgRector::class,
-        BooleanInIfConditionRuleFixerRector::class,
-        BooleanInBooleanNotRuleFixerRector::class,
         RemoveUnusedPrivateMethodRector::class,
         SimplifyUselessVariableRector::class,
         NullableCompareToNullRector::class,
         EncapsedStringsToSprintfRector::class,
-        StaticClosureRector::class,
         SplitDoubleAssignRector::class,
     ]);
diff --git a/src/Exceptions/UnknownApplicationConfiguration.php b/src/Exceptions/UnknownApplicationConfiguration.php
new file mode 100644
index 0000000..6f0cf89
--- /dev/null
+++ b/src/Exceptions/UnknownApplicationConfiguration.php
@@ -0,0 +1,9 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Psalm\LaravelPlugin\Exceptions;
+
+final class UnknownApplicationConfiguration extends \LogicException
+{
+}
diff --git a/src/Fakes/FakeModelsCommand.php b/src/Fakes/FakeModelsCommand.php
index 555aff5..fc5d8ba 100644
--- a/src/Fakes/FakeModelsCommand.php
+++ b/src/Fakes/FakeModelsCommand.php
@@ -21,9 +21,15 @@ class FakeModelsCommand extends ModelsCommand
     /** @var list<class-string<\Illuminate\Database\Eloquent\Model>> */
     private array $model_classes = [];
 
-    public function __construct(Filesystem $files, private SchemaAggregator $schema)
+    private SchemaAggregator $schema;
+
+    /**
+     * While the setter of a required property is an anti-pattern,
+     * this is the only way to be less independent of changes in the parent ModelsCommand constructor.
+     */
+    public function setSchemaAggregator(SchemaAggregator $schemaAggregator): void
     {
-        parent::__construct($files);
+        $this->schema = $schemaAggregator;
     }
 
     /** @return list<class-string<\Illuminate\Database\Eloquent\Model>> */
@@ -109,7 +115,6 @@ class FakeModelsCommand extends ModelsCommand
             }
 
             if ($column->nullable) {
-                /** @psalm-suppress MixedArrayAssignment */
                 $this->nullableColumns[$column_name] = true;
             }
 
diff --git a/src/Handlers/Eloquent/ModelFactoryTypeProvider.php b/src/Handlers/Eloquent/ModelFactoryTypeProvider.php
new file mode 100644
index 0000000..a028d9e
--- /dev/null
+++ b/src/Handlers/Eloquent/ModelFactoryTypeProvider.php
@@ -0,0 +1,61 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Psalm\LaravelPlugin\Handlers\Eloquent;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Psalm\Plugin\EventHandler\Event\PropertyTypeProviderEvent;
+use Psalm\Plugin\EventHandler\PropertyTypeProviderInterface;
+use Psalm\LaravelPlugin\Providers\ModelStubProvider;
+use Psalm\Type;
+
+use function strtolower;
+
+final class ModelFactoryTypeProvider implements PropertyTypeProviderInterface
+{
+    /** @return list<class-string<\Illuminate\Database\Eloquent\Model>> */
+    #[\Override]
+    public static function getClassLikeNames(): array
+    {
+        return ModelStubProvider::getModelClasses();
+    }
+
+    #[\Override]
+    public static function getPropertyType(PropertyTypeProviderEvent $event): ?Type\Union
+    {
+        if ($event->getPropertyName() !== 'factory') {
+            return null;
+        }
+
+        $source = $event->getSource();
+        if ($source === null) {
+            return null;
+        }
+
+        $classlike = $source->getCodebase()->classlike_storage_provider->get($event->getFqClasslikeName());
+
+        $usesHasFactory = isset($classlike->used_traits[strtolower(HasFactory::class)]);
+        if (! $usesHasFactory) {
+            return null;
+        }
+
+        $hasFactoryProperty = isset($classlike->properties['factory']);
+        // Check for static $factory property
+        if ($hasFactoryProperty && isset($classlike->properties['factory']->type)) {
+            $factoryType = $classlike->properties['factory']->type;
+            foreach ($factoryType->getAtomicTypes() as $type) {
+                if ($type instanceof Type\Atomic\TNamedObject) {
+                    return new Type\Union([new Type\Atomic\TNamedObject($type->value)]);
+                }
+            }
+        }
+
+        // Default to Factory<static>
+        return new Type\Union([
+            new Type\Atomic\TGenericObject('Illuminate\Database\Eloquent\Factories\Factory', [
+                new Type\Union([new Type\Atomic\TNamedObject($event->getFqClasslikeName())])
+            ])
+        ]);
+    }
+}
diff --git a/src/Handlers/Eloquent/ModelRelationshipPropertyHandler.php b/src/Handlers/Eloquent/ModelRelationshipPropertyHandler.php
index 594306b..503a7ba 100644
--- a/src/Handlers/Eloquent/ModelRelationshipPropertyHandler.php
+++ b/src/Handlers/Eloquent/ModelRelationshipPropertyHandler.php
@@ -4,11 +4,11 @@ namespace Psalm\LaravelPlugin\Handlers\Eloquent;
 
 use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Database\Eloquent\Relations\BelongsToMany;
+use Illuminate\Database\Eloquent\Relations\Relation;
 use Illuminate\Database\Eloquent\Relations\HasMany;
 use Illuminate\Database\Eloquent\Relations\HasManyThrough;
 use Illuminate\Database\Eloquent\Relations\MorphMany;
 use Illuminate\Database\Eloquent\Relations\MorphToMany;
-use PhpParser;
 use Psalm\Codebase;
 use Psalm\LaravelPlugin\Providers\ModelStubProvider;
 use Psalm\Plugin\EventHandler\Event\PropertyExistenceProviderEvent;
@@ -23,6 +23,7 @@ use Psalm\Type\Atomic\TInt;
 use Psalm\Type\Union;
 
 use function in_array;
+use function is_a;
 
 class ModelRelationshipPropertyHandler implements
     PropertyExistenceProviderInterface,
@@ -154,7 +155,25 @@ class ModelRelationshipPropertyHandler implements
 
     private static function relationExists(Codebase $codebase, string $fq_classlike_name, string $property_name): bool
     {
-        // @todo: ensure this is a relation method
-        return $codebase->methodExists($fq_classlike_name . '::' . $property_name);
+        $method = $fq_classlike_name . '::' . $property_name;
+
+        if (!$codebase->methodExists($method)) {
+            return false;
+        }
+
+        // ensure this is a relation method
+
+        $return_type = $codebase->getMethodReturnType($method, $fq_classlike_name);
+        if (!$return_type) {
+            return false;
+        }
+
+        foreach ($return_type->getAtomicTypes() as $type) {
+            if ($type instanceof TGenericObject && is_a($type->value, Relation::class, true)) {
+                return true;
+            }
+        }
+
+        return false;
     }
 }
diff --git a/src/Handlers/Eloquent/Schema/SchemaAggregator.php b/src/Handlers/Eloquent/Schema/SchemaAggregator.php
index bd8fa24..727dbcd 100644
--- a/src/Handlers/Eloquent/Schema/SchemaAggregator.php
+++ b/src/Handlers/Eloquent/Schema/SchemaAggregator.php
@@ -57,7 +57,6 @@ class SchemaAggregator
     {
         $nodeFinder = new NodeFinder();
 
-        /** @var PhpParser\Node\Stmt\Class_[] $classes */
         $classes = $nodeFinder->findInstanceOf($stmts, PhpParser\Node\Stmt\Class_::class);
 
         foreach ($classes as $stmt) {
@@ -236,7 +235,7 @@ class SchemaAggregator
                      *
                      * Process this ->nullable(false)
                      */
-                    if (count($root_var->args) > 0) {
+                    if ($root_var->args !== []) {
                         $first_argument_of_nullable = $root_var->args[0];
                         if (
                             $first_argument_of_nullable instanceof PhpParser\Node\Arg
diff --git a/src/Plugin.php b/src/Plugin.php
index 2da1512..2cdd676 100644
--- a/src/Plugin.php
+++ b/src/Plugin.php
@@ -10,6 +10,7 @@ use Psalm\LaravelPlugin\Handlers\Auth\GuardHandler;
 use Psalm\LaravelPlugin\Handlers\Auth\RequestHandler;
 use Psalm\LaravelPlugin\Handlers\Eloquent\ModelMethodHandler;
 use Psalm\LaravelPlugin\Handlers\Eloquent\ModelPropertyAccessorHandler;
+use Psalm\LaravelPlugin\Handlers\Eloquent\ModelFactoryTypeProvider;
 use Psalm\LaravelPlugin\Handlers\Eloquent\ModelRelationshipPropertyHandler;
 use Psalm\LaravelPlugin\Handlers\Eloquent\RelationsMethodHandler;
 use Psalm\LaravelPlugin\Handlers\Helpers\CacheHandler;
@@ -43,7 +44,12 @@ class Plugin implements PluginEntryPointInterface
             ApplicationProvider::bootApp();
             $this->generateStubFiles();
         } catch (\Throwable $throwable) {
-            fwrite(\STDERR, "Laravel plugin error: “{$throwable->getMessage()}”\n");
+            $failOnInternalError = ((string) $config?->failOnInternalError) === 'true';
+            if ($failOnInternalError) {
+                throw $throwable;
+            }
+
+            fwrite(\STDERR, "\nLaravel plugin error on generating stub files: \"{$throwable->getMessage()}\"\n");
             return;
         }
 
@@ -73,9 +79,9 @@ class Plugin implements PluginEntryPointInterface
     /** @return list<string> */
     protected function getTaintAnalysisStubs(): array
     {
-        return array_merge(
-            glob(dirname(__DIR__) . '/stubs/TaintAnalysis/Http/*.stubphp'),
-        );
+        return [
+            ...glob(dirname(__DIR__) . '/stubs/common/TaintAnalysis/Http/*.stubphp') ?: []
+        ];
     }
 
     /** @return list<string> */
@@ -83,10 +89,10 @@ class Plugin implements PluginEntryPointInterface
     {
         [$majorVersion] = explode('.', $version);
 
-        return array_merge(
-            glob(dirname(__DIR__) . '/stubs/' . $majorVersion . '/*.stubphp'),
-            glob(dirname(__DIR__) . '/stubs/' . $majorVersion . '/**/*.stubphp'),
-        );
+        return [
+            ...glob(dirname(__DIR__) . '/stubs/' . $majorVersion . '/*.stubphp') ?: [],
+            ...glob(dirname(__DIR__) . '/stubs/' . $majorVersion . '/**/*.stubphp') ?: [],
+        ];
     }
 
     private function registerStubs(RegistrationInterface $registration): void
@@ -121,6 +127,8 @@ class Plugin implements PluginEntryPointInterface
 
         require_once 'Handlers/Eloquent/ModelRelationshipPropertyHandler.php';
         $registration->registerHooksFromClass(ModelRelationshipPropertyHandler::class);
+        require_once 'Handlers/Eloquent/ModelFactoryTypeProvider.php';
+        $registration->registerHooksFromClass(ModelFactoryTypeProvider::class);
         require_once 'Handlers/Eloquent/ModelPropertyAccessorHandler.php';
         $registration->registerHooksFromClass(ModelPropertyAccessorHandler::class);
         require_once 'Handlers/Eloquent/RelationsMethodHandler.php';
diff --git a/src/Providers/ApplicationProvider.php b/src/Providers/ApplicationProvider.php
index dacb4f1..5b6d043 100644
--- a/src/Providers/ApplicationProvider.php
+++ b/src/Providers/ApplicationProvider.php
@@ -7,6 +7,10 @@ namespace Psalm\LaravelPlugin\Providers;
 use Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider;
 use Illuminate\Container\Container;
 use Illuminate\Contracts\Console\Kernel;
+use Illuminate\View\Factory;
+use Illuminate\View\FileViewFinder;
+use Illuminate\View\Engines\EngineResolver;
+use Illuminate\View\Engines\PhpEngine;
 use Illuminate\Foundation\Application as LaravelApplication;
 use Orchestra\Testbench\Concerns\CreatesApplication;
 
@@ -14,7 +18,6 @@ use function define;
 use function defined;
 use function dirname;
 use function file_exists;
-use function get_class;
 use function getcwd;
 use function microtime;
 
@@ -26,16 +29,11 @@ final class ApplicationProvider
 
     public static function bootApp(): void
     {
-        $app = self::getApp();
-
-        /** @var \Illuminate\Contracts\Console\Kernel $consoleApp */
-        $consoleApp = $app->make(Kernel::class);
-        // @todo do not bootstrap \Illuminate\Foundation\Bootstrap\HandleExceptions
-        $consoleApp->bootstrap();
-
-        $app->register(IdeHelperServiceProvider::class);
+        self::getApp();
     }
 
+    private static bool $booted = false;
+
     public static function getApp(): LaravelApplication
     {
         if (self::$app instanceof Container) {
@@ -46,14 +44,14 @@ final class ApplicationProvider
             define('LARAVEL_START', microtime(true));
         }
 
-        if (file_exists($applicationPath = getcwd() . '/bootstrap/app.php')) { // Applications and Local Dev
+        if (file_exists($applicationPath = (getcwd() ?: '.') . '/bootstrap/app.php')) { // Applications and Local Dev
             /** @psalm-suppress MixedAssignment */
             $app = require $applicationPath;
         } elseif (file_exists($applicationPath = dirname(__DIR__, 5) . '/bootstrap/app.php')) { // plugin installed to vendor
             /** @psalm-suppress MixedAssignment */
             $app = require $applicationPath;
         } else { // Laravel Packages
-            $app = (new self())->createApplication(); // Orchestra\Testbench
+            $app = (new self())->createApplication(); // Orchestra\Testbench (e.g., test:type command)
         }
 
         if (! $app instanceof LaravelApplication) {
@@ -62,6 +60,31 @@ final class ApplicationProvider
 
         self::$app = $app;
 
+        // Initialize view system first
+        if (!$app->bound('view')) {
+            $filesystem = new \Illuminate\Filesystem\Filesystem();
+            $viewFinder = new FileViewFinder($filesystem, []);
+            $engineResolver = new EngineResolver();
+            $engineResolver->register('php', fn() => new PhpEngine($filesystem));
+            $app->singleton('view', fn() => new Factory($engineResolver, $viewFinder, $app['events']));
+        }
+
+        if (!self::$booted) {
+            // Bootstrap console app
+            $consoleApp = $app->make(Kernel::class);
+            $app->bind('Illuminate\Foundation\Bootstrap\HandleExceptions', function () {
+                return new class {
+                    public function bootstrap(): void
+                    {
+                    }
+                };
+            });
+            $consoleApp->bootstrap();
+
+            $app->register(IdeHelperServiceProvider::class);
+            self::$booted = true;
+        }
+
         return $app;
     }
 
@@ -82,24 +105,18 @@ final class ApplicationProvider
         // we want to keep the default psalm exception handler, otherwise the Laravel one will always return exit codes
         // of 0
         //$app->make('Illuminate\Foundation\Bootstrap\HandleExceptions')->bootstrap($app);
-        /** @psalm-suppress MixedMethodCall */
         $app->make(\Illuminate\Foundation\Bootstrap\RegisterFacades::class)->bootstrap($app);
-        /** @psalm-suppress MixedMethodCall */
         $app->make(\Illuminate\Foundation\Bootstrap\SetRequestForConsole::class)->bootstrap($app);
-        /** @psalm-suppress MixedMethodCall */
         $app->make(\Illuminate\Foundation\Bootstrap\RegisterProviders::class)->bootstrap($app);
 
         $this->getEnvironmentSetUp($app);
 
-        /** @psalm-suppress MixedMethodCall */
         $app->make(\Illuminate\Foundation\Bootstrap\BootProviders::class)->bootstrap($app);
 
         foreach ($this->getPackageBootstrappers($app) as $bootstrap) {
-            /** @psalm-suppress MixedMethodCall */
             $app->make($bootstrap)->bootstrap($app);
         }
 
-        /** @psalm-suppress MixedMethodCall */
         $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
 
         /** @var \Illuminate\Routing\Router $router */
@@ -127,5 +144,23 @@ final class ApplicationProvider
         $config->set('ide-helper.model_locations', [
             '../../../../tests/Application/app/Models',
         ]);
+
+        // Set up view paths for ide-helper
+        $viewPath = dirname((new \ReflectionClass(IdeHelperServiceProvider::class))->getFileName(), 2) . '/resources/views';
+
+        if (!$app->bound('view')) {
+            $filesystem = new \Illuminate\Filesystem\Filesystem();
+
+            // Set up the view finder
+            $viewFinder = new FileViewFinder($filesystem, [$viewPath]);
+
+            // Set up the engine resolver
+            $engineResolver = new EngineResolver();
+            $engineResolver->register('php', fn() => new PhpEngine($filesystem));
+
+            // Create and bind the view factory
+            $app->singleton('view', fn() => new Factory($engineResolver, $viewFinder, $app['events']));
+        }
+        $app['view']->addNamespace('ide-helper', $viewPath);
     }
 }
diff --git a/src/Providers/ConfigRepositoryProvider.php b/src/Providers/ConfigRepositoryProvider.php
index b393940..b7afe9a 100644
--- a/src/Providers/ConfigRepositoryProvider.php
+++ b/src/Providers/ConfigRepositoryProvider.php
@@ -8,10 +8,8 @@ use Illuminate\Contracts\Config\Repository;
 
 final class ConfigRepositoryProvider
 {
-    /** @psalm-suppress MixedInferredReturnType */
     public static function get(): Repository
     {
-        /** @psalm-suppress MixedReturnStatement */
         return ApplicationProvider::getApp()->get(Repository::class);
     }
 }
diff --git a/src/Providers/FacadeStubProvider.php b/src/Providers/FacadeStubProvider.php
index d6773fa..e40d946 100644
--- a/src/Providers/FacadeStubProvider.php
+++ b/src/Providers/FacadeStubProvider.php
@@ -31,10 +31,15 @@ final class FacadeStubProvider implements GeneratesStubs
 
         $fake_filesystem = new FakeFilesystem();
 
+        /**
+         * @var \Illuminate\View\Factory $viewFactory
+         */
+        $viewFactory = $app->make('view');
+
         $stubs_generator_command = new GeneratorCommand(
             $config,
             $fake_filesystem,
-            ViewFactoryProvider::get(),
+            $viewFactory
         );
 
         $stubs_generator_command->setLaravel($app);
diff --git a/src/Providers/ModelStubProvider.php b/src/Providers/ModelStubProvider.php
index 326dbc1..c0ecebf 100644
--- a/src/Providers/ModelStubProvider.php
+++ b/src/Providers/ModelStubProvider.php
@@ -3,6 +3,7 @@
 namespace Psalm\LaravelPlugin\Providers;
 
 use Psalm\Internal\Analyzer\ProjectAnalyzer;
+use Psalm\LaravelPlugin\Exceptions\UnknownApplicationConfiguration;
 use Psalm\LaravelPlugin\Fakes\FakeFilesystem;
 use Psalm\LaravelPlugin\Fakes\FakeModelsCommand;
 use Psalm\LaravelPlugin\Handlers\Eloquent\Schema\SchemaAggregator;
@@ -12,6 +13,7 @@ use Symfony\Component\Console\Output\NullOutput;
 use function glob;
 use function method_exists;
 use function unlink;
+use function is_array;
 
 final class ModelStubProvider implements GeneratesStubs
 {
@@ -33,7 +35,12 @@ final class ModelStubProvider implements GeneratesStubs
 
         $schema_aggregator = new SchemaAggregator();
 
-        foreach (glob($migrations_directory . '*.php') as $file) {
+        $migrationFilePathnames = glob($migrations_directory . '*.php');
+        if (! is_array($migrationFilePathnames)) {
+            throw new UnknownApplicationConfiguration("No migration files found in {$migrations_directory} directory.");
+        }
+
+        foreach ($migrationFilePathnames as $file) {
             $schema_aggregator->addStatements($codebase->getStatementsForFile($file));
         }
 
@@ -41,9 +48,10 @@ final class ModelStubProvider implements GeneratesStubs
 
         $models_generator_command = new FakeModelsCommand(
             $fake_filesystem,
-            $schema_aggregator
+            $app->make(\Illuminate\Contracts\Config\Repository::class),
+            $app->make(\Illuminate\View\Factory::class)
         );
-
+        $models_generator_command->setSchemaAggregator($schema_aggregator);
         $models_generator_command->setLaravel($app);
 
         @unlink(self::getStubFileLocation());
diff --git a/src/Providers/ViewFactoryProvider.php b/src/Providers/ViewFactoryProvider.php
deleted file mode 100644
index 8b3d1f6..0000000
--- a/src/Providers/ViewFactoryProvider.php
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-
-namespace Psalm\LaravelPlugin\Providers;
-
-use Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider;
-use Illuminate\Events\Dispatcher;
-use Illuminate\View\Engines\EngineResolver;
-use Illuminate\View\Engines\PhpEngine;
-use Illuminate\View\Factory;
-use Illuminate\View\FileViewFinder;
-use Psalm\LaravelPlugin\Fakes\FakeFilesystem;
-use ReflectionClass;
-use UnexpectedValueException;
-
-use function dirname;
-
-final class ViewFactoryProvider
-{
-    public static function get(): Factory
-    {
-        $service_helper_reflection = new ReflectionClass(IdeHelperServiceProvider::class);
-
-        $file_path = $service_helper_reflection->getFileName();
-
-        if ($file_path === false) {
-            throw new UnexpectedValueException('Service helper should have a file path');
-        }
-
-        $resolver = new EngineResolver();
-        $fake_filesystem = new FakeFilesystem();
-        $resolver->register('php', function () use ($fake_filesystem): PhpEngine {
-            return new PhpEngine($fake_filesystem);
-        });
-        $finder = new FileViewFinder($fake_filesystem, [dirname($file_path) . '/../resources/views']);
-        $factory = new Factory($resolver, $finder, new Dispatcher());
-        $factory->addExtension('php', 'php');
-        return $factory;
-    }
-}
diff --git a/stubs/common/Database/Migrations/Migrator.stubphp b/stubs/common/Database/Migrations/Migrator.stubphp
index a1580a7..28da5fe 100644
--- a/stubs/common/Database/Migrations/Migrator.stubphp
+++ b/stubs/common/Database/Migrations/Migrator.stubphp
@@ -7,10 +7,10 @@ class Migrator
     /**
      * Execute the given callback using the given connection as the default connection.
      *
-     * @template TCalbackReturn
+     * @template TCallbackReturn
      * @param string $name
-     * @param callable(): TCalbackReturn $callback
-     * @return TCalbackReturn
+     * @param callable(): TCallbackReturn $callback
+     * @return TCallbackReturn
      */
     public function usingConnection($name, callable $callback) {}
 }


Test patch:
diff --git a/.github/workflows/test-laravel.yml b/.github/workflows/test-laravel.yml
index 50851e4..967e3b8 100644
--- a/.github/workflows/test-laravel.yml
+++ b/.github/workflows/test-laravel.yml
@@ -1,4 +1,4 @@
-name: Test laravel projects
+name: Test Laravel app
 
 on:
   push:
@@ -19,13 +19,22 @@ on:
 jobs:
   build:
     runs-on: ubuntu-latest
-
-    name: Test Laravel
+    name: Test Laravel app
 
     steps:
     - name: Checkout code
       uses: actions/checkout@v4
 
-    - name: Test Laravel
+    - name: Setup PHP
+      uses: shivammathur/setup-php@v2
+      with:
+        php-version: 8.3
+        coverage: none
+
+    - name: Build a Laravel project and run Psalm on it
       run: |
         ./tests/Application/laravel-test.sh
+
+    - run: |
+        echo "Psalm analysis failed on a fresh Laravel project. Please consider updating baseline: tests/Application/laravel-test-baseline.xml"
+      if: ${{ failure() }}
\ No newline at end of file
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b84c7bc..ee2b104 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -6,7 +6,7 @@ on:
     paths:
       - '**.php'
       - '**.stubphp'
-      - '**.feature'
+      - '**.phpt'
       - '**.yml'
       - 'composer.json'
       - '.github/workflows/test.yml'
@@ -14,7 +14,7 @@ on:
     paths:
       - '**.php'
       - '**.stubphp'
-      - '**.feature'
+      - '**.phpt'
       - 'composer.json'
       - '.github/workflows/test.yml'
   schedule:
@@ -51,13 +51,10 @@ jobs:
     strategy:
       fail-fast: true
       matrix:
-        php: [8.4, 8.3, 8.2, 8.1]
-        laravel: [^11.0, ^10.0]
-        dependencies: [lowest, highest]
-        exclude:
-          - php: 8.1
-            laravel: ^11.0
-    name: Type P${{ matrix.php }} | L${{ matrix.laravel }} | ${{ matrix.dependencies == 'highest' && '↑' || '↓' }}
+        php: [8.4, 8.3, 8.2]
+        laravel: [^11.35]
+        dependencies: [highest]
+    name: Type test P${{ matrix.php }} | L${{ matrix.laravel }} | ${{ matrix.dependencies == 'highest' && '↑' || '↓' }}
     steps:
       - name: Checkout code
         uses: actions/checkout@v4
diff --git a/tests/Application/README.md b/tests/Application/README.md
index da013e4..8573148 100644
--- a/tests/Application/README.md
+++ b/tests/Application/README.md
@@ -4,6 +4,6 @@ Idea of application test: create an [almost] empty Laravel app and run Psalm ove
 
 ## FAQ
 
- - Q1: How to update baselines
- - A1: Inside .sh files change `--use-baseline` to `--set-baseline`, run sh files and revert changes in .sh files.
+ - Q1: How to update the baseline file for the Laravel Application test?
+ - A1: Run `laravel-test.sh` script with the `-u` (or `--update`) flag, e.g., `./laravel-test.sh -u`.
 ____
diff --git a/tests/Application/laravel-test-baseline.xml b/tests/Application/laravel-test-baseline.xml
index 355f442..24a684d 100644
--- a/tests/Application/laravel-test-baseline.xml
+++ b/tests/Application/laravel-test-baseline.xml
@@ -1,136 +1,171 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<files psalm-version="5.7.7@e028ba46ba0d7f9a78bc3201c251e137383e145f">
+<files psalm-version="6.1.0@827971f8bc7a28bb4f842f34bf8901521de1cea3">
   <file src="app/Broadcasting/ExampleChannel.php">
     <InvalidReturnType>
-      <code>array|bool</code>
+      <code><![CDATA[array|bool]]></code>
     </InvalidReturnType>
     <UnusedClass>
-      <code>ExampleChannel</code>
+      <code><![CDATA[ExampleChannel]]></code>
     </UnusedClass>
   </file>
   <file src="app/Casts/ExampleCast.php">
     <MissingTemplateParam>
-      <code>CastsAttributes</code>
+      <code><![CDATA[CastsAttributes]]></code>
     </MissingTemplateParam>
-    <MoreSpecificImplementedParamType>
-      <code>$attributes</code>
-      <code>$attributes</code>
-    </MoreSpecificImplementedParamType>
     <UnusedClass>
-      <code>ExampleCast</code>
+      <code><![CDATA[ExampleCast]]></code>
     </UnusedClass>
   </file>
   <file src="app/Console/Commands/ExampleCommand.php">
     <MissingReturnType>
-      <code>handle</code>
+      <code><![CDATA[handle]]></code>
     </MissingReturnType>
+    <NonInvariantDocblockPropertyType>
+      <code><![CDATA[$description]]></code>
+    </NonInvariantDocblockPropertyType>
+    <PropertyNotSetInConstructor>
+      <code><![CDATA[ExampleCommand]]></code>
+      <code><![CDATA[ExampleCommand]]></code>
+      <code><![CDATA[ExampleCommand]]></code>
+      <code><![CDATA[ExampleCommand]]></code>
+      <code><![CDATA[ExampleCommand]]></code>
+    </PropertyNotSetInConstructor>
     <UnusedClass>
-      <code>ExampleCommand</code>
+      <code><![CDATA[ExampleCommand]]></code>
     </UnusedClass>
   </file>
+  <file src="app/Events/ExampleEvent.php">
+    <PossiblyUnusedMethod>
+      <code><![CDATA[broadcastOn]]></code>
+    </PossiblyUnusedMethod>
+  </file>
   <file src="app/Exceptions/ExampleException.php">
     <UnusedClass>
-      <code>ExampleException</code>
+      <code><![CDATA[ExampleException]]></code>
     </UnusedClass>
   </file>
-  <file src="app/Exceptions/Handler.php">
-    <UnusedClosureParam>
-      <code>$e</code>
-    </UnusedClosureParam>
-  </file>
   <file src="app/Http/Controllers/ExampleController.php">
     <UnusedClass>
-      <code>ExampleController</code>
+      <code><![CDATA[ExampleController]]></code>
     </UnusedClass>
   </file>
   <file src="app/Http/Middleware/ExampleMiddleware.php">
     <UnusedClass>
-      <code>ExampleMiddleware</code>
+      <code><![CDATA[ExampleMiddleware]]></code>
     </UnusedClass>
   </file>
-  <file src="app/Http/Middleware/ValidateSignature.php">
-    <PossiblyUnusedProperty>
-      <code>$except</code>
-    </PossiblyUnusedProperty>
-  </file>
   <file src="app/Http/Requests/ExampleRequest.php">
-    <DeprecatedInterface>
-      <code><![CDATA[array<string, \Illuminate\Contracts\Validation\Rule|array|string>]]></code>
-    </DeprecatedInterface>
+    <PropertyNotSetInConstructor>
+      <code><![CDATA[ExampleRequest]]></code>
+      <code><![CDATA[ExampleRequest]]></code>
+      <code><![CDATA[ExampleRequest]]></code>
+      <code><![CDATA[ExampleRequest]]></code>
+      <code><![CDATA[ExampleRequest]]></code>
+      <code><![CDATA[ExampleRequest]]></code>
+      <code><![CDATA[ExampleRequest]]></code>
+      <code><![CDATA[ExampleRequest]]></code>
+      <code><![CDATA[ExampleRequest]]></code>
+    </PropertyNotSetInConstructor>
     <UnusedClass>
-      <code>ExampleRequest</code>
+      <code><![CDATA[ExampleRequest]]></code>
     </UnusedClass>
   </file>
   <file src="app/Http/Resources/ExampleResource.php">
     <MixedReturnTypeCoercion>
       <code><![CDATA[array<string, mixed>]]></code>
-      <code>parent::toArray($request)</code>
+      <code><![CDATA[parent::toArray($request)]]></code>
     </MixedReturnTypeCoercion>
     <UnusedClass>
-      <code>ExampleResource</code>
+      <code><![CDATA[ExampleResource]]></code>
     </UnusedClass>
   </file>
+  <file src="app/Jobs/ExampleJob.php">
+    <PossiblyUnusedMethod>
+      <code><![CDATA[handle]]></code>
+    </PossiblyUnusedMethod>
+  </file>
   <file src="app/Listeners/ExampleListener.php">
     <UnusedClass>
-      <code>ExampleListener</code>
+      <code><![CDATA[ExampleListener]]></code>
     </UnusedClass>
   </file>
   <file src="app/Mail/ExampleMail.php">
     <PossiblyUnusedMethod>
-      <code>attachments</code>
-      <code>content</code>
-      <code>envelope</code>
+      <code><![CDATA[__construct]]></code>
+      <code><![CDATA[attachments]]></code>
+      <code><![CDATA[content]]></code>
+      <code><![CDATA[envelope]]></code>
     </PossiblyUnusedMethod>
+    <PropertyNotSetInConstructor>
+      <code><![CDATA[ExampleMail]]></code>
+      <code><![CDATA[ExampleMail]]></code>
+      <code><![CDATA[ExampleMail]]></code>
+      <code><![CDATA[ExampleMail]]></code>
+      <code><![CDATA[ExampleMail]]></code>
+      <code><![CDATA[ExampleMail]]></code>
+      <code><![CDATA[ExampleMail]]></code>
+      <code><![CDATA[ExampleMail]]></code>
+    </PropertyNotSetInConstructor>
+  </file>
+  <file src="app/Models/Example.php">
+    <UnusedClass>
+      <code><![CDATA[Example]]></code>
+    </UnusedClass>
   </file>
   <file src="app/Models/Scopes/ExampleScope.php">
     <UnusedClass>
-      <code>ExampleScope</code>
+      <code><![CDATA[ExampleScope]]></code>
     </UnusedClass>
   </file>
   <file src="app/Models/User.php">
     <NonInvariantDocblockPropertyType>
-      <code>$casts</code>
-      <code>$fillable</code>
-      <code>$hidden</code>
+      <code><![CDATA[$fillable]]></code>
+      <code><![CDATA[$hidden]]></code>
     </NonInvariantDocblockPropertyType>
   </file>
+  <file src="app/Notifications/ExampleNotification.php">
+    <PossiblyUnusedMethod>
+      <code><![CDATA[__construct]]></code>
+      <code><![CDATA[toArray]]></code>
+      <code><![CDATA[toMail]]></code>
+      <code><![CDATA[via]]></code>
+    </PossiblyUnusedMethod>
+    <PropertyNotSetInConstructor>
+      <code><![CDATA[ExampleNotification]]></code>
+    </PropertyNotSetInConstructor>
+  </file>
   <file src="app/Observers/ExampleObserver.php">
     <UnusedClass>
-      <code>ExampleObserver</code>
+      <code><![CDATA[ExampleObserver]]></code>
     </UnusedClass>
   </file>
   <file src="app/Policies/ExamplePolicy.php">
     <UnusedClass>
-      <code>ExamplePolicy</code>
+      <code><![CDATA[ExamplePolicy]]></code>
     </UnusedClass>
   </file>
-  <file src="app/Providers/EventServiceProvider.php">
-    <NonInvariantDocblockPropertyType>
-      <code>$listen</code>
-    </NonInvariantDocblockPropertyType>
+  <file src="app/Providers/AppServiceProvider.php">
+    <UnusedClass>
+      <code><![CDATA[AppServiceProvider]]></code>
+    </UnusedClass>
   </file>
   <file src="app/Providers/ExampleProvider.php">
     <UnusedClass>
-      <code>ExampleProvider</code>
+      <code><![CDATA[ExampleProvider]]></code>
     </UnusedClass>
   </file>
-  <file src="app/Providers/RouteServiceProvider.php">
-    <RiskyTruthyFalsyComparison>
-        <code><![CDATA[$request->user()?->id]]></code>
-    </RiskyTruthyFalsyComparison>
-  </file>
   <file src="app/Rules/ExampleRule.php">
     <UnusedClass>
-      <code>ExampleRule</code>
+      <code><![CDATA[ExampleRule]]></code>
     </UnusedClass>
   </file>
   <file src="app/View/Components/ExampleComponent.php">
     <PropertyNotSetInConstructor>
-      <code>ExampleComponent</code>
-      <code>ExampleComponent</code>
+      <code><![CDATA[ExampleComponent]]></code>
+      <code><![CDATA[ExampleComponent]]></code>
     </PropertyNotSetInConstructor>
     <UnusedClass>
-      <code>ExampleComponent</code>
+      <code><![CDATA[ExampleComponent]]></code>
     </UnusedClass>
   </file>
 </files>
diff --git a/tests/Application/laravel-test-psalm.xml b/tests/Application/laravel-test-psalm.xml
index 9c03cfc..d9fd584 100644
--- a/tests/Application/laravel-test-psalm.xml
+++ b/tests/Application/laravel-test-psalm.xml
@@ -5,9 +5,9 @@
     findUnusedBaselineEntry="false"
     findUnusedCode="true"
     resolveFromConfigFile="false"
-    xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
+    xsi:schemaLocation="https://getpsalm.org/schema/config ../../vendor/vimeo/psalm/config.xsd"
     errorBaseline="../../tests/Application/laravel-test-baseline.xml"
-    phpVersion="8.0"
+    phpVersion="8.2"
 >
     <projectFiles>
         <directory name="app"/>
@@ -16,11 +16,14 @@
         </ignoreFiles>
     </projectFiles>
     <plugins>
-        <pluginClass class="Psalm\LaravelPlugin\Plugin"/>
+        <pluginClass class="Psalm\LaravelPlugin\Plugin">
+            <failOnInternalError>true</failOnInternalError>
+        </pluginClass>
     </plugins>
 
     <issueHandlers>
         <ParamNameMismatch errorLevel="suppress" />
         <PossiblyUnusedParam errorLevel="suppress" /><!-- Default Listeners, Notifications have empty methods with unused params, created just for example -->
+        <ClassMustBeFinal errorLevel="suppress" />
     </issueHandlers>
 </psalm>
diff --git a/tests/Application/laravel-test.sh b/tests/Application/laravel-test.sh
index b7e5113..b1d805e 100755
--- a/tests/Application/laravel-test.sh
+++ b/tests/Application/laravel-test.sh
@@ -1,19 +1,117 @@
-#!/bin/bash
+#!/usr/bin/env bash
 
+# Laravel Test Environment Setup Script
+# This script sets up a fresh Laravel installation and runs Psalm analysis
+
+# Exit on error. Append "|| true" if you expect an error.
 set -e
+# Exit on error in any nested commands
+set -o pipefail
+# Catch the error in case a variable is not set
+set -u
+
+# See https://github.com/laravel/laravel/tags for Laravel versions
+LARAVEL_INSTALLER_VERSION=11.6
+
+# Terminal colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# Default values
+UPDATE_BASELINE=false
+VERBOSE=false
+REMOVE=false
+
+# Function to display script usage
+show_help() {
+    cat << EOF
+Usage: $(basename "$0") [options]
+
+Sets up a fresh Laravel installation and runs Psalm analysis.
+
+Options:
+    -h, --help      Show this help message
+    -u, --update    Update Psalm baseline
+    -v, --verbose   Enable verbose output
+    -r, --remove   Remove Laravel project directory after execution
+
+Environment variables:
+    COMPOSER_MEMORY_LIMIT    Memory limit for Composer (default: -1)
+EOF
+}
 
+# Function to display error messages
+error() {
+    echo -e "${RED}Error: $1${NC}" >&2
+    exit 1
+}
+
+# Function to display info messages
+info() {
+    echo -e "${GREEN}$1${NC}"
+}
+
+# Function to display debug messages
+debug() {
+    if [ "$VERBOSE" = true ]; then
+        echo -e "${YELLOW}Debug: $1${NC}"
+    fi
+}
+
+# Cleanup function
+cleanup() {
+    if [ -d "$APP_INSTALLATION_PATH" ]; then
+        info "Removing the installation directory..."
+        rm -rf "$APP_INSTALLATION_PATH"
+    fi
+}
+
+# Parse command line arguments
+while [[ $# -gt 0 ]]; do
+    case $1 in
+        -h|--help)
+            show_help
+            exit 0
+            ;;
+        -u|--update)
+            UPDATE_BASELINE=true
+            shift
+            ;;
+        -r|--remove)
+            REMOVE=true
+            shift
+            ;;
+        -v|--verbose)
+            VERBOSE=true
+            shift
+            ;;
+        *)
+            error "Unknown option: $1\nUse --help for usage information."
+            ;;
+    esac
+done
+
+if [ "$REMOVE" = true ]; then
+    # Set up trap to clean up on script exit
+    trap cleanup EXIT
+fi
+
+# Get absolute path of script directory
 CURRENT_SCRIPT_PATH="$( cd "$(dirname "$0")" ; pwd -P )"
 APP_INSTALLATION_PATH="$(dirname "$(dirname "$CURRENT_SCRIPT_PATH")")/tests-app/laravel-example"
 
-echo "Cleaning up previous installation"
-rm -rf $APP_INSTALLATION_PATH
+if [ -d "$APP_INSTALLATION_PATH" ]; then
+    info "Removing previous installation"
+    rm -rf "$APP_INSTALLATION_PATH"
+fi
 
-echo "Installing Laravel"
-# See https://github.com/laravel/laravel/tags for Laravel versions
-composer create-project --quiet --prefer-dist laravel/laravel $APP_INSTALLATION_PATH 10.0 --quiet --prefer-dist
-cd $APP_INSTALLATION_PATH
+info "Creating a new Laravel project (installer v${LARAVEL_INSTALLER_VERSION})"
+composer create-project --quiet --prefer-dist laravel/laravel "$APP_INSTALLATION_PATH" $LARAVEL_INSTALLER_VERSION
+cd "$APP_INSTALLATION_PATH"
 
-echo "Preparing Laravel"
+info "Making different types of classes for Laravel"
 ./artisan make:cast ExampleCast
 ./artisan make:channel ExampleChannel
 ./artisan make:component ExampleComponent
@@ -37,12 +135,26 @@ echo "Preparing Laravel"
 ./artisan make:scope ExampleScope
 ./artisan make:seeder ExampleSeeder
 
-echo "Adding package from source"
+info "Adding package from source"
 composer config repositories.0 '{"type": "path", "url": "../../"}'
 composer config minimum-stability 'dev'
 COMPOSER_MEMORY_LIMIT=-1 composer require --dev "psalm/plugin-laravel:*" --update-with-all-dependencies
 
-echo "Analyzing Laravel"
-./vendor/bin/psalm -c ../../tests/Application/laravel-test-psalm.xml --use-baseline=../../tests/Application/laravel-test-baseline.xml
+info "Analyzing Laravel"
+PSALM_CONFIG="../../tests/Application/laravel-test-psalm.xml"
+PSALM_BASELINE="../../tests/Application/laravel-test-baseline.xml"
+
+if [ "$UPDATE_BASELINE" = true ]; then
+    info "Updating Psalm baseline"
+    ./vendor/bin/psalm --config="$PSALM_CONFIG" --set-baseline="$PSALM_BASELINE"
+    info "Baseline file $PSALM_BASELINE is updated, please check the changes and commit them."
+else
+    info "Running Psalm analysis"
+    ./vendor/bin/psalm --config="$PSALM_CONFIG" --use-baseline="$PSALM_BASELINE"
+fi
+
+echo
 
-echo -e "\nA sample Laravel application installed at the $APP_INSTALLATION_PATH directory, feel free to remove it."
+if [ "$REMOVE" = false ]; then
+    info "A sample Laravel application installed at the $APP_INSTALLATION_PATH directory, feel free to remove it."
+fi
diff --git a/tests/README.md b/tests/README.md
index 219055f..6c9f85c 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -1,6 +1,6 @@
 # Tests
 
 There are 3 types of tests:
-1. Type (main one): Use phpt files to run Psalm over code snippets in the context of fake Laravel app [using orchestra/testbench]
-2. Application: create an [almost] empty Laravel app and run Psalm over its codebase.
-3. Unit: use PHPUnit to test some internal logic without running Psalm.
+1. **Type** (the main one): uses .phpt files to run Psalm over code snippets in the context of a fake Laravel app [using orchestra/testbench]
+2. **Application**: creates an empty Laravel app, adds some typical classes of different types and run Psalm over its codebase.
+3. **Unit**: uses PHPUnit to test some internal logic without running Psalm.
diff --git a/tests/Type/PsalmTest.php b/tests/Type/PsalmTest.php
index 5060bf8..5f3d265 100644
--- a/tests/Type/PsalmTest.php
+++ b/tests/Type/PsalmTest.php
@@ -14,8 +14,8 @@ final class PsalmTest extends TestCase
 {
     private ?PsalmTester $psalmTester = null;
 
-    /** @return iterable<string, list{string}> */
-    public static function providePhptFiles(): iterable
+    /** @return \Generator<string, array{0: string}> */
+    public static function providePhptFiles(): \Generator
     {
         $baseDir = __DIR__ . \DIRECTORY_SEPARATOR . 'tests' . \DIRECTORY_SEPARATOR;
         $testExtension = 'phpt';
@@ -24,14 +24,11 @@ final class PsalmTest extends TestCase
         $itr = new \RecursiveIteratorIterator($dirItr);
         $regItr = new \RegexIterator($itr, "/^.+.{$testExtension}\$/", \RegexIterator::GET_MATCH);
 
-        $filePaths = [];
-
         foreach ($regItr as $file) {
             $filepath = $file[0];
-            $filePaths[str_replace($baseDir, '', $filepath)] = [$filepath];
+            $relativeFilepath = str_replace($baseDir, '', $filepath);
+            yield $relativeFilepath => [$filepath];
         }
-
-        return $filePaths;
     }
 
     #[DataProvider('providePhptFiles')]
diff --git a/tests/Type/psalm.xml b/tests/Type/psalm.xml
index af27679..ca05f20 100644
--- a/tests/Type/psalm.xml
+++ b/tests/Type/psalm.xml
@@ -11,9 +11,11 @@
     sealAllMethods="true"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns="https://getpsalm.org/schema/config"
-    xsi:schemaLocation="https://getpsalm.org/schema/config ../vendor/vimeo/psalm/config.xsd"
+    xsi:schemaLocation="https://getpsalm.org/schema/config ../../vendor/vimeo/psalm/config.xsd"
 >
     <plugins>
-        <pluginClass class="\Psalm\LaravelPlugin\Plugin"/>
+        <pluginClass class="Psalm\LaravelPlugin\Plugin">
+            <failOnInternalError>true</failOnInternalError>
+        </pluginClass>
     </plugins>
 </psalm>
diff --git a/tests/Type/tests/ContainerTest.phpt b/tests/Type/tests/ContainerTest.phpt
index 3556126..6f252b9 100644
--- a/tests/Type/tests/ContainerTest.phpt
+++ b/tests/Type/tests/ContainerTest.phpt
@@ -64,5 +64,4 @@ function cannotResolveUnknownDependency(): \Illuminate\Log\LogManager
 ?>
 --EXPECTF--
 UndefinedMagicMethod on line %d: Magic method Illuminate\Foundation\Application::undefined_method does not exist
-MixedInferredReturnType on line %d: Could not verify return type 'Illuminate\Log\LogManager' for cannotResolveUnknownDependency
 MixedReturnStatement on line %d: Could not infer a return type