diff --git a/phpstan.src.neon.dist b/phpstan.src.neon.dist index f5d4e57505de..2a395a14f4ed 100644 --- a/phpstan.src.neon.dist +++ b/phpstan.src.neon.dist @@ -10,7 +10,6 @@ parameters: - "#Caught class [a-zA-Z0-9\\\\_]+ not found.#" - "#Class [a-zA-Z0-9\\\\_]+ not found.#" - "#has invalid type#" - - "#should always throw an exception or terminate script execution#" - "#Instantiated class [a-zA-Z0-9\\\\_]+ not found.#" - "#Unsafe usage of new static#" excludePaths: diff --git a/src/Illuminate/Collections/helpers.php b/src/Illuminate/Collections/helpers.php index 1854137ee6bf..4150c38ef621 100644 --- a/src/Illuminate/Collections/helpers.php +++ b/src/Illuminate/Collections/helpers.php @@ -224,9 +224,12 @@ function last($array) /** * Return the default value of the given value. * - * @param mixed $value - * @param mixed ...$args - * @return mixed + * @template TValue + * @template TArgs + * + * @param TValue|\Closure(TArgs): TValue $value + * @param TArgs ...$args + * @return TValue */ function value($value, ...$args) { diff --git a/src/Illuminate/Foundation/helpers.php b/src/Illuminate/Foundation/helpers.php index 264b3e5440b0..3fce37038614 100644 --- a/src/Illuminate/Foundation/helpers.php +++ b/src/Illuminate/Foundation/helpers.php @@ -109,9 +109,11 @@ function action($name, $parameters = [], $absolute = true) /** * Get the available container instance. * - * @param string|null $abstract + * @template TClass + * + * @param string|class-string|null $abstract * @param array $parameters - * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Foundation\Application|mixed + * @return ($abstract is class-string ? TClass : ($abstract is null ? \Illuminate\Foundation\Application : mixed)) */ function app($abstract = null, array $parameters = []) { @@ -155,7 +157,7 @@ function asset($path, $secure = null) * Get the available auth instance. * * @param string|null $guard - * @return \Illuminate\Contracts\Auth\Factory|\Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard + * @return ($guard is null ? \Illuminate\Contracts\Auth\Factory : \Illuminate\Contracts\Auth\StatefulGuard) */ function auth($guard = null) { @@ -228,28 +230,29 @@ function broadcast($event = null) * * If an array is passed, we'll assume you want to put to the cache. * - * @param mixed ...$arguments key|key,default|data,expiration|null - * @return mixed|\Illuminate\Cache\CacheManager + * @param string|array|null $key key|data + * @param mixed $default default|expiration|null + * @return ($key is null ? \Illuminate\Cache\CacheManager : ($key is string ? mixed : bool)) * * @throws \InvalidArgumentException */ - function cache(...$arguments) + function cache($key = null, $default = null) { - if (empty($arguments)) { + if (is_null($key)) { return app('cache'); } - if (is_string($arguments[0])) { - return app('cache')->get(...$arguments); + if (is_string($key)) { + return app('cache')->get($key, $default); } - if (! is_array($arguments[0])) { + if (! is_array($key)) { throw new InvalidArgumentException( 'When setting a value in the cache, you must pass an array of key / value pairs.' ); } - return app('cache')->put(key($arguments[0]), reset($arguments[0]), $arguments[1] ?? null); + return app('cache')->put(key($key), reset($key), ttl: $default); } } @@ -259,9 +262,9 @@ function cache(...$arguments) * * If an array is passed as the key, we will assume you want to set an array of values. * - * @param array|string|null $key + * @param array|string|null $key * @param mixed $default - * @return mixed|\Illuminate\Config\Repository + * @return ($key is null ? \Illuminate\Config\Repository : ($key is string ? mixed : null)) */ function config($key = null, $default = null) { @@ -297,13 +300,16 @@ function config_path($path = '') * @param array|string|null $key * @param mixed $default * @return mixed|\Illuminate\Log\Context\Repository + * @return ($key is string ? mixed : \Illuminate\Log\Context\Repository) */ function context($key = null, $default = null) { + $context = app(ContextRepository::class); + return match (true) { - is_null($key) => app(ContextRepository::class), - is_array($key) => app(ContextRepository::class)->add($key), - default => app(ContextRepository::class)->get($key, $default), + is_null($key) => $context, + is_array($key) => $context->add($key), + default => $context->get($key, $default), }; } } @@ -321,7 +327,7 @@ function context($key = null, $default = null) * @param bool $httpOnly * @param bool $raw * @param string|null $sameSite - * @return \Illuminate\Cookie\CookieJar|\Symfony\Component\HttpFoundation\Cookie + * @return ($name is null ? \Illuminate\Cookie\CookieJar : \Symfony\Component\HttpFoundation\Cookie) */ function cookie($name = null, $value = null, $minutes = 0, $path = null, $domain = null, $secure = null, $httpOnly = true, $raw = false, $sameSite = null) { @@ -400,6 +406,7 @@ function decrypt($value, $unserialize = true) * * @param mixed $job * @return \Illuminate\Foundation\Bus\PendingDispatch + * @return ($job is \Closure ? \Illuminate\Foundation\Bus\PendingClosureDispatch : \Illuminate\Foundation\Bus\PendingDispatch) */ function dispatch($job) { @@ -499,7 +506,7 @@ function info($message, $context = []) * * @param string|null $message * @param array $context - * @return \Illuminate\Log\LogManager|null + * @return ($message is null ? \Illuminate\Log\LogManager : null) */ function logger($message = null, array $context = []) { @@ -529,7 +536,7 @@ function lang_path($path = '') * Get a log driver instance. * * @param string|null $driver - * @return \Illuminate\Log\LogManager|\Psr\Log\LoggerInterface + * @return ($driver is null ? \Illuminate\Log\LogManager : \Psr\Log\LoggerInterface) */ function logs($driver = null) { @@ -658,7 +665,7 @@ function public_path($path = '') * @param int $status * @param array $headers * @param bool|null $secure - * @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse + * @return ($to is null ? \Illuminate\Routing\Redirector : \Illuminate\Http\RedirectResponse) */ function redirect($to = null, $status = 302, $headers = [], $secure = null) { @@ -723,9 +730,9 @@ function report_unless($boolean, $exception) /** * Get an instance of the current request or an input item from the request. * - * @param array|string|null $key + * @param list|string|null $key * @param mixed $default - * @return mixed|\Illuminate\Http\Request|string|array|null + * @return ($key is null ? \Illuminate\Http\Request : ($key is string ? mixed : array)) */ function request($key = null, $default = null) { @@ -747,13 +754,13 @@ function request($key = null, $default = null) /** * Catch a potential exception and return a default value. * - * @template TRescueValue - * @template TRescueFallback + * @template TValue + * @template TFallback * - * @param callable(): TRescueValue $callback - * @param (callable(\Throwable): TRescueFallback)|TRescueFallback $rescue - * @param bool|callable $report - * @return TRescueValue|TRescueFallback + * @param callable(): TValue $callback + * @param (callable(\Throwable): TFallback)|TFallback $rescue + * @param bool|callable(\Throwable): bool $report + * @return TValue|TFallback */ function rescue(callable $callback, $rescue = null, $report = true) { @@ -773,9 +780,11 @@ function rescue(callable $callback, $rescue = null, $report = true) /** * Resolve a service from the container. * - * @param string $name + * @template TClass + * + * @param string|class-string $name * @param array $parameters - * @return mixed + * @return ($name is class-string ? TClass : mixed) */ function resolve($name, array $parameters = []) { @@ -803,9 +812,9 @@ function resource_path($path = '') * @param \Illuminate\Contracts\View\View|string|array|null $content * @param int $status * @param array $headers - * @return \Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory + * @return ($content is null ? \Illuminate\Contracts\Routing\ResponseFactory : \Illuminate\Http\Response) */ - function response($content = '', $status = 200, array $headers = []) + function response($content = null, $status = 200, array $headers = []) { $factory = app(ResponseFactory::class); @@ -813,7 +822,7 @@ function response($content = '', $status = 200, array $headers = []) return $factory; } - return $factory->make($content, $status, $headers); + return $factory->make($content ?? '', $status, $headers); } } @@ -865,9 +874,9 @@ function secure_url($path, $parameters = []) * * If an array is passed as the key, we will assume you want to set an array of values. * - * @param array|string|null $key + * @param array|string|null $key * @param mixed $default - * @return mixed|\Illuminate\Session\Store|\Illuminate\Session\SessionManager + * @return ($key is null ? \Illuminate\Session\SessionManager : ($key is string ? mixed : null)) */ function session($key = null, $default = null) { @@ -932,7 +941,7 @@ function today($tz = null) * @param string|null $key * @param array $replace * @param string|null $locale - * @return \Illuminate\Contracts\Translation\Translator|string|array|null + * @return ($key is null ? \Illuminate\Contracts\Translation\Translator : array|string) */ function trans($key = null, $replace = [], $locale = null) { @@ -986,7 +995,7 @@ function __($key = null, $replace = [], $locale = null) * @param string|null $path * @param mixed $parameters * @param bool|null $secure - * @return \Illuminate\Contracts\Routing\UrlGenerator|string + * @return ($path is null ? \Illuminate\Contracts\Routing\UrlGenerator : string) */ function url($path = null, $parameters = [], $secure = null) { @@ -1002,13 +1011,13 @@ function url($path = null, $parameters = [], $secure = null) /** * Create a new Validator instance. * - * @param array $data + * @param array|null $data * @param array $rules * @param array $messages * @param array $attributes - * @return \Illuminate\Contracts\Validation\Validator|\Illuminate\Contracts\Validation\Factory + * @return ($data is null ? \Illuminate\Contracts\Validation\Factory : \Illuminate\Contracts\Validation\Validator) */ - function validator(array $data = [], array $rules = [], array $messages = [], array $attributes = []) + function validator(?array $data = null, array $rules = [], array $messages = [], array $attributes = []) { $factory = app(ValidationFactory::class); @@ -1016,7 +1025,7 @@ function validator(array $data = [], array $rules = [], array $messages = [], ar return $factory; } - return $factory->make($data, $rules, $messages, $attributes); + return $factory->make($data ?? [], $rules, $messages, $attributes); } } @@ -1027,7 +1036,7 @@ function validator(array $data = [], array $rules = [], array $messages = [], ar * @param string|null $view * @param \Illuminate\Contracts\Support\Arrayable|array $data * @param array $mergeData - * @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory + * @return ($view is null ? \Illuminate\Contracts\View\Factory : \Illuminate\Contracts\View\View) */ function view($view = null, $data = [], $mergeData = []) { diff --git a/src/Illuminate/Support/helpers.php b/src/Illuminate/Support/helpers.php index 5e461898a568..d46bf6ffcfd3 100644 --- a/src/Illuminate/Support/helpers.php +++ b/src/Illuminate/Support/helpers.php @@ -39,6 +39,10 @@ function append_config(array $array) /** * Determine if the given value is "blank". * + * @phpstan-assert-if-false !=null|'' $value + * + * @phpstan-assert-if-true !=numeric|bool $value + * * @param mixed $value * @return bool */ @@ -150,6 +154,10 @@ function env($key, $default = null) /** * Determine if a value is "filled". * + * @phpstan-assert-if-true !=null|'' $value + * + * @phpstan-assert-if-false !=numeric|bool $value + * * @param mixed $value * @return bool */ @@ -192,10 +200,12 @@ function literal(...$arguments) /** * Get an item from an object using "dot" notation. * - * @param object $object + * @template TValue of object + * + * @param TValue $object * @param string|null $key * @param mixed $default - * @return mixed + * @return ($key is empty ? TValue : mixed) */ function object_get($object, $key, $default = null) { @@ -239,9 +249,12 @@ function once(callable $callback) /** * Provide access to optional objects. * - * @param mixed $value - * @param callable|null $callback - * @return mixed + * @template TValue + * @template TReturn + * + * @param TValue $value + * @param (callable(TValue): TReturn)|null $callback + * @return ($callback is null ? \Illuminate\Support\Optional : ($value is null ? null : TReturn)) */ function optional($value = null, ?callable $callback = null) { @@ -276,11 +289,13 @@ function preg_replace_array($pattern, array $replacements, $subject) /** * Retry an operation a given number of times. * - * @param int|array $times - * @param callable $callback - * @param int|\Closure $sleepMilliseconds - * @param callable|null $when - * @return mixed + * @template TValue + * + * @param int|array $times + * @param callable(int): TValue $callback + * @param int|\Closure(int, \Throwable): int $sleepMilliseconds + * @param (callable(\Throwable): bool)|null $when + * @return TValue * * @throws \Throwable */ @@ -323,7 +338,7 @@ function retry($times, callable $callback, $sleepMilliseconds = 0, $when = null) * Get a new stringable object from the given string. * * @param string|null $string - * @return \Illuminate\Support\Stringable|mixed + * @return ($string is null ? object : \Illuminate\Support\Stringable) */ function str($string = null) { @@ -372,12 +387,13 @@ function tap($value, $callback = null) /** * Throw the given exception if the given condition is true. * + * @template TValue * @template TException of \Throwable * - * @param mixed $condition + * @param TValue $condition * @param TException|class-string|string $exception * @param mixed ...$parameters - * @return mixed + * @return TValue * * @throws TException */ @@ -399,12 +415,13 @@ function throw_if($condition, $exception = 'RuntimeException', ...$parameters) /** * Throw the given exception unless the given condition is true. * + * @template TValue * @template TException of \Throwable * - * @param mixed $condition + * @param TValue $condition * @param TException|class-string|string $exception * @param mixed ...$parameters - * @return mixed + * @return TValue * * @throws TException */ @@ -439,14 +456,14 @@ function trait_uses_recursive($trait) /** * Transform the given value if it is present. * - * @template TValue of mixed - * @template TReturn of mixed - * @template TDefault of mixed + * @template TValue + * @template TReturn + * @template TDefault * * @param TValue $value * @param callable(TValue): TReturn $callback - * @param TDefault|callable(TValue): TDefault|null $default - * @return ($value is empty ? ($default is null ? null : TDefault) : TReturn) + * @param TDefault|callable(TValue): TDefault $default + * @return ($value is empty ? TDefault : TReturn) */ function transform($value, callable $callback, $default = null) { diff --git a/tests/Foundation/FoundationHelpersTest.php b/tests/Foundation/FoundationHelpersTest.php index 90b56692715d..819397922425 100644 --- a/tests/Foundation/FoundationHelpersTest.php +++ b/tests/Foundation/FoundationHelpersTest.php @@ -4,6 +4,7 @@ use Exception; use Illuminate\Container\Container; +use Illuminate\Contracts\Cache\Repository as CacheRepository; use Illuminate\Contracts\Config\Repository; use Illuminate\Contracts\Support\Responsable; use Illuminate\Foundation\Application; @@ -13,7 +14,6 @@ use Illuminate\Support\Str; use Mockery as m; use PHPUnit\Framework\TestCase; -use stdClass; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; class FoundationHelpersTest extends TestCase @@ -26,17 +26,17 @@ protected function tearDown(): void public function testCache() { $app = new Application; - $app['cache'] = $cache = m::mock(stdClass::class); + $app['cache'] = $cache = m::mock(CacheRepository::class); // 1. cache() - $this->assertInstanceOf(stdClass::class, cache()); + $this->assertInstanceOf(CacheRepository::class, cache()); // 2. cache(['foo' => 'bar'], 1); $cache->shouldReceive('put')->once()->with('foo', 'bar', 1); cache(['foo' => 'bar'], 1); // 3. cache('foo'); - $cache->shouldReceive('get')->once()->with('foo')->andReturn('bar'); + $cache->shouldReceive('get')->once()->with('foo', null)->andReturn('bar'); $this->assertSame('bar', cache('foo')); // 4. cache('foo', null); diff --git a/types/Collections/helpers.php b/types/Collections/helpers.php new file mode 100644 index 000000000000..ddfc56ad1656 --- /dev/null +++ b/types/Collections/helpers.php @@ -0,0 +1,11 @@ + 42)); +assertType('int', value(function ($foo) { + assertType('bool', $foo); + + return 42; +}, true)); diff --git a/types/Foundation/Helpers.php b/types/Foundation/Helpers.php new file mode 100644 index 000000000000..d3317b6fd15e --- /dev/null +++ b/types/Foundation/Helpers.php @@ -0,0 +1,69 @@ + 'bar'], 42)); +assertType('mixed', cache('foo', 42)); + +assertType('Illuminate\Config\Repository', config()); +assertType('null', config(['foo' => 'bar'])); +assertType('mixed', config('foo')); + +assertType('Illuminate\Log\Context\Repository', context()); +assertType('Illuminate\Log\Context\Repository', context(['foo' => 'bar'])); +assertType('mixed', context('foo')); + +assertType('Illuminate\Cookie\CookieJar', cookie()); +assertType('Symfony\Component\HttpFoundation\Cookie', cookie('foo')); + +assertType('Illuminate\Foundation\Bus\PendingDispatch', dispatch('foo')); +assertType('Illuminate\Foundation\Bus\PendingClosureDispatch', dispatch(fn () => 1)); + +assertType('Illuminate\Log\LogManager', logger()); +assertType('null', logger('foo')); + +assertType('Illuminate\Log\LogManager', logs()); +assertType('Psr\Log\LoggerInterface', logs('foo')); + +assertType('int|null', rescue(fn () => 123)); +assertType('int', rescue(fn () => 123, 345)); +assertType('int', rescue(fn () => 123, fn () => 345)); + +assertType('Illuminate\Routing\Redirector', redirect()); +assertType('Illuminate\Http\RedirectResponse', redirect('foo')); + +assertType('mixed', resolve('foo')); +assertType('Illuminate\Config\Repository', resolve(Repository::class)); + +assertType('Illuminate\Http\Request', request()); +assertType('mixed', request('foo')); +assertType('array', request(['foo', 'bar'])); + +assertType('Illuminate\Contracts\Routing\ResponseFactory', response()); +assertType('Illuminate\Http\Response', response('foo')); + +assertType('Illuminate\Session\SessionManager', session()); +assertType('mixed', session('foo')); +assertType('null', session(['foo' => 'bar'])); + +assertType('Illuminate\Contracts\Translation\Translator', trans()); +assertType('array|string', trans('foo')); + +assertType('Illuminate\Contracts\Validation\Factory', validator()); +assertType('Illuminate\Contracts\Validation\Validator', validator([])); + +assertType('Illuminate\Contracts\View\Factory', view()); +assertType('Illuminate\Contracts\View\View', view('foo')); + +assertType('Illuminate\Contracts\Routing\UrlGenerator', url()); +assertType('string', url('foo')); diff --git a/types/Support/Helpers.php b/types/Support/Helpers.php index 27a6b5b3a98e..80f8a27f9d0e 100644 --- a/types/Support/Helpers.php +++ b/types/Support/Helpers.php @@ -2,73 +2,59 @@ use function PHPStan\Testing\assertType; -assertType('int', once(fn () => 1)); - -assertType('User', once(fn () => new User())); +/** @var bool|float|int|string|null $value */ +if (filled($value)) { + assertType('bool|float|int|non-empty-string', $value); +} else { + assertType('string|null', $value); +} + +if (blank($value)) { + assertType('string|null', $value); +} else { + assertType('bool|float|int|non-empty-string', $value); +} + +assertType('User', object_get(new User(), null)); +assertType('User', object_get(new User(), '')); +assertType('mixed', object_get(new User(), 'name')); -$value = once(function () { // @phpstan-ignore-line - // -}); - -assertType('null', $value); +assertType('int', once(fn () => 1)); +assertType('null', once(function () { /** @phpstan-ignore function.void (testing void) */ +})); -assertType('User', with(new User())); -assertType('bool', with(new User())->save()); +assertType('Illuminate\Support\Optional', optional()); +assertType('null', optional(null, fn () => 1)); +assertType('int', optional('foo', function ($value) { + assertType('string', $value); -assertType('User', with(new User(), function (User $user) { - return $user; -})); -assertType('User', with(new User(), function (User $user): User { - return $user; + return 1; })); -assertType('User', with(new User(), function ($user) { - /** @var User $user */ - return $user; -})); -assertType('User', with(new User(), function ($user): User { - /** @var User $user */ - return $user; -})); +assertType('int', retry(5, fn () => 1)); -assertType('int', with(new User(), function ($user) { - assertType('User', $user); +assertType('object', str()); +assertType('Illuminate\Support\Stringable', str('foo')); - return 10; -})); -assertType('int', with(new User(), function ($user): int { +assertType('User', tap(new User(), function ($user) { assertType('User', $user); - - return 10; })); +assertType('Illuminate\Support\HigherOrderTapProxy', tap(new User())); -assertType('User', with(new User(), function ($user) { - return $user; -})); -assertType('User', with(new User(), function ($user): User { - return $user; -})); +assertType('bool', throw_if(true, Exception::class)); -// falls back to default if provided -assertType('int|null', transform(optional(), fn () => 1)); -// default as callable -assertType('int|string', transform(optional(), fn () => 1, fn () => 'string')); +assertType('bool', throw_unless(true, Exception::class)); -// non empty values -assertType('int', transform('filled', fn () => 1)); +assertType('int', transform('filled', fn () => 1, true)); assertType('int', transform(['filled'], fn () => 1)); -assertType('int', transform(new User(), fn () => 1)); - -// "empty" values -assertType('null', transform(null, fn () => 1)); assertType('null', transform('', fn () => 1)); -assertType('null', transform([], fn () => 1)); - -assertType('int|null', rescue(fn () => 123)); -assertType('int', rescue(fn () => 123, 345)); -assertType('int', rescue(fn () => 123, fn () => 345)); +assertType('bool', transform('', fn () => 1, true)); +assertType('bool', transform('', fn () => 1, fn () => true)); -assertType('User', tap(new User(), function ($user) { +assertType('User', with(new User())); +assertType('bool', with(new User())->save()); +assertType('int', with(new User(), function ($user) { assertType('User', $user); + + return 10; })); -assertType('Illuminate\Support\HigherOrderTapProxy', tap(new User()));