updated streamline-setup v2

This commit is contained in:
2025-01-15 08:53:49 -08:00
committed by alec.turner
parent a2ce9248f0
commit 4b569f81b0
20228 changed files with 2932048 additions and 63204 deletions
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Can Vural <can9119@gmail.com>, Nuno Maduro <enunomaduro@gmail.com>,
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Contracts\Foundation\Application;
use Laravel\Lumen\Application as LumenApplication;
use Larastan\Larastan\ApplicationResolver;
use Orchestra\Testbench\Concerns\CreatesApplication;
if (! defined('LARAVEL_START')) {
define('LARAVEL_START', microtime(true));
}
if (file_exists($applicationPath = getcwd().'/bootstrap/app.php')) { // Applications and Local Dev
$app = require $applicationPath;
} elseif (file_exists($applicationPath = dirname(__DIR__, 3).'/bootstrap/app.php')) { // Relative path from default vendor dir
$app = require $applicationPath;
} elseif (trait_exists(CreatesApplication::class)) { // Packages
$app = ApplicationResolver::resolve();
}
if (isset($app)) {
if ($app instanceof Application) {
$app->make(Kernel::class)->bootstrap();
} elseif ($app instanceof LumenApplication) {
$app->boot();
}
if (! defined('LARAVEL_VERSION')) {
define('LARAVEL_VERSION', $app->version());
}
}
@@ -0,0 +1,86 @@
{
"name": "larastan/larastan",
"type": "phpstan-extension",
"description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel",
"keywords": ["laravel", "larastan", "phpstan", "php", "package", "static analysis", "code analysis", "code analyse"],
"license": "MIT",
"authors": [
{
"name": "Can Vural",
"email": "can9119@gmail.com"
},
{
"name": "Nuno Maduro",
"email": "enunomaduro@gmail.com"
}
],
"require": {
"php": "^8.0.2",
"ext-json": "*",
"illuminate/console": "^9.52.16 || ^10.28.0 || ^11.16",
"illuminate/container": "^9.52.16 || ^10.28.0 || ^11.16",
"illuminate/contracts": "^9.52.16 || ^10.28.0 || ^11.16",
"illuminate/database": "^9.52.16 || ^10.28.0 || ^11.16",
"illuminate/http": "^9.52.16 || ^10.28.0 || ^11.16",
"illuminate/pipeline": "^9.52.16 || ^10.28.0 || ^11.16",
"illuminate/support": "^9.52.16 || ^10.28.0 || ^11.16",
"phpmyadmin/sql-parser": "^5.9.0",
"phpstan/phpstan": "^1.12.11"
},
"require-dev": {
"doctrine/coding-standard": "^12.0",
"laravel/framework": "^9.52.16 || ^10.28.0 || ^11.16",
"mockery/mockery": "^1.5.1",
"nikic/php-parser": "^4.19.1",
"orchestra/canvas": "^7.11.1 || ^8.11.0 || ^9.0.2",
"orchestra/testbench-core": "^7.33.0 || ^8.13.0 || ^9.0.9",
"phpstan/phpstan-deprecation-rules": "^1.2",
"phpunit/phpunit": "^9.6.13 || ^10.5.16"
},
"suggest": {
"orchestra/testbench": "Using Larastan for analysing a package needs Testbench"
},
"autoload-dev": {
"psr-4": {
"App\\": "tests/application/app/",
"Database\\Factories\\": "tests/application/database/factories/",
"Database\\Migrations\\": "tests/application/database/migrations/"
},
"classmap": [
"tests/"
]
},
"minimum-stability": "dev",
"prefer-stable": true,
"autoload": {
"psr-4": {
"Larastan\\Larastan\\": "src/"
}
},
"config": {
"sort-packages": true,
"preferred-install": "dist",
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
},
"phpstan": {
"includes": [
"extension.neon"
]
}
},
"scripts": {
"test:cs": "phpcs",
"test:types": "phpstan analyse --ansi --memory-limit 256M",
"test:unit": "phpunit --colors=always -d memory_limit=1408M",
"test": [
"@test:types",
"@test:unit"
]
}
}
@@ -0,0 +1,570 @@
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\EnumerableGenericStaticMethodDynamicMethodReturnTypeExtension
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\ReturnTypes\EnumerableGenericStaticMethodDynamicStaticMethodReturnTypeExtension
tags:
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
-
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
@@ -0,0 +1,61 @@
<?xml version="1.0"?>
<ruleset>
<arg name="basepath" value="."/>
<arg name="extensions" value="php"/>
<arg name="parallel" value="80"/>
<arg name="cache" value=".phpcs-cache"/>
<arg name="colors"/>
<!-- Ignore warnings, show progress of the run and show sniff names -->
<arg value="nps"/>
<!-- Directories to be checked -->
<file>src</file>
<file>tests</file>
<exclude-pattern>tests/*/data</exclude-pattern>
<exclude-pattern>tests/enum-definition.php</exclude-pattern>
<exclude-pattern>tests/bootstrap.php</exclude-pattern>
<exclude-pattern>tests/application</exclude-pattern>
<!-- Include full Doctrine Coding Standard -->
<rule ref="Doctrine">
<exclude name="SlevomatCodingStandard.PHP.RequireExplicitAssertion"/>
</rule>
<rule ref="PSR1.Methods.CamelCapsMethodName.NotCamelCaps">
<exclude-pattern>tests/*</exclude-pattern>
</rule>
<rule ref="Squiz.Arrays.ArrayDeclaration.ValueNoNewline">
<exclude-pattern>tests/Type/GenericModelPropertyTypeTest.php</exclude-pattern>
</rule>
<rule ref="Generic.PHP.ForbiddenFunctions">
<properties>
<property name="forbiddenFunctions" type="array">
<element key="chop" value="rtrim"/>
<element key="close" value="closedir"/>
<element key="delete" value="unset"/>
<element key="doubleval" value="floatval"/>
<element key="extract" value="null"/>
<element key="fputs" value="fwrite"/>
<element key="ini_alter" value="ini_set"/>
<element key="is_double" value="is_float"/>
<element key="is_integer" value="is_int"/>
<element key="is_long" value="is_int"/>
<element key="is_null" value="null"/>
<element key="is_real" value="is_float"/>
<element key="is_writeable" value="is_writable"/>
<element key="join" value="implode"/>
<element key="key_exists" value="array_key_exists"/>
<element key="pos" value="current"/>
<element key="settype" value="null"/>
<element key="show_source" value="highlight_file"/>
<element key="sizeof" value="count"/>
<element key="strchr" value="strstr"/>
</property>
</properties>
</rule>
</ruleset>
@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Foundation\PackageManifest;
use Larastan\Larastan\Internal\ComposerHelper;
use Orchestra\Testbench\Foundation\Application as Testbench;
use Orchestra\Testbench\Foundation\Bootstrap\CreateVendorSymlink;
use Orchestra\Testbench\Foundation\Config;
use ReflectionException;
use function class_exists;
use function define;
use function defined;
use function file_exists;
use function getcwd;
use function sprintf;
/** @internal */
final class ApplicationResolver
{
/**
* Create symlink on vendor path.
*/
public static function createSymlinkToVendorPath(Application $app, string $vendorDir): void
{
if (class_exists(CreateVendorSymlink::class)) {
(new CreateVendorSymlink($vendorDir))->bootstrap($app);
return;
}
$filesystem = new Filesystem();
$laravelVendorPath = $app->basePath('vendor');
if (
sprintf('%s/autoload.php', $laravelVendorPath) !== sprintf('%s/autoload.php', $vendorDir)
) {
if ($filesystem->exists($app->bootstrapPath('cache/packages.php'))) {
$filesystem->delete($app->bootstrapPath('cache/packages.php'));
}
$filesystem->delete($laravelVendorPath);
$filesystem->link($vendorDir, $laravelVendorPath);
}
$app->flush();
}
/**
* Creates an application and registers service providers found.
*
* @throws ReflectionException
*/
public static function resolve(): Application
{
/** @var non-empty-string $workingPath */
$workingPath = getcwd();
if (! defined('TESTBENCH_WORKING_PATH')) {
define('TESTBENCH_WORKING_PATH', $workingPath);
}
$composerConfig = ComposerHelper::getComposerConfig($workingPath);
if ($composerConfig) {
$vendorDir = ComposerHelper::getVendorDirFromComposerConfig($workingPath, $composerConfig);
} else {
$vendorDir = $workingPath . '/vendor';
}
$resolvingCallback = static function ($app): void {
$packageManifest = $app->make(PackageManifest::class);
if (file_exists($packageManifest->manifestPath)) {
return;
}
$packageManifest->build();
};
if (class_exists(Config::class)) {
$config = Config::loadFromYaml($workingPath);
self::createSymlinkToVendorPath(Testbench::create($config['laravel'], null, ['extra' => ['dont-discover' => ['*']]]), $vendorDir);
return Testbench::create(
$config['laravel'],
$resolvingCallback,
['enables_package_discoveries' => true, 'extra' => $config->getExtraAttributes()],
);
}
self::createSymlinkToVendorPath(Testbench::create(Testbench::applicationBasePath(), null, ['extra' => ['dont-discover' => ['*']]]), $vendorDir);
return Testbench::create(
null,
$resolvingCallback,
['enables_package_discoveries' => true],
);
}
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Collectors;
use Illuminate\Mail\Mailable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\View\ViewName;
use PhpParser\Node;
use PhpParser\Node\Identifier;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Type\ObjectType;
use function count;
use function in_array;
/** @implements Collector<Node\Expr\MethodCall, string> */
final class UsedEmailViewCollector implements Collector
{
public function getNodeType(): string
{
return Node\Expr\MethodCall::class;
}
/** @param Node\Expr\MethodCall $node */
public function processNode(Node $node, Scope $scope): string|null
{
$name = $node->name;
if (! $name instanceof Identifier) {
return null;
}
if (! in_array($name->name, ['markdown', 'view'], true)) {
return null;
}
if (count($node->getArgs()) === 0) {
return null;
}
$class = $node->var;
$type = $scope->getType($class);
if (
! (new ObjectType(Mailable::class))->isSuperTypeOf($type)->yes()
&& ! (new ObjectType(MailMessage::class))->isSuperTypeOf($type)->yes()
) {
return null;
}
$template = $node->getArgs()[0]->value;
if (! $template instanceof Node\Scalar\String_) {
return null;
}
return ViewName::normalize($template->value);
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Collectors;
use Illuminate\Support\Facades\Route;
use Illuminate\View\ViewName;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Type\ObjectType;
use function count;
/** @implements Collector<Node\Expr\StaticCall, string> */
final class UsedRouteFacadeViewCollector implements Collector
{
public function getNodeType(): string
{
return Node\Expr\StaticCall::class;
}
/** @param Node\Expr\StaticCall $node */
public function processNode(Node $node, Scope $scope): string|null
{
$name = $node->name;
if (! $name instanceof Node\Identifier) {
return null;
}
if ($name->name !== 'view') {
return null;
}
if (count($node->getArgs()) < 2) {
return null;
}
$class = $node->class;
if (! $class instanceof Node\Name) {
return null;
}
$class = $scope->resolveName($class);
if (! (new ObjectType(Route::class))->isSuperTypeOf(new ObjectType($class))->yes()) {
return null;
}
$template = $node->getArgs()[1]->value;
if (! $template instanceof Node\Scalar\String_) {
return null;
}
return ViewName::normalize($template->value);
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Collectors;
use Illuminate\Support\Facades\View;
use Illuminate\View\ViewName;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Type\ObjectType;
use function count;
/** @implements Collector<Node\Expr\StaticCall, string> */
final class UsedViewFacadeMakeCollector implements Collector
{
public function getNodeType(): string
{
return Node\Expr\StaticCall::class;
}
/** @param Node\Expr\StaticCall $node */
public function processNode(Node $node, Scope $scope): string|null
{
$name = $node->name;
if (! $name instanceof Node\Identifier) {
return null;
}
if ($name->name !== 'make') {
return null;
}
if (count($node->getArgs()) < 1) {
return null;
}
$class = $node->class;
if (! $class instanceof Node\Name) {
return null;
}
$class = $scope->resolveName($class);
if (! (new ObjectType(View::class))->isSuperTypeOf(new ObjectType($class))->yes()) {
return null;
}
$template = $node->getArgs()[0]->value;
if (! $template instanceof Node\Scalar\String_) {
return null;
}
return ViewName::normalize($template->value);
}
}
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Collectors;
use Illuminate\View\ViewName;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use function count;
/** @implements Collector<Node\Expr\FuncCall, string> */
final class UsedViewFunctionCollector implements Collector
{
public function getNodeType(): string
{
return Node\Expr\FuncCall::class;
}
/** @param Node\Expr\FuncCall $node */
public function processNode(Node $node, Scope $scope): string|null
{
$funcName = $node->name;
if (! $funcName instanceof Node\Name) {
return null;
}
$funcName = $scope->resolveName($funcName);
if ($funcName !== 'view') {
return null;
}
// TODO: maybe make sure this function is coming from Laravel
if (count($node->getArgs()) < 1) {
return null;
}
$template = $node->getArgs()[0]->value;
if (! $template instanceof Node\Scalar\String_) {
return null;
}
return ViewName::normalize($template->value);
}
}
@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Collectors;
use Larastan\Larastan\Support\ViewFileHelper;
use PhpParser\Node;
use PHPStan\Parser\Parser;
use PHPStan\Parser\ParserErrorsException;
use function array_filter;
use function array_map;
use function array_merge;
use function count;
use function preg_match_all;
use const PREG_SET_ORDER;
final class UsedViewInAnotherViewCollector
{
/** @see https://regex101.com/r/OyHHCY/1 */
private const VIEW_NAME_REGEX = '/@(extends|include(If|Unless|When|First)?)(\(.*?([\'"])(.*?)([\'"])([),]))/m';
public function __construct(private Parser $parser, private ViewFileHelper $viewFileHelper)
{
}
/** @return list<string> */
public function getUsedViews(): array
{
$usedViews = [];
foreach ($this->viewFileHelper->getAllViewFilePaths() as $viewFile) {
try {
$parserNodes = $this->parser->parseFile($viewFile);
$usedViews = array_merge($usedViews, $this->processNodes($parserNodes));
} catch (ParserErrorsException) {
continue;
}
}
return $usedViews;
}
/**
* @param Node\Stmt[] $nodes
*
* @return list<string>
*/
private function processNodes(array $nodes): array
{
$nodes = array_filter($nodes, static function (Node $node) {
return $node instanceof Node\Stmt\InlineHTML;
});
if (count($nodes) === 0) {
return [];
}
$usedViews = [];
foreach ($nodes as $node) {
preg_match_all(self::VIEW_NAME_REGEX, $node->value, $matches, PREG_SET_ORDER, 0);
$usedViews = array_merge($usedViews, array_map(static function ($match) {
return $match[5];
}, $matches));
}
return $usedViews;
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Collectors;
use Illuminate\Contracts\View\Factory;
use Illuminate\View\ViewName;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Type\ObjectType;
use function count;
/** @implements Collector<Node\Expr\MethodCall, string> */
final class UsedViewMakeCollector implements Collector
{
public function getNodeType(): string
{
return Node\Expr\MethodCall::class;
}
/** @param Node\Expr\MethodCall $node */
public function processNode(Node $node, Scope $scope): string|null
{
$name = $node->name;
if (! $name instanceof Node\Identifier) {
return null;
}
if ($name->name !== 'make') {
return null;
}
if (count($node->getArgs()) < 1) {
return null;
}
$class = $node->var;
if (! (new ObjectType(Factory::class))->isSuperTypeOf($scope->getType($class))->yes()) {
return null;
}
$template = $node->getArgs()[0]->value;
if (! $template instanceof Node\Scalar\String_) {
return null;
}
return ViewName::normalize($template->value);
}
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Concerns;
use Illuminate\Container\Container;
use Illuminate\Contracts\Container\Container as ContainerContract;
use Throwable;
/** @internal */
trait HasContainer
{
protected \Illuminate\Contracts\Container\Container|null $container = null;
public function setContainer(ContainerContract $container): void
{
$this->container = $container;
}
/**
* Returns the current broker.
*/
public function getContainer(): ContainerContract
{
return $this->container ?? Container::getInstance();
}
/**
* Resolve the given type from the container.
*/
public function resolve(string $abstract): mixed
{
try {
$concrete = $this->getContainer()->make($abstract);
} catch (Throwable) {
return null;
}
return $concrete;
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Concerns;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use function array_keys;
use function array_reduce;
use function in_array;
use function is_array;
trait LoadsAuthModel
{
/** @phpstan-return list<class-string> */
private function getAuthModels(ConfigRepository $config, string|null $guard = null): array
{
$guards = $config->get('auth.guards');
$providers = $config->get('auth.providers');
if (! is_array($guards) || ! is_array($providers)) {
return [];
}
return array_reduce(
$guard === null ? array_keys($guards) : [$guard],
static function ($carry, $guardName) use ($guards, $providers) {
$provider = $guards[$guardName]['provider'] ?? null;
if (! $provider) {
return $carry;
}
$authModel = $providers[$provider]['model'] ?? null;
if (! $authModel || in_array($authModel, $carry, strict: true)) {
return $carry;
}
$carry[] = $authModel;
return $carry;
},
initial: [],
);
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Contracts\Methods;
use Illuminate\Contracts\Container\Container as ContainerContract;
use LogicException;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\Php\PhpMethodReflectionFactory;
use PHPStan\Reflection\ReflectionProvider;
/** @internal */
interface PassableContract
{
public function setContainer(ContainerContract $container): void;
public function getClassReflection(): ClassReflection;
public function setClassReflection(ClassReflection $classReflection): PassableContract;
public function getMethodName(): string;
public function hasFound(): bool;
public function searchOn(string $class): bool;
/** @throws LogicException */
public function getMethodReflection(): MethodReflection;
public function setMethodReflection(MethodReflection $methodReflection): void;
/**
* Declares that the provided method can be called statically.
*/
public function setStaticAllowed(bool $staticAllowed): void;
/**
* Returns whether the method can be called statically.
*/
public function isStaticAllowed(): bool;
/** @param class-string $class */
public function sendToPipeline(string $class, bool $staticAllowed = false): bool;
public function getReflectionProvider(): ReflectionProvider;
public function getMethodReflectionFactory(): PhpMethodReflectionFactory;
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Contracts\Methods\Pipes;
use Closure;
use Larastan\Larastan\Contracts\Methods\PassableContract;
/** @internal */
interface PipeContract
{
public function handle(PassableContract $passable, Closure $next): void;
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Contracts\Types;
use PHPStan\Type\Type;
/** @internal */
interface PassableContract
{
public function getType(): Type;
public function setType(Type $type): void;
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Contracts\Types\Pipes;
use Closure;
use Larastan\Larastan\Contracts\Types\PassableContract;
/** @internal */
interface PipeContract
{
public function handle(PassableContract $passable, Closure $next): void;
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Internal;
use JsonException;
use function basename;
use function file_get_contents;
use function getenv;
use function is_file;
use function is_string;
use function json_decode;
use function trim;
use const JSON_THROW_ON_ERROR;
/** @internal */
final class ComposerHelper
{
/** @return array<string, mixed> */
public static function getComposerConfig(string $root): array|null
{
$composerJsonPath = self::getComposerJsonPath($root);
if (! is_file($composerJsonPath)) {
return null;
}
try {
$composerJsonContents = @file_get_contents($composerJsonPath);
if ($composerJsonContents === false) {
return null;
}
return json_decode($composerJsonContents, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
return null;
}
}
private static function getComposerJsonPath(string $root): string
{
$envComposer = getenv('COMPOSER');
$fileName = is_string($envComposer) ? $envComposer : 'composer.json';
return $root . '/' . basename(trim($fileName));
}
/** @param array<string, mixed> $composerConfig */
public static function getVendorDirFromComposerConfig(string $root, array $composerConfig): string
{
$vendorDirectory = $composerConfig['config']['vendor-dir'] ?? 'vendor';
return $root . '/' . trim($vendorDirectory, '/');
}
}
@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Internal;
use InvalidArgumentException;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\NullType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use function count;
final class ConsoleApplicationHelper
{
public function __construct(private ConsoleApplicationResolver $consoleApplicationResolver)
{
}
public function getArguments(ClassReflection $classReflection, Scope $scope): Type|null
{
$argTypes = [];
foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) {
try {
$arguments = $command->getDefinition()->getArguments();
$builder = ConstantArrayTypeBuilder::createEmpty();
foreach ($arguments as $name => $argument) {
$argumentType = $this->getArgumentType($scope, $argument);
$builder->setOffsetValueType(new ConstantStringType($name), $argumentType);
}
$argTypes[] = $builder->getArray();
} catch (InvalidArgumentException) {
// noop
}
}
return count($argTypes) > 0 ? TypeCombinator::union(...$argTypes) : null;
}
public function getArgumentType(Scope $scope, InputArgument $argument): Type
{
if ($argument->isArray()) {
$argType = new ArrayType(new IntegerType(), new StringType());
if (! $argument->isRequired() && $argument->getDefault() !== []) {
$argType = TypeCombinator::union($argType, $scope->getTypeFromValue($argument->getDefault()));
}
} else {
$argType = new StringType();
if (! $argument->isRequired()) {
$argType = TypeCombinator::union($argType, $scope->getTypeFromValue($argument->getDefault()));
}
}
return $argType;
}
public function getOptions(ClassReflection $classReflection, Scope $scope): Type|null
{
$optTypes = [];
foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) {
try {
$options = $command->getDefinition()->getOptions();
$builder = ConstantArrayTypeBuilder::createEmpty();
foreach ($options as $name => $option) {
$argumentType = $this->getOptionType($scope, $option);
$builder->setOffsetValueType(new ConstantStringType($name), $argumentType);
}
$optTypes[] = $builder->getArray();
} catch (InvalidArgumentException) {
// noop
}
}
return count($optTypes) > 0 ? TypeCombinator::union(...$optTypes) : null;
}
public function getOptionType(Scope $scope, InputOption $option): Type
{
if (! $option->acceptValue()) {
if ($option->isNegatable()) {
return new UnionType([new BooleanType(), new NullType()]);
}
return new BooleanType();
}
$optType = TypeCombinator::union(new StringType(), new NullType());
if ($option->isValueRequired() && ($option->isArray() || $option->getDefault() !== null)) {
$optType = TypeCombinator::removeNull($optType);
}
if ($option->isArray()) {
$optType = new ArrayType(new IntegerType(), $optType);
}
return TypeCombinator::union($optType, $scope->getTypeFromValue($option->getDefault()));
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Internal;
use Illuminate\Console\Application;
use Illuminate\Console\Command;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Type\ObjectType;
use function app;
/** @internal */
final class ConsoleApplicationResolver
{
private Application|null $application = null;
/** @return Command[] */
public function findCommands(ClassReflection $classReflection): array
{
$consoleApplication = $this->getApplication();
$classType = new ObjectType($classReflection->getName());
if (! (new ObjectType('Illuminate\Console\Command'))->isSuperTypeOf($classType)->yes()) {
return [];
}
$commands = [];
foreach ($consoleApplication->all() as $name => $command) {
if (! $classType->isSuperTypeOf(new ObjectType($command::class))->yes()) {
continue;
}
$commands[$name] = $command;
}
return $commands; // @phpstan-ignore-line
}
private function getApplication(): Application
{
if ($this->application === null) {
$this->application = new Application(app(Container::class), app(Dispatcher::class), app()->version());
$this->application->setContainerCommandLoader();
}
return $this->application;
}
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Internal;
use function version_compare;
final class LaravelVersion
{
public static function hasLaravel1115Generics(): bool
{
return version_compare(self::getLaravelVersion(), '11.15.0', '>=');
}
public static function getBuilderModelGenericName(): string
{
return self::hasLaravel1115Generics() ? 'TModel' : 'TModelClass';
}
public static function hasCollectedByAttribute(): bool
{
return version_compare(self::getLaravelVersion(), '11.28.0', '>=');
}
private static function getLaravelVersion(): string
{
return LARAVEL_VERSION;
}
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan;
use PHPStan\PhpDoc\StubFilesExtension;
use SplFileInfo;
use Symfony\Component\Finder\Finder;
use function array_keys;
use function array_values;
use function iterator_to_array;
use function version_compare;
final class LarastanStubFilesExtension implements StubFilesExtension
{
/** @inheritDoc */
public function getFiles(): array
{
$stubDirectories = Finder::create()->directories()->name('/^\d+/')->in(__DIR__ . '/../stubs')->depth(0);
// Include only applicable versions
$stubDirectories
->filter(static fn (SplFileInfo $file) => version_compare($file->getFilename(), LARAVEL_VERSION, '<='))
->sort(static fn (SplFileInfo $a, SplFileInfo $b) => version_compare($a->getFilename(), $b->getFilename()));
$files = [];
$stubDirs = [__DIR__ . '/../stubs/common', ...array_keys(iterator_to_array($stubDirectories))];
$stubFiles = Finder::create()->files()->name('*.stub')->in($stubDirs);
foreach ($stubFiles as $stubFile) {
$files[$stubFile->getRelativePathname()] = $stubFile->getRealPath();
}
return array_values($files);
}
}
@@ -0,0 +1,235 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Methods;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Str;
use Larastan\Larastan\Reflection\AnnotationScopeMethodParameterReflection;
use Larastan\Larastan\Reflection\DynamicWhereParameterReflection;
use Larastan\Larastan\Reflection\EloquentBuilderMethodReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MissingMethodFromReflectionException;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;
use function array_key_exists;
use function array_shift;
use function count;
use function in_array;
use function preg_split;
use function substr;
use function ucfirst;
use const PREG_SPLIT_DELIM_CAPTURE;
class BuilderHelper
{
public const MODEL_RETRIEVAL_METHODS = ['first', 'find', 'findMany', 'findOrFail', 'firstOrFail', 'sole'];
public const MODEL_CREATION_METHODS = ['make', 'create', 'forceCreate', 'findOrNew', 'firstOrNew', 'updateOrCreate', 'firstOrCreate', 'createOrFirst'];
/**
* The methods that should be returned from query builder.
*
* @var string[]
*/
public array $passthru = [
'average',
'avg',
'count',
'dd',
'dump',
'doesntExist',
'exists',
'getBindings',
'getConnection',
'getGrammar',
'insert',
'insertGetId',
'insertOrIgnore',
'insertUsing',
'max',
'min',
'raw',
'sum',
'toSql',
'toRawSql',
'dumpRawSql',
'ddRawSql',
];
public function __construct(
private ReflectionProvider $reflectionProvider,
private bool $checkProperties,
private MacroMethodsClassReflectionExtension $macroMethodsClassReflectionExtension,
) {
}
public function dynamicWhere(
string $methodName,
Type $returnObject,
): EloquentBuilderMethodReflection|null {
if (! Str::startsWith($methodName, 'where')) {
return null;
}
if (count($returnObject->getObjectClassReflections()) > 0 && $this->checkProperties) {
$returnClassReflection = $returnObject->getObjectClassReflections()[0];
$modelType = $returnClassReflection->getActiveTemplateTypeMap()->getType('TModelClass')
?? $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') {
continue;
}
$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,
[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.
*
* @throws MissingMethodFromReflectionException
* @throws ShouldNotHappenException
*/
public function searchOnEloquentBuilder(ClassReflection $eloquentBuilder, string $methodName, Type $modelType): MethodReflection|null
{
// Check for macros first
if ($this->macroMethodsClassReflectionExtension->hasMethod($eloquentBuilder, $methodName)) {
return $this->macroMethodsClassReflectionExtension->getMethod($eloquentBuilder, $methodName);
}
$scopeName = 'scope' . ucfirst($methodName);
foreach ($modelType->getObjectClassReflections() as $reflection) {
// Check for @method phpdoc tags
if (array_key_exists($scopeName, $reflection->getMethodTags())) {
$methodTag = $reflection->getMethodTags()[$scopeName];
$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(
$scopeName,
$reflection,
$parameters,
$methodTag->getReturnType(),
);
}
if ($reflection->hasNativeMethod($scopeName)) {
$methodReflection = $reflection->getNativeMethod($scopeName);
$parametersAcceptor = $methodReflection->getVariants()[0];
$parameters = $parametersAcceptor->getParameters();
// We shift the parameters,
// because first parameter is the Builder
array_shift($parameters);
$returnType = $parametersAcceptor->getReturnType();
return new EloquentBuilderMethodReflection(
$scopeName,
$methodReflection->getDeclaringClass(),
$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);
}
// Check for query builder macros
if ($this->macroMethodsClassReflectionExtension->hasMethod($queryBuilderReflection, $methodName)) {
return $this->macroMethodsClassReflectionExtension->getMethod($queryBuilderReflection, $methodName);
}
return $this->dynamicWhere($methodName, new GenericObjectType($eloquentBuilder->getName(), [$modelType]));
}
/**
* @throws MissingMethodFromReflectionException
* @throws ShouldNotHappenException
*/
public function determineBuilderName(string $modelClassName): string
{
$method = $this->reflectionProvider->getClass($modelClassName)->getNativeMethod('newEloquentBuilder');
$returnType = $method->getVariants()[0]->getReturnType();
if (in_array(EloquentBuilder::class, $returnType->getReferencedClasses(), true)) {
return EloquentBuilder::class;
}
$classNames = $returnType->getObjectClassNames();
if (count($classNames) === 1) {
return $classNames[0];
}
return $returnType->describe(VerbosityLevel::value());
}
}
@@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Methods;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\SoftDeletes;
use Larastan\Larastan\Internal\LaravelVersion;
use Larastan\Larastan\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\ReflectionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateObjectType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\ObjectType;
use function array_key_exists;
use function array_map;
use function array_merge;
use function array_values;
use function in_array;
final class EloquentBuilderForwardsCallsExtension implements MethodsClassReflectionExtension
{
/** @var array<string, MethodReflection> */
private array $cache = [];
public function __construct(private BuilderHelper $builderHelper, private 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) {
$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|null
{
if (! $classReflection->is(EloquentBuilder::class)) {
return null;
}
$loopReflection = $classReflection;
do {
$modelType = $loopReflection->getActiveTemplateTypeMap()->getType(LaravelVersion::getBuilderModelGenericName());
if ($modelType !== null) {
break;
}
$loopReflection = $loopReflection->getParentClass();
} while ($loopReflection !== null);
if ($modelType === null) {
return null;
}
if ($modelType instanceof TemplateObjectType) {
$modelType = $modelType->getBound();
}
$ref = $this->builderHelper->searchOnEloquentBuilder($classReflection, $methodName, $modelType);
if ($ref === null) {
// Special case for `SoftDeletes` trait
if (
in_array($methodName, ['withTrashed', 'onlyTrashed', 'withoutTrashed', 'restore', 'createOrRestore', 'restoreOrCreate'], true) &&
array_key_exists(SoftDeletes::class, array_merge(...array_map(static fn ($r) => $r->getTraits(true), $modelType->getObjectClassReflections())))
) {
$ref = $this->reflectionProvider->getClass(SoftDeletes::class)->getMethod($methodName, new OutOfClassScope());
if ($methodName === 'restore') {
return new EloquentBuilderMethodReflection(
$methodName,
$classReflection,
$ref->getVariants()[0]->getParameters(),
new IntegerType(),
$ref->getVariants()[0]->isVariadic(),
);
}
if ($methodName === 'restoreOrCreate' || $methodName === 'createOrRestore') {
return new EloquentBuilderMethodReflection(
$methodName,
$classReflection,
$ref->getVariants()[0]->getParameters(),
$modelType,
$ref->getVariants()[0]->isVariadic(),
);
}
return new EloquentBuilderMethodReflection(
$methodName,
$classReflection,
$ref->getVariants()[0]->getParameters(),
new GenericObjectType($classReflection->getName(), [$modelType]),
$ref->getVariants()[0]->isVariadic(),
);
}
return null;
}
// Macros have their own reflection. And return type, parameters, etc. are already set with the closure.
if ($ref instanceof Macro) {
return $ref;
}
$parametersAcceptor = $ref->getVariants()[0];
if (in_array($methodName, $this->builderHelper->passthru, true)) {
$returnType = $parametersAcceptor->getReturnType();
} elseif ($classReflection->isGeneric()) {
$returnType = new GenericObjectType($classReflection->getName(), array_values($classReflection->getTemplateTypeMap()->getTypes()));
} else {
$returnType = new ObjectType($classReflection->getName());
}
// Returning custom reflection
// to ensure return type is always `EloquentBuilder<Model>`
return new EloquentBuilderMethodReflection(
$methodName,
$classReflection,
$parametersAcceptor->getParameters(),
$returnType,
$parametersAcceptor->isVariadic(),
);
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\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;
use function array_key_exists;
/** @internal */
final class Extension implements MethodsClassReflectionExtension
{
private Kernel $kernel;
/** @var MethodReflection[] */
private array $methodReflections = [];
public function __construct(PhpMethodReflectionFactory $methodReflectionFactory, ReflectionProvider $reflectionProvider, Kernel|null $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()];
}
}
@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Methods;
use Illuminate\Database\Eloquent\Collection;
use Larastan\Larastan\Support\HigherOrderCollectionProxyHelper;
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\TrinaryLogic;
use PHPStan\Type;
use function count;
final class HigherOrderCollectionProxyExtension implements MethodsClassReflectionExtension
{
public function __construct(private HigherOrderCollectionProxyHelper $higherOrderCollectionProxyHelper)
{
}
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
return $this->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');
/** @var Type\Type $collectionType */
$collectionType = $activeTemplateTypeMap->getType('TCollection');
$collectionClassName = count($collectionType->getObjectClassNames()) === 0
? Collection::class
: $collectionType->getObjectClassNames()[0];
$modelMethodReflection = $valueType->getMethod($methodName, new OutOfClassScope());
$modelMethodReturnType = $modelMethodReflection->getVariants()[0]->getReturnType();
$returnType = $this->higherOrderCollectionProxyHelper->determineReturnType($methodType->getValue(), $valueType, $modelMethodReturnType, $collectionClassName);
return new class ($classReflection, $methodName, $modelMethodReflection, $returnType) implements MethodReflection
{
public function __construct(private ClassReflection $classReflection, private string $methodName, private MethodReflection $modelMethodReflection, private Type\Type $returnType)
{
}
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|null
{
return null;
}
public function getName(): string
{
return $this->methodName;
}
public function getPrototype(): ClassMemberReflection
{
return $this;
}
/** @return FunctionVariant[] */
public function getVariants(): array
{
return [
new FunctionVariant(
$this->modelMethodReflection->getVariants()[0]->getTemplateTypeMap(),
$this->modelMethodReflection->getVariants()[0]->getResolvedTemplateTypeMap(),
$this->modelMethodReflection->getVariants()[0]->getParameters(),
$this->modelMethodReflection->getVariants()[0]->isVariadic(),
$this->returnType,
),
];
}
public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getDeprecatedDescription(): string|null
{
return null;
}
public function isFinal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getThrowType(): \PHPStan\Type\Type|null
{
return null;
}
public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
};
}
}
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Methods;
use Illuminate\Support\HigherOrderTapProxy;
use PHPStan\Analyser\OutOfClassScope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\ShouldNotHappenException;
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 === null || $templateType->getObjectClassReflections() === []) {
return false;
}
return $templateType->hasMethod($methodName)->yes();
}
public function getMethod(
ClassReflection $classReflection,
string $methodName,
): MethodReflection {
$templateType = $classReflection->getActiveTemplateTypeMap()->getType('TClass');
if ($templateType === null || $templateType->getObjectClassReflections() === [] || ! $templateType->hasMethod($methodName)->yes()) {
throw new ShouldNotHappenException();
}
return $templateType->getMethod($methodName, new OutOfClassScope());
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Methods;
use Illuminate\Pipeline\Pipeline;
use Larastan\Larastan\Concerns;
use Larastan\Larastan\Contracts\Methods\PassableContract;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\Php\PhpMethodReflectionFactory;
use PHPStan\Reflection\ReflectionProvider;
/** @internal */
final class Kernel
{
use Concerns\HasContainer;
public function __construct(
private PhpMethodReflectionFactory $methodReflectionFactory,
private ReflectionProvider $reflectionProvider,
) {
}
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\Contracts::class,
Pipes\Facades::class,
Pipes\Managers::class,
Pipes\Auths::class,
],
)
->then(
static function ($method): void {
},
);
return $passable;
}
}
@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Methods;
use Illuminate\Validation\ValidationException;
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\ClosureType;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use function array_key_exists;
final class Macro implements MethodReflection
{
/**
* The is static.
*/
private bool $isStatic = false;
/**
* Map of macro methods and thrown exception classes.
*
* @var string[]
*/
private array $methodThrowTypeMap = [
'validate' => ValidationException::class,
'validateWithBag' => ValidationException::class,
];
public function __construct(private ClassReflection $classReflection, private string $methodName, private ClosureType $closureType)
{
}
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.
*/
public function setIsStatic(bool $isStatic): void
{
$this->isStatic = $isStatic;
}
public function getDocComment(): string|null
{
return null;
}
public function getName(): string
{
return $this->methodName;
}
public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getPrototype(): ClassMemberReflection
{
return $this;
}
/** @inheritDoc */
public function getVariants(): array
{
return [
new FunctionVariant(TemplateTypeMap::createEmpty(), null, $this->closureType->getParameters(), $this->closureType->isVariadic(), $this->closureType->getReturnType()),
];
}
public function getDeprecatedDescription(): string|null
{
return null;
}
public function getThrowType(): Type|null
{
if (array_key_exists($this->methodName, $this->methodThrowTypeMap)) {
return new ObjectType($this->methodThrowTypeMap[$this->methodName]);
}
return null;
}
public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
}
@@ -0,0 +1,204 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Methods;
use Carbon\Carbon;
use Carbon\Traits\Macro as CarbonMacro;
use Closure;
use Illuminate\Auth\RequestGuard;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use Larastan\Larastan\Concerns\HasContainer;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\Reflection\MissingMethodFromReflectionException;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\ClosureTypeFactory;
use ReflectionException;
use Throwable;
use function array_key_exists;
use function explode;
use function get_class;
use function is_array;
use function is_callable;
use function is_string;
use function str_contains;
class MacroMethodsClassReflectionExtension implements MethodsClassReflectionExtension
{
use HasContainer;
/** @var array<string, MethodReflection> */
private array $methods = [];
public function __construct(private ReflectionProvider $reflectionProvider, private ClosureTypeFactory $closureTypeFactory)
{
}
/**
* @throws ReflectionException
* @throws ShouldNotHappenException
* @throws MissingMethodFromReflectionException
*/
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
/** @var class-string[] $classNames */
$classNames = [];
$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 = $concrete::class;
if ($className && $this->reflectionProvider->getClass($className)->hasTraitUse(Macroable::class)) {
$classNames = [$className];
$macroTraitProperty = 'macros';
}
}
} elseif (
$this->hasIndirectTraitUse($classReflection, Macroable::class) ||
$classReflection->is(Builder::class) ||
$classReflection->is(QueryBuilder::class)
) {
$classNames = [$classReflection->getName()];
$macroTraitProperty = 'macros';
if ($classReflection->isSubclassOf(Builder::class)) {
$classNames[] = Builder::class;
}
} elseif ($classReflection->isSubclassOf(Facade::class)) {
$facadeClass = $classReflection->getName();
if ($facadeClass === Auth::class) {
$classNames = ['Illuminate\Auth\SessionGuard', RequestGuard::class];
$macroTraitProperty = 'macros';
} else {
$concrete = null;
try {
$concrete = $facadeClass::getFacadeRoot();
} catch (Throwable) {
}
if ($concrete) {
$facadeClassName = $concrete::class;
if ($facadeClassName) {
$classNames = [$facadeClassName];
$macroTraitProperty = 'macros';
}
}
}
}
if ($this->hasIndirectTraitUse($classReflection, CarbonMacro::class) && Carbon::hasMacro($methodName)) {
$methodReflection = new Macro(
$classReflection,
$methodName,
$this->closureTypeFactory->fromClosureObject(Closure::fromCallable(Carbon::getMacro($methodName))), // @phpstan-ignore-line hasMacro guarantees no null return
);
$this->methods[$classReflection->getName() . '-' . $methodName] = $methodReflection;
return true;
}
if ($classNames !== [] && $macroTraitProperty) {
foreach ($classNames as $className) {
$macroClassReflection = $this->reflectionProvider->getClass($className);
if (! $macroClassReflection->getNativeReflection()->hasProperty($macroTraitProperty)) {
continue;
}
$refProperty = $macroClassReflection->getNativeReflection()->getProperty($macroTraitProperty);
$refProperty->setAccessible(true);
$found = array_key_exists($methodName, $refProperty->getValue());
if (! $found) {
continue;
}
$macroDefinition = $refProperty->getValue()[$methodName];
if (is_string($macroDefinition)) {
if (str_contains($macroDefinition, '::')) {
$macroDefinition = explode('::', $macroDefinition, 2);
$macroClassName = $macroDefinition[0];
if (! $this->reflectionProvider->hasClass($macroClassName) || ! $this->reflectionProvider->getClass($macroClassName)->hasNativeMethod($macroDefinition[1])) {
throw new ShouldNotHappenException('Class ' . $macroClassName . ' does not exist');
}
$methodReflection = $this->reflectionProvider->getClass($macroClassName)->getNativeMethod($macroDefinition[1]);
} elseif (is_callable($macroDefinition)) {
$methodReflection = new Macro(
$macroClassReflection,
$methodName,
$this->closureTypeFactory->fromClosureObject(Closure::fromCallable($macroDefinition)),
);
} else {
throw new ShouldNotHappenException('Function ' . $macroDefinition . ' does not exist');
}
} elseif (is_array($macroDefinition)) {
if (is_string($macroDefinition[0])) {
$macroClassName = $macroDefinition[0];
} else {
$macroClassName = get_class($macroDefinition[0]);
}
if ($macroClassName === false || ! $this->reflectionProvider->hasClass($macroClassName) || ! $this->reflectionProvider->getClass($macroClassName)->hasNativeMethod($macroDefinition[1])) {
throw new ShouldNotHappenException('Class ' . $macroClassName . ' does not exist');
}
$methodReflection = $this->reflectionProvider->getClass($macroClassName)->getNativeMethod($macroDefinition[1]);
} else {
$methodReflection = new Macro(
$macroClassReflection,
$methodName,
$this->closureTypeFactory->fromClosureObject($refProperty->getValue()[$methodName]),
);
$methodReflection->setIsStatic(true);
}
$this->methods[$classReflection->getName() . '-' . $methodName] = $methodReflection;
break;
}
}
return $found;
}
public function getMethod(
ClassReflection $classReflection,
string $methodName,
): MethodReflection {
return $this->methods[$classReflection->getName() . '-' . $methodName];
}
private function hasIndirectTraitUse(ClassReflection $class, string $traitName): bool
{
foreach ($class->getTraits() as $trait) {
if ($this->hasIndirectTraitUse($trait, $traitName)) {
return true;
}
}
return $class->hasTraitUse($traitName);
}
}
@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Methods;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
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\ParametersAcceptor;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use function array_key_exists;
class ModelFactoryMethodsClassReflectionExtension implements MethodsClassReflectionExtension
{
public function __construct(
private ReflectionProvider $reflectionProvider,
) {
}
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
if (! $classReflection->isSubclassOf(Factory::class)) {
return false;
}
$modelType = $classReflection->getActiveTemplateTypeMap()->getType('TModel');
// Generic type is not specified
if ($modelType === null) {
if (! $classReflection->isGeneric() && $classReflection->getParentClass()?->isGeneric()) {
$modelType = $classReflection->getParentClass()->getActiveTemplateTypeMap()->getType('TModel');
}
}
if ($modelType === null) {
return false;
}
if ($modelType->getObjectClassReflections() !== []) {
$modelReflection = $modelType->getObjectClassReflections()[0];
} else {
$modelReflection = $this->reflectionProvider->getClass(Model::class);
}
if ($methodName === 'trashed' && array_key_exists(SoftDeletes::class, $modelReflection->getTraits(true))) {
return true;
}
if (! Str::startsWith($methodName, ['for', 'has'])) {
return false;
}
$relationship = Str::camel(Str::substr($methodName, 3));
return $modelType->hasMethod($relationship)->yes();
}
public function getMethod(
ClassReflection $classReflection,
string $methodName,
): MethodReflection {
return new class ($classReflection, $methodName) implements MethodReflection
{
public function __construct(private ClassReflection $classReflection, private string $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|null
{
return null;
}
public function getName(): string
{
return $this->methodName;
}
public function getPrototype(): ClassMemberReflection
{
return $this;
}
/** @return ParametersAcceptor[] */
public function getVariants(): array
{
$returnType = new ObjectType($this->classReflection->getName());
$stateParameter = $this->classReflection->getMethod('state', new OutOfClassScope())->getVariants()[0]->getParameters()[0];
$countParameter = $this->classReflection->getMethod('count', new OutOfClassScope())->getVariants()[0]->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|null
{
return null;
}
public function isFinal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getThrowType(): Type|null
{
return null;
}
public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
};
}
}
@@ -0,0 +1,236 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Methods;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Larastan\Larastan\Reflection\EloquentBuilderMethodReflection;
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\Reflection\MissingMethodFromReflectionException;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\Php\DummyParameter;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StaticType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\TypeWithClassName;
use function array_key_exists;
use function array_map;
use function in_array;
final class ModelForwardsCallsExtension implements MethodsClassReflectionExtension
{
/** @var array<string, MethodReflection> */
private array $cache = [];
public function __construct(private BuilderHelper $builderHelper, private ReflectionProvider $reflectionProvider, private 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;
}
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|null
{
if (! $classReflection->is(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
{
private ClassReflection $classReflection;
private string $methodName;
private MethodReflection $methodReflection;
public function __construct(ClassReflection $classReflection, string $methodName, MethodReflection $methodReflection)
{
$this->classReflection = $classReflection;
$this->methodName = $methodName;
$this->methodReflection = $methodReflection;
}
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|null
{
return null;
}
public function getName(): string
{
return $this->methodName;
}
public function getPrototype(): ClassMemberReflection
{
return $this;
}
/** @return ParametersAcceptor[] */
public function getVariants(): array
{
return $this->methodReflection->getVariants();
}
public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getDeprecatedDescription(): string|null
{
return null;
}
public function isFinal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getThrowType(): Type|null
{
return null;
}
public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createYes();
}
};
}
$builderReflection = $this->reflectionProvider->getClass($builderName)->withTypes([new ObjectType($classReflection->getName())]);
if ($builderReflection->isGeneric()) {
$genericBuilderAndModelType = new GenericObjectType($builderName, [new ObjectType($classReflection->getName())]);
} else {
$genericBuilderAndModelType = new ObjectType($builderName);
}
if ($builderReflection->hasNativeMethod($methodName)) {
$reflection = $builderReflection->getNativeMethod($methodName);
$parametersAcceptor = $this->transformStaticParameters($reflection, $genericBuilderAndModelType);
$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,
$builderReflection,
$parametersAcceptor->getParameters(),
$returnType,
$parametersAcceptor->isVariadic(),
);
}
if ($this->eloquentBuilderForwardsCallsExtension->hasMethod($builderReflection, $methodName)) {
return $this->eloquentBuilderForwardsCallsExtension->getMethod($builderReflection, $methodName);
}
return null;
}
private function transformStaticParameters(MethodReflection $method, ObjectType $builder): ParametersAcceptor
{
$acceptor = $method->getVariants()[0];
return new FunctionVariant($acceptor->getTemplateTypeMap(), $acceptor->getResolvedTemplateTypeMap(), array_map(function (
ParameterReflection $parameter,
) use ($builder): ParameterReflection {
return new DummyParameter(
$parameter->getName(),
$this->transformStaticType($parameter->getType(), $builder),
$parameter->isOptional(),
$parameter->passedByReference(),
$parameter->isVariadic(),
$parameter->getDefaultValue(),
);
}, $acceptor->getParameters()), $acceptor->isVariadic(), $this->transformStaticType($acceptor->getReturnType(), $builder));
}
private function transformStaticType(Type $type, ObjectType $builder): Type
{
return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($builder): Type {
if ($type instanceof StaticType) {
return $builder;
}
return $traverse($type);
});
}
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\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);
});
}
}
@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Methods;
use Illuminate\Contracts\Pipeline\Pipeline;
use Larastan\Larastan\Concerns;
use Larastan\Larastan\Contracts\Methods\PassableContract;
use Larastan\Larastan\Reflection\StaticMethodReflection;
use LogicException;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\Php\PhpMethodReflectionFactory;
use PHPStan\Reflection\ReflectionProvider;
/** @internal */
final class Passable implements PassableContract
{
use Concerns\HasContainer;
private MethodReflection|null $methodReflection = null;
private bool $staticAllowed = false;
public function __construct(
private PhpMethodReflectionFactory $methodReflectionFactory,
private ReflectionProvider $reflectionProvider,
private Pipeline $pipeline,
private ClassReflection $classReflection,
private string $methodName,
) {
}
public function getClassReflection(): ClassReflection
{
return $this->classReflection;
}
public function setClassReflection(ClassReflection $classReflection): PassableContract
{
$this->classReflection = $classReflection;
return $this;
}
public function getMethodName(): string
{
return $this->methodName;
}
public function hasFound(): bool
{
return $this->methodReflection !== null;
}
public function searchOn(string $class): bool
{
if (! $this->reflectionProvider->hasClass($class)) {
return false;
}
$classReflection = $this->reflectionProvider->getClass($class);
$found = $classReflection->hasNativeMethod($this->methodName);
if ($found) {
$this->setMethodReflection($classReflection->getNativeMethod($this->methodName));
}
return $found;
}
public function getMethodReflection(): MethodReflection
{
if ($this->methodReflection === null) {
throw new LogicException("MethodReflection doesn't exist");
}
return $this->methodReflection;
}
public function setMethodReflection(MethodReflection $methodReflection): void
{
$this->methodReflection = $methodReflection;
}
public function setStaticAllowed(bool $staticAllowed): void
{
$this->staticAllowed = $staticAllowed;
}
public function isStaticAllowed(): bool
{
return $this->staticAllowed;
}
public function sendToPipeline(string $class, bool $staticAllowed = false): bool
{
if (! $this->reflectionProvider->hasClass($class)) {
return false;
}
$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): void {
if ($passable->hasFound()) {
$this->setMethodReflection($passable->getMethodReflection());
$this->setStaticAllowed($passable->isStaticAllowed());
}
$this->setClassReflection($originalClassReflection);
},
);
$result = $this->hasFound();
if ($result) {
$this->setMethodReflection(new StaticMethodReflection($this->getMethodReflection()));
}
return $result;
}
public function getReflectionProvider(): ReflectionProvider
{
return $this->reflectionProvider;
}
public function getMethodReflectionFactory(): PhpMethodReflectionFactory
{
return $this->methodReflectionFactory;
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Methods\Pipes;
use Closure;
use Illuminate\Auth\AuthManager;
use Illuminate\Contracts\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\CanResetPassword;
use Illuminate\Contracts\Auth\Factory;
use Illuminate\Contracts\Auth\Guard;
use Larastan\Larastan\Concerns;
use Larastan\Larastan\Contracts\Methods\PassableContract;
use Larastan\Larastan\Contracts\Methods\Pipes\PipeContract;
use function in_array;
/** @internal */
final class Auths implements PipeContract
{
use Concerns\HasContainer;
use Concerns\LoadsAuthModel;
/** @var string[] */
private array $classes = [
Authenticatable::class,
CanResetPassword::class,
Authorizable::class,
];
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)) {
$authModels = $this->getAuthModels($config);
foreach ($authModels as $authModel) {
if ($passable->sendToPipeline($authModel)) {
$found = true;
break;
}
}
} elseif ($classReflectionName === Factory::class || $classReflectionName === AuthManager::class) {
$found = $passable->sendToPipeline(
Guard::class,
);
}
if ($found) {
return;
}
$next($passable);
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Methods\Pipes;
use Closure;
use Illuminate\Support\Str;
use Larastan\Larastan\Concerns;
use Larastan\Larastan\Contracts\Methods\PassableContract;
use Larastan\Larastan\Contracts\Methods\Pipes\PipeContract;
use PHPStan\Reflection\ClassReflection;
/** @internal */
final class Contracts implements PipeContract
{
use Concerns\HasContainer;
public function handle(PassableContract $passable, Closure $next): void
{
$found = false;
foreach ($this->concretes($passable->getClassReflection()) as $concrete) {
if ($passable->sendToPipeline($concrete)) {
$found = true;
break;
}
}
if ($found) {
return;
}
$next($passable);
}
/** @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 = $concrete::class;
if ($class) {
return [$class];
}
}
}
return [];
}
}
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Methods\Pipes;
use Closure;
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\Str;
use Larastan\Larastan\Contracts\Methods\PassableContract;
use Larastan\Larastan\Contracts\Methods\Pipes\PipeContract;
use Larastan\Larastan\Reflection\ReflectionHelper;
use Throwable;
use function assert;
use function class_exists;
use function sprintf;
use function strrpos;
use function substr;
/** @internal */
final class Facades implements PipeContract
{
public function handle(PassableContract $passable, Closure $next): void
{
$classReflection = $passable->getClassReflection();
$found = false;
if ($classReflection->isSubclassOf(Facade::class)) {
$facadeClass = $classReflection->getName();
if (ReflectionHelper::hasMethodTag($classReflection, $passable->getMethodName())) {
$next($passable);
return;
}
$concrete = null;
try {
$concrete = $facadeClass::getFacadeRoot();
} catch (Throwable) {
}
if ($concrete) {
$class = $concrete::class;
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) {
return;
}
$next($passable);
}
private function getFake(string $facade): string
{
$shortClassName = substr($facade, strrpos($facade, '\\') + 1);
return sprintf('\\Illuminate\\Support\\Testing\\Fakes\\%sFake', $shortClassName);
}
}
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Methods\Pipes;
use Closure;
use Illuminate\Support\Manager;
use InvalidArgumentException;
use Larastan\Larastan\Concerns;
use Larastan\Larastan\Contracts\Methods\PassableContract;
use Larastan\Larastan\Contracts\Methods\Pipes\PipeContract;
/** @internal */
final class Managers implements PipeContract
{
use Concerns\HasContainer;
public function handle(PassableContract $passable, Closure $next): void
{
$classReflection = $passable->getClassReflection();
$found = false;
if ($classReflection->isSubclassOf(Manager::class) && ! $classReflection->isAbstract()) {
$driver = null;
$concrete = $this->resolve(
$classReflection->getName(),
);
try {
$driver = $concrete->driver();
} catch (InvalidArgumentException) {
// ..
}
if ($driver !== null) {
$class = $driver::class;
if ($class) {
$found = $passable->sendToPipeline($class);
}
}
}
if ($found) {
return;
}
$next($passable);
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Methods\Pipes;
use Closure;
use Larastan\Larastan\Contracts\Methods\PassableContract;
use Larastan\Larastan\Contracts\Methods\Pipes\PipeContract;
/** @internal */
final class SelfClass implements PipeContract
{
public function handle(PassableContract $passable, Closure $next): void
{
$className = $passable->getClassReflection()
->getName();
if ($passable->searchOn($className)) {
return;
}
$next($passable);
}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Methods;
use Larastan\Larastan\Reflection\DynamicWhereMethodReflection;
use PHPStan\Reflection;
use function str_starts_with;
class RedirectResponseMethodsClassReflectionExtension implements Reflection\MethodsClassReflectionExtension
{
public function hasMethod(Reflection\ClassReflection $classReflection, string $methodName): bool
{
if ($classReflection->getName() !== 'Illuminate\Http\RedirectResponse') {
return false;
}
// @phpcs:ignore
if (! str_starts_with($methodName, 'with')) {
return false;
}
return true;
}
public function getMethod(
Reflection\ClassReflection $classReflection,
string $methodName,
): Reflection\MethodReflection {
return new DynamicWhereMethodReflection($classReflection, $methodName);
}
}
@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Methods;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Larastan\Larastan\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\ReflectionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ThisType;
use function array_key_exists;
final class RelationForwardsCallsExtension implements MethodsClassReflectionExtension
{
/** @var array<string, MethodReflection> */
private array $cache = [];
public function __construct(private BuilderHelper $builderHelper, private ReflectionProvider $reflectionProvider)
{
}
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|null
{
if (! $classReflection->is(Relation::class)) {
return null;
}
$relatedModel = $classReflection->getActiveTemplateTypeMap()->getType('TRelatedModel');
if ($relatedModel === null) {
return null;
}
if ($relatedModel->getObjectClassReflections() !== []) {
$modelReflection = $relatedModel->getObjectClassReflections()[0];
} else {
$modelReflection = $this->reflectionProvider->getClass(Model::class);
}
if (! $modelReflection->is(Model::class)) {
return null;
}
$builderName = $this->builderHelper->determineBuilderName($modelReflection->getName());
$builderType = new GenericObjectType($builderName, [new ObjectType($modelReflection->getName())]);
if (! $builderType->hasMethod($methodName)->yes()) {
return null;
}
$reflection = $builderType->getMethod($methodName, new OutOfClassScope());
$parametersAcceptor = $reflection->getVariants()[0];
$returnType = $parametersAcceptor->getReturnType();
if ((new ObjectType(Builder::class))->isSuperTypeOf($returnType)->yes()) {
$returnType = new ThisType($classReflection);
}
return new EloquentBuilderMethodReflection(
$methodName,
$reflection->getDeclaringClass(),
$parametersAcceptor->getParameters(),
$returnType,
$parametersAcceptor->isVariadic(),
);
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Methods;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Filesystem\FilesystemManager;
use Illuminate\Support\Facades\Storage;
use Larastan\Larastan\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
{
public function __construct(private 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;
}
// @phpcs:ignore
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()),
);
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Methods;
use Larastan\Larastan\Reflection\DynamicWhereMethodReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use function in_array;
use function str_starts_with;
class ViewWithMethodsClassReflectionExtension implements MethodsClassReflectionExtension
{
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
if (! in_array($classReflection->getName(), ['Illuminate\View\View', 'Illuminate\Contracts\View\View'], true)) {
return false;
}
// @phpcs:ignore
if (! str_starts_with($methodName, 'with')) {
return false;
}
return true;
}
public function getMethod(
ClassReflection $classReflection,
string $methodName,
): MethodReflection {
return new DynamicWhereMethodReflection($classReflection, $methodName);
}
}
@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Properties;
use Illuminate\Database\Eloquent\Collection;
use Larastan\Larastan\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 __construct(private HigherOrderCollectionProxyHelper $higherOrderCollectionProxyHelper)
{
}
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
{
return $this->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');
/** @var Type\Type $collectionType */
$collectionType = $activeTemplateTypeMap->getType('TCollection');
$propertyType = $modelType->getProperty($propertyName, new OutOfClassScope())->getReadableType();
if ($collectionType->getObjectClassNames() !== []) {
$collectionClassName = $collectionType->getObjectClassNames()[0];
} else {
$collectionClassName = Collection::class;
}
$returnType = $this->higherOrderCollectionProxyHelper->determineReturnType($methodType->getValue(), $modelType, $propertyType, $collectionClassName);
return new class ($classReflection, $returnType) implements PropertyReflection
{
public function __construct(private ClassReflection $classReflection, private Type\Type $returnType)
{
}
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|null
{
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(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getDeprecatedDescription(): string|null
{
return null;
}
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
};
}
}
@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Properties;
use PHPStan\File\FileHelper;
use PHPStan\Parser\Parser;
use PHPStan\Parser\ParserErrorsException;
use PHPStan\Reflection\ReflectionProvider;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RegexIterator;
use SplFileInfo;
use function count;
use function database_path;
use function is_dir;
use function iterator_to_array;
use function uasort;
class MigrationHelper
{
public function __construct(
private Parser $parser,
/** @var string[] */
private array $databaseMigrationPath,
private FileHelper $fileHelper,
private bool $disableMigrationScan,
private ReflectionProvider $reflectionProvider,
) {
}
/**
* @param array<string, SchemaTable> $tables
*
* @return array<string, SchemaTable>
*/
public function initializeTables(array $tables = []): array
{
if ($this->disableMigrationScan) {
return $tables;
}
if (count($this->databaseMigrationPath) === 0) {
$this->databaseMigrationPath = [database_path('migrations')];
}
$schemaAggregator = new SchemaAggregator($this->reflectionProvider, $tables);
$filesArray = $this->getMigrationFiles();
if (empty($filesArray)) {
return $tables;
}
uasort($filesArray, static function (SplFileInfo $a, SplFileInfo $b) {
return $a->getFilename() <=> $b->getFilename();
});
foreach ($filesArray as $file) {
try {
$schemaAggregator->addStatements($this->parser->parseFile($file->getPathname()));
} catch (ParserErrorsException) {
continue;
}
}
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)) {
continue;
}
$migrationFiles += iterator_to_array(
new RegexIterator(
new RecursiveIteratorIterator(new RecursiveDirectoryIterator($absolutePath)),
'/\.php$/i',
),
);
}
return $migrationFiles;
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Properties;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\PropertiesClassReflectionExtension;
use PHPStan\Reflection\PropertyReflection;
/** @internal */
final class ModelAccessorExtension implements PropertiesClassReflectionExtension
{
public function __construct(
private ModelPropertyHelper $modelPropertyHelper,
) {
}
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
{
return $this->modelPropertyHelper->hasAccessor($classReflection, $propertyName, strictGenerics: true);
}
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
{
return $this->modelPropertyHelper->getAccessor($classReflection, $propertyName);
}
}
@@ -0,0 +1,269 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Properties;
use Carbon\Carbon;
use Carbon\CarbonImmutable;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
use Illuminate\Database\Eloquent\Casts\ArrayObject;
use Illuminate\Database\Eloquent\Casts\AsArrayObject;
use Illuminate\Database\Eloquent\Casts\AsCollection;
use Illuminate\Database\Eloquent\Casts\AsEncryptedArrayObject;
use Illuminate\Database\Eloquent\Casts\AsEncryptedCollection;
use Illuminate\Database\Eloquent\Casts\AsStringable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon as IlluminateCarbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Stringable as IlluminateStringable;
use PHPStan\Analyser\OutOfClassScope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MissingMethodFromReflectionException;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Accessory\AccessoryNumericStringType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BenevolentUnionType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\FloatType;
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 ReflectionException;
use stdClass;
use Stringable;
use function array_combine;
use function array_key_exists;
use function array_map;
use function array_merge;
use function class_exists;
use function explode;
use function str_replace;
use function version_compare;
class ModelCastHelper
{
/** @var array<string, array<string, string>> */
private array $modelCasts = [];
public function __construct(
protected ReflectionProvider $reflectionProvider,
) {
}
public function getReadableType(string $cast, Type $originalType): Type
{
$cast = $this->parseCast($cast);
$attributeType = match ($cast) {
'int', 'integer', 'timestamp' => new IntegerType(),
'real', 'float', 'double' => new FloatType(),
'decimal' => TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType()),
'string' => new StringType(),
'bool', 'boolean' => new BooleanType(),
'object' => new ObjectType(stdClass::class),
'array', 'json' => new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType()),
'collection' => new ObjectType(Collection::class),
'date', 'datetime' => $this->getDateType(),
'immutable_date', 'immutable_datetime' => new ObjectType(CarbonImmutable::class),
AsArrayObject::class, AsEncryptedArrayObject::class => new ObjectType(ArrayObject::class),
AsCollection::class, AsEncryptedCollection::class => new GenericObjectType(Collection::class, [new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType()]),
AsStringable::class => new ObjectType(IlluminateStringable::class),
default => null,
};
if ($attributeType) {
return $attributeType;
}
if (! $this->reflectionProvider->hasClass($cast)) {
return $originalType;
}
$classReflection = $this->reflectionProvider->getClass($cast);
if ($classReflection->isEnum()) {
return new ObjectType($cast);
}
if ($classReflection->isSubclassOf(Castable::class)) {
$methodReflection = $classReflection->getNativeMethod('castUsing');
$castUsingReturn = $methodReflection->getVariants()[0]->getReturnType();
if ($castUsingReturn->getObjectClassReflections() !== []) {
$classReflection = $castUsingReturn->getObjectClassReflections()[0];
}
}
if ($classReflection->isSubclassOf(CastsAttributes::class)) {
$methodReflection = $classReflection->getNativeMethod('get');
return $methodReflection->getVariants()[0]->getReturnType();
}
if ($classReflection->isSubclassOf(CastsInboundAttributes::class)) {
return $originalType;
}
return new MixedType();
}
public function getWriteableType(string $cast, Type $originalType): Type
{
$cast = $this->parseCast($cast);
$attributeType = match ($cast) {
'int', 'integer', 'timestamp' => new IntegerType(),
'real', 'float', 'double' => new FloatType(),
'decimal' => TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType(), new FloatType()),
'string' => new StringType(),
'bool', 'boolean' => TypeCombinator::union(new BooleanType(), new ConstantIntegerType(0), new ConstantIntegerType(1)),
'object' => new ObjectType(stdClass::class),
'array', 'json' => new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType()),
'collection' => new ObjectType(Collection::class),
'date', 'datetime' => $this->getDateType(),
'immutable_date', 'immutable_datetime' => new ObjectType(CarbonImmutable::class),
AsArrayObject::class, AsCollection::class,
AsEncryptedArrayObject::class, AsEncryptedCollection::class => new MixedType(),
AsStringable::class => TypeCombinator::union(new StringType(), new ObjectType(Stringable::class)),
default => null,
};
if ($attributeType) {
return $attributeType;
}
if (! $this->reflectionProvider->hasClass($cast)) {
return $originalType;
}
$classReflection = $this->reflectionProvider->getClass($cast);
if ($classReflection->isEnum()) {
return new ObjectType($cast);
}
if ($classReflection->isSubclassOf(Castable::class)) {
$methodReflection = $classReflection->getNativeMethod('castUsing');
$castUsingReturn = $methodReflection->getVariants()[0]->getReturnType();
if ($castUsingReturn->getObjectClassReflections() !== []) {
$classReflection = $castUsingReturn->getObjectClassReflections()[0];
}
}
if (
$classReflection->isSubclassOf(CastsAttributes::class)
|| $classReflection->isSubclassOf(CastsInboundAttributes::class)
) {
$methodReflection = $classReflection->getNativeMethod('set');
$parameters = $methodReflection->getVariants()[0]->getParameters();
$valueParameter = Arr::first($parameters, static fn (ParameterReflection $parameterReflection) => $parameterReflection->getName() === 'value');
if ($valueParameter) {
return $valueParameter->getType();
}
}
return new MixedType();
}
public function getDateType(): Type
{
$dateClass = class_exists(Date::class)
? Date::now()::class
: IlluminateCarbon::class;
if ($dateClass === IlluminateCarbon::class) {
return TypeCombinator::union(new ObjectType($dateClass), new ObjectType(Carbon::class));
}
return new ObjectType($dateClass);
}
private function parseCast(string $cast): string
{
foreach (explode(':', $cast) as $part) {
// If the cast is prefixed with `encrypted:` we need to skip to the next
if ($part === 'encrypted') {
continue;
}
return $part;
}
return $cast;
}
public function hasCastForProperty(ClassReflection $modelClassReflection, string $propertyName): bool
{
if (! array_key_exists($modelClassReflection->getName(), $this->modelCasts)) {
$modelCasts = $this->getModelCasts($modelClassReflection);
} else {
$modelCasts = $this->modelCasts[$modelClassReflection->getName()];
}
return array_key_exists($propertyName, $modelCasts);
}
public function getCastForProperty(ClassReflection $modelClassReflection, string $propertyName): string|null
{
if (! array_key_exists($modelClassReflection->getName(), $this->modelCasts)) {
$modelCasts = $this->getModelCasts($modelClassReflection);
} else {
$modelCasts = $this->modelCasts[$modelClassReflection->getName()];
}
return $modelCasts[$propertyName] ?? null;
}
/**
* @return array<string, string>
*
* @throws ShouldNotHappenException
* @throws MissingMethodFromReflectionException
*/
private function getModelCasts(ClassReflection $modelClassReflection): array
{
try {
/** @var Model $modelInstance */
$modelInstance = $modelClassReflection->getNativeReflection()->newInstanceWithoutConstructor();
} catch (ReflectionException) {
throw new ShouldNotHappenException();
}
$modelCasts = $modelInstance->getCasts();
if (version_compare(LARAVEL_VERSION, '11.0.0', '>=')) {
$castsMethodReturnType = $modelClassReflection->getMethod(
'casts',
new OutOfClassScope(),
)->getVariants()[0]->getReturnType();
if ($castsMethodReturnType->isConstantArray()->yes()) {
$modelCasts = array_merge(
$modelCasts,
array_combine(
array_map(static fn ($key) => $key->getValue(), $castsMethodReturnType->getKeyTypes()), // @phpstan-ignore-line
array_map(static fn ($value) => str_replace('\\\\', '\\', $value->getValue()), $castsMethodReturnType->getValueTypes()), // @phpstan-ignore-line
),
);
}
}
return $modelCasts;
}
}
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Properties;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\PropertyReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Type;
class ModelProperty implements PropertyReflection
{
public function __construct(private ClassReflection $declaringClass, private Type $readableType, private Type $writableType, private bool $writeable = true)
{
}
public function getDeclaringClass(): ClassReflection
{
return $this->declaringClass;
}
public function isStatic(): bool
{
return false;
}
public function isPrivate(): bool
{
return false;
}
public function isPublic(): bool
{
return true;
}
public function isReadable(): bool
{
return true;
}
public function isWritable(): bool
{
return $this->writeable;
}
public function getDocComment(): string|null
{
return null;
}
public function getReadableType(): Type
{
return $this->readableType;
}
public function getWritableType(): Type
{
return $this->writableType;
}
public function canChangeTypeAfterAssignment(): bool
{
return false;
}
public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getDeprecatedDescription(): string|null
{
return null;
}
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Properties;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\PropertiesClassReflectionExtension;
use PHPStan\Reflection\PropertyReflection;
/** @internal */
final class ModelPropertyExtension implements PropertiesClassReflectionExtension
{
public function __construct(
private ModelPropertyHelper $modelPropertyHelper,
) {
}
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
{
if ($this->modelPropertyHelper->hasAccessor($classReflection, $propertyName, strictGenerics: false)) {
return false;
}
return $this->modelPropertyHelper->hasDatabaseProperty($classReflection, $propertyName);
}
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
{
return $this->modelPropertyHelper->getDatabaseProperty($classReflection, $propertyName);
}
}
@@ -0,0 +1,243 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Properties;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Larastan\Larastan\Reflection\ReflectionHelper;
use PHPStan\PhpDoc\TypeStringResolver;
use PHPStan\Reflection\ClassReflection;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType;
use PHPStan\Type\TypeCombinator;
use ReflectionException;
use function array_key_exists;
use function array_map;
use function count;
use function in_array;
use function is_string;
use function method_exists;
class ModelPropertyHelper
{
/** @var array<string, SchemaTable> */
private array $tables = [];
public function __construct(
private TypeStringResolver $stringResolver,
private MigrationHelper $migrationHelper,
private SquashedMigrationHelper $squashedMigrationHelper,
private ModelCastHelper $modelCastHelper,
) {
}
/**
* Determine if the model has a database property.
*/
public function hasDatabaseProperty(ClassReflection|string $classReflectionOrTable, string $propertyName): bool
{
if (! $this->migrationsLoaded()) {
$this->loadMigrations();
}
if (is_string($classReflectionOrTable)) {
if (! array_key_exists($classReflectionOrTable, $this->tables)) {
return false;
}
return array_key_exists($propertyName, $this->tables[$classReflectionOrTable]->columns);
}
if (! $classReflectionOrTable->isSubclassOf(Model::class)) {
return false;
}
if ($classReflectionOrTable->isAbstract()) {
return false;
}
if (ReflectionHelper::hasPropertyTag($classReflectionOrTable, $propertyName)) {
return false;
}
try {
/** @var Model $modelInstance */
$modelInstance = $classReflectionOrTable->getNativeReflection()->newInstanceWithoutConstructor();
} catch (ReflectionException) {
return false;
}
if ($propertyName === $modelInstance->getKeyName()) {
return true;
}
$tableName = $modelInstance->getTable();
if (! array_key_exists($tableName, $this->tables)) {
return false;
}
return array_key_exists($propertyName, $this->tables[$tableName]->columns);
}
public function getDatabaseProperty(ClassReflection $classReflection, string $propertyName): ModelProperty
{
try {
/** @var Model $modelInstance */
$modelInstance = $classReflection->getNativeReflection()->newInstanceWithoutConstructor();
} catch (ReflectionException) {
throw new ShouldNotHappenException();
}
$tableName = $modelInstance->getTable();
if (
$propertyName === $modelInstance->getKeyName()
&& (! array_key_exists($tableName, $this->tables) || ! array_key_exists($propertyName, $this->tables[$tableName]->columns))
) {
return new ModelProperty(
$classReflection,
$this->stringResolver->resolve($modelInstance->getKeyType()),
$this->stringResolver->resolve($modelInstance->getKeyType()),
);
}
$column = $this->tables[$tableName]->columns[$propertyName];
if ($this->hasDate($modelInstance, $propertyName)) {
$readableType = $this->modelCastHelper->getDateType();
$writableType = TypeCombinator::union($this->modelCastHelper->getDateType(), new StringType());
} else {
$cast = $this->modelCastHelper->getCastForProperty($classReflection, $propertyName);
if ($cast !== null) {
$readableType = $this->modelCastHelper->getReadableType(
$cast,
$this->stringResolver->resolve($column->readableType),
);
$writableType = $this->modelCastHelper->getWriteableType(
$cast,
$this->stringResolver->resolve($column->writeableType),
);
} elseif (in_array($column->readableType, ['enum', 'set'], true)) {
if ($column->options === null || count($column->options) < 1) {
$readableType = $writableType = new StringType();
} else {
$readableType = $writableType = TypeCombinator::union(...array_map(
static fn ($option) => new ConstantStringType($option),
$column->options,
));
}
} else {
$readableType = $this->stringResolver->resolve($column->readableType);
$writableType = $this->stringResolver->resolve($column->writeableType);
}
}
if ($column->nullable) {
$readableType = TypeCombinator::addNull($readableType);
$writableType = TypeCombinator::addNull($writableType);
}
return new ModelProperty(
$classReflection,
$readableType,
$writableType,
);
}
/**
* Determine if the model has a property accessor.
*
* @param bool $strictGenerics Requires the Attribute generics to be defined.
*/
public function hasAccessor(ClassReflection $classReflection, string $propertyName, bool $strictGenerics = true): bool
{
if (! $classReflection->isSubclassOf(Model::class)) {
return false;
}
$camelCase = Str::camel($propertyName);
if (! $classReflection->hasNativeMethod($camelCase)) {
return $classReflection->hasNativeMethod('get' . Str::studly($propertyName) . 'Attribute');
}
$methodReflection = $classReflection->getNativeMethod($camelCase);
if ($methodReflection->isPublic() || $methodReflection->isPrivate()) {
return false;
}
$returnType = $methodReflection->getVariants()[0]->getReturnType();
if (! $strictGenerics) {
return (new ObjectType(Attribute::class))->isSuperTypeOf($returnType)->yes();
}
if ($returnType->getObjectClassReflections() === [] || ! $returnType->getObjectClassReflections()[0]->isGeneric()) {
return false;
}
return (new GenericObjectType(Attribute::class, [new MixedType(), new MixedType()]))->isSuperTypeOf($returnType)->yes();
}
public function getAccessor(ClassReflection $classReflection, string $propertyName): ModelProperty
{
$studlyName = Str::studly($propertyName);
if ($classReflection->hasNativeMethod($studlyName)) {
$methodReflection = $classReflection->getNativeMethod($studlyName);
$returnType = $methodReflection->getVariants()[0]->getReturnType();
return new ModelProperty(
$classReflection,
$returnType->getTemplateType(Attribute::class, 'TGet'),
$returnType->getTemplateType(Attribute::class, 'TSet'),
);
}
$method = $classReflection->getNativeMethod('get' . Str::studly($propertyName) . 'Attribute');
return new ModelProperty(
$classReflection,
$method->getVariants()[0]->getReturnType(),
$method->getVariants()[0]->getReturnType(),
);
}
private function migrationsLoaded(): bool
{
return count($this->tables) > 0;
}
private function loadMigrations(): void
{
// First try to create tables from squashed migrations, if there are any
// Then scan the normal migration files for further changes to tables.
$tables = $this->squashedMigrationHelper->initializeTables();
$this->tables = $this->migrationHelper->initializeTables($tables);
}
private function hasDate(Model $modelInstance, string $propertyName): bool
{
$dates = $modelInstance->getDates();
// In order to support SoftDeletes
if (method_exists($modelInstance, 'getDeletedAtColumn')) {
$dates[] = $modelInstance->getDeletedAtColumn();
}
return in_array($propertyName, $dates, true);
}
}
@@ -0,0 +1,208 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Properties;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Str;
use Larastan\Larastan\Concerns;
use Larastan\Larastan\Reflection\ReflectionHelper;
use Larastan\Larastan\Support\CollectionHelper;
use Larastan\Larastan\Types\RelationParserHelper;
use PHPStan\Analyser\OutOfClassScope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ExtendedMethodReflection;
use PHPStan\Reflection\PropertiesClassReflectionExtension;
use PHPStan\Reflection\PropertyReflection;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\NeverType;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\UnionType;
use function str_ends_with;
use function version_compare;
/** @internal */
final class ModelRelationsExtension implements PropertiesClassReflectionExtension
{
use Concerns\HasContainer;
public function __construct(
private RelationParserHelper $relationParserHelper,
private CollectionHelper $collectionHelper,
) {
}
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
{
if (! $classReflection->isSubclassOf(Model::class)) {
return false;
}
if (ReflectionHelper::hasPropertyTag($classReflection, $propertyName)) {
return false;
}
if (str_ends_with($propertyName, '_count')) {
$propertyName = Str::before($propertyName, '_count');
$methodNames = [Str::camel($propertyName), $propertyName];
} else {
$methodNames = [$propertyName];
}
foreach ($methodNames as $methodName) {
if (! $classReflection->hasNativeMethod($methodName)) {
continue;
}
$returnType = $classReflection->getNativeMethod($methodName)->getVariants()[0]->getReturnType();
if ((new ObjectType(Relation::class))->isSuperTypeOf($returnType)->yes()) {
return true;
}
}
return false;
}
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
{
if (str_ends_with($propertyName, '_count')) {
return new ModelProperty($classReflection, IntegerRangeType::createAllGreaterThanOrEqualTo(0), new NeverType(), false);
}
$methodReflection = $classReflection->getMethod($propertyName, new OutOfClassScope());
if (version_compare(LARAVEL_VERSION, '11.0.0', '<')) {
$relationType = $this->getRelationTypeBeforeL11($methodReflection);
} else {
$returnType = $methodReflection->getVariants()[0]->getReturnType();
$relationType = TypeTraverser::map($returnType, function (Type $type, callable $traverse) use ($methodReflection): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}
if (! (new ObjectType(Relation::class))->isSuperTypeOf($type)->yes()) {
return $type;
}
$related = $type->getTemplateType(Relation::class, 'TRelatedModel');
// generics not provided, need to new up a Relation with the correct
// TRelatedModel type so the TResult will populate correctly
if ($related->getObjectClassNames()[0] === Model::class) {
$modelName = $this->relationParserHelper->findModelsInRelationMethod($methodReflection)[0] ?? Model::class;
$type = new GenericObjectType($type->getObjectClassNames()[0], [new ObjectType($modelName)]);
}
/** @phpstan-ignore phpstanApi.getTemplateType (non-existent template on < L11) */
return $type->getTemplateType(Relation::class, 'TResult');
});
$relationType = $this->collectionHelper->replaceCollectionsInType($relationType);
}
return new ModelProperty($classReflection, $relationType, new NeverType(), false);
}
private function getRelationTypeBeforeL11(ExtendedMethodReflection $method): Type
{
$returnType = $method->getVariants()[0]->getReturnType();
/** @phpstan-ignore phpstanApi.instanceofType (deprecated, special shortcut we take) */
if ($returnType instanceof GenericObjectType) {
$relatedModel = $returnType->getTypes()[0];
if ($relatedModel->getObjectClassNames() === []) {
$relatedModelClassNames = [Model::class];
} else {
$relatedModelClassNames = $relatedModel->getObjectClassNames();
}
} else {
$modelName = $this->relationParserHelper->findModelsInRelationMethod($method)[0] ?? Model::class;
$relatedModel = new ObjectType($modelName);
$relatedModelClassNames = [$modelName];
}
return TypeTraverser::map($returnType, function (Type $type, callable $traverse) use ($relatedModelClassNames, $relatedModel) {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}
if ($type->getObjectClassNames() === []) {
return $traverse($type);
}
if ($type instanceof GenericObjectType) {
$relatedModel = $type->getTypes()[0];
$relatedModelClassNames = $relatedModel->getObjectClassNames();
}
if (
(new ObjectType(BelongsToMany::class))->isSuperTypeOf($type)->yes()
|| (new ObjectType(HasMany::class))->isSuperTypeOf($type)->yes()
|| (
(new ObjectType(HasManyThrough::class))->isSuperTypeOf($type)->yes()
// HasOneThrough extends HasManyThrough
&& ! (new ObjectType(HasOneThrough::class))->isSuperTypeOf($type)->yes()
)
|| (new ObjectType(MorphMany::class))->isSuperTypeOf($type)->yes()
|| (new ObjectType(MorphToMany::class))->isSuperTypeOf($type)->yes()
|| Str::contains($type->getObjectClassNames()[0], 'Many') // fallback
) {
$types = [];
foreach ($relatedModelClassNames as $relatedModelClassName) {
$types[] = $this->collectionHelper->determineCollectionClass($relatedModelClassName);
}
if ($types !== []) {
return TypeCombinator::union(...$types);
}
}
if (
(new ObjectType(MorphTo::class))->isSuperTypeOf($type)->yes()
|| Str::endsWith($type->getObjectClassNames()[0], 'MorphTo') // fallback
) {
// There was no generic type, or it was just Model
if ($relatedModel->getObjectClassNames()[0] === Model::class) {
return TypeCombinator::addNull($relatedModel);
}
$types = [];
foreach ($relatedModelClassNames as $relatedModelClassName) {
$types[] = new ObjectType($relatedModelClassName);
}
if ($types !== []) {
return TypeCombinator::addNull(TypeCombinator::union(...$types));
}
}
return new UnionType([
$relatedModel,
new NullType(),
]);
});
}
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Properties;
use ReflectionNamedType;
/** @internal */
final class ReflectionTypeContainer extends ReflectionNamedType
{
public function __construct(private string $type)
{
}
public function allowsNull(): bool
{
return false;
}
public function isBuiltin(): bool
{
return false;
}
public function __toString(): string
{
return $this->getName();
}
public function getName(): string
{
return $this->type;
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Properties\Schema;
use PhpMyAdmin\SqlParser\Components\DataType;
final class PhpMyAdminDataTypeToPhpTypeConverter
{
public function convert(DataType $dataType): string
{
return match ($dataType->name) {
'CHAR', 'VARCHAR', 'TINYTEXT', 'TEXT', 'MEDIUMTEXT', 'LONGTEXT', 'BINARY', 'VARBINARY', 'DATE', 'DATETIME', 'TIMESTAMP', 'TIME', 'TINYBLOB', 'BLOB', 'MEDIUMBLOB', 'JSON' => 'string',
'BIT', 'TINYINT', 'SMALLINT', 'MEDIUMINT', 'INT', 'INTEGER', 'BIGINT', 'YEAR' => 'int',
'DECIMAL', 'DEC', 'NUMERIC', 'FIXED', 'FLOAT', 'DOUBLE', 'DOUBLE PRECISION', 'REAL' => 'float',
'BOOL', 'BOOLEAN' => 'bool',
default => 'mixed',
};
}
}
@@ -0,0 +1,666 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Properties;
use Exception;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use PhpParser;
use PhpParser\NodeFinder;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\ObjectType;
use ReflectionException;
use function array_key_exists;
use function array_merge;
use function class_basename;
use function count;
use function is_string;
use function property_exists;
use function strtolower;
/** @see https://github.com/psalm/laravel-psalm-plugin/blob/master/src/SchemaAggregator.php */
final class SchemaAggregator
{
/** @param array<string, SchemaTable> $tables */
public function __construct(private ReflectionProvider $reflectionProvider, public array $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
) {
continue;
}
$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\MethodCall
&& $stmt->expr->var instanceof PhpParser\Node\Expr\StaticCall
&& $stmt->expr->var->class instanceof PhpParser\Node\Name
&& $stmt->expr->var->name instanceof PhpParser\Node\Identifier
&& ($stmt->expr->var->name->toString() === 'connection' || $stmt->expr->var->name->toString() === 'setConnection')
&& ($stmt->expr->var->class->toCodeString() === '\Schema' || (new ObjectType('Illuminate\Support\Facades\Schema'))->isSuperTypeOf(new ObjectType($stmt->expr->var->class->toCodeString()))->yes())
) {
$statement = $stmt->expr;
} elseif (
$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() === '\Schema' || (new ObjectType('Illuminate\Support\Facades\Schema'))->isSuperTypeOf(new ObjectType($stmt->expr->class->toCodeString()))->yes())
) {
$statement = $stmt->expr;
} else {
continue;
}
if (! $statement->name instanceof PhpParser\Node\Identifier) {
continue;
}
switch ($statement->name->name) {
case 'create':
$this->alterTable($statement, true);
break;
case 'table':
$this->alterTable($statement, false);
break;
case 'drop':
case 'dropIfExists':
$this->dropTable($statement);
break;
case 'rename':
$this->renameTableThroughStaticCall($statement);
}
}
}
private function alterTable(PhpParser\Node\Expr\StaticCall|PhpParser\Node\Expr\MethodCall $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
&& ! (new ObjectType('Illuminate\Database\Schema\Blueprint'))->isSuperTypeOf(new ObjectType($call->getArgs()[1]->value->params[0]->type->toCodeString()))->yes()
)
) {
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)
) {
return;
}
$argName = $call->getArgs()[1]->value->params[0]->var->name;
$this->processColumnUpdates($tableName, $argName, $this->getUpdateStatements($updateClosure));
}
/**
* @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)
) {
continue;
}
$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'
&& $this->getNullableArgumentValue($rootVar) === true
) {
$nullable = true;
}
$firstMethodCall = $rootVar;
$rootVar = $rootVar->var;
}
if (
! ($rootVar instanceof PhpParser\Node\Expr\Variable)
|| $rootVar->name !== $argName
|| ! ($firstMethodCall->name instanceof PhpParser\Node\Identifier)
) {
continue;
}
$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;
}
$type = $this->getModelReferenceType($modelClass);
$table->setColumn(new SchemaColumn($columnName, $type ?? 'int', $nullable));
continue;
}
if (! $firstArg instanceof PhpParser\Node\Scalar\String_) {
if ($firstArg instanceof PhpParser\Node\Expr\Array_ && $firstMethodCall->name->name === 'dropColumn') {
foreach ($firstArg->items as $arrayItem) {
if ($arrayItem === null || ! ($arrayItem->value instanceof PhpParser\Node\Scalar\String_)) {
continue;
}
$table->dropColumn($arrayItem->value->value);
}
}
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;
}
$defaultsMap = [
'softDeletes' => 'deleted_at',
'softDeletesTz' => 'deleted_at',
'softDeletesDatetime' => 'deleted_at',
'dropSoftDeletes' => 'deleted_at',
'dropSoftDeletesTz' => 'deleted_at',
'uuid' => 'uuid',
'id' => 'id',
'ulid' => 'ulid',
'ipAddress' => 'ip_address',
'macAddress' => 'mac_address',
];
if (! array_key_exists($firstMethodCall->name->name, $defaultsMap)) {
continue;
}
$columnName = $defaultsMap[$firstMethodCall->name->name];
} else {
$columnName = $firstArg->value;
}
$secondArgArray = null;
if ($secondArg instanceof PhpParser\Node\Expr\Array_) {
$secondArgArray = [];
foreach ($secondArg->items as $arrayItem) {
if ($arrayItem === null || ! ($arrayItem->value instanceof PhpParser\Node\Scalar\String_)) {
continue;
}
$secondArgArray[] = $arrayItem->value->value;
}
}
$this->processStatementAlterMethod(
strtolower($firstMethodCall->name->name),
$firstMethodCall,
$table,
$columnName,
$nullable,
$secondArg,
$argName,
$tableName,
$secondArgArray,
$stmt,
);
}
}
private function dropTable(PhpParser\Node\Expr\StaticCall|PhpParser\Node\Expr\MethodCall $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 renameTableThroughStaticCall(PhpParser\Node\Expr\StaticCall|PhpParser\Node\Expr\MethodCall $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;
$this->renameTable($oldTableName, $newTableName);
}
private function renameTableThroughMethodCall(SchemaTable $oldTable, PhpParser\Node\Expr\MethodCall $call): void
{
if (
! isset($call->args[0])
|| ! $call->getArgs()[0]->value instanceof PhpParser\Node\Scalar\String_
) {
return;
}
/** @var PhpParser\Node\Scalar\String_ $methodCallArgument */
$methodCallArgument = $call->getArgs()[0]->value;
$oldTableName = $oldTable->name;
$newTableName = $methodCallArgument->value;
$this->renameTable($oldTableName, $newTableName);
}
private function renameTable(string $oldTableName, string $newTableName): void
{
if (! isset($this->tables[$oldTableName])) {
return;
}
$table = $this->tables[$oldTableName];
unset($this->tables[$oldTableName]);
$table->name = $newTableName;
$this->tables[$newTableName] = $table;
}
private function getModelReferenceType(string $modelClass): string|null
{
$classReflection = $this->reflectionProvider->getClass($modelClass);
try {
/** @var Model $modelInstance */
$modelInstance = $classReflection->getNativeReflection()->newInstanceWithoutConstructor();
} catch (ReflectionException) {
return null;
}
$tableName = $modelInstance->getTable();
if (! array_key_exists($tableName, $this->tables)) {
return null;
}
$table = $this->tables[$tableName];
$column = $modelInstance->getKeyName();
if (! array_key_exists($column, $table->columns)) {
return null;
}
return $table->columns[$column]->readableType;
}
private function getNullableArgumentValue(PhpParser\Node\Expr\MethodCall $rootVar): bool
{
if (! array_key_exists(0, $rootVar->args)) {
return true;
}
$arg = $rootVar->args[0];
if (! ($arg instanceof PhpParser\Node\Arg)) {
return true;
}
$argExpression = $arg->value;
if (! ($argExpression instanceof PhpParser\Node\Expr\ConstFetch)) {
return true;
}
return $argExpression->name->getFirst() === 'true';
}
/** @return PhpParser\Node\Stmt\Expression[] */
private function getUpdateStatements(PhpParser\Node\Expr $updateClosure): array
{
if (! property_exists($updateClosure, 'stmts')) {
return [];
}
$statements = [];
$nodeFinder = new NodeFinder();
foreach ($updateClosure->stmts as $updateStatement) {
if ($updateStatement instanceof PhpParser\Node\Stmt\If_) {
$statements = array_merge(
$statements,
$nodeFinder->findInstanceOf($updateStatement, PhpParser\Node\Stmt\Expression::class),
);
continue;
}
$statements[] = $updateStatement;
}
return $statements;
}
/**
* @param array<int, mixed> $secondArgArray
*
* @throws Exception
*/
private function processStatementAlterMethod(
string $method,
PhpParser\Node\Expr\MethodCall|null $firstMethodCall,
SchemaTable $table,
string $columnName,
bool $nullable,
mixed $secondArg,
PhpParser\Node\Expr|string $argName,
string $tableName,
array|null $secondArgArray,
PhpParser\Node\Stmt\Expression $stmt,
): void {
switch ($method) {
case 'addcolumn':
$this->processStatementAlterMethod(
strtolower($firstMethodCall->args[0]->value->value ?? ''),
null,
$table,
$firstMethodCall->args[1]->value->value ?? '',
$nullable,
$secondArg,
$argName,
$tableName,
$secondArgArray,
$stmt,
);
return;
case 'biginteger':
case 'increments':
case 'id':
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':
case 'foreignid':
$table->setColumn(new SchemaColumn($columnName, 'int', $nullable));
return;
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 'ulid':
case 'uuid':
case 'binary':
$table->setColumn(new SchemaColumn($columnName, 'string', $nullable));
return;
case 'boolean':
$table->setColumn(new SchemaColumn($columnName, 'bool', $nullable));
return;
case 'geometry':
case 'geometrycollection':
case 'multipoint':
case 'multipolygon':
case 'multipolygonz':
case 'point':
case 'polygon':
case 'computed':
$table->setColumn(new SchemaColumn($columnName, 'mixed', $nullable));
return;
case 'double':
case 'float':
case 'unsigneddecimal':
case 'decimal':
$table->setColumn(new SchemaColumn($columnName, 'float', $nullable));
return;
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);
}
return;
case 'dropcolumn':
case 'dropifexists':
case 'dropsoftdeletes':
case 'dropsoftdeletestz':
case 'removecolumn':
case 'drop':
$table->dropColumn($columnName);
return;
case 'dropforeign':
case 'dropindex':
case 'dropprimary':
case 'dropunique':
case 'foreign':
case 'index':
case 'primary':
case 'renameindex':
case 'spatialIndex':
case 'unique':
case 'dropspatialindex':
return;
case 'dropmorphs':
$table->dropColumn($columnName . '_type');
$table->dropColumn($columnName . '_id');
return;
case 'enum':
$table->setColumn(new SchemaColumn($columnName, 'enum', $nullable, $secondArgArray));
return;
case 'morphs':
$table->setColumn(new SchemaColumn($columnName . '_type', 'string', $nullable));
$table->setColumn(new SchemaColumn($columnName . '_id', 'int', $nullable));
return;
case 'nullablemorphs':
$table->setColumn(new SchemaColumn($columnName . '_type', 'string', true));
$table->setColumn(new SchemaColumn($columnName . '_id', 'int', true));
return;
case 'nullableuuidmorphs':
$table->setColumn(new SchemaColumn($columnName . '_type', 'string', true));
$table->setColumn(new SchemaColumn($columnName . '_id', 'string', true));
return;
case 'rename':
/** @var PhpParser\Node\Expr\MethodCall $methodCall */
$methodCall = $stmt->expr;
$this->renameTableThroughMethodCall($table, $methodCall);
return;
case 'renamecolumn':
if ($secondArg instanceof PhpParser\Node\Scalar\String_) {
$table->renameColumn($columnName, $secondArg->value);
}
return;
case 'set':
$table->setColumn(new SchemaColumn($columnName, 'set', $nullable, $secondArgArray));
return;
case 'softdeletestz':
case 'timestamptz':
case 'timetz':
case 'year':
case 'softdeletes':
$table->setColumn(new SchemaColumn($columnName, 'string', true));
return;
case 'uuidmorphs':
$table->setColumn(new SchemaColumn($columnName . '_type', 'string', $nullable));
$table->setColumn(new SchemaColumn($columnName . '_id', 'string', $nullable));
return;
default:
// We know a property exists with a name, we just don't know its type.
$table->setColumn(new SchemaColumn($columnName, 'mixed', $nullable));
}
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Properties;
/** @see https://github.com/psalm/laravel-psalm-plugin/blob/master/src/SchemaColumn.php */
final class SchemaColumn
{
public string $writeableType;
public function __construct(
public string $name,
public string $readableType,
public bool $nullable = false,
/** @var array<int, string> */
public array|null $options = null,
) {
$this->writeableType = $readableType;
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Properties;
/** @see https://github.com/psalm/laravel-psalm-plugin/blob/master/src/SchemaTable.php */
final class SchemaTable
{
/** @var array<string, SchemaColumn> */
public array $columns = [];
public function __construct(public string $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]);
}
}
@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Properties;
use Larastan\Larastan\Properties\Schema\PhpMyAdminDataTypeToPhpTypeConverter;
use PhpMyAdmin\SqlParser\Components\CreateDefinition;
use PhpMyAdmin\SqlParser\Exceptions\ParserException;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statement;
use PhpMyAdmin\SqlParser\Statements\CreateStatement;
use PHPStan\File\FileHelper;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RegexIterator;
use SplFileInfo;
use function array_filter;
use function array_key_exists;
use function database_path;
use function file_get_contents;
use function is_array;
use function is_dir;
use function iterator_to_array;
use function ksort;
final class SquashedMigrationHelper
{
/** @param string[] $schemaPaths */
public function __construct(
private array $schemaPaths,
private FileHelper $fileHelper,
private PhpMyAdminDataTypeToPhpTypeConverter $converter,
private bool $disableSchemaScan,
) {
}
/** @return SchemaTable[] */
public function initializeTables(): array
{
if ($this->disableSchemaScan) {
return [];
}
if (empty($this->schemaPaths)) {
$this->schemaPaths = [database_path('schema')];
}
$filesArray = $this->getSchemaFiles();
if (empty($filesArray)) {
return [];
}
ksort($filesArray);
/** @var array<string, SchemaTable> $tables */
$tables = [];
foreach ($filesArray as $file) {
$fileContents = file_get_contents($file->getPathname());
if ($fileContents === false) {
continue;
}
try {
$parser = new Parser($fileContents);
} catch (ParserException) {
// TODO: re-throw the exception with a clear message?
continue;
}
/** @var CreateStatement[] $createStatements */
$createStatements = array_filter($parser->statements, static fn (Statement $statement) => $statement instanceof CreateStatement && $statement->name !== null);
foreach ($createStatements as $createStatement) {
if ($createStatement->name?->table === null || array_key_exists($createStatement->name->table, $tables)) {
continue;
}
$table = new SchemaTable($createStatement->name->table);
if (! is_array($createStatement->fields)) {
continue;
}
foreach ($createStatement->fields as $field) {
if ($field->name === null || $field->type === null) {
continue;
}
$table->setColumn(new SchemaColumn($field->name, $this->converter->convert($field->type), $this->isNullable($field)));
}
$tables[$createStatement->name->table] = $table;
}
}
return $tables;
}
/** @return SplFileInfo[] */
private function getSchemaFiles(): array
{
/** @var SplFileInfo[] $schemaFiles */
$schemaFiles = [];
foreach ($this->schemaPaths as $additionalPath) {
$absolutePath = $this->fileHelper->absolutizePath($additionalPath);
if (! is_dir($absolutePath)) {
continue;
}
$schemaFiles += iterator_to_array(
new RegexIterator(
new RecursiveIteratorIterator(new RecursiveDirectoryIterator($absolutePath)),
'/\.dump|\.sql/i',
),
);
}
return $schemaFiles;
}
private function isNullable(CreateDefinition $definition): bool
{
return ! $definition->options?->has('NOT NULL');
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Reflection;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\PassedByReference;
use PHPStan\Type\Type;
final class AnnotationScopeMethodParameterReflection implements ParameterReflection
{
public function __construct(private string $name, private Type $type, private PassedByReference $passedByReference, private bool $isOptional, private bool $isVariadic, private Type|null $defaultValue = null)
{
}
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|null
{
return $this->defaultValue;
}
}
@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\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 FunctionVariant[]|null */
private array|null $variants = null;
/** @param AnnotationScopeMethodParameterReflection[] $parameters */
public function __construct(private string $name, private ClassReflection $declaringClass, private Type $returnType, private array $parameters, private bool $isStatic, private bool $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|null
{
return null;
}
public function isFinal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getThrowType(): Type|null
{
return null;
}
public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
public function getDocComment(): string|null
{
return null;
}
}
@@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Reflection;
use PHPStan\Reflection;
use PHPStan\Reflection\FunctionVariant;
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;
final class DynamicWhereMethodReflection implements Reflection\MethodReflection
{
public function __construct(private Reflection\ClassReflection $classReflection, private string $methodName)
{
}
public function getDeclaringClass(): 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|null
{
return null;
}
public function getName(): string
{
return $this->methodName;
}
public function getPrototype(): Reflection\ClassMemberReflection
{
return $this;
}
/** @return FunctionVariant[] */
public function getVariants(): array
{
return [
new FunctionVariant(
TemplateTypeMap::createEmpty(),
TemplateTypeMap::createEmpty(),
[
new class implements ParameterReflection
{
public function getName(): string
{
return 'dynamic-with';
}
public function isOptional(): bool
{
return false;
}
public function getType(): Type
{
return new MixedType();
}
public function passedByReference(): PassedByReference
{
return Reflection\PassedByReference::createNo();
}
public function isVariadic(): bool
{
return false;
}
public function getDefaultValue(): Type|null
{
return null;
}
},
],
false,
new ObjectType($this->classReflection->getName()),
),
];
}
public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getDeprecatedDescription(): string|null
{
return null;
}
public function isFinal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getThrowType(): Type|null
{
return null;
}
public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\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|null
{
return null;
}
}
@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\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
{
private Type $returnType;
/** @param ParameterReflection[] $parameters */
public function __construct(private string $methodName, private ClassReflection $classReflection, private array $parameters, Type|null $returnType = null, private bool $isVariadic = false)
{
$this->returnType = $returnType ?? new ObjectType(Builder::class);
}
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|null
{
return null;
}
public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getDeprecatedDescription(): string|null
{
return null;
}
public function isFinal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getThrowType(): Type|null
{
return null;
}
public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
}
@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Reflection;
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
final class ModelScopeMethodReflection implements MethodReflection
{
public function __construct(private string $methodName, private ClassReflection $classReflection, private ClassReflection $relation)
{
}
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 getName(): string
{
return $this->methodName;
}
public function getPrototype(): ClassMemberReflection
{
return $this;
}
/**
* {@inheritDoc}
*/
public function getVariants(): array
{
return [
new FunctionVariant(
TemplateTypeMap::createEmpty(),
null,
[],
false,
new ObjectType($this->relation->getName()),
),
];
}
public function getDocComment(): string|null
{
return null;
}
public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getDeprecatedDescription(): string|null
{
return null;
}
public function isFinal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getThrowType(): Type|null
{
return null;
}
public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Reflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\Mixin\MixinMethodsClassReflectionExtension;
use PHPStan\Reflection\Mixin\MixinPropertiesClassReflectionExtension;
use function array_key_exists;
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;
}
}
/** @phpstan-ignore-next-line */
return (new MixinPropertiesClassReflectionExtension([$classReflection->getName()]))
->hasProperty($classReflection, $propertyName);
}
/**
* Does the given class or any of its ancestors have an `@method*` annotation with the given name?
*/
public static function hasMethodTag(ClassReflection $classReflection, string $methodName): bool
{
if (array_key_exists($methodName, $classReflection->getMethodTags())) {
return true;
}
foreach ($classReflection->getAncestors() as $ancestor) {
if (array_key_exists($methodName, $ancestor->getMethodTags())) {
return true;
}
}
/** @phpstan-ignore-next-line */
return (new MixinMethodsClassReflectionExtension([$classReflection->getName()]))
->hasMethod($classReflection, $methodName);
}
}
@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\Reflection;
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Type;
class StaticMethodReflection implements MethodReflection
{
public function __construct(private 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|null
{
return $this->methodReflection->getDocComment();
}
public function getName(): string
{
return $this->methodReflection->getName();
}
public function getPrototype(): ClassMemberReflection
{
return $this->methodReflection->getPrototype();
}
/** @return ParametersAcceptor[] */
public function getVariants(): array
{
return $this->methodReflection->getVariants();
}
public function isDeprecated(): TrinaryLogic
{
return $this->methodReflection->isDeprecated();
}
public function getDeprecatedDescription(): string|null
{
return $this->methodReflection->getDeprecatedDescription();
}
public function isFinal(): TrinaryLogic
{
return $this->methodReflection->isFinal();
}
public function isInternal(): TrinaryLogic
{
return $this->methodReflection->isInternal();
}
public function getThrowType(): Type|null
{
return $this->methodReflection->getThrowType();
}
public function hasSideEffects(): TrinaryLogic
{
return $this->methodReflection->hasSideEffects();
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes;
use Illuminate\Foundation\Application;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\BooleanType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use function count;
class AppEnvironmentReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
public function getClass(): string
{
return Application::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'environment';
}
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope,
): Type {
if (count($methodCall->getArgs()) === 0) {
return new StringType();
}
return new BooleanType();
}
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes;
use Illuminate\Support\Facades\App;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\Type;
final class AppMakeDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
{
public function __construct(
private AppMakeHelper $appMakeHelper,
) {
}
public function getClass(): string
{
return App::class;
}
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'make' || $methodReflection->getName() === 'makeWith';
}
public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type
{
return $this->appMakeHelper->resolveTypeFromCall($methodCall, $scope);
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes;
use Larastan\Larastan\Concerns\HasContainer;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Type\ErrorType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use Throwable;
use function count;
final class AppMakeHelper
{
use HasContainer;
public function resolveTypeFromCall(FuncCall|MethodCall|StaticCall $call, Scope $scope): Type
{
$args = $call->getArgs();
if (count($args) === 0) {
return new ErrorType();
}
$argType = $scope->getType($args[0]->value);
$constantStrings = $argType->getConstantStrings();
if (count($constantStrings) > 0) {
$types = [];
foreach ($constantStrings as $constantString) {
try {
/** @var object|null $resolved */
$resolved = $this->resolve($constantString->getValue());
if ($resolved === null) {
return new ErrorType();
}
$types[] = new ObjectType($resolved::class);
} catch (Throwable) {
return new ErrorType();
}
}
return TypeCombinator::union(...$types);
}
return new MixedType();
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes;
use Illuminate\Contracts\Foundation\Application;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Type;
use function in_array;
final class ApplicationMakeDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
public function __construct(
private AppMakeHelper $appMakeHelper,
) {
}
public function getClass(): string
{
return Application::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return in_array($methodReflection->getName(), ['make', 'makeWith', 'resolve'], true);
}
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
{
return $this->appMakeHelper->resolveTypeFromCall($methodCall, $scope);
}
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes;
use Illuminate\Support\Facades\Auth;
use Larastan\Larastan\Concerns;
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;
use PHPStan\Type\TypeCombinator;
use function array_map;
use function count;
/** @internal */
final class AuthExtension implements DynamicStaticMethodReturnTypeExtension
{
use Concerns\HasContainer;
use Concerns\LoadsAuthModel;
public function getClass(): string
{
return Auth::class;
}
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'user';
}
public function getTypeFromStaticMethodCall(
MethodReflection $methodReflection,
StaticCall $methodCall,
Scope $scope,
): Type|null {
$config = $this->getContainer()->get('config');
$authModels = [];
if ($config !== null) {
$authModels = $this->getAuthModels($config);
}
if (count($authModels) === 0) {
return null;
}
return TypeCombinator::addNull(
TypeCombinator::union(...array_map(
static fn (string $authModel): Type => new ObjectType($authModel),
$authModels,
)),
);
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes;
use Illuminate\Auth\AuthManager;
use Larastan\Larastan\Concerns;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function array_map;
use function count;
final class AuthManagerExtension implements DynamicMethodReturnTypeExtension
{
use Concerns\HasContainer;
use Concerns\LoadsAuthModel;
public function getClass(): string
{
return AuthManager::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'user';
}
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope,
): Type|null {
$config = $this->getContainer()->get('config');
$authModels = [];
if ($config !== null) {
$authModels = $this->getAuthModels($config);
}
if (count($authModels) === 0) {
return null;
}
return TypeCombinator::addNull(
TypeCombinator::union(...array_map(
static fn (string $authModel): Type => new ObjectType($authModel),
$authModels,
)),
);
}
}
@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\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 Larastan\Larastan\Internal\LaravelVersion;
use Larastan\Larastan\Methods\ModelTypeHelper;
use Larastan\Larastan\Support\CollectionHelper;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\ArrayType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
use function in_array;
/** @internal */
final class BuilderModelFindExtension implements DynamicMethodReturnTypeExtension
{
public function __construct(private ReflectionProvider $reflectionProvider, private CollectionHelper $collectionHelper)
{
}
public function getClass(): string
{
return Builder::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
$methodName = $methodReflection->getName();
if (! Str::startsWith($methodName, 'find')) {
return false;
}
$model = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType(LaravelVersion::getBuilderModelGenericName());
if ($model === null || $model->getObjectClassNames() === []) {
return false;
}
return $this->reflectionProvider->getClass(Builder::class)->hasNativeMethod($methodName) ||
$this->reflectionProvider->getClass(QueryBuilder::class)->hasNativeMethod($methodName);
}
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope,
): Type|null {
if (count($methodCall->getArgs()) < 1) {
return null;
}
/** @var Type $modelClassType */
$modelClassType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType(LaravelVersion::getBuilderModelGenericName());
if ((new ObjectType(Model::class))->isSuperTypeOf($modelClassType)->no()) {
return null;
}
$returnType = $methodReflection->getVariants()[0]->getReturnType();
$argType = $scope->getType($methodCall->getArgs()[0]->value);
if ($argType instanceof MixedType) {
return $returnType;
}
$models = [];
foreach ($modelClassType->getObjectClassReflections() as $objectClassReflection) {
$modelName = $objectClassReflection->getName();
$returnType = ModelTypeHelper::replaceStaticTypeWithModel($returnType, $modelName);
if ($argType->isIterable()->yes()) {
if (in_array(Collection::class, $returnType->getReferencedClasses(), true)) {
$models[] = $this->collectionHelper->determineCollectionClass($modelName);
continue;
}
$models[] = TypeCombinator::remove($returnType, new ObjectType($modelName));
} else {
$models[] = TypeCombinator::remove(
TypeCombinator::remove(
$returnType,
new ArrayType(new MixedType(), $modelClassType),
),
new ObjectType(Collection::class),
);
}
}
return count($models) > 0 ? TypeCombinator::union(...$models) : null;
}
}
@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes;
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\ShouldNotHappenException;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function assert;
use function count;
use function in_array;
use function is_string;
class CollectionFilterRejectDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
public function getClass(): string
{
return Enumerable::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return in_array($methodReflection->getName(), ['filter', 'reject'], true);
}
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope,
): Type|null {
$calledOnType = $scope->getType($methodCall->var);
if ($calledOnType->getObjectClassReflections() === [] || ! $calledOnType->getObjectClassReflections()[0]->isGeneric()) {
return null;
}
$keyType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TKey');
$valueType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TValue');
if ($keyType === null || $valueType === null) {
return null;
}
$methodName = $methodReflection->getName();
assert($methodName === 'filter' || $methodName === 'reject', 'proven in isMethodSupported');
if (count($methodCall->getArgs()) < 1) {
$modifiedType = match ($methodName) {
'filter' => TypeCombinator::removeFalsey($valueType),
'reject' => TypeCombinator::removeTruthy($valueType)
};
return new GenericObjectType($calledOnType->getObjectClassNames()[0], [$keyType, $modifiedType]);
}
$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 ShouldNotHappenException();
}
$itemVariableName = $var->name;
$node = new Variable($itemVariableName);
// @phpstan-ignore-next-line
$scope = $scope->assignExpression($node, $valueType);
$scope = match ($methodName) {
'filter' => $scope->filterByTruthyValue($expr),
'reject' => $scope->filterByFalseyValue($expr),
};
$valueType = $scope->getVariableType($itemVariableName);
}
return new GenericObjectType($calledOnType->getObjectClassNames()[0], [$keyType, $valueType]);
}
}
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes;
use Illuminate\Support\Enumerable;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\BooleanType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\FloatType;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\NullType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
use function count;
class CollectionWhereNotNullDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
public function getClass(): string
{
return Enumerable::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'whereNotNull';
}
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope,
): Type|null {
$calledOnType = $scope->getType($methodCall->var);
if ($calledOnType->getObjectClassNames() === []) {
return null;
}
$keyType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TKey');
$valueType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TValue') ??
$methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TModel');
if ($keyType === null || $valueType === null) {
return null;
}
$nonFalseyTypes = TypeCombinator::removeNull($valueType);
if (! $this->argumentIsString($methodCall, $scope)) {
return new GenericObjectType($calledOnType->getObjectClassNames()[0], [$keyType, $nonFalseyTypes]);
}
$scalarTypes = new UnionType([
new StringType(),
new IntegerType(),
new FloatType(),
new BooleanType(),
]);
$nonFalseyTypes = TypeCombinator::remove($nonFalseyTypes, $scalarTypes);
return new GenericObjectType($calledOnType->getObjectClassNames()[0], [$keyType, $nonFalseyTypes]);
}
public function argumentIsString(MethodCall $methodCall, Scope $scope): bool
{
if (count($methodCall->getArgs()) === 0) {
return false;
}
$argType = $scope->getType($methodCall->getArgs()[0]->value);
return (new NullType())->isSuperTypeOf($argType)->no();
}
}
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes\ConsoleCommand;
use InvalidArgumentException;
use Larastan\Larastan\Internal\ConsoleApplicationHelper;
use Larastan\Larastan\Internal\ConsoleApplicationResolver;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
use function in_array;
class ArgumentDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
public function __construct(private ConsoleApplicationResolver $consoleApplicationResolver, private ConsoleApplicationHelper $consoleApplicationHelper)
{
}
public function getClass(): string
{
return 'Illuminate\Console\Command';
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return in_array($methodReflection->getName(), ['argument', 'arguments'], true);
}
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope,
): Type|null {
$classReflection = $scope->getClassReflection();
if ($classReflection === null) {
return null;
}
$args = $methodCall->getArgs();
$defaultReturnType = ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType();
if ($args === [] || $methodReflection->getName() === 'arguments') {
return $this->consoleApplicationHelper->getArguments($classReflection, $scope);
}
$argStrings = $scope->getType($args[0]->value)->getConstantStrings();
if (count($argStrings) === 0) {
return null;
}
$returnTypes = [];
foreach ($argStrings as $argString) {
$argName = $argString->getValue();
$argTypes = [];
foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) {
try {
$command->mergeApplicationDefinition();
$argument = $command->getDefinition()->getArgument($argName);
$argTypes[] = $this->consoleApplicationHelper->getArgumentType($scope, $argument);
} catch (InvalidArgumentException) {
// noop
}
$returnTypes[] = count($argTypes) > 0 ? TypeCombinator::union(...$argTypes) : $defaultReturnType;
}
}
return count($returnTypes) > 0 ? TypeCombinator::union(...$returnTypes) : null;
}
}
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes\ConsoleCommand;
use Larastan\Larastan\Internal\ConsoleApplicationResolver;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Type;
use function array_unique;
use function count;
class HasArgumentDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
public function __construct(private ConsoleApplicationResolver $consoleApplicationResolver)
{
}
public function getClass(): string
{
return 'Illuminate\Console\Command';
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'hasArgument';
}
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope,
): Type|null {
$classReflection = $scope->getClassReflection();
if ($classReflection === null) {
return null;
}
if ($methodCall->getArgs() === []) {
return null;
}
$constantStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings();
if (count($constantStrings) !== 1) {
return null;
}
$returnTypes = [];
foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) {
$command->mergeApplicationDefinition();
$returnTypes[] = $command->getDefinition()->hasArgument($constantStrings[0]->getValue());
}
if (count($returnTypes) === 0) {
return null;
}
$returnTypes = array_unique($returnTypes);
return count($returnTypes) === 1 ? new ConstantBooleanType($returnTypes[0]) : null;
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes\ConsoleCommand;
use Larastan\Larastan\Internal\ConsoleApplicationResolver;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Type;
use function array_unique;
use function count;
class HasOptionDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
public function __construct(private ConsoleApplicationResolver $consoleApplicationResolver)
{
}
public function getClass(): string
{
return 'Illuminate\Console\Command';
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'hasOption';
}
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope,
): Type|null {
$classReflection = $scope->getClassReflection();
if ($classReflection === null) {
return null;
}
if ($methodCall->getArgs() === []) {
return null;
}
$constantStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings();
if (count($constantStrings) !== 1) {
return null;
}
$argName = $constantStrings[0]->getValue();
$returnTypes = [];
foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) {
$command->mergeApplicationDefinition();
$returnTypes[] = $command->getDefinition()->hasOption($argName) || $command->getDefinition()->hasShortcut($argName);
}
if (count($returnTypes) === 0) {
return null;
}
$returnTypes = array_unique($returnTypes);
return count($returnTypes) === 1 ? new ConstantBooleanType($returnTypes[0]) : null;
}
}
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes\ConsoleCommand;
use InvalidArgumentException;
use Larastan\Larastan\Internal\ConsoleApplicationHelper;
use Larastan\Larastan\Internal\ConsoleApplicationResolver;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
use function in_array;
class OptionDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
public function __construct(private ConsoleApplicationResolver $consoleApplicationResolver, private ConsoleApplicationHelper $consoleApplicationHelper)
{
}
public function getClass(): string
{
return 'Illuminate\Console\Command';
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return in_array($methodReflection->getName(), ['option', 'options'], true);
}
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope,
): Type|null {
$classReflection = $scope->getClassReflection();
if ($classReflection === null) {
return null;
}
$args = $methodCall->getArgs();
$defaultReturnType = ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType();
if ($args === [] || $methodReflection->getName() === 'options') {
return $this->consoleApplicationHelper->getOptions($classReflection, $scope);
}
$argStrings = $scope->getType($args[0]->value)->getConstantStrings();
if (count($argStrings) === 0) {
return null;
}
$returnTypes = [];
foreach ($argStrings as $argString) {
$argName = $argString->getValue();
$argTypes = [];
foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) {
try {
$command->mergeApplicationDefinition();
$definition = $command->getDefinition();
if ($definition->hasShortcut($argName)) {
$argument = $definition->getOptionForShortcut($argName);
} else {
$argument = $definition->getOption($argName);
}
$argTypes[] = $this->consoleApplicationHelper->getOptionType($scope, $argument);
} catch (InvalidArgumentException) {
// noop
}
$returnTypes[] = count($argTypes) > 0 ? TypeCombinator::union(...$argTypes) : $defaultReturnType;
}
}
return count($returnTypes) > 0 ? TypeCombinator::union(...$returnTypes) : null;
}
}
@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes;
use Larastan\Larastan\Concerns\HasContainer;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\ErrorType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
use function is_object;
class ContainerArrayAccessDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
use HasContainer;
public function __construct(private string $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|null {
$args = $methodCall->getArgs();
if (count($args) === 0) {
return null;
}
$argType = $scope->getType($args[0]->value);
$argStrings = $argType->getConstantStrings();
if ($argStrings === []) {
return null;
}
$argTypes = [];
foreach ($argStrings as $argString) {
$resolvedValue = $this->resolve($argString->getValue());
if ($resolvedValue === null) {
$argTypes[] = new ErrorType();
continue;
}
if (is_object($resolvedValue)) {
$class = $resolvedValue::class;
$argTypes[] = new ObjectType($class);
continue;
}
$argTypes[] = $scope->getTypeFromValue($resolvedValue);
}
return TypeCombinator::union(...$argTypes);
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes;
use Illuminate\Contracts\Container\Container;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Type;
use function in_array;
final class ContainerMakeDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
public function __construct(
private AppMakeHelper $appMakeHelper,
) {
}
public function getClass(): string
{
return Container::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return in_array($methodReflection->getName(), ['make', 'makeWith', 'resolve'], true);
}
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
{
return $this->appMakeHelper->resolveTypeFromCall($methodCall, $scope);
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes;
use Illuminate\Support\Facades\Date;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function get_class;
use function in_array;
use function now;
class DateExtension implements DynamicStaticMethodReturnTypeExtension
{
public function getClass(): string
{
return Date::class;
}
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
{
return in_array($methodReflection->getName(), [
'create',
'createFromDate',
'createFromTime',
'createFromTimeString',
'createFromTimestamp',
'createFromTimestampMs',
'createFromTimestampUTC',
'createMidnightDate',
'fromSerialized',
'getTestNow',
'instance',
'maxValue',
'minValue',
'now',
'parse',
'today',
'tomorrow',
'yesterday',
'createFromFormat',
'createSafe',
'make',
], true);
}
public function getTypeFromStaticMethodCall(
MethodReflection $methodReflection,
StaticCall $methodCall,
Scope $scope,
): Type {
$dateType = new ObjectType(get_class(now()));
if (in_array($methodReflection->getName(), ['createFromFormat', 'createSafe'], true)) {
return TypeCombinator::union($dateType, new ConstantBooleanType(false));
}
if (in_array($methodReflection->getName(), ['getTestNow', 'make'], true)) {
return TypeCombinator::addNull($dateType);
}
return $dateType;
}
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BenevolentUnionType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\MixedType;
use PHPStan\Type\NullType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use function count;
/** @internal */
final class DoubleUnderscoreHelperReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === '__';
}
public function getTypeFromFunctionCall(
FunctionReflection $functionReflection,
FuncCall $functionCall,
Scope $scope,
): Type {
if (count($functionCall->args) === 0) {
return new NullType();
}
return new BenevolentUnionType([
new ArrayType(new MixedType(), new MixedType()),
new StringType(),
]);
}
}
@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Larastan\Larastan\Internal\LaravelVersion;
use Larastan\Larastan\Support\CollectionHelper;
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\IntegerType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use function in_array;
final class EloquentBuilderExtension implements DynamicMethodReturnTypeExtension
{
public function __construct(private ReflectionProvider $reflectionProvider, private CollectionHelper $collectionHelper)
{
}
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->hasType(LaravelVersion::getBuilderModelGenericName())) {
return false;
}
if ($templateTypeMap->getType(LaravelVersion::getBuilderModelGenericName())?->getObjectClassNames() === []) {
return false;
}
return $builderReflection->hasNativeMethod($methodReflection->getName());
}
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope,
): Type|null {
$returnType = ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType();
$templateTypeMap = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap();
$modelType = $templateTypeMap->getType(LaravelVersion::getBuilderModelGenericName());
if ($modelType === null) {
return null;
}
$classNames = $modelType->getObjectClassNames();
if ($classNames !== [] && $modelType->isObject()->yes() && in_array(Collection::class, $returnType->getReferencedClasses(), true)) {
$collectionClassName = $this->collectionHelper->determineCollectionClassName($classNames[0]);
$collectionReflection = $this->reflectionProvider->getClass($collectionClassName);
if (! $collectionReflection->isGeneric()) {
// Not generic. So return the type as is
return new ObjectType($collectionClassName);
}
$typeMap = $collectionReflection->getActiveTemplateTypeMap();
// Specifies key and value
if ($typeMap->count() === 2) {
return new GenericObjectType($collectionClassName, [new IntegerType(), $modelType]);
}
// Specifies only value
if (($typeMap->count() === 1) && $typeMap->hasType('TModel')) {
return new GenericObjectType($collectionClassName, [$modelType]);
}
}
return null;
}
}
@@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Enumerable;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\UnionType;
use function array_map;
use function array_merge;
use function in_array;
class EnumerableGenericStaticMethodDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
public function getClass(): string
{
return Enumerable::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
if ($methodReflection->getDeclaringClass()->getName() === EloquentCollection::class) {
return in_array($methodReflection->getName(), ['find', 'map', 'mapWithKeys', 'unique'], true);
}
$methods = [
'chunk',
'chunkWhile',
'collapse',
'combine',
'concat',
'crossJoin',
'flatMap',
'flip',
'groupBy',
'keyBy',
'keys',
'map',
'mapInto',
'mapToDictionary',
'mapToGroups',
'mapWithKeys',
'mergeRecursive',
'pad',
'partition',
'pluck',
'random',
'sliding',
'split',
'splitIn',
'values',
'wrap',
'zip',
];
if ($methodReflection->getDeclaringClass()->getName() === Collection::class) {
$methods = array_merge($methods, ['pop', 'shift']);
}
return in_array($methodReflection->getName(), $methods, true);
}
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope,
): Type {
$returnType = ParametersAcceptorSelector::selectFromArgs(
$scope,
$methodCall->getArgs(),
$methodReflection->getVariants(),
)->getReturnType();
if ((! $returnType instanceof UnionType) && $returnType->isObject()->no()) {
return $returnType;
}
// map and mapWithKeys returns base collection,
// if items does not include Eloquent model.
// This logic is in stub files, so here we check if the return type is base collection
if (
$methodReflection->getDeclaringClass()->getName() === EloquentCollection::class &&
in_array($methodReflection->getName(), ['map', 'mapWithKeys'], true) &&
$returnType->getObjectClassNames()[0] === Collection::class
) {
return $returnType;
}
$calledOnType = $scope->getType($methodCall->var);
if ($calledOnType->getObjectClassReflections() === []) {
return $returnType;
}
$classReflection = $calledOnType->getObjectClassReflections()[0];
// Special cases for methods returning single models
if ((new ObjectType(Model::class))->isSuperTypeOf($returnType)->yes()) {
return $returnType;
}
// If it's a UnionType, traverse the types and try to find a collection object type
if ($returnType instanceof UnionType) {
return $returnType->traverse(function (Type $type) use ($classReflection) {
// @phpcs:ignore
if ($type instanceof GenericObjectType && ($innerReflection = $type->getClassReflection()) !== null) { // @phpstan-ignore-line
return $this->handleGenericObjectType($classReflection, $innerReflection);
}
return $type;
});
}
if ($returnType->getObjectClassReflections() === []) {
return $returnType;
}
return $this->handleGenericObjectType($classReflection, $returnType->getObjectClassReflections()[0]);
}
private function handleGenericObjectType(ClassReflection $classReflection, ClassReflection $returnTypeClassReflection): ObjectType
{
if ($classReflection->getActiveTemplateTypeMap()->count() !== $returnTypeClassReflection->getActiveTemplateTypeMap()->count()) {
return new ObjectType($classReflection->getName());
}
$genericTypes = $returnTypeClassReflection->typeMapToList($returnTypeClassReflection->getActiveTemplateTypeMap());
if ($genericTypes === []) {
return new ObjectType($classReflection->getName());
}
// If the key type is gonna be a model, we change it to string
if ((new ObjectType(Model::class))->isSuperTypeOf($genericTypes[0])->yes()) {
$genericTypes[0] = new StringType();
}
$genericTypes = array_map(static function (Type $type) use ($classReflection) {
return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($classReflection): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}
// @phpcs:ignore
if ($type instanceof GenericObjectType && (($innerTypeReflection = $type->getClassReflection()) !== null)) {
$genericTypes = $innerTypeReflection->typeMapToList($innerTypeReflection->getActiveTemplateTypeMap());
if ($classReflection->isSubclassOf($type->getClassName())) {
return new GenericObjectType($classReflection->getName(), $genericTypes);
}
}
return $traverse($type);
});
}, $genericTypes);
return new GenericObjectType($classReflection->getName(), $genericTypes);
}
}
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes;
use Illuminate\Support\Collection;
use Illuminate\Support\Enumerable;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\ErrorType;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use function in_array;
class EnumerableGenericStaticMethodDynamicStaticMethodReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
{
public function __construct(private ReflectionProvider $reflectionProvider)
{
}
public function getClass(): string
{
return Enumerable::class;
}
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
{
return in_array($methodReflection->getName(), [
'make',
'wrap',
'times',
'range',
], true);
}
public function getTypeFromStaticMethodCall(
MethodReflection $methodReflection,
StaticCall $methodCall,
Scope $scope,
): Type {
$returnType = ParametersAcceptorSelector::selectFromArgs(
$scope,
$methodCall->getArgs(),
$methodReflection->getVariants(),
)->getReturnType();
if (! $returnType instanceof UnionType && $returnType->isObject()->no()) {
return $returnType;
}
$class = $methodCall->class;
if (! $class instanceof Name) {
return new ErrorType();
}
if (! $this->reflectionProvider->hasClass((string) $class)) {
return $returnType;
}
$classReflection = $this->reflectionProvider->getClass((string) $class);
// If it's called on Support collection, just return.
if ($classReflection->getName() === Collection::class) {
return $returnType;
}
// If it's a UnionType, traverse the types and try to find a collection object type
if ($returnType instanceof UnionType) {
return $returnType->traverse(function (Type $type) use ($classReflection) {
// @phpcs:ignore
if ($type instanceof GenericObjectType && ($innerReflection = $type->getClassReflection()) !== null) { // @phpstan-ignore-line
return $this->handleGenericObjectType($classReflection, $innerReflection);
}
return $type;
});
}
$returnTypeClassReflections = $returnType->getObjectClassReflections();
if ($returnTypeClassReflections === []) {
return $returnType;
}
return $this->handleGenericObjectType($classReflection, $returnTypeClassReflections[0]);
}
private function handleGenericObjectType(ClassReflection $classReflection, ClassReflection $returnTypeClassReflection): ObjectType
{
if ($classReflection->getActiveTemplateTypeMap()->count() !== $returnTypeClassReflection->getActiveTemplateTypeMap()->count()) {
return new ObjectType($classReflection->getName());
}
return new GenericObjectType(
$classReflection->getName(),
$returnTypeClassReflection->typeMapToList($returnTypeClassReflection->getActiveTemplateTypeMap()),
);
}
}
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Model;
use Larastan\Larastan\Types\Factory\ModelFactoryType;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\TrinaryLogic;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\ErrorType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function in_array;
class FactoryDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
public function getClass(): string
{
return Factory::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return true;
}
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope,
): Type|null {
$calledOnType = $scope->getType($methodCall->var);
$originalReturnType = ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType();
if (! $calledOnType instanceof ModelFactoryType) {
return null;
}
if (in_array($methodReflection->getName(), ['count', 'times'], true)) {
if ($methodCall->getArgs() === []) {
return new ErrorType();
}
$argType = $scope->getType($methodCall->getArgs()[0]->value);
if ((new IntegerType())->isSuperTypeOf($argType)->yes()) {
return new ModelFactoryType($calledOnType->getClassName(), null, null, TrinaryLogic::createNo());
}
if ((new NullType())->isSuperTypeOf($argType)->yes()) {
return new ModelFactoryType($calledOnType->getClassName(), null, null, TrinaryLogic::createYes());
}
return new ModelFactoryType($calledOnType->getClassName(), null, null, TrinaryLogic::createMaybe());
}
if (in_array($methodReflection->getName(), ['create', 'createQuietly', 'make'], true)) {
if ($calledOnType->isSingleModel()->yes()) {
return TypeCombinator::remove($originalReturnType, new ObjectType(Collection::class));
}
if ($calledOnType->isSingleModel()->no()) {
return TypeCombinator::remove($originalReturnType, new ObjectType(Model::class));
}
}
if (! $originalReturnType->isSuperTypeOf($calledOnType)->yes()) {
return null;
}
return $calledOnType;
}
}
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes;
use Illuminate\Auth\RequestGuard;
use Illuminate\Auth\TokenGuard;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Support\Facades\Auth;
use Larastan\Larastan\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;
use PHPStan\Type\TypeCombinator;
use function array_key_exists;
use function count;
class GuardDynamicStaticMethodReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
{
use HasContainer;
public function getClass(): string
{
return Auth::class;
}
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'guard';
}
public function getTypeFromStaticMethodCall(
MethodReflection $methodReflection,
StaticCall $methodCall,
Scope $scope,
): Type {
$defaultReturnType = TypeCombinator::intersect(new ObjectType(Guard::class), new ObjectType(StatefulGuard::class));
$config = $this->getContainer()->get('config');
if ($config === null) {
return $defaultReturnType;
}
/** @var string $defaultGuard */
$defaultGuard = $config->get('auth.defaults.guard');
if (count($methodCall->getArgs()) === 0) {
/** @var array<string, mixed> $guards */
$guards = $config->get('auth.guards');
if (! array_key_exists($defaultGuard, $guards)) {
return $defaultReturnType;
}
return $this->findTypeFromGuardDriver($guards[$defaultGuard]['driver']) ?? $defaultReturnType;
}
$argType = $scope->getType($methodCall->getArgs()[0]->value);
$argStrings = $argType->getConstantStrings();
if (count($argStrings) !== 1) {
return $defaultReturnType;
}
return $this->findTypeFromGuardDriver($argStrings[0]->getValue()) ?? $defaultReturnType;
}
private function findTypeFromGuardDriver(string $driver): Type|null
{
return match ($driver) {
'session' => new ObjectType('Illuminate\Auth\SessionGuard'),
'token' => new ObjectType(TokenGuard::class),
'passport' => new ObjectType(RequestGuard::class),
default => null,
};
}
}
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes;
use Illuminate\Contracts\Auth\Guard;
use Larastan\Larastan\Concerns;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function array_map;
use function count;
final class GuardExtension implements DynamicMethodReturnTypeExtension
{
use Concerns\HasContainer;
use Concerns\LoadsAuthModel;
public function getClass(): string
{
return Guard::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'user';
}
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope,
): Type|null {
$config = $this->getContainer()->get('config');
$authModels = [];
if ($config !== null) {
$guard = $this->getGuardFromMethodCall($scope, $methodCall);
$authModels = $this->getAuthModels($config, $guard);
}
if (count($authModels) === 0) {
return null;
}
return TypeCombinator::addNull(
TypeCombinator::union(...array_map(
static fn (string $authModel): Type => new ObjectType($authModel),
$authModels,
)),
);
}
private function getGuardFromMethodCall(Scope $scope, MethodCall $methodCall): string|null
{
if (
! ($methodCall->var instanceof StaticCall) &&
! ($methodCall->var instanceof MethodCall) &&
! ($methodCall->var instanceof FuncCall)
) {
return null;
}
if (count($methodCall->var->args) !== 1) {
return null;
}
$guardType = $scope->getType($methodCall->var->getArgs()[0]->value);
$constantStrings = $guardType->getConstantStrings();
if (count($constantStrings) !== 1) {
return null;
}
return $constantStrings[0]->getValue();
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes\Helpers;
use Illuminate\Foundation\Application;
use Larastan\Larastan\ReturnTypes\AppMakeHelper;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use function count;
class AppExtension implements DynamicFunctionReturnTypeExtension
{
public function __construct(
private AppMakeHelper $appMakeHelper,
) {
}
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);
}
return $this->appMakeHelper->resolveTypeFromCall($functionCall, $scope);
}
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes\Helpers;
use Illuminate\Contracts\Auth\Factory;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\StatefulGuard;
use Larastan\Larastan\Concerns;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
/** @internal */
final class AuthExtension implements DynamicFunctionReturnTypeExtension
{
use Concerns\HasContainer;
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'auth';
}
public function getTypeFromFunctionCall(
FunctionReflection $functionReflection,
FuncCall $functionCall,
Scope $scope,
): Type {
if (count($functionCall->getArgs()) < 1) {
/** @var ?object $class */
$class = $this->resolve(Factory::class);
if ($class === null) {
return new ObjectType(Factory::class);
}
return new ObjectType($class::class);
}
return TypeCombinator::intersect(new ObjectType(Guard::class), new ObjectType(StatefulGuard::class));
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes\Helpers;
use Illuminate\Support\Collection;
use Larastan\Larastan\Support\CollectionHelper;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\BenevolentUnionType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use function count;
final class CollectExtension implements DynamicFunctionReturnTypeExtension
{
public function __construct(private CollectionHelper $collectionHelper)
{
}
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'collect';
}
public function getTypeFromFunctionCall(
FunctionReflection $functionReflection,
FuncCall $functionCall,
Scope $scope,
): Type|null {
if (count($functionCall->getArgs()) < 1) {
return new GenericObjectType(Collection::class, [new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType()]);
}
$valueType = $scope->getType($functionCall->getArgs()[0]->value);
return $this->collectionHelper->determineGenericCollectionTypeFromType($valueType);
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes\Helpers;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use function get_class;
use function now;
/** @internal */
final class NowAndTodayExtension implements DynamicFunctionReturnTypeExtension
{
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'now' || $functionReflection->getName() === 'today';
}
public function getTypeFromFunctionCall(
FunctionReflection $functionReflection,
FuncCall $functionCall,
Scope $scope,
): Type {
return new ObjectType(get_class(now()));
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes\Helpers;
use Illuminate\Contracts\Routing\ResponseFactory;
use Illuminate\Http\Response;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use function count;
/** @internal */
final class ResponseExtension implements DynamicFunctionReturnTypeExtension
{
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'response';
}
public function getTypeFromFunctionCall(
FunctionReflection $functionReflection,
FuncCall $functionCall,
Scope $scope,
): Type {
if (count($functionCall->getArgs()) === 0) {
return new ObjectType(ResponseFactory::class);
}
return new ObjectType(Response::class);
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes\Helpers;
use Illuminate\Support\Stringable;
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;
use function count;
class StrExtension implements DynamicFunctionReturnTypeExtension
{
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'str';
}
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type|null
{
if (count($functionCall->getArgs()) === 1) {
return new ObjectType(Stringable::class);
}
return new MixedType();
}
}
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\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;
use function count;
class TapExtension implements DynamicFunctionReturnTypeExtension
{
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'tap';
}
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();
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes\Helpers;
use Illuminate\Contracts\Validation\Factory;
use Illuminate\Validation\Validator;
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;
use function count;
/** @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(Factory::class);
}
return new IntersectionType([
new ObjectType(Validator::class),
new ObjectType(\Illuminate\Contracts\Validation\Validator::class),
]);
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\ReturnTypes\Helpers;
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;
use function count;
/** @internal */
final class ValueExtension implements DynamicFunctionReturnTypeExtension
{
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'value';
}
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);
return ParametersAcceptorSelector::selectFromArgs(
$scope,
$functionCall->getArgs(),
$callbackType->getCallableParametersAcceptors($scope),
)->getReturnType();
}
return $scope->getType($arg);
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\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\Type;
use function count;
/** @internal */
final class HigherOrderTapProxyExtension implements DynamicMethodReturnTypeExtension
{
public function getClass(): string
{
return HigherOrderTapProxy::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return true;
}
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope,
): Type {
$type = $scope->getType($methodCall->var);
if ($type instanceof GenericObjectType) { // @phpstan-ignore-line
$types = $type->getTypes();
if (count($types) === 1 && $types[0]->getObjectClassNames() !== []) {
return $types[0];
}
}
return new MixedType();
}
}
@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace Larastan\Larastan\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 Illuminate\Support\Str;
use Larastan\Larastan\Methods\BuilderHelper;
use Larastan\Larastan\Support\CollectionHelper;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MissingMethodFromReflectionException;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function array_intersect;
use function count;
use function in_array;
/** @internal */
final class ModelDynamicStaticMethodReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
{
public function __construct(
private BuilderHelper $builderHelper,
private CollectionHelper $collectionHelper,
private ReflectionProvider $reflectionProvider,
) {
}
public function getClass(): string
{
return Model::class;
}
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
{
$name = $methodReflection->getName();
if ($name === '__construct') {
return false;
}
// Another extension handles this case
if (Str::startsWith($name, 'find')) {
return false;
}
if (in_array($name, ['get', 'hydrate', 'fromQuery'], true)) {
return true;
}
return $this->reflectionProvider->getClass(Model::class)->hasNativeMethod($name);
}
public function getTypeFromStaticMethodCall(
MethodReflection $methodReflection,
StaticCall $methodCall,
Scope $scope,
): Type|null {
$method = $methodReflection->getDeclaringClass()
->getMethod($methodReflection->getName(), $scope);
$returnType = ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $method->getVariants())->getReturnType();
if (count(array_intersect([EloquentBuilder::class, QueryBuilder::class, Collection::class], $returnType->getReferencedClasses())) === 0) {
return null;
}
if (count(array_intersect([EloquentBuilder::class], $returnType->getReferencedClasses())) > 0) {
if ($methodCall->class instanceof Name) {
return new GenericObjectType(
$this->builderHelper->determineBuilderName($scope->resolveName($methodCall->class)),
[$scope->resolveTypeByName($methodCall->class)],
);
}
$type = $scope->getType($methodCall->class);
if ($type->isClassStringType()->yes()) {
$type = $type->getClassStringObjectType();
}
$classNames = $type->getObjectClassNames();
$types = [];
foreach ($classNames as $className) {
if (! $this->reflectionProvider->hasClass($className)) {
continue;
}
try {
$types[] = new GenericObjectType(
$this->builderHelper->determineBuilderName($className),
[new ObjectType($className)],
);
} catch (MissingMethodFromReflectionException) {
}
}
if ($types !== []) {
return TypeCombinator::union(...$types);
}
}
if (in_array(Collection::class, $returnType->getReferencedClasses(), true)) {
if ($methodCall->class instanceof Name) {
$modelNames = [$scope->resolveName($methodCall->class)];
} else {
$modelNames = $scope->getType($methodCall->class)->getObjectTypeOrClassStringObjectType()->getObjectClassNames();
}
$types = [];
foreach ($modelNames as $modelName) {
$types[] = $this->collectionHelper->determineCollectionClass($modelName);
}
if ($types !== []) {
return TypeCombinator::union(...$types);
}
}
return $returnType;
}
}

Some files were not shown because too many files have changed in this diff Show More