From b191b9ac1d1ca7a186a3b7bc42459c645d3af799 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Wed, 29 Oct 2025 09:38:59 +0800 Subject: [PATCH 01/18] feat: add PHPStan type checking configuration and composer script Add PHPStan configuration for type assertion testing and a new composer script for running type checks. This includes: - New phpstan.types.neon.dist configuration file for type checking - New types/ directory with helper function type assertions - New composer script 'check:types' for running type analysis This enhancement allows developers to validate type assertions for helper functions and ensure type safety across the codebase. --- composer.json | 1 + phpstan.types.neon.dist | 4 ++++ types/Helpers/Functions.php | 21 +++++++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 phpstan.types.neon.dist create mode 100644 types/Helpers/Functions.php diff --git a/composer.json b/composer.json index a5405fb24..42231e478 100644 --- a/composer.json +++ b/composer.json @@ -311,6 +311,7 @@ }, "scripts": { "analyse": "@php vendor/bin/phpstan analyse --memory-limit=-1", + "check:types": "phpstan analyze --configuration=\"phpstan.types.neon.dist\" --memory-limit=-1", "cs-fix": "@php vendor/bin/php-cs-fixer fix $1 --verbose", "gen:readme": [ "./bin/regenerate-readme.sh > src/.github/profile/README.md", diff --git a/phpstan.types.neon.dist b/phpstan.types.neon.dist new file mode 100644 index 000000000..be73ed150 --- /dev/null +++ b/phpstan.types.neon.dist @@ -0,0 +1,4 @@ +parameters: + level: max + paths: + - types diff --git a/types/Helpers/Functions.php b/types/Helpers/Functions.php new file mode 100644 index 000000000..4d5a0076a --- /dev/null +++ b/types/Helpers/Functions.php @@ -0,0 +1,21 @@ + Date: Wed, 29 Oct 2025 10:24:08 +0800 Subject: [PATCH 02/18] Add comprehensive PHPStan type tests for all helper functions (#971) * Initial plan * feat: add comprehensive type tests for all helper functions Co-authored-by: huangdijia <8337659+huangdijia@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: huangdijia <8337659+huangdijia@users.noreply.github.com> --- types/Helpers/Command/Functions.php | 17 +++ types/Helpers/Functions.php | 171 +++++++++++++++++++++++++++- 2 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 types/Helpers/Command/Functions.php diff --git a/types/Helpers/Command/Functions.php b/types/Helpers/Command/Functions.php new file mode 100644 index 000000000..af1c3d043 --- /dev/null +++ b/types/Helpers/Command/Functions.php @@ -0,0 +1,17 @@ + 'value'])); diff --git a/types/Helpers/Functions.php b/types/Helpers/Functions.php index 4d5a0076a..98f5d3000 100644 --- a/types/Helpers/Functions.php +++ b/types/Helpers/Functions.php @@ -8,14 +8,181 @@ * @document https://github.com/friendsofhyperf/components/blob/main/README.md * @contact huangdijia@gmail.com */ +use Carbon\Carbon; +use FriendsOfHyperf\Support\Environment; +use Hyperf\Contract\SessionInterface; +use Hyperf\HttpMessage\Cookie\Cookie; +use Hyperf\HttpMessage\Cookie\CookieJarInterface; +use Hyperf\HttpServer\Contract\RequestInterface; +use Hyperf\HttpServer\Contract\ResponseInterface; use Hyperf\Support\Fluent; +use Hyperf\Validation\Contract\ValidatorFactoryInterface; +use Hyperf\Validation\Contract\ValidatorInterface; +use Psr\Container\ContainerInterface; +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Psr\SimpleCache\CacheInterface; use function FriendsOfHyperf\Helpers\app; use function FriendsOfHyperf\Helpers\base_path; +use function FriendsOfHyperf\Helpers\blank; +use function FriendsOfHyperf\Helpers\cache; +use function FriendsOfHyperf\Helpers\class_namespace; +use function FriendsOfHyperf\Helpers\cookie; +use function FriendsOfHyperf\Helpers\di; +use function FriendsOfHyperf\Helpers\dispatch; +use function FriendsOfHyperf\Helpers\enum_value; +use function FriendsOfHyperf\Helpers\environment; +use function FriendsOfHyperf\Helpers\event; +use function FriendsOfHyperf\Helpers\filled; +use function FriendsOfHyperf\Helpers\fluent; +use function FriendsOfHyperf\Helpers\get_client_ip; +use function FriendsOfHyperf\Helpers\info; +use function FriendsOfHyperf\Helpers\literal; +use function FriendsOfHyperf\Helpers\logger; +use function FriendsOfHyperf\Helpers\logs; +use function FriendsOfHyperf\Helpers\now; +use function FriendsOfHyperf\Helpers\object_get; +use function FriendsOfHyperf\Helpers\preg_replace_array; +use function FriendsOfHyperf\Helpers\request; +use function FriendsOfHyperf\Helpers\rescue; +use function FriendsOfHyperf\Helpers\resolve; +use function FriendsOfHyperf\Helpers\response; +use function FriendsOfHyperf\Helpers\session; +use function FriendsOfHyperf\Helpers\throw_if; +use function FriendsOfHyperf\Helpers\throw_unless; +use function FriendsOfHyperf\Helpers\today; +use function FriendsOfHyperf\Helpers\transform; +use function FriendsOfHyperf\Helpers\validator; +use function FriendsOfHyperf\Helpers\when; use function PHPStan\Testing\assertType; +// app() tests +assertType('mixed', app()); +assertType('Hyperf\Support\Fluent', app(Fluent::class)); +assertType('Closure', app(fn() => 'test')); + +// base_path() tests assertType('string', base_path()); assertType('string', base_path('foo/bar')); -assertType('mixed', app()); -assertType('mixed', app(Fluent::class)); +// blank() tests +assertType('bool', blank(null)); +assertType('bool', blank('')); +assertType('bool', blank('test')); +assertType('bool', blank([])); + +// cache() tests +assertType('Psr\SimpleCache\CacheInterface', cache()); +assertType('mixed', cache('key')); + +// class_namespace() tests +assertType('string', class_namespace(Fluent::class)); +assertType('string', class_namespace(new Fluent())); + +// di() tests +assertType('Psr\Container\ContainerInterface', di()); +assertType('Hyperf\Support\Fluent', di(Fluent::class)); + +// enum_value() tests +assertType('mixed', enum_value('test')); +assertType('string', enum_value('test', 'default')); + +// event() tests +$testEvent = new class() {}; +assertType('class@anonymous*', event($testEvent)); + +// filled() tests +assertType('bool', filled(null)); +assertType('bool', filled('test')); + +// fluent() tests +assertType('Hyperf\Support\Fluent', fluent([])); +assertType('Hyperf\Support\Fluent', fluent(['key' => 'value'])); + +// get_client_ip() tests +assertType('string', get_client_ip()); + +// literal() tests +assertType('stdClass', literal()); +assertType('stdClass', literal(key: 'value')); + +// logger() tests +assertType('Psr\Log\LoggerInterface', logger()); +assertType('mixed', logger('test message')); + +// logs() tests +assertType('Psr\Log\LoggerInterface', logs()); +assertType('Psr\Log\LoggerInterface', logs('custom')); + +// object_get() tests +$obj = (object)['key' => 'value']; +assertType('stdClass', object_get($obj)); +assertType('mixed', object_get($obj, 'key')); + +// preg_replace_array() tests +assertType('string', preg_replace_array('/test/', ['replacement'], 'test string')); + +// request() tests +assertType('Hyperf\HttpServer\Contract\RequestInterface', request()); +assertType('mixed', request('key')); +assertType('array', request(['key1', 'key2'])); + +// rescue() tests +assertType('string', rescue(fn() => 'result')); +assertType('string', rescue(fn() => throw new \Exception(), 'fallback')); + +// resolve() tests +assertType('Hyperf\Support\Fluent', resolve(Fluent::class)); +assertType('Closure', resolve(fn() => 'test')); + +// response() tests +assertType('Psr\Http\Message\ResponseInterface', response()); +assertType('Psr\Http\Message\ResponseInterface', response('content')); + +// throw_if() tests +assertType('bool', throw_if(false, 'Exception')); + +// throw_unless() tests +assertType('bool', throw_unless(true, 'Exception')); + +// transform() tests +assertType('string', transform('value', fn($v) => $v)); +assertType('mixed', transform(null, fn($v) => $v)); + +// validator() tests +assertType('Hyperf\Validation\Contract\ValidatorFactoryInterface', validator()); +assertType('Hyperf\Contract\ValidatorInterface', validator([], [])); + +// when() tests +assertType('mixed', when(true, 'value')); +assertType('mixed', when(false, 'value', 'default')); + +// cookie() tests +assertType('Hyperf\HttpMessage\Cookie\CookieJarInterface', cookie()); +assertType('Hyperf\HttpMessage\Cookie\Cookie', cookie('name', 'value')); + +// dispatch() tests - returns bool +// Note: dispatch() has complex return types based on job type, testing the common case +assertType('bool', dispatch(new class implements \Hyperf\AsyncQueue\JobInterface { + public function handle() {} +})); + +// environment() tests +assertType('FriendsOfHyperf\Support\Environment|bool', environment()); +assertType('bool', environment('production')); + +// info() tests +assertType('mixed', info('message')); + +// now() tests +assertType('Carbon\Carbon', now()); +assertType('Carbon\Carbon', now('UTC')); + +// session() tests +assertType('Hyperf\Contract\SessionInterface', session()); +assertType('mixed', session('key')); + +// today() tests +assertType('Carbon\Carbon', today()); +assertType('Carbon\Carbon', today('UTC')); From 59894aa6de1a54c9d88c73dea67347597dd820de Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:22:38 +0800 Subject: [PATCH 03/18] Move call() tests to Functions.php and update type assertions Deleted Command/Functions.php and moved its call() tests to Helpers/Functions.php. Updated type assertions and test cases in Helpers/Functions.php for improved accuracy and consistency with current return types. Co-Authored-By: Deeka Wong <8337659+huangdijia@users.noreply.github.com> --- types/Helpers/Command/Functions.php | 17 ------- types/Helpers/Functions.php | 78 ++++++++++++++++------------- 2 files changed, 44 insertions(+), 51 deletions(-) delete mode 100644 types/Helpers/Command/Functions.php diff --git a/types/Helpers/Command/Functions.php b/types/Helpers/Command/Functions.php deleted file mode 100644 index af1c3d043..000000000 --- a/types/Helpers/Command/Functions.php +++ /dev/null @@ -1,17 +0,0 @@ - 'value'])); diff --git a/types/Helpers/Functions.php b/types/Helpers/Functions.php index 98f5d3000..2da3c537c 100644 --- a/types/Helpers/Functions.php +++ b/types/Helpers/Functions.php @@ -8,26 +8,16 @@ * @document https://github.com/friendsofhyperf/components/blob/main/README.md * @contact huangdijia@gmail.com */ -use Carbon\Carbon; use FriendsOfHyperf\Support\Environment; -use Hyperf\Contract\SessionInterface; use Hyperf\HttpMessage\Cookie\Cookie; -use Hyperf\HttpMessage\Cookie\CookieJarInterface; -use Hyperf\HttpServer\Contract\RequestInterface; -use Hyperf\HttpServer\Contract\ResponseInterface; use Hyperf\Support\Fluent; -use Hyperf\Validation\Contract\ValidatorFactoryInterface; -use Hyperf\Validation\Contract\ValidatorInterface; -use Psr\Container\ContainerInterface; -use Psr\EventDispatcher\EventDispatcherInterface; -use Psr\Log\LoggerInterface; -use Psr\SimpleCache\CacheInterface; use function FriendsOfHyperf\Helpers\app; use function FriendsOfHyperf\Helpers\base_path; use function FriendsOfHyperf\Helpers\blank; use function FriendsOfHyperf\Helpers\cache; use function FriendsOfHyperf\Helpers\class_namespace; +use function FriendsOfHyperf\Helpers\Command\call; use function FriendsOfHyperf\Helpers\cookie; use function FriendsOfHyperf\Helpers\di; use function FriendsOfHyperf\Helpers\dispatch; @@ -59,8 +49,8 @@ // app() tests assertType('mixed', app()); -assertType('Hyperf\Support\Fluent', app(Fluent::class)); -assertType('Closure', app(fn() => 'test')); +assertType('mixed', app(Fluent::class)); +assertType('Closure', app(fn () => 'test')); // base_path() tests assertType('string', base_path()); @@ -73,7 +63,7 @@ assertType('bool', blank([])); // cache() tests -assertType('Psr\SimpleCache\CacheInterface', cache()); +assertType('mixed', cache()); assertType('mixed', cache('key')); // class_namespace() tests @@ -82,15 +72,15 @@ // di() tests assertType('Psr\Container\ContainerInterface', di()); -assertType('Hyperf\Support\Fluent', di(Fluent::class)); +assertType('mixed', di(Fluent::class)); // enum_value() tests assertType('mixed', enum_value('test')); -assertType('string', enum_value('test', 'default')); +assertType('mixed', enum_value('test', 'default')); // event() tests -$testEvent = new class() {}; -assertType('class@anonymous*', event($testEvent)); +$testEvent = new class {}; +assertType('AnonymousClass7af6ae4c28737d2b2877adcdeb4da107', event($testEvent)); // filled() tests assertType('bool', filled(null)); @@ -116,8 +106,8 @@ assertType('Psr\Log\LoggerInterface', logs('custom')); // object_get() tests -$obj = (object)['key' => 'value']; -assertType('stdClass', object_get($obj)); +$obj = (object) ['key' => 'value']; +assertType('object{key: string}&stdClass', object_get($obj)); assertType('mixed', object_get($obj, 'key')); // preg_replace_array() tests @@ -129,16 +119,16 @@ assertType('array', request(['key1', 'key2'])); // rescue() tests -assertType('string', rescue(fn() => 'result')); -assertType('string', rescue(fn() => throw new \Exception(), 'fallback')); +assertType('string|null', rescue(fn () => 'result')); +assertType('string', rescue(fn () => throw new Exception(), 'fallback')); // resolve() tests -assertType('Hyperf\Support\Fluent', resolve(Fluent::class)); -assertType('Closure', resolve(fn() => 'test')); +assertType('mixed', resolve(Fluent::class)); +assertType('Closure', resolve(fn () => 'test')); // response() tests -assertType('Psr\Http\Message\ResponseInterface', response()); -assertType('Psr\Http\Message\ResponseInterface', response('content')); +assertType('Hyperf\HttpServer\Contract\ResponseInterface|Psr\Http\Message\ResponseInterface', response()); +assertType('Hyperf\HttpServer\Contract\ResponseInterface|Psr\Http\Message\ResponseInterface', response('content')); // throw_if() tests assertType('bool', throw_if(false, 'Exception')); @@ -147,12 +137,12 @@ assertType('bool', throw_unless(true, 'Exception')); // transform() tests -assertType('string', transform('value', fn($v) => $v)); -assertType('mixed', transform(null, fn($v) => $v)); +assertType('string', transform('value', fn ($v) => $v)); +assertType('null', transform(null, fn ($v) => $v)); // validator() tests -assertType('Hyperf\Validation\Contract\ValidatorFactoryInterface', validator()); -assertType('Hyperf\Contract\ValidatorInterface', validator([], [])); +assertType('Hyperf\Contract\ValidatorInterface|Hyperf\Validation\Contract\ValidatorFactoryInterface', validator()); +assertType('Hyperf\Contract\ValidatorInterface|Hyperf\Validation\Contract\ValidatorFactoryInterface', validator([], [])); // when() tests assertType('mixed', when(true, 'value')); @@ -164,13 +154,29 @@ // dispatch() tests - returns bool // Note: dispatch() has complex return types based on job type, testing the common case -assertType('bool', dispatch(new class implements \Hyperf\AsyncQueue\JobInterface { - public function handle() {} +assertType('bool', dispatch(new class implements Hyperf\AsyncQueue\JobInterface { + public function handle(): void + { + } + + public function fail(\Throwable $e): void + { + } + + public function getMaxAttempts(): int + { + return 0; + } + + public function setMaxAttempts(int $maxAttempts): static + { + return $this; + } })); // environment() tests -assertType('FriendsOfHyperf\Support\Environment|bool', environment()); -assertType('bool', environment('production')); +assertType('bool|FriendsOfHyperf\Support\Environment', environment()); +assertType('bool|FriendsOfHyperf\Support\Environment', environment('production')); // info() tests assertType('mixed', info('message')); @@ -186,3 +192,7 @@ public function handle() {} // today() tests assertType('Carbon\Carbon', today()); assertType('Carbon\Carbon', today('UTC')); + +// call() tests +assertType('int', call('command:name')); +assertType('int', call('command:name', ['arg' => 'value'])); From 5c47d7e6dbc27e10fd7592b2908723f66a32da9a Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:25:04 +0800 Subject: [PATCH 04/18] fix: remove unnecessary backslash from Throwable type hint in dispatch job --- types/Helpers/Functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/Helpers/Functions.php b/types/Helpers/Functions.php index 2da3c537c..61de7da8e 100644 --- a/types/Helpers/Functions.php +++ b/types/Helpers/Functions.php @@ -159,7 +159,7 @@ public function handle(): void { } - public function fail(\Throwable $e): void + public function fail(Throwable $e): void { } From 1c809ee6dad46c9e415388153a5b1e898cb803d9 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:57:40 +0800 Subject: [PATCH 05/18] refactor: rename check:types script to type-testing for clarity --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 42231e478..c325c1760 100644 --- a/composer.json +++ b/composer.json @@ -311,7 +311,7 @@ }, "scripts": { "analyse": "@php vendor/bin/phpstan analyse --memory-limit=-1", - "check:types": "phpstan analyze --configuration=\"phpstan.types.neon.dist\" --memory-limit=-1", + "type-testing": "phpstan analyze --configuration=\"phpstan.types.neon.dist\" --memory-limit=-1", "cs-fix": "@php vendor/bin/php-cs-fixer fix $1 --verbose", "gen:readme": [ "./bin/regenerate-readme.sh > src/.github/profile/README.md", From 1c34be5d48ad6071c096dd6a7437d3e30e60c0d4 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:01:54 +0800 Subject: [PATCH 06/18] fix: move type-testing script to the correct position in composer.json --- composer.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index c325c1760..94443fae0 100644 --- a/composer.json +++ b/composer.json @@ -311,7 +311,6 @@ }, "scripts": { "analyse": "@php vendor/bin/phpstan analyse --memory-limit=-1", - "type-testing": "phpstan analyze --configuration=\"phpstan.types.neon.dist\" --memory-limit=-1", "cs-fix": "@php vendor/bin/php-cs-fixer fix $1 --verbose", "gen:readme": [ "./bin/regenerate-readme.sh > src/.github/profile/README.md", @@ -335,6 +334,8 @@ ], "test:lint": "@php vendor/bin/php-cs-fixer fix --dry-run --diff --ansi", "test:types": "@php vendor/bin/pest --type-coverage", - "test:unit": "@php vendor/bin/pest" + "test:unit": "@php vendor/bin/pest", + + "type-testing": "phpstan analyze --configuration=\"phpstan.types.neon.dist\" --memory-limit=-1" } } From 38865132ed46c59a4eda554a9ae13fab6fda963e Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:03:17 +0800 Subject: [PATCH 07/18] fix: remove unnecessary blank line before type-testing script in composer.json --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 94443fae0..277801581 100644 --- a/composer.json +++ b/composer.json @@ -335,7 +335,6 @@ "test:lint": "@php vendor/bin/php-cs-fixer fix --dry-run --diff --ansi", "test:types": "@php vendor/bin/pest --type-coverage", "test:unit": "@php vendor/bin/pest", - "type-testing": "phpstan analyze --configuration=\"phpstan.types.neon.dist\" --memory-limit=-1" } } From 6939a70d3376df836703070cd3d2e58c712bc02d Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:13:33 +0800 Subject: [PATCH 08/18] feat: add Support.php with various utility classes and type assertions --- types/Support/Support.php | 63 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 types/Support/Support.php diff --git a/types/Support/Support.php b/types/Support/Support.php new file mode 100644 index 000000000..021536ce6 --- /dev/null +++ b/types/Support/Support.php @@ -0,0 +1,63 @@ + 'value')); + +assertType(Onceable::class . '|null', Onceable::tryFromTrace(debug_backtrace(), static fn (): int => 1)); + +$timebox = new Timebox(); + +assertType('int', $timebox->call(static fn (Timebox $box): int => 42, 500)); + +assertType(Timebox::class, $timebox->returnEarly()); +assertType(Timebox::class, $timebox->dontReturnEarly()); + +assertType(Sleep::class, Sleep::for(1)); +assertType(Sleep::class, Sleep::sleep(1)); +assertType(Sleep::class, Sleep::usleep(1)); +assertType(Sleep::class, Sleep::until(1)); + +$sleep = Sleep::for(1)->and(1)->seconds(); +assertType(Sleep::class, $sleep); + +assertType('int', Number::parseInt('100')); +assertType('float', Number::parseFloat('100.5')); +assertType('float|int', Number::parse('100')); +assertType('string|false', Number::format(1000)); +assertType('float|int', Number::clamp(5, 1, 10)); + +$html = new HtmlString('
Hello
'); +assertType('string', $html->toHtml()); +assertType('bool', $html->isEmpty()); +assertType('bool', $html->isNotEmpty()); + +$environment = new Environment('production'); +assertType('string|null', $environment->get()); +assertType('bool', $environment->is('production')); + +assertType('Dotenv\Repository\RepositoryInterface', Env::getRepository()); + +$command = new RedisCommand('SET', ['key', 'value']); +assertType('string', (string) $command); From 447611e155e3cb1a1d2c982a33355f203f81782d Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:24:18 +0800 Subject: [PATCH 09/18] fix: add return type hints for __invoke methods in ClientBuilderFactory and HubFactory --- .../src/Factory/ClientBuilderFactory.php | 2 +- src/sentry/src/Factory/HubFactory.php | 3 +- types/Sentry/Sentry.php | 189 ++++++++++++++++++ 3 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 types/Sentry/Sentry.php diff --git a/src/sentry/src/Factory/ClientBuilderFactory.php b/src/sentry/src/Factory/ClientBuilderFactory.php index 71cd4ac05..8a6f5839d 100644 --- a/src/sentry/src/Factory/ClientBuilderFactory.php +++ b/src/sentry/src/Factory/ClientBuilderFactory.php @@ -36,7 +36,7 @@ class ClientBuilderFactory 'tracing', ]; - public function __invoke(ContainerInterface $container) + public function __invoke(ContainerInterface $container): ClientBuilder { $userConfig = $container->get(ConfigInterface::class)->get('sentry', []); $userConfig['enable_tracing'] ??= true; diff --git a/src/sentry/src/Factory/HubFactory.php b/src/sentry/src/Factory/HubFactory.php index 480229104..11dd34454 100644 --- a/src/sentry/src/Factory/HubFactory.php +++ b/src/sentry/src/Factory/HubFactory.php @@ -19,6 +19,7 @@ use Sentry\ClientBuilder; use Sentry\Integration as SdkIntegration; use Sentry\State\Hub; +use Sentry\State\HubInterface; use function Hyperf\Support\make; @@ -28,7 +29,7 @@ */ class HubFactory { - public function __invoke(ContainerInterface $container) + public function __invoke(ContainerInterface $container): HubInterface { $clientBuilder = $container->get(ClientBuilder::class); $options = $clientBuilder->getOptions(); diff --git a/types/Sentry/Sentry.php b/types/Sentry/Sentry.php new file mode 100644 index 000000000..e0e157f30 --- /dev/null +++ b/types/Sentry/Sentry.php @@ -0,0 +1,189 @@ + + */ + private array $items; + + /** + * @param arraytest
')->toHtmlString()); + +// Stringable::whenIsAscii() tests +assertType('Hyperf\Stringable\Stringable', Str::of('hello')->whenIsAscii(fn ($s) => $s->upper())); +assertType('Hyperf\Stringable\Stringable', Str::of('test')->whenIsAscii(fn ($s) => $s->upper(), fn ($s) => $s->lower())); + +// Stringable::doesntEndWith() tests +assertType('bool', Str::of('hello world')->doesntEndWith('world')); +assertType('bool', Str::of('test')->doesntEndWith(['ing', 'ed'])); + +// Stringable::doesntStartWith() tests +assertType('bool', Str::of('hello world')->doesntStartWith('hello')); +assertType('bool', Str::of('test')->doesntStartWith(['pre', 'post'])); From f3d70401a6067129812dc538bc20bcb2f51d69fd Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:26:00 +0800 Subject: [PATCH 11/18] feat: add PHPStan type testing for cache component (#972) * Initial plan * feat: add comprehensive type testing for cache component Co-authored-by: huangdijia <8337659+huangdijia@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: huangdijia <8337659+huangdijia@users.noreply.github.com> --- types/Cache/Facade.php | 96 ++++++++++++++++++++++++++++++ types/Cache/Manager.php | 44 ++++++++++++++ types/Cache/Repository.php | 117 +++++++++++++++++++++++++++++++++++++ 3 files changed, 257 insertions(+) create mode 100644 types/Cache/Facade.php create mode 100644 types/Cache/Manager.php create mode 100644 types/Cache/Repository.php diff --git a/types/Cache/Facade.php b/types/Cache/Facade.php new file mode 100644 index 000000000..76c21bee7 --- /dev/null +++ b/types/Cache/Facade.php @@ -0,0 +1,96 @@ + 'value1', 'key2' => 'value2'])); +assertType('bool', Cache::putMany(['key1' => 'value1'], 60)); + +// Cache::add() tests +assertType('bool', Cache::add('key', 'value')); +assertType('bool', Cache::add('key', 'value', 60)); + +// Cache::forever() tests +assertType('bool', Cache::forever('key', 'value')); + +// Cache::forget() tests +assertType('bool', Cache::forget('key')); + +// Cache::flush() tests +assertType('bool', Cache::flush()); + +// Cache::increment() tests +assertType('bool|int', Cache::increment('key')); +assertType('bool|int', Cache::increment('key', 5)); + +// Cache::decrement() tests +assertType('bool|int', Cache::decrement('key')); +assertType('bool|int', Cache::decrement('key', 5)); + +// Cache::remember() tests +assertType('string', Cache::remember('key', 60, fn () => 'value')); +assertType('int', Cache::remember('key', 60, fn () => 123)); +assertType('array', Cache::remember('key', 60, fn () => [])); + +// Cache::rememberForever() tests +assertType('string', Cache::rememberForever('key', fn () => 'value')); +assertType('int', Cache::rememberForever('key', fn () => 123)); + +// Cache::sear() tests +assertType('string', Cache::sear('key', fn () => 'value')); +assertType('int', Cache::sear('key', fn () => 123)); + +// Cache::flexible() tests +assertType('string', Cache::flexible('key', [60, 120], fn () => 'value')); +assertType('int', Cache::flexible('key', [60, 120], fn () => 123)); +assertType('array', Cache::flexible('key', [60, 120], fn () => [])); diff --git a/types/Cache/Manager.php b/types/Cache/Manager.php new file mode 100644 index 000000000..9526be2b3 --- /dev/null +++ b/types/Cache/Manager.php @@ -0,0 +1,44 @@ +store()); +assertType('FriendsOfHyperf\Cache\Contract\Repository', $manager->store('default')); +assertType('FriendsOfHyperf\Cache\Contract\Repository', $manager->store('redis')); + +// CacheManager::driver() tests +assertType('FriendsOfHyperf\Cache\Contract\Repository', $manager->driver()); +assertType('FriendsOfHyperf\Cache\Contract\Repository', $manager->driver('default')); + +// CacheManager::resolve() tests +assertType('FriendsOfHyperf\Cache\Contract\Repository', $manager->resolve('default')); + +// Factory::store() tests +assertType('FriendsOfHyperf\Cache\Contract\Repository', $factory->store()); +assertType('FriendsOfHyperf\Cache\Contract\Repository', $factory->store('default')); + +// Factory::driver() tests +assertType('FriendsOfHyperf\Cache\Contract\Repository', $factory->driver()); +assertType('FriendsOfHyperf\Cache\Contract\Repository', $factory->driver('default')); + +// Factory::resolve() tests +assertType('FriendsOfHyperf\Cache\Contract\Repository', $factory->resolve('default')); diff --git a/types/Cache/Repository.php b/types/Cache/Repository.php new file mode 100644 index 000000000..9275de13c --- /dev/null +++ b/types/Cache/Repository.php @@ -0,0 +1,117 @@ +has('key')); + +// Repository::missing() tests +assertType('bool', $repository->missing('key')); + +// Repository::get() tests +assertType('mixed', $repository->get('key')); +assertType('mixed', $repository->get('key', 'default')); + +// Repository::set() tests +assertType('bool', $repository->set('key', 'value')); +assertType('bool', $repository->set('key', 'value', 60)); + +// Repository::delete() tests +assertType('bool', $repository->delete('key')); + +// Repository::clear() tests +assertType('bool', $repository->clear()); + +// Repository::many() tests +assertType('iterable', $repository->many(['key1', 'key2'])); + +// Repository::getMultiple() tests +assertType('iterable', $repository->getMultiple(['key1', 'key2'])); +assertType('iterable', $repository->getMultiple(['key1', 'key2'], 'default')); + +// Repository::setMultiple() tests +assertType('bool', $repository->setMultiple(['key1' => 'value1', 'key2' => 'value2'])); +assertType('bool', $repository->setMultiple(['key1' => 'value1'], 60)); + +// Repository::deleteMultiple() tests +assertType('bool', $repository->deleteMultiple(['key1', 'key2'])); + +// Repository::pull() tests +assertType('mixed', $repository->pull('key')); +assertType('mixed', $repository->pull('key', 'default')); + +// Repository::put() tests +assertType('bool', $repository->put('key', 'value')); +assertType('bool', $repository->put('key', 'value', 60)); +assertType('bool', $repository->put(['key1' => 'value1', 'key2' => 'value2'], 60)); + +// Repository::putMany() tests +assertType('bool', $repository->putMany(['key1' => 'value1', 'key2' => 'value2'])); +assertType('bool', $repository->putMany(['key1' => 'value1'], 60)); + +// Repository::add() tests +assertType('bool', $repository->add('key', 'value')); +assertType('bool', $repository->add('key', 'value', 60)); + +// Repository::forever() tests +assertType('bool', $repository->forever('key', 'value')); + +// Repository::forget() tests +assertType('bool', $repository->forget('key')); + +// Repository::flush() tests +assertType('bool', $repository->flush()); + +// Repository::increment() tests +assertType('bool|int', $repository->increment('key')); +assertType('bool|int', $repository->increment('key', 5)); + +// Repository::decrement() tests +assertType('bool|int', $repository->decrement('key')); +assertType('bool|int', $repository->decrement('key', 5)); + +// Repository::remember() tests +assertType('string', $repository->remember('key', 60, fn () => 'value')); +assertType('int', $repository->remember('key', 60, fn () => 123)); +assertType('array', $repository->remember('key', 60, fn () => [])); + +// Repository::rememberForever() tests +assertType('string', $repository->rememberForever('key', fn () => 'value')); +assertType('int', $repository->rememberForever('key', fn () => 123)); + +// Repository::sear() tests +assertType('string', $repository->sear('key', fn () => 'value')); +assertType('int', $repository->sear('key', fn () => 123)); + +// Repository::flexible() tests +assertType('string', $repository->flexible('key', [60, 120], fn () => 'value')); +assertType('int', $repository->flexible('key', [60, 120], fn () => 123)); +assertType('array', $repository->flexible('key', [60, 120], fn () => [])); + +// Repository::getDriver() tests +assertType('Hyperf\Cache\Driver\DriverInterface', $repository->getDriver()); + +// Repository::getStore() tests +assertType('Hyperf\Cache\Driver\DriverInterface', $repository->getStore()); + +// CacheRepository specific tests +assertType('Hyperf\Cache\Driver\DriverInterface', $cacheRepository->getDriver()); +assertType('Hyperf\Cache\Driver\DriverInterface', $cacheRepository->getStore()); From fd48474092c68539a46c91158c9bb3b5e77b1f3c Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:26:26 +0800 Subject: [PATCH 12/18] fix: remove unused imports in Cache and Macros components --- types/Cache/Facade.php | 2 -- types/Cache/Manager.php | 1 - types/Cache/Repository.php | 1 - types/Macros/Request.php | 4 ++-- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/types/Cache/Facade.php b/types/Cache/Facade.php index 76c21bee7..ad46e216f 100644 --- a/types/Cache/Facade.php +++ b/types/Cache/Facade.php @@ -8,9 +8,7 @@ * @document https://github.com/friendsofhyperf/components/blob/main/README.md * @contact huangdijia@gmail.com */ -use FriendsOfHyperf\Cache\Contract\Repository; use FriendsOfHyperf\Cache\Facade\Cache; -use Hyperf\Cache\Driver\DriverInterface; use function PHPStan\Testing\assertType; diff --git a/types/Cache/Manager.php b/types/Cache/Manager.php index 9526be2b3..ed026c998 100644 --- a/types/Cache/Manager.php +++ b/types/Cache/Manager.php @@ -10,7 +10,6 @@ */ use FriendsOfHyperf\Cache\CacheManager; use FriendsOfHyperf\Cache\Contract\Factory; -use FriendsOfHyperf\Cache\Contract\Repository; use function PHPStan\Testing\assertType; diff --git a/types/Cache/Repository.php b/types/Cache/Repository.php index 9275de13c..02306225e 100644 --- a/types/Cache/Repository.php +++ b/types/Cache/Repository.php @@ -10,7 +10,6 @@ */ use FriendsOfHyperf\Cache\Contract\Repository; use FriendsOfHyperf\Cache\Repository as CacheRepository; -use Hyperf\Cache\Driver\DriverInterface; use function PHPStan\Testing\assertType; diff --git a/types/Macros/Request.php b/types/Macros/Request.php index ae6071523..5468f9296 100644 --- a/types/Macros/Request.php +++ b/types/Macros/Request.php @@ -14,7 +14,7 @@ use function PHPStan\Testing\assertType; // Create a mock request for testing -Context::set(\Psr\Http\Message\ServerRequestInterface::class, new \Hyperf\HttpMessage\Server\Request('GET', '/')); +Context::set(Psr\Http\Message\ServerRequestInterface::class, new Hyperf\HttpMessage\Server\Request('GET', '/')); $request = new Request(); // Request::boolean() tests @@ -32,7 +32,7 @@ assertType('Carbon\Carbon|null', $request->date('deleted_at', 'Y-m-d H:i:s', 'UTC')); // Request::enum() tests -assertType('mixed', $request->enum('status', \BackedEnum::class)); +assertType('mixed', $request->enum('status', BackedEnum::class)); // Request::exists() tests assertType('bool', $request->exists('key')); From 6bf4907578c5c1c63f0a14e0c80de49da50bd376 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:41:57 +0800 Subject: [PATCH 13/18] feat: update PHPStan configuration and enhance type assertions in various components --- phpstan.types.neon.dist | 8 + .../output/Hyperf/Collection/Collection.php | 9 + .../Hyperf/Collection/LazyCollection.php | 18 ++ .../output/Hyperf/HttpServer/Request.php | 302 ++++++++++++++++++ src/macros/output/Hyperf/Stringable/Str.php | 37 ++- .../output/Hyperf/Stringable/Stringable.php | 20 ++ types/Cache/Facade.php | 6 +- types/Cache/Repository.php | 10 +- types/Macros/Arr.php | 4 +- types/Macros/Request.php | 4 +- types/Macros/Str.php | 6 +- 11 files changed, 406 insertions(+), 18 deletions(-) create mode 100644 src/macros/output/Hyperf/HttpServer/Request.php diff --git a/phpstan.types.neon.dist b/phpstan.types.neon.dist index be73ed150..9a0996e02 100644 --- a/phpstan.types.neon.dist +++ b/phpstan.types.neon.dist @@ -2,3 +2,11 @@ parameters: level: max paths: - types + scanFiles: + - src/macros/output/Hyperf/Collection/Arr.php + - src/macros/output/Hyperf/Collection/Collection.php + - src/macros/output/Hyperf/Collection/LazyCollection.php + - src/macros/output/Hyperf/HttpServer/Contract/RequestInterface.php + - src/macros/output/Hyperf/HttpServer/Request.php + - src/macros/output/Hyperf/Stringable/Str.php + - src/macros/output/Hyperf/Stringable/Stringable.php diff --git a/src/macros/output/Hyperf/Collection/Collection.php b/src/macros/output/Hyperf/Collection/Collection.php index 76e159a35..3d3fe598f 100644 --- a/src/macros/output/Hyperf/Collection/Collection.php +++ b/src/macros/output/Hyperf/Collection/Collection.php @@ -13,6 +13,15 @@ class Collection { + /** + * Create a new collection. + * + * @param mixed $items + */ + public function __construct($items = []) + { + } + /** * Determine if the collection contains a single element. * @deprecated since v3.1, use `containsOneItem` instead, will be removed in v3.2. diff --git a/src/macros/output/Hyperf/Collection/LazyCollection.php b/src/macros/output/Hyperf/Collection/LazyCollection.php index e5486bcbc..0fd18ad33 100644 --- a/src/macros/output/Hyperf/Collection/LazyCollection.php +++ b/src/macros/output/Hyperf/Collection/LazyCollection.php @@ -13,6 +13,24 @@ class LazyCollection { + /** + * Create a new lazy collection instance. + * + * @param mixed $items + * @return static + */ + public static function make($items = []) + { + } + + /** + * Determine if the collection contains a single element. + * @return bool + */ + public function isSingle() + { + } + /** * Collapse the collection of items into a single array while preserving its keys. * diff --git a/src/macros/output/Hyperf/HttpServer/Request.php b/src/macros/output/Hyperf/HttpServer/Request.php new file mode 100644 index 000000000..82cb5ecb9 --- /dev/null +++ b/src/macros/output/Hyperf/HttpServer/Request.php @@ -0,0 +1,302 @@ + $enumClass + * @return null|TEnum + */ + public function enum($key, $enumClass) + { + } + + /** + * Determine if the request contains a given input item key. + * + * @param array|string $key + */ + public function exists($key): bool + { + } + + /** + * Retrieve input from the request as a Stringable instance. + * + * @param string $key + * @param mixed $default + * @return \Hyperf\Stringable\Stringable + */ + public function str($key, $default = null) + { + } + + /** + * Retrieve input from the request as a Stringable instance. + * + * @param string $key + * @param mixed $default + * @return \Hyperf\Stringable\Stringable + */ + public function string($key, $default = null) + { + } + + /** + * Retrieve input as an integer value. + * + * @param string $key + * @param int $default + */ + public function integer($key, $default = 0): int + { + } + + public function validate(array $rules, array $messages = [], array $customAttributes = []): array + { + } + + public function validateWithBag(string $errorBag, array $rules, array $messages = [], array $customAttributes = []): array + { + } +} diff --git a/src/macros/output/Hyperf/Stringable/Str.php b/src/macros/output/Hyperf/Stringable/Str.php index dbcc9946d..8c1295679 100644 --- a/src/macros/output/Hyperf/Stringable/Str.php +++ b/src/macros/output/Hyperf/Stringable/Str.php @@ -13,17 +13,28 @@ class Str { + /** + * Return the remainder of a string after the first occurrence of a given value. + * + * @param string $subject + * @param string $search + * @return Stringable + */ + public static function of($string) + { + } + /** * Set the callable that will be used to generate UUIDs. */ - public static function createUuidsUsing(?callable $factory = null) + public static function createUuidsUsing(?callable $factory = null): void { } /** * Indicate that UUIDs should be created normally and not using a custom factory. */ - public static function createUuidsNormally() + public static function createUuidsNormally(): void { } @@ -67,4 +78,26 @@ public static function inlineMarkdown($string, array $options = []) public static function transliterate($string, $unknown = '?', $strict = false) { } + + /** + * Determine if a given string doesn't end with a given substring. + * + * @param string $haystack + * @param string|array $needles + * @return bool + */ + public static function doesntEndWith($haystack, $needles) + { + } + + /** + * Determine if a given string doesn't start with a given substring. + * + * @param string $haystack + * @param string|array $needles + * @return bool + */ + public static function doesntStartWith($haystack, $needles) + { + } } diff --git a/src/macros/output/Hyperf/Stringable/Stringable.php b/src/macros/output/Hyperf/Stringable/Stringable.php index d95631f80..3f2c4fe99 100644 --- a/src/macros/output/Hyperf/Stringable/Stringable.php +++ b/src/macros/output/Hyperf/Stringable/Stringable.php @@ -86,4 +86,24 @@ public function toHtmlString() public function whenIsAscii($callback, $default = null) { } + + /** + * Determine if a given string doesn't end with a given substring. + * + * @param string|array $needles + * @return bool + */ + public function doesntEndWith($needles) + { + } + + /** + * Determine if a given string doesn't start with a given substring. + * + * @param string|array $needles + * @return bool + */ + public function doesntStartWith($needles) + { + } } diff --git a/types/Cache/Facade.php b/types/Cache/Facade.php index ad46e216f..7638c24f6 100644 --- a/types/Cache/Facade.php +++ b/types/Cache/Facade.php @@ -32,7 +32,6 @@ // Cache::get() tests assertType('mixed', Cache::get('key')); -assertType('mixed', Cache::get('key', 'default')); assertType('string', Cache::get('key', 'default')); assertType('int', Cache::get('key', 123)); @@ -41,7 +40,6 @@ // Cache::pull() tests assertType('mixed', Cache::pull('key')); -assertType('mixed', Cache::pull('key', 'default')); assertType('string', Cache::pull('key', 'default')); // Cache::put() tests @@ -78,7 +76,7 @@ // Cache::remember() tests assertType('string', Cache::remember('key', 60, fn () => 'value')); assertType('int', Cache::remember('key', 60, fn () => 123)); -assertType('array', Cache::remember('key', 60, fn () => [])); +assertType('array{}', Cache::remember('key', 60, fn () => [])); // Cache::rememberForever() tests assertType('string', Cache::rememberForever('key', fn () => 'value')); @@ -91,4 +89,4 @@ // Cache::flexible() tests assertType('string', Cache::flexible('key', [60, 120], fn () => 'value')); assertType('int', Cache::flexible('key', [60, 120], fn () => 123)); -assertType('array', Cache::flexible('key', [60, 120], fn () => [])); +assertType('array{}', Cache::flexible('key', [60, 120], fn () => [])); diff --git a/types/Cache/Repository.php b/types/Cache/Repository.php index 02306225e..114c30b54 100644 --- a/types/Cache/Repository.php +++ b/types/Cache/Repository.php @@ -43,8 +43,8 @@ assertType('iterable', $repository->many(['key1', 'key2'])); // Repository::getMultiple() tests -assertType('iterable', $repository->getMultiple(['key1', 'key2'])); -assertType('iterable', $repository->getMultiple(['key1', 'key2'], 'default')); +assertType('iterable