diff --git a/composer.json b/composer.json index 75b6fa585e..7c17954b4a 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "psr/http-factory": "^1.0", "psr/http-message": "^1.0|^2.0", "psr/log": "^3.0.0", - "rector/rector": "^2.3.2", + "rector/rector": "2.3.1", "symfony/cache": "^7.3", "symfony/filesystem": "^7.3", "symfony/mailer": "^7.2.6", @@ -74,7 +74,7 @@ "patrickbussmann/oauth2-apple": "^0.3", "phpat/phpat": "^0.11.0", "phpbench/phpbench": "84.x-dev", - "phpstan/phpstan": "2.1.38", + "phpstan/phpstan": "2.1.33", "phpunit/phpunit": "^12.5.8", "predis/predis": "^3.0.0", "riskio/oauth2-auth0": "^2.4", @@ -253,7 +253,7 @@ "test": "composer phpunit", "test:stop": "composer phpunit -- --stop-on-error --stop-on-failure", "lint": "vendor/bin/mago lint --potentially-unsafe --minimum-fail-level=note", - "phpstan": "vendor/bin/phpstan analyse src tests --memory-limit=1G", + "phpstan": "vendor/bin/phpstan analyse --memory-limit=1G", "rector": "vendor/bin/rector process --no-ansi", "merge": "php -d\"error_reporting = E_ALL & ~E_DEPRECATED\" vendor/bin/monorepo-builder merge", "intl:plural": "./packages/intl/bin/plural-rules.php", diff --git a/packages/auth/src/AccessControl/AccessControl.php b/packages/auth/src/AccessControl/AccessControl.php index effe129674..5d4f18fbb5 100644 --- a/packages/auth/src/AccessControl/AccessControl.php +++ b/packages/auth/src/AccessControl/AccessControl.php @@ -2,21 +2,21 @@ namespace Tempest\Auth\AccessControl; +use Tempest\Auth\Exceptions\AccessWasDenied; use UnitEnum; /** - * @template Subject of object - * @template Resource of object + * @template TSubject of object + * @template TResource of object */ interface AccessControl { /** * Checks if the action is granted for the given resource and subject. If not, an exception is thrown. * - * @template Resource of object * @param UnitEnum|string $action An arbitrary action to check access for, e.g. 'view', 'edit', etc. - * @param Resource|class-string $resource A model instance or class string of a model to check access for. - * @param null|Subject $subject An optional subject to check access against, e.g. a user or service account. + * @param TResource|class-string $resource A model instance or class string of a model to check access for. + * @param null|TSubject $subject An optional subject to check access against, e.g. a user or service account. * * @throws AccessWasDenied */ @@ -25,10 +25,9 @@ public function ensureGranted(UnitEnum|string $action, object|string $resource, /** * Checks if the action is granted for the given resource and subject. * - * @template Resource of object * @param UnitEnum|string $action An arbitrary action to check access for, e.g. 'view', 'edit', etc. - * @param Resource|class-string $resource A model instance or class string of a model to check access for. - * @param null|Subject $subject An optional subject to check access against, e.g. a user or service account. + * @param TResource|class-string $resource A model instance or class string of a model to check access for. + * @param null|TSubject $subject An optional subject to check access against, e.g. a user or service account. */ public function isGranted(UnitEnum|string $action, object|string $resource, ?object $subject = null): AccessDecision; } diff --git a/packages/auth/src/AccessControl/PolicyBasedAccessControl.php b/packages/auth/src/AccessControl/PolicyBasedAccessControl.php index 98b353f148..3751de141e 100644 --- a/packages/auth/src/AccessControl/PolicyBasedAccessControl.php +++ b/packages/auth/src/AccessControl/PolicyBasedAccessControl.php @@ -16,9 +16,9 @@ use UnitEnum; /** - * @template Subject of object - * @template Resource of object - * @implements AccessControl + * @template TSubject of object + * @template TResource of object + * @implements AccessControl */ final readonly class PolicyBasedAccessControl implements AccessControl { @@ -119,11 +119,9 @@ private function ensureParameterAcceptsInput(?ParameterReflector $reflector, mix return; } - if (! ($type = $reflector?->getType())) { - return; - } + $type = $reflector->getType(); - if ($type?->accepts($input)) { + if ($type->accepts($input)) { return; } diff --git a/packages/auth/src/Authentication/AuthenticatableResolverInitializer.php b/packages/auth/src/Authentication/AuthenticatableResolverInitializer.php index e2e5c01e36..24b7b8072e 100644 --- a/packages/auth/src/Authentication/AuthenticatableResolverInitializer.php +++ b/packages/auth/src/Authentication/AuthenticatableResolverInitializer.php @@ -5,15 +5,12 @@ use Tempest\Container\Container; use Tempest\Container\Initializer; use Tempest\Container\Singleton; -use Tempest\Database\Database; final class AuthenticatableResolverInitializer implements Initializer { #[Singleton] public function initialize(Container $container): AuthenticatableResolver { - return new DatabaseAuthenticatableResolver( - database: $container->get(Database::class), - ); + return new DatabaseAuthenticatableResolver(); } } diff --git a/packages/auth/src/Authentication/DatabaseAuthenticatableResolver.php b/packages/auth/src/Authentication/DatabaseAuthenticatableResolver.php index c8e99b67c0..43cdd356c3 100644 --- a/packages/auth/src/Authentication/DatabaseAuthenticatableResolver.php +++ b/packages/auth/src/Authentication/DatabaseAuthenticatableResolver.php @@ -4,22 +4,15 @@ use Tempest\Auth\Exceptions\AuthenticatableModelWasInvalid; use Tempest\Auth\Exceptions\ModelWasNotAuthenticatable; -use Tempest\Database\Database; use function Tempest\Database\inspect; use function Tempest\Database\query; final readonly class DatabaseAuthenticatableResolver implements AuthenticatableResolver { - public function __construct( - private Database $database, - ) {} - public function resolve(int|string $id, string $class): ?Authenticatable { - if (! is_a($class, Authenticatable::class, allow_string: true)) { - throw new ModelWasNotAuthenticatable($class); - } + $this->ensureClassIsAuthenticatable($class); return query($class)->findById($id); } @@ -40,4 +33,11 @@ public function resolveId(Authenticatable $authenticatable): int|string return $id; } + + private function ensureClassIsAuthenticatable(string $class): void + { + if (! is_a($class, Authenticatable::class, allow_string: true)) { + throw new ModelWasNotAuthenticatable($class); + } + } } diff --git a/packages/auth/src/Exceptions/ModelWasNotAuthenticatable.php b/packages/auth/src/Exceptions/ModelWasNotAuthenticatable.php index 8df9835649..fc1a0292d1 100644 --- a/packages/auth/src/Exceptions/ModelWasNotAuthenticatable.php +++ b/packages/auth/src/Exceptions/ModelWasNotAuthenticatable.php @@ -8,7 +8,7 @@ final class ModelWasNotAuthenticatable extends Exception implements AuthenticationException { public function __construct( - private readonly string $class, + string $class, ) { parent::__construct( sprintf('`%s` must be an instance of `%s`', $class, Authenticatable::class), diff --git a/packages/auth/src/OAuth/GenericOAuthClient.php b/packages/auth/src/OAuth/GenericOAuthClient.php index 9759711227..9b591f26ee 100644 --- a/packages/auth/src/OAuth/GenericOAuthClient.php +++ b/packages/auth/src/OAuth/GenericOAuthClient.php @@ -53,8 +53,12 @@ public function __construct( public function buildAuthorizationUrl(array $scopes = [], array $options = []): string { + if ($scopes === []) { + $scopes = $this->config->scopes; + } + return $this->provider->getAuthorizationUrl([ - 'scope' => $scopes ?? $this->config->scopes, + 'scope' => $scopes, 'redirect_uri' => $this->uri->createUri($this->config->redirectTo), ...$options, ]); @@ -71,16 +75,27 @@ public function createRedirect(array $scopes = [], array $options = []): Redirec public function getState(): ?string { - return $this->provider->getState(); + return $this->provider->getState() ?: null; } public function requestAccessToken(string $code): AccessToken { try { - return $this->provider->getAccessToken('authorization_code', [ + $token = $this->provider->getAccessToken('authorization_code', [ 'code' => $code, 'redirect_uri' => $this->uri->createUri($this->config->redirectTo), ]); + + if ($token instanceof AccessToken) { + return $token; + } + + return new AccessToken([ + ...$token->getValues(), + 'access_token' => $token->getToken(), + 'refresh_token' => $token->getRefreshToken(), + 'expires' => $token->getExpires(), + ]); } catch (IdentityProviderException $exception) { throw OAuthTokenCouldNotBeRetrieved::fromProviderException($exception); } diff --git a/packages/auth/src/OAuth/OAuthClient.php b/packages/auth/src/OAuth/OAuthClient.php index ee704abd43..63eb5566fe 100644 --- a/packages/auth/src/OAuth/OAuthClient.php +++ b/packages/auth/src/OAuth/OAuthClient.php @@ -43,8 +43,6 @@ public function fetchUser(AccessToken $token): OAuthUser; /** * Authenticates a user based on the given OAuth callback request. * - * @template T of Authenticatable - * * @param Closure(OAuthUser): T $map A callback that should return an authenticatable model from the given OAuthUser. Typically, the callback is also responsible for saving the user to the database. */ public function authenticate(Request $request, Closure $map): Authenticatable; diff --git a/packages/auth/tests/AuthenticationAndOAuthSafetyTest.php b/packages/auth/tests/AuthenticationAndOAuthSafetyTest.php new file mode 100644 index 0000000000..f3e8732d0d --- /dev/null +++ b/packages/auth/tests/AuthenticationAndOAuthSafetyTest.php @@ -0,0 +1,49 @@ +expectException(ModelWasNotAuthenticatable::class); + + $resolve->invoke($resolver, 1, \stdClass::class); + } + + #[Test] + public function generic_oauth_client_state_is_null_before_generating_authorization_url(): void + { + $provider = new GenericProvider([ + 'clientId' => 'client-id', + 'clientSecret' => 'client-secret', // @mago-expect lint:no-literal-password + 'redirectUri' => 'https://example.com/callback', + 'urlAuthorize' => 'https://provider.test/authorize', + 'urlAccessToken' => 'https://provider.test/token', // @mago-expect lint:no-literal-password + 'urlResourceOwnerDetails' => 'https://provider.test/user', + ]); + + $reflection = new ReflectionClass(GenericOAuthClient::class); + + /** @var GenericOAuthClient $client */ + $client = $reflection->newInstanceWithoutConstructor(); + $reflection->getProperty('provider')->setValue($client, $provider); + + $this->assertNull($client->getState()); + } +} diff --git a/packages/cache/src/Cache.php b/packages/cache/src/Cache.php index ee9ba65cb4..e46f189e96 100644 --- a/packages/cache/src/Cache.php +++ b/packages/cache/src/Cache.php @@ -37,11 +37,11 @@ public function put(Stringable|string $key, mixed $value, null|Duration|DateTime /** * Sets the specified keys to the specified values in the cache. Optionally, specify an expiration. * - * @template TKey of array-key + * @template TKey of Stringable|string * @template TValue * - * @param iterable $array - * @return array + * @param iterable $values + * @return array */ public function putMany(iterable $values, null|Duration|DateTimeInterface $expiration = null): array; @@ -53,11 +53,10 @@ public function get(Stringable|string $key): mixed; /** * Gets the values associated with the specified keys from the cache. If a key does not exist, null is returned for that key. * - * @template TKey of array-key - * @template TValue + * @template TKey of Stringable|string * - * @param iterable $array - * @return array + * @param iterable $key + * @return array */ public function getMany(iterable $key): array; @@ -79,7 +78,7 @@ public function decrement(Stringable|string $key, int $by = 1): int; /** * If the specified key already exists in the cache, the value is returned and the `$callback` is not executed. Otherwise, the result of the callback is stored, then returned. * - * @var null|Duration $stale Allow the value to be stale for the specified amount of time in addition to the time-to-live specified by `$expiration`. When a value is stale, it will still be returned as-is, but it will be refreshed in the background. + * @param null|Duration $stale Allow the value to be stale for the specified amount of time in addition to the time-to-live specified by `$expiration`. When a value is stale, it will still be returned as-is, but it will be refreshed in the background. */ public function resolve(Stringable|string $key, Closure $callback, null|Duration|DateTimeInterface $expiration = null, ?Duration $stale = null): mixed; diff --git a/packages/cache/src/Commands/CacheClearCommand.php b/packages/cache/src/Commands/CacheClearCommand.php index f3ad69265e..d91c25a541 100644 --- a/packages/cache/src/Commands/CacheClearCommand.php +++ b/packages/cache/src/Commands/CacheClearCommand.php @@ -30,7 +30,6 @@ private const string DEFAULT_CACHE = 'default'; public function __construct( - private Cache $cache, private Container $container, ) {} @@ -59,7 +58,7 @@ private function clearInternalCaches(bool $all = false): void { $caches = [ConfigCache::class, ViewCache::class, IconCache::class, DiscoveryCache::class]; - if ($all === false && count($caches) > 1) { + if (! $all) { $caches = $this->ask( question: 'Which caches do you want to clear?', options: $caches, diff --git a/packages/cache/src/Commands/CacheStatusCommand.php b/packages/cache/src/Commands/CacheStatusCommand.php index 7df85538b0..766eda0d27 100644 --- a/packages/cache/src/Commands/CacheStatusCommand.php +++ b/packages/cache/src/Commands/CacheStatusCommand.php @@ -81,7 +81,6 @@ public function __invoke(bool $internal = true): void $this->console->header('User caches'); - /** @var Cache $cache */ foreach ($caches as $cache) { $this->console->keyValue( key: $this->getCacheName($cache), diff --git a/packages/cache/src/InternalCacheInsightsProvider.php b/packages/cache/src/InternalCacheInsightsProvider.php index 5e8cd9b788..b1b0d039c9 100644 --- a/packages/cache/src/InternalCacheInsightsProvider.php +++ b/packages/cache/src/InternalCacheInsightsProvider.php @@ -7,6 +7,7 @@ use Tempest\Core\DiscoveryCacheStrategy; use Tempest\Core\Insight; use Tempest\Core\InsightsProvider; +use Tempest\Core\InsightType; use Tempest\Icon\IconCache; use Tempest\View\ViewCache; @@ -25,14 +26,14 @@ public function getInsights(): array { return [ 'Discovery' => match ($this->discoveryCache->valid) { - false => new Insight('Invalid', Insight::ERROR), + false => new Insight('Invalid', InsightType::ERROR), true => match ($this->discoveryCache->enabled) { true => match ($this->discoveryCache->strategy) { - DiscoveryCacheStrategy::FULL => new Insight('Enabled', Insight::SUCCESS), - DiscoveryCacheStrategy::PARTIAL => new Insight('Enabled (partial)', Insight::SUCCESS), + DiscoveryCacheStrategy::FULL => new Insight('Enabled', InsightType::SUCCESS), + DiscoveryCacheStrategy::PARTIAL => new Insight('Enabled (partial)', InsightType::SUCCESS), default => null, // INVALID and NONE are handled }, - false => new Insight('Disabled', Insight::WARNING), + false => new Insight('Disabled', InsightType::WARNING), }, }, 'Configuration' => $this->getInsight($this->configCache->enabled), @@ -44,9 +45,9 @@ public function getInsights(): array private function getInsight(bool $enabled): Insight { if ($enabled) { - return new Insight('ENABLED', Insight::SUCCESS); + return new Insight('ENABLED', InsightType::SUCCESS); } - return new Insight('DISABLED', Insight::WARNING); + return new Insight('DISABLED', InsightType::WARNING); } } diff --git a/packages/cache/src/Testing/TestingCache.php b/packages/cache/src/Testing/TestingCache.php index 516e2abe89..3313a8352f 100644 --- a/packages/cache/src/Testing/TestingCache.php +++ b/packages/cache/src/Testing/TestingCache.php @@ -1,5 +1,7 @@ $this->cache->enabled; - set => $this->cache->enabled = $value; + public bool $enabled = true { + set { + $this->cache->enabled = $value; + $this->enabled = $value; + } } private Cache $cache; diff --git a/packages/cache/src/UserCacheInsightsProvider.php b/packages/cache/src/UserCacheInsightsProvider.php index b97df59ae4..81d1ef18e5 100644 --- a/packages/cache/src/UserCacheInsightsProvider.php +++ b/packages/cache/src/UserCacheInsightsProvider.php @@ -1,5 +1,7 @@ toArray(); } - /** @var Insight[] */ + /** + * @return array{0: ?Insight, 1: Insight} + */ private function getInsight(Cache $cache): array { $type = $cache instanceof GenericCache @@ -50,10 +55,10 @@ private function getInsight(Cache $cache): array : null; if ($cache->enabled) { - return [$type, new Insight('ENABLED', Insight::SUCCESS)]; + return [$type, new Insight('ENABLED', InsightType::SUCCESS)]; } - return [$type, new Insight('DISABLED', Insight::WARNING)]; + return [$type, new Insight('DISABLED', InsightType::WARNING)]; } private function getCacheName(Cache $cache): string diff --git a/packages/command-bus/src/HandleAsyncCommand.php b/packages/command-bus/src/HandleAsyncCommand.php index ee4e9378f8..7947189fa8 100644 --- a/packages/command-bus/src/HandleAsyncCommand.php +++ b/packages/command-bus/src/HandleAsyncCommand.php @@ -5,7 +5,6 @@ namespace Tempest\CommandBus; use DateTimeImmutable; -use Tempest\Console\Console; use Tempest\Console\ConsoleCommand; use Tempest\Console\ExitCode; use Tempest\Console\HasConsole; @@ -22,7 +21,6 @@ public function __construct( private CommandBusConfig $commandBusConfig, private Container $container, - private Console $console, private CommandRepository $repository, ) {} diff --git a/packages/console/src/Components/Interactive/TaskComponent.php b/packages/console/src/Components/Interactive/TaskComponent.php index 6d5960c04e..4ccb73edec 100644 --- a/packages/console/src/Components/Interactive/TaskComponent.php +++ b/packages/console/src/Components/Interactive/TaskComponent.php @@ -172,16 +172,14 @@ private function resolveHandler(null|Process|Closure $handler): ?Closure if ($handler instanceof Process) { return static function (Closure $log) use ($handler): bool { - return $handler->run(function (string $output, string $buffer) use ($log): bool { - if ($output === Process::ERR) { - return true; + return $handler->run(function (string $type, string $buffer) use ($log): void { + if ($type === Process::ERR) { + return; } if ($line = trim($buffer)) { $log($line); } - - return true; }) === 0; }; } diff --git a/packages/console/src/Console.php b/packages/console/src/Console.php index 30b4a574b0..056ce55845 100644 --- a/packages/console/src/Console.php +++ b/packages/console/src/Console.php @@ -53,7 +53,7 @@ public function component(InteractiveConsoleComponent $component, array $validat /** * Asks the user a question and returns the answer. * - * @param null|array|iterable|class-string $options + * @param null|iterable|class-string $options * @param mixed|null $default * @param \Tempest\Validation\Rule[] $validation */ diff --git a/packages/console/src/Exceptions/UnknownArgumentsException.php b/packages/console/src/Exceptions/UnknownArgumentsException.php index 67cc70dbd0..1d2beb269a 100644 --- a/packages/console/src/Exceptions/UnknownArgumentsException.php +++ b/packages/console/src/Exceptions/UnknownArgumentsException.php @@ -3,15 +3,15 @@ namespace Tempest\Console\Exceptions; use Tempest\Console\Console; -use Tempest\Console\ConsoleCommand; use Tempest\Console\Input\ConsoleInputArgument; use Tempest\Support\Arr\ImmutableArray; final class UnknownArgumentsException extends ConsoleException { + /** + * @param ImmutableArray $invalidArguments + */ public function __construct( - private readonly ConsoleCommand $consoleCommand, - /** @var \Tempest\Console\Input\ConsoleInputArgument[] $invalidArguments */ private readonly ImmutableArray $invalidArguments, ) {} diff --git a/packages/console/src/Middleware/ResolveOrRescueMiddleware.php b/packages/console/src/Middleware/ResolveOrRescueMiddleware.php index 8f05f8afe4..1f252fc2e5 100644 --- a/packages/console/src/Middleware/ResolveOrRescueMiddleware.php +++ b/packages/console/src/Middleware/ResolveOrRescueMiddleware.php @@ -84,7 +84,7 @@ private function getSimilarCommands(ImmutableString $search): ImmutableArray $currentName = str($consoleCommand->getName()); // Already added to suggestions - if ($suggestions->hasValue($currentName->toString())) { + if ($suggestions->hasKey($currentName->toString())) { continue; } diff --git a/packages/console/src/Middleware/ValidateNamedArgumentsMiddleware.php b/packages/console/src/Middleware/ValidateNamedArgumentsMiddleware.php index f1fdf03700..c7b4cd1435 100644 --- a/packages/console/src/Middleware/ValidateNamedArgumentsMiddleware.php +++ b/packages/console/src/Middleware/ValidateNamedArgumentsMiddleware.php @@ -38,7 +38,6 @@ public function __invoke(Invocation $invocation, ConsoleMiddlewareCallable $next if ($invalidInput->isNotEmpty()) { throw new UnknownArgumentsException( - consoleCommand: $invocation->consoleCommand, invalidArguments: $invalidInput, ); } diff --git a/packages/console/src/Stubs/console.config.stub.php b/packages/console/src/Stubs/console.config.stub.php index b4efa218d7..06c4060ba4 100644 --- a/packages/console/src/Stubs/console.config.stub.php +++ b/packages/console/src/Stubs/console.config.stub.php @@ -3,20 +3,7 @@ declare(strict_types=1); use Tempest\Console\ConsoleConfig; -use Tempest\Console\Middleware\ConsoleExceptionMiddleware; -use Tempest\Console\Middleware\HelpMiddleware; -use Tempest\Console\Middleware\InvalidCommandMiddleware; -use Tempest\Console\Middleware\OverviewMiddleware; -use Tempest\Console\Middleware\ResolveOrRescueMiddleware; -use Tempest\Core\Middleware; return new ConsoleConfig( name: 'Console Name', - middleware: new Middleware( - OverviewMiddleware::class, - ConsoleExceptionMiddleware::class, - ResolveOrRescueMiddleware::class, - InvalidCommandMiddleware::class, - HelpMiddleware::class, - ), ); diff --git a/packages/console/src/Stubs/log.config.stub.php b/packages/console/src/Stubs/log.config.stub.php index 4dc3c310cb..9e8dc0151f 100644 --- a/packages/console/src/Stubs/log.config.stub.php +++ b/packages/console/src/Stubs/log.config.stub.php @@ -2,14 +2,8 @@ declare(strict_types=1); -use Tempest\Log\Channels\AppendLogChannel; -use Tempest\Log\LogConfig; +use Tempest\Log\Config\SimpleLogConfig; -return new LogConfig( - channels: [ - new AppendLogChannel( - path: __DIR__ . '/../logs/project.log', - ), - ], - serverLogPath: '/path/to/nginx.log', +return new SimpleLogConfig( + path: __DIR__ . '/../logs/project.log', ); diff --git a/packages/console/src/Testing/ConsoleTester.php b/packages/console/src/Testing/ConsoleTester.php index 073436ed24..088f695c5c 100644 --- a/packages/console/src/Testing/ConsoleTester.php +++ b/packages/console/src/Testing/ConsoleTester.php @@ -313,10 +313,8 @@ public function withPrompting(): self return $this; } - public function dd(): self + public function dd(): never { ld($this->output->asUnformattedString()); - - return $this; } } diff --git a/packages/console/tests/Enums/ShellTest.php b/packages/console/tests/Enums/ShellTest.php index dcf569e969..c2f3b87f90 100644 --- a/packages/console/tests/Enums/ShellTest.php +++ b/packages/console/tests/Enums/ShellTest.php @@ -96,12 +96,10 @@ public function getRcFile(): void public function getPostInstallInstructions(): void { $zshInstructions = Shell::ZSH->getPostInstallInstructions(); - $this->assertIsArray($zshInstructions); $this->assertNotEmpty($zshInstructions); $this->assertStringContainsString('fpath', $zshInstructions[0]); $bashInstructions = Shell::BASH->getPostInstallInstructions(); - $this->assertIsArray($bashInstructions); $this->assertNotEmpty($bashInstructions); $this->assertStringContainsStringIgnoringCase('source', $bashInstructions[0]); } diff --git a/packages/container/tests/Fixtures/AutowireWithEnumTags.php b/packages/container/tests/Fixtures/AutowireWithEnumTags.php index d4caa8d495..447bfde100 100644 --- a/packages/container/tests/Fixtures/AutowireWithEnumTags.php +++ b/packages/container/tests/Fixtures/AutowireWithEnumTags.php @@ -4,7 +4,7 @@ use Tempest\Container\Tag; -class AutowireWithEnumTags +final class AutowireWithEnumTags { public function __construct( #[Tag(EnumTag::FOO)] diff --git a/packages/container/tests/Fixtures/DecoratedClass.php b/packages/container/tests/Fixtures/DecoratedClass.php index 4664908c60..da11df34ad 100644 --- a/packages/container/tests/Fixtures/DecoratedClass.php +++ b/packages/container/tests/Fixtures/DecoratedClass.php @@ -4,6 +4,6 @@ namespace Tempest\Container\Tests\Fixtures; -class DecoratedClass implements DecoratedInterface +final class DecoratedClass implements DecoratedInterface { } diff --git a/packages/container/tests/Fixtures/DecoratorClass.php b/packages/container/tests/Fixtures/DecoratorClass.php index ef07523121..3e8d3577bc 100644 --- a/packages/container/tests/Fixtures/DecoratorClass.php +++ b/packages/container/tests/Fixtures/DecoratorClass.php @@ -7,7 +7,7 @@ use Tempest\Container\Decorates; #[Decorates(DecoratedInterface::class)] -class DecoratorClass implements DecoratedInterface +final class DecoratorClass implements DecoratedInterface { public function __construct( public DecoratedInterface $decorated, diff --git a/packages/container/tests/Fixtures/DecoratorInvalid.php b/packages/container/tests/Fixtures/DecoratorInvalid.php index 131715af8a..a7205e2acb 100644 --- a/packages/container/tests/Fixtures/DecoratorInvalid.php +++ b/packages/container/tests/Fixtures/DecoratorInvalid.php @@ -7,7 +7,7 @@ use Tempest\Container\Decorates; #[Decorates(DecoratedInterface::class)] -class DecoratorInvalid +final class DecoratorInvalid { public function __construct( public DecoratedInterface $decorated, diff --git a/packages/container/tests/Fixtures/DecoratorSecondClass.php b/packages/container/tests/Fixtures/DecoratorSecondClass.php index 93084edfc6..6fe9129c3a 100644 --- a/packages/container/tests/Fixtures/DecoratorSecondClass.php +++ b/packages/container/tests/Fixtures/DecoratorSecondClass.php @@ -7,7 +7,7 @@ use Tempest\Container\Decorates; #[Decorates(DecoratedInterface::class)] -class DecoratorSecondClass implements DecoratedInterface +final class DecoratorSecondClass implements DecoratedInterface { public function __construct( public DecoratedInterface $decorated, diff --git a/packages/container/tests/Fixtures/DecoratorWithoutConstructor.php b/packages/container/tests/Fixtures/DecoratorWithoutConstructor.php index 72e79034ab..6adabcc371 100644 --- a/packages/container/tests/Fixtures/DecoratorWithoutConstructor.php +++ b/packages/container/tests/Fixtures/DecoratorWithoutConstructor.php @@ -7,6 +7,6 @@ use Tempest\Container\Decorates; #[Decorates(DecoratedInterface::class)] -class DecoratorWithoutConstructor implements DecoratedInterface +final class DecoratorWithoutConstructor implements DecoratedInterface { } diff --git a/packages/core/src/Commands/DiscoveryGenerateCommand.php b/packages/core/src/Commands/DiscoveryGenerateCommand.php index 16faee3bec..ca286ddfa6 100644 --- a/packages/core/src/Commands/DiscoveryGenerateCommand.php +++ b/packages/core/src/Commands/DiscoveryGenerateCommand.php @@ -12,7 +12,6 @@ use Tempest\Core\DiscoveryCache; use Tempest\Core\DiscoveryCacheStrategy; use Tempest\Core\DiscoveryConfig; -use Tempest\Core\Environment; use Tempest\Core\FrameworkKernel; use Tempest\Core\Kernel; use Tempest\Core\Kernel\LoadDiscoveryClasses; @@ -25,7 +24,6 @@ public function __construct( private Kernel $kernel, private DiscoveryCache $discoveryCache, - private Environment $environment, ) {} #[ConsoleCommand( diff --git a/packages/core/src/EnvironmentInsightsProvider.php b/packages/core/src/EnvironmentInsightsProvider.php index 7ab27261fa..07c333756a 100644 --- a/packages/core/src/EnvironmentInsightsProvider.php +++ b/packages/core/src/EnvironmentInsightsProvider.php @@ -21,7 +21,7 @@ public function getInsights(): array 'Composer version' => $this->getComposerVersion(), 'Operating system' => $this->getOperatingSystem(), 'Environment' => $this->environment->value, - 'Application URL' => $this->appConfig->baseUri ?: new Insight('Not set', Insight::ERROR), + 'Application URL' => $this->appConfig->baseUri ?: new Insight('Not set', InsightType::ERROR), ]; } @@ -34,14 +34,14 @@ private function getComposerVersion(): Insight|string $output = shell_exec('composer --version --no-ansi 2>&1'); if (! $output) { - return new Insight('Not installed', Insight::ERROR); + return new Insight('Not installed', InsightType::ERROR); } return \Tempest\Support\Regex\get_match( subject: $output, pattern: "/Composer version (?\S+)/", match: 'version', - default: new Insight('Unknown', Insight::ERROR), + default: new Insight('Unknown', InsightType::ERROR), ); } diff --git a/packages/core/src/Insight.php b/packages/core/src/Insight.php index 079662d45b..c5d22bf973 100644 --- a/packages/core/src/Insight.php +++ b/packages/core/src/Insight.php @@ -1,28 +1,22 @@ match ($this->type) { - self::ERROR => "" . mb_strtoupper($this->value) . '', - self::SUCCESS => "" . mb_strtoupper($this->value) . '', - self::WARNING => "" . mb_strtoupper($this->value) . '', - self::NORMAL => $this->value, + InsightType::ERROR => "" . mb_strtoupper($this->value) . '', + InsightType::SUCCESS => "" . mb_strtoupper($this->value) . '', + InsightType::WARNING => "" . mb_strtoupper($this->value) . '', + InsightType::NORMAL => $this->value, }; } public function __construct( - private(set) string $value, - private string $type = self::NORMAL, + private(set) readonly string $value, + private readonly InsightType $type = InsightType::NORMAL, ) {} } diff --git a/packages/core/src/InsightType.php b/packages/core/src/InsightType.php new file mode 100644 index 0000000000..61e920ea07 --- /dev/null +++ b/packages/core/src/InsightType.php @@ -0,0 +1,13 @@ +assertNotNull($key->value); - $this->assertTrue(strlen($key->value) === $key->algorithm->getKeyLength()); + $this->assertNotEmpty($key->value); + $this->assertSame(strlen($key->value), $key->algorithm->getKeyLength()); $this->assertSame(EncryptionAlgorithm::AES_256_GCM, $key->algorithm); } diff --git a/packages/cryptography/tests/Signing/SignerTest.php b/packages/cryptography/tests/Signing/SignerTest.php index 8d003b7f63..eb70a22a6f 100644 --- a/packages/cryptography/tests/Signing/SignerTest.php +++ b/packages/cryptography/tests/Signing/SignerTest.php @@ -83,7 +83,7 @@ public function test_no_signing_key(): void $signer = $this->createSigner(new SigningConfig( algorithm: SigningAlgorithm::SHA256, - key: '', + key: '', // @phpstan-ignore argument.type minimumExecutionDuration: false, )); diff --git a/packages/database/src/Builder/QueryBuilders/CountQueryBuilder.php b/packages/database/src/Builder/QueryBuilders/CountQueryBuilder.php index e9d5a6fb99..b2b7305600 100644 --- a/packages/database/src/Builder/QueryBuilders/CountQueryBuilder.php +++ b/packages/database/src/Builder/QueryBuilders/CountQueryBuilder.php @@ -21,11 +21,14 @@ * @template TModel of object * @implements \Tempest\Database\Builder\QueryBuilders\BuildsQuery * @implements \Tempest\Database\Builder\QueryBuilders\SupportsWhereStatements - * @use \Tempest\Database\Builder\QueryBuilders\HasWhereQueryBuilderMethods */ final class CountQueryBuilder implements BuildsQuery, SupportsWhereStatements { - use HasConditions, OnDatabase, HasWhereQueryBuilderMethods, TransformsQueryBuilder; + use HasConditions; + use OnDatabase; + /** @use HasWhereQueryBuilderMethods */ + use HasWhereQueryBuilderMethods; + use TransformsQueryBuilder; private CountStatement $count; @@ -63,11 +66,11 @@ public function __construct(string|object $model, ?string $column = null) */ public static function fromQueryBuilder(BuildsQuery&SupportsWhereStatements $source, ?string $column = null): CountQueryBuilder { - $builder = new self($source->model->model, $column); + $builder = new self($source->model->getName(), $column); $builder->bind(...$source->bindings); foreach ($source->wheres as $where) { - $builder->wheres[] = $where; + $builder->appendWhere($where); } if ($source instanceof SupportsJoins) { @@ -80,6 +83,7 @@ public static function fromQueryBuilder(BuildsQuery&SupportsWhereStatements $sou } } + /** @var CountQueryBuilder $builder */ return $builder; } diff --git a/packages/database/src/Builder/QueryBuilders/DeleteQueryBuilder.php b/packages/database/src/Builder/QueryBuilders/DeleteQueryBuilder.php index 9da1c62355..d7e40a8dd5 100644 --- a/packages/database/src/Builder/QueryBuilders/DeleteQueryBuilder.php +++ b/packages/database/src/Builder/QueryBuilders/DeleteQueryBuilder.php @@ -16,11 +16,14 @@ * @template TModel of object * @implements \Tempest\Database\Builder\QueryBuilders\BuildsQuery * @implements \Tempest\Database\Builder\QueryBuilders\SupportsWhereStatements - * @use \Tempest\Database\Builder\QueryBuilders\HasWhereQueryBuilderMethods */ final class DeleteQueryBuilder implements BuildsQuery, SupportsWhereStatements { - use HasConditions, OnDatabase, HasWhereQueryBuilderMethods, TransformsQueryBuilder; + use HasConditions; + use OnDatabase; + /** @use HasWhereQueryBuilderMethods */ + use HasWhereQueryBuilderMethods; + use TransformsQueryBuilder; private DeleteStatement $delete; @@ -50,13 +53,14 @@ public function __construct(string|object $model) */ public static function fromQueryBuilder(BuildsQuery&SupportsWhereStatements $source): DeleteQueryBuilder { - $builder = new self($source->model->model); + $builder = new self($source->model->getName()); $builder->bind(...$source->bindings); foreach ($source->wheres as $where) { - $builder->wheres[] = $where; + $builder->appendWhere($where); } + /** @var DeleteQueryBuilder $builder */ return $builder; } diff --git a/packages/database/src/Builder/QueryBuilders/HasConvenientWhereMethods.php b/packages/database/src/Builder/QueryBuilders/HasConvenientWhereMethods.php index ca98ef9a4c..f3a15d1af6 100644 --- a/packages/database/src/Builder/QueryBuilders/HasConvenientWhereMethods.php +++ b/packages/database/src/Builder/QueryBuilders/HasConvenientWhereMethods.php @@ -13,7 +13,6 @@ /** * @template TModel of object - * @phpstan-require-implements \Tempest\Database\Builder\QueryBuilders\SupportsWhereStatements * * Shared methods for building WHERE conditions and convenience WHERE methods. */ diff --git a/packages/database/src/Builder/QueryBuilders/HasWhereQueryBuilderMethods.php b/packages/database/src/Builder/QueryBuilders/HasWhereQueryBuilderMethods.php index 0353395b7c..82d6aa1dc3 100644 --- a/packages/database/src/Builder/QueryBuilders/HasWhereQueryBuilderMethods.php +++ b/packages/database/src/Builder/QueryBuilders/HasWhereQueryBuilderMethods.php @@ -4,20 +4,26 @@ use Closure; use Tempest\Database\Builder\WhereOperator; +use Tempest\Database\QueryStatements\WhereGroupStatement; use Tempest\Database\QueryStatements\WhereStatement; use function Tempest\Support\str; /** * @template TModel of object - * @phpstan-require-implements \Tempest\Database\Builder\QueryBuilders\BuildsQuery - * @phpstan-require-implements \Tempest\Database\Builder\QueryBuilders\SupportsWhereStatements - * @use \Tempest\Database\Builder\QueryBuilders\HasConvenientWhereMethods + * @phpstan-require-implements \Tempest\Database\Builder\QueryBuilders\BuildsQuery + * @phpstan-require-implements \Tempest\Database\Builder\QueryBuilders\SupportsWhereStatements */ trait HasWhereQueryBuilderMethods { + /** @use HasConvenientWhereMethods */ use HasConvenientWhereMethods; + protected function appendWhere(WhereStatement|WhereGroupStatement $where): void + { + $this->wheres->offsetSet(null, $where); + } + /** * Adds a SQL `WHERE` condition to the query. If the `$statement` looks like raw SQL, the method will assume it is and call `whereRaw`. Otherwise, `whereField` will be called. * @@ -52,7 +58,7 @@ public function whereField(string $field, mixed $value, string|WhereOperator $op return $this->andWhere($field, $value, $operator); } - $this->wheres[] = new WhereStatement($condition['sql']); + $this->appendWhere(new WhereStatement($condition['sql'])); $this->bind(...$condition['bindings']); return $this; @@ -69,7 +75,7 @@ public function andWhere(string $field, mixed $value, WhereOperator $operator = $fieldDefinition = $this->model->getFieldDefinition($field); $condition = $this->buildCondition((string) $fieldDefinition, $operator, $value); - $this->wheres[] = new WhereStatement("AND {$condition['sql']}"); + $this->appendWhere(new WhereStatement("AND {$condition['sql']}")); $this->bind(...$condition['bindings']); return $this; @@ -86,7 +92,7 @@ public function orWhere(string $field, mixed $value, WhereOperator $operator = W $fieldDefinition = $this->model->getFieldDefinition($field); $condition = $this->buildCondition((string) $fieldDefinition, $operator, $value); - $this->wheres[] = new WhereStatement("OR {$condition['sql']}"); + $this->appendWhere(new WhereStatement("OR {$condition['sql']}")); $this->bind(...$condition['bindings']); return $this; @@ -103,7 +109,7 @@ public function whereRaw(string $statement, mixed ...$bindings): self return $this->andWhereRaw($statement, ...$bindings); } - $this->wheres[] = new WhereStatement($statement); + $this->appendWhere(new WhereStatement($statement)); $this->bind(...$bindings); return $this; @@ -116,7 +122,7 @@ public function whereRaw(string $statement, mixed ...$bindings): self */ public function andWhereRaw(string $rawCondition, mixed ...$bindings): self { - $this->wheres[] = new WhereStatement("AND {$rawCondition}"); + $this->appendWhere(new WhereStatement("AND {$rawCondition}")); $this->bind(...$bindings); return $this; @@ -129,7 +135,7 @@ public function andWhereRaw(string $rawCondition, mixed ...$bindings): self */ public function orWhereRaw(string $rawCondition, mixed ...$bindings): self { - $this->wheres[] = new WhereStatement("OR {$rawCondition}"); + $this->appendWhere(new WhereStatement("OR {$rawCondition}")); $this->bind(...$bindings); return $this; @@ -143,12 +149,13 @@ public function orWhereRaw(string $rawCondition, mixed ...$bindings): self */ public function whereGroup(Closure $callback): self { + /** @var WhereGroupBuilder $groupBuilder */ $groupBuilder = new WhereGroupBuilder($this->model); $callback($groupBuilder); $group = $groupBuilder->build(); if (! $group->conditions->isEmpty()) { - $this->wheres[] = $group; + $this->appendWhere($group); $this->bind(...$groupBuilder->getBindings()); } @@ -164,7 +171,7 @@ public function whereGroup(Closure $callback): self public function andWhereGroup(Closure $callback): self { if ($this->wheres->isNotEmpty()) { - $this->wheres[] = new WhereStatement('AND'); + $this->appendWhere(new WhereStatement('AND')); } return $this->whereGroup($callback); @@ -179,7 +186,7 @@ public function andWhereGroup(Closure $callback): self public function orWhereGroup(Closure $callback): self { if ($this->wheres->isNotEmpty()) { - $this->wheres[] = new WhereStatement('OR'); + $this->appendWhere(new WhereStatement('OR')); } return $this->whereGroup($callback); diff --git a/packages/database/src/Builder/QueryBuilders/InsertQueryBuilder.php b/packages/database/src/Builder/QueryBuilders/InsertQueryBuilder.php index a438e90351..2d18de7e0b 100644 --- a/packages/database/src/Builder/QueryBuilders/InsertQueryBuilder.php +++ b/packages/database/src/Builder/QueryBuilders/InsertQueryBuilder.php @@ -509,7 +509,7 @@ private function serializeIterableValue(string $key, mixed $value): mixed return $value; } - if (! $this->model?->reflector->hasProperty($key)) { + if (! $this->model->reflector->hasProperty($key)) { return $value; } diff --git a/packages/database/src/Builder/QueryBuilders/SelectQueryBuilder.php b/packages/database/src/Builder/QueryBuilders/SelectQueryBuilder.php index 202d91afa6..8c48debd12 100644 --- a/packages/database/src/Builder/QueryBuilders/SelectQueryBuilder.php +++ b/packages/database/src/Builder/QueryBuilders/SelectQueryBuilder.php @@ -39,11 +39,14 @@ * @implements \Tempest\Database\Builder\QueryBuilders\SupportsWhereStatements * @implements \Tempest\Database\Builder\QueryBuilders\SupportsJoins * @implements \Tempest\Database\Builder\QueryBuilders\SupportsRelations - * @use \Tempest\Database\Builder\QueryBuilders\HasWhereQueryBuilderMethods */ final class SelectQueryBuilder implements BuildsQuery, SupportsWhereStatements, SupportsJoins, SupportsRelations { - use HasConditions, OnDatabase, HasWhereQueryBuilderMethods, TransformsQueryBuilder; + use HasConditions; + use OnDatabase; + /** @use HasWhereQueryBuilderMethods */ + use HasWhereQueryBuilderMethods; + use TransformsQueryBuilder; public ModelInspector $model; @@ -65,7 +68,6 @@ final class SelectQueryBuilder implements BuildsQuery, SupportsWhereStatements, public ImmutableArray $wheres { get => $this->select->where; - set => $this->select->where; } /** @@ -93,7 +95,7 @@ public function first(mixed ...$bindings): mixed $query = $this->build(...$bindings); if (! $this->model->isObjectModel()) { - return $query->fetchFirst(); + return $this->coerceFirstResult($query->fetchFirst()); } $result = map($query->fetch()) @@ -152,11 +154,11 @@ public function get(PrimaryKey $id): mixed */ public static function fromQueryBuilder(BuildsQuery&SupportsWhereStatements $source, mixed ...$fields): SelectQueryBuilder { - $builder = new self($source->model->model, ...$fields); + $builder = new self($source->model->getName(), ...$fields); $builder->bind(...$source->bindings); foreach ($source->wheres as $where) { - $builder->wheres[] = $where; + $builder->appendWhere($where); } if ($source instanceof SupportsJoins) { @@ -169,6 +171,7 @@ public static function fromQueryBuilder(BuildsQuery&SupportsWhereStatements $sou } } + /** @var SelectQueryBuilder $builder */ return $builder; } @@ -398,6 +401,15 @@ private function clone(): self return clone $this; } + /** + * @param mixed $result + * @return TModel|null + */ + private function coerceFirstResult(mixed $result): mixed + { + return $result; + } + /** * Gets all resolved relations with their join statements. * diff --git a/packages/database/src/Builder/QueryBuilders/SupportsWhereStatements.php b/packages/database/src/Builder/QueryBuilders/SupportsWhereStatements.php index cb2f22af5d..3aebecb688 100644 --- a/packages/database/src/Builder/QueryBuilders/SupportsWhereStatements.php +++ b/packages/database/src/Builder/QueryBuilders/SupportsWhereStatements.php @@ -3,6 +3,8 @@ namespace Tempest\Database\Builder\QueryBuilders; use Tempest\Database\Builder\WhereOperator; +use Tempest\Database\QueryStatements\WhereGroupStatement; +use Tempest\Database\QueryStatements\WhereStatement; use Tempest\Support\Arr\ImmutableArray; /** @@ -13,7 +15,7 @@ interface SupportsWhereStatements /** * The current WHERE statements for this query builder. * - * @var ImmutableArray + * @var ImmutableArray */ public ImmutableArray $wheres { get; diff --git a/packages/database/src/Builder/QueryBuilders/TransformsQueryBuilder.php b/packages/database/src/Builder/QueryBuilders/TransformsQueryBuilder.php index d8e89b05ea..3b503fea8b 100644 --- a/packages/database/src/Builder/QueryBuilders/TransformsQueryBuilder.php +++ b/packages/database/src/Builder/QueryBuilders/TransformsQueryBuilder.php @@ -11,7 +11,7 @@ trait TransformsQueryBuilder { /** * Returns a new instance of the query builder with the given callback applied. - * @param callable(static) $callback + * @param callable(static): mixed $callback */ public function transform(callable $callback): static { diff --git a/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php b/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php index b0ebd8fd46..65d9442205 100644 --- a/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php +++ b/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php @@ -31,11 +31,14 @@ * @template TModel of object * @implements \Tempest\Database\Builder\QueryBuilders\BuildsQuery * @implements \Tempest\Database\Builder\QueryBuilders\SupportsWhereStatements - * @use \Tempest\Database\Builder\QueryBuilders\HasWhereQueryBuilderMethods */ final class UpdateQueryBuilder implements BuildsQuery, SupportsWhereStatements { - use HasConditions, OnDatabase, HasWhereQueryBuilderMethods, TransformsQueryBuilder; + use HasConditions; + use OnDatabase; + /** @use HasWhereQueryBuilderMethods */ + use HasWhereQueryBuilderMethods; + use TransformsQueryBuilder; private UpdateStatement $update; @@ -83,13 +86,14 @@ public function __construct( */ public static function fromQueryBuilder(BuildsQuery&SupportsWhereStatements $source, mixed ...$values): UpdateQueryBuilder { - $builder = new self($source->model->model, $values, get(SerializerFactory::class)); + $builder = new self($source->model->getName(), $values, get(SerializerFactory::class)); $builder->bind(...$source->bindings); foreach ($source->wheres as $where) { - $builder->wheres[] = $where; + $builder->appendWhere($where); } + /** @var UpdateQueryBuilder $builder */ return $builder; } @@ -550,10 +554,12 @@ public function whereField(string $field, mixed $value, string|WhereOperator $op $condition = $this->buildCondition((string) $fieldDefinition, $operator, $value); if ($this->wheres->isNotEmpty()) { - return $this->andWhere($field, $value, $operator); + $this->andWhere($field, $value, $operator); + + return $this; } - $this->wheres[] = new WhereStatement($condition['sql']); + $this->appendWhere(new WhereStatement($condition['sql'])); $this->bind(...$condition['bindings']); return $this; @@ -587,10 +593,6 @@ private function hasRelationUpdates(): bool private function isRelationField(string $field): bool { - if (! $this->model) { - return false; - } - return $this->model->getHasMany($field) || $this->model->getHasOne($field); } diff --git a/packages/database/src/Builder/QueryBuilders/WhereGroupBuilder.php b/packages/database/src/Builder/QueryBuilders/WhereGroupBuilder.php index 6fac0afeeb..caab175606 100644 --- a/packages/database/src/Builder/QueryBuilders/WhereGroupBuilder.php +++ b/packages/database/src/Builder/QueryBuilders/WhereGroupBuilder.php @@ -154,13 +154,14 @@ public function orWhereRaw(string $rawCondition, mixed ...$bindings): self /** * Adds another nested where statement. The callback accepts a builder, which may be used to add more nested `WHERE` statements. * - * @param Closure(WhereGroupBuilder):void $callback + * @param Closure(WhereGroupBuilder):void $callback * @param 'AND'|'OR' $operator * * @return self */ public function whereGroup(Closure $callback, string $operator = 'AND'): self { + /** @var WhereGroupBuilder $groupBuilder */ $groupBuilder = new WhereGroupBuilder($this->model); $callback($groupBuilder); @@ -181,7 +182,7 @@ public function whereGroup(Closure $callback, string $operator = 'AND'): self /** * Adds another nested `AND WHERE` statement. The callback accepts a builder, which may be used to add more nested `WHERE` statements. * - * @param Closure(WhereGroupBuilder):void $callback + * @param Closure(WhereGroupBuilder):void $callback * * @return self */ @@ -193,7 +194,7 @@ public function andWhereGroup(Closure $callback): self /** * Adds another nested `OR WHERE` statement. The callback accepts a builder, which may be used to add more nested `WHERE` statements. * - * @param Closure(WhereGroupBuilder):void $callback + * @param Closure(WhereGroupBuilder):void $callback * * @return self */ diff --git a/packages/database/src/Commands/MakeMigrationCommand.php b/packages/database/src/Commands/MakeMigrationCommand.php index 33e33814cb..7e6c09507f 100644 --- a/packages/database/src/Commands/MakeMigrationCommand.php +++ b/packages/database/src/Commands/MakeMigrationCommand.php @@ -89,6 +89,7 @@ public function __invoke( $table = $this->resolveTableName($table); [$migrationName, $className] = $this->resolveNames($name, $alter); + /** @var MigrationType $migrationType */ $stub = $this->resolveStub($migrationType, $alter); $targetPath = match ($migrationType) { diff --git a/packages/database/src/DatabaseInsightsProvider.php b/packages/database/src/DatabaseInsightsProvider.php index 1117d01c12..a3afaba7e8 100644 --- a/packages/database/src/DatabaseInsightsProvider.php +++ b/packages/database/src/DatabaseInsightsProvider.php @@ -4,6 +4,7 @@ use Tempest\Core\Insight; use Tempest\Core\InsightsProvider; +use Tempest\Core\InsightType; use Tempest\Database\Config\DatabaseConfig; use Tempest\Database\Config\MysqlConfig; use Tempest\Database\Config\PostgresConfig; @@ -38,7 +39,7 @@ private function getDatabaseEngine(): string SQLiteConfig::class => 'SQLite', PostgresConfig::class => 'PostgreSQL', MysqlConfig::class => 'MySQL', - default => ['Unknown', null], + default => 'Unknown', }; } @@ -53,7 +54,7 @@ private function getDatabaseVersion(): Insight }; if (! $versionQuery) { - return new Insight('Unknown', Insight::ERROR); + return new Insight('Unknown', InsightType::ERROR); } try { @@ -63,11 +64,11 @@ private function getDatabaseVersion(): Insight match: 'version', )); } catch (\Throwable $e) { - return new Insight('Unavailable', Insight::ERROR); + return new Insight('Unavailable', InsightType::ERROR); } } - private function getSQLitePath(): null|Insight|string + private function getSQLitePath(): ?string { if (! $this->databaseConfig instanceof SQLiteConfig) { return null; diff --git a/packages/database/src/GenericDatabase.php b/packages/database/src/GenericDatabase.php index e91004e9c4..06d0184027 100644 --- a/packages/database/src/GenericDatabase.php +++ b/packages/database/src/GenericDatabase.php @@ -10,6 +10,7 @@ use Tempest\Database\Builder\QueryBuilders\BuildsQuery; use Tempest\Database\Config\DatabaseDialect; use Tempest\Database\Connection\Connection; +use Tempest\Database\Connection\PDOConnection; use Tempest\Database\Exceptions\QueryWasInvalid; use Tempest\Database\Transactions\TransactionManager; use Tempest\Mapper\SerializerFactory; @@ -17,6 +18,8 @@ use Throwable; use UnitEnum; +// TODO: add DatabaseConnection to the Connection interface instead (4.x) +/** @property PDOConnection $connection */ final class GenericDatabase implements Database { private ?PDOStatement $lastStatement = null; diff --git a/packages/database/src/IsDatabaseModel.php b/packages/database/src/IsDatabaseModel.php index e0f82e984f..82fa44e22b 100644 --- a/packages/database/src/IsDatabaseModel.php +++ b/packages/database/src/IsDatabaseModel.php @@ -6,6 +6,7 @@ use Tempest\Database\Builder\QueryBuilders\CountQueryBuilder; use Tempest\Database\Builder\QueryBuilders\InsertQueryBuilder; +use Tempest\Database\Builder\QueryBuilders\QueryBuilder; use Tempest\Database\Builder\QueryBuilders\SelectQueryBuilder; use Tempest\Database\Exceptions\RelationWasMissing; use Tempest\Database\Exceptions\ValueWasMissing; @@ -21,76 +22,84 @@ trait IsDatabaseModel #[IsBindingValue, SkipValidation] public PrimaryKey $id; + /** + * @return QueryBuilder + */ + protected static function queryBuilder(): QueryBuilder + { + return query(static::class); + } + /** * Returns a builder for selecting records using this model's table. * - * @return SelectQueryBuilder + * @return SelectQueryBuilder */ public static function select(): SelectQueryBuilder { - return query(self::class)->select(); + return static::queryBuilder()->select(); } /** * Returns a builder for inserting records using this model's table. * - * @return InsertQueryBuilder + * @return InsertQueryBuilder */ public static function insert(): InsertQueryBuilder { - return query(self::class)->insert(); + return static::queryBuilder()->insert(); } /** * Returns a builder for counting records using this model's table. * - * @return CountQueryBuilder + * @return CountQueryBuilder */ public static function count(): CountQueryBuilder { - return query(self::class)->count(); + return static::queryBuilder()->count(); } /** * Creates a new instance of this model without persisting it to the database. */ - public static function new(mixed ...$params): self + public static function new(mixed ...$params): static { - return query(self::class)->new(...$params); + return static::queryBuilder()->new(...$params); } /** * Finds a model instance by its ID. */ - public static function findById(string|int|PrimaryKey $id): self + public static function findById(string|int|PrimaryKey $id): ?static { - return self::get($id); + return static::get($id); } /** * Finds a model instance by its ID. Use through {@see Tempest\Router\Bindable}. */ - public static function resolve(string $input): self + public static function resolve(string $input): static { - return query(self::class)->resolve($input); + return static::queryBuilder()->resolve($input); } /** * Gets a model instance by its ID, optionally loading the given relationships. */ - public static function get(string|int|PrimaryKey $id, array $relations = []): ?self + public static function get(string|int|PrimaryKey $id, array $relations = []): ?static { - return query(self::class)->get($id, $relations); + return static::queryBuilder()->get($id, $relations); } /** * Gets all records from the model's table. * - * @return self[] + * @return static[] */ public static function all(array $relations = []): array { - return query(self::class)->all($relations); + return static::queryBuilder()->all($relations); } /** @@ -101,11 +110,11 @@ public static function all(array $relations = []): array * MagicUser::find(name: 'Frieren'); * ``` * - * @return SelectQueryBuilder + * @return SelectQueryBuilder */ public static function find(mixed ...$conditions): SelectQueryBuilder { - return query(self::class)->find(...$conditions); + return static::queryBuilder()->find(...$conditions); } /** @@ -116,11 +125,11 @@ public static function find(mixed ...$conditions): SelectQueryBuilder * MagicUser::create(name: 'Frieren', kind: Kind::ELF); * ``` * - * @return self + * @return static */ - public static function create(mixed ...$params): self + public static function create(mixed ...$params): static { - return query(self::class)->create(...$params); + return static::queryBuilder()->create(...$params); } /** @@ -136,11 +145,11 @@ public static function create(mixed ...$params): self * * @param array $find Properties to search for in the existing model. * @param array $update Properties to update or set on the model if it is found or created. - * @return self + * @return static */ - public static function findOrNew(array $find, array $update): self + public static function findOrNew(array $find, array $update): static { - return query(self::class)->findOrNew($find, $update); + return static::queryBuilder()->findOrNew($find, $update); } /** @@ -157,15 +166,15 @@ public static function findOrNew(array $find, array $update): self * @param array $find Properties to search for in the existing model. * @param array $update Properties to update or set on the model if it is found or created. */ - public static function updateOrCreate(array $find, array $update): self + public static function updateOrCreate(array $find, array $update): static { - return query(self::class)->updateOrCreate($find, $update); + return static::queryBuilder()->updateOrCreate($find, $update); } /** * Refreshes the model instance with the latest data from the database. */ - public function refresh(): self + public function refresh(): static { $model = inspect($this); @@ -176,7 +185,7 @@ public function refresh(): self $primaryKeyProperty = $model->getPrimaryKeyProperty(); $primaryKeyValue = $primaryKeyProperty->getValue($this); - $new = self::select() + $new = static::select() ->with(...$loadedRelations->map(fn (Relation $relation) => $relation->name)) ->get($primaryKeyValue); @@ -200,14 +209,14 @@ public function refresh(): self /** * Loads the specified relations on the model instance. */ - public function load(string ...$relations): self + public function load(string ...$relations): static { $model = inspect($this); $primaryKeyProperty = $model->getPrimaryKeyProperty(); $primaryKeyValue = $primaryKeyProperty->getValue($this); - $new = self::get($primaryKeyValue, $relations); + $new = static::get($primaryKeyValue, $relations); $fieldsToUpdate = arr($relations) ->map(fn (string $relation) => str($relation)->before('.')->toString()) @@ -223,7 +232,7 @@ public function load(string ...$relations): self /** * Saves the model to the database. If the model has no primary key, this method always inserts. */ - public function save(): self + public function save(): static { $model = inspect($this); $model->validate(...inspect($this)->getPropertyValues()); @@ -266,7 +275,7 @@ public function save(): self /** * Updates the specified columns and persist the model to the database. */ - public function update(mixed ...$params): self + public function update(mixed ...$params): static { $model = inspect($this); diff --git a/packages/database/src/Migrations/MigrationManager.php b/packages/database/src/Migrations/MigrationManager.php index 530ac36d6c..bfcabca2a6 100644 --- a/packages/database/src/Migrations/MigrationManager.php +++ b/packages/database/src/Migrations/MigrationManager.php @@ -194,10 +194,6 @@ public function executeUp(MigratesUp $migration): void $statement = $migration->up(); - if ($statement === null) { - return; - } - if ($statement instanceof CompoundStatement) { $statements = $statement->statements; } else { diff --git a/packages/database/src/Migrations/RunnableMigrations.php b/packages/database/src/Migrations/RunnableMigrations.php index 8c532dbead..c90a8de193 100644 --- a/packages/database/src/Migrations/RunnableMigrations.php +++ b/packages/database/src/Migrations/RunnableMigrations.php @@ -10,11 +10,11 @@ use Tempest\Database\MigratesUp; use Traversable; -/** @implements IteratorAggregate */ +/** @implements IteratorAggregate */ final class RunnableMigrations implements IteratorAggregate { /** - * @param MigratesUp[] $migrations + * @param array $migrations */ public function __construct( private array $migrations = [], @@ -22,6 +22,9 @@ public function __construct( usort($this->migrations, static fn (MigratesUp|MigratesDown $a, MigratesUp|MigratesDown $b) => strnatcmp($a->name, $b->name)); } + /** + * @return Traversable + */ public function getIterator(): Traversable { return new ArrayIterator($this->migrations); diff --git a/packages/database/src/QueryStatements/HasWhereStatements.php b/packages/database/src/QueryStatements/HasWhereStatements.php index ba77fa4dcf..91869b1046 100644 --- a/packages/database/src/QueryStatements/HasWhereStatements.php +++ b/packages/database/src/QueryStatements/HasWhereStatements.php @@ -6,7 +6,7 @@ interface HasWhereStatements { - /** @var ImmutableArray */ + /** @var ImmutableArray */ public ImmutableArray $where { get; } diff --git a/packages/database/src/QueryStatements/SelectStatement.php b/packages/database/src/QueryStatements/SelectStatement.php index 6f7371e4b9..926b5afc52 100644 --- a/packages/database/src/QueryStatements/SelectStatement.php +++ b/packages/database/src/QueryStatements/SelectStatement.php @@ -2,7 +2,6 @@ namespace Tempest\Database\QueryStatements; -use Stringable; use Tempest\Database\Builder\TableDefinition; use Tempest\Database\Config\DatabaseDialect; use Tempest\Database\QueryStatement; @@ -45,29 +44,30 @@ public function withFields(ImmutableArray $fields): self public function compile(DatabaseDialect $dialect): string { - $columns = $this->fields->isEmpty() - ? '*' - : $this->fields - ->flatMap(function (string|Stringable|FieldStatement $field) { - if ($field instanceof FieldStatement) { - return $field; - } - - return str($field)->explode(',')->toArray(); - }) - ->map(function (string|Stringable|FieldStatement $field) use ($dialect) { - if (! $field instanceof FieldStatement) { - $field = new FieldStatement($field); - } - - return $field->compile($dialect); - }) - ->implode(', '); + $columns = '*'; + + if ($this->fields->isNotEmpty()) { + $compiledColumns = []; + + foreach ($this->fields as $field) { + if ($field instanceof FieldStatement) { + $compiledColumns[] = $field->compile($dialect); + + continue; + } + + foreach (str($field)->explode(',') as $splitField) { + $compiledColumns[] = new FieldStatement($splitField)->compile($dialect); + } + } - $query = new ImmutableArray([ + $columns = implode(', ', $compiledColumns); + } + + $query = [ 'SELECT ' . $columns, 'FROM ' . $this->table, - ]); + ]; if ($this->join->isNotEmpty()) { $query[] = $this->join @@ -111,12 +111,11 @@ public function compile(DatabaseDialect $dialect): string if ($this->raw->isNotEmpty()) { $query[] = $this->raw ->map(fn (RawStatement $raw) => $raw->compile($dialect)) - ->implode(' '); + ->implode(' ') + ->toString(); } - $compiled = $query->implode(' '); - - return $compiled; + return implode(' ', $query); } public function __clone(): void diff --git a/packages/database/src/RawSql.php b/packages/database/src/RawSql.php index 2f8023061d..de322f2f3d 100644 --- a/packages/database/src/RawSql.php +++ b/packages/database/src/RawSql.php @@ -9,7 +9,7 @@ final class RawSql { - private ?RawSqlDatabaseContext $context { + private RawSqlDatabaseContext $context { get => $this->context ??= new RawSqlDatabaseContext($this->dialect); } @@ -28,7 +28,7 @@ public function compile(): string return $this->replaceNamedBindings($this->sql, $resolvedBindings); } - return $this->replacePositionalBindings($this->sql, array_values($resolvedBindings)); + return $this->replacePositionalBindings($this->sql, $resolvedBindings); } public function toImmutableString(): ImmutableString diff --git a/packages/database/src/Serializers/DataTransferObjectSerializer.php b/packages/database/src/Serializers/DataTransferObjectSerializer.php index 60162dfcfe..a9aacf5f06 100644 --- a/packages/database/src/Serializers/DataTransferObjectSerializer.php +++ b/packages/database/src/Serializers/DataTransferObjectSerializer.php @@ -46,7 +46,7 @@ public static function accepts(PropertyReflector|TypeReflector $type): bool return $type->isClass() && $type->asClass()->getAttribute(SerializeAs::class) !== null; } - public function serialize(mixed $input): array|string + public function serialize(mixed $input): string { if (is_array($input)) { return Json\encode($this->serializeWithType($input)); diff --git a/packages/database/src/Serializers/HashedSerializer.php b/packages/database/src/Serializers/HashedSerializer.php index b6b2b2b776..b02ed9f07a 100644 --- a/packages/database/src/Serializers/HashedSerializer.php +++ b/packages/database/src/Serializers/HashedSerializer.php @@ -3,6 +3,7 @@ namespace Tempest\Database\Serializers; use Tempest\Cryptography\Password\PasswordHasher; +use Tempest\Mapper\Exceptions\ValueCouldNotBeSerialized; use Tempest\Mapper\Serializer; final readonly class HashedSerializer implements Serializer @@ -14,7 +15,7 @@ public function __construct( public function serialize(mixed $input): string { if (! is_string($input)) { - return $input; + throw new ValueCouldNotBeSerialized('string'); } if (! $this->passwordHasher->analyze($input)) { diff --git a/packages/database/src/functions.php b/packages/database/src/functions.php index c91b2ff343..cdf059e174 100644 --- a/packages/database/src/functions.php +++ b/packages/database/src/functions.php @@ -10,7 +10,7 @@ * * @template TModel of object * @param class-string|string|TModel $model - * @return QueryBuilder + * @return ($model is class-string|TModel ? QueryBuilder : QueryBuilder) */ function query(string|object $model): QueryBuilder { @@ -22,7 +22,7 @@ function query(string|object $model): QueryBuilder * * @template TModel of object * @param class-string|string|TModel $model - * @return ModelInspector + * @return ModelInspector * @internal */ function inspect(string|object $model): ModelInspector diff --git a/packages/database/tests/TypeInference/QueryBuilderGenericsTypeAssertions.php b/packages/database/tests/TypeInference/QueryBuilderGenericsTypeAssertions.php new file mode 100644 index 0000000000..50b835e24e --- /dev/null +++ b/packages/database/tests/TypeInference/QueryBuilderGenericsTypeAssertions.php @@ -0,0 +1,105 @@ +', $queryBuilder); + +$selectQueryBuilder = $queryBuilder->select(); +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\SelectQueryBuilder', $selectQueryBuilder); + +$whereField = $selectQueryBuilder->whereField('id', 1); +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\SelectQueryBuilder', $whereField); + +$where = $selectQueryBuilder->where('id', 1); +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\SelectQueryBuilder', $where); + +$whereIn = $selectQueryBuilder->whereIn('id', [1, 2]); +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\SelectQueryBuilder', $whereIn); + +$selectQueryBuilder->whereGroup(static function ($group): void { + \PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\WhereGroupBuilder', $group); + + $group->whereGroup(static function ($nestedGroup): void { + \PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\WhereGroupBuilder', $nestedGroup); + }); + + $group->andWhereGroup(static function ($nestedGroup): void { + \PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\WhereGroupBuilder', $nestedGroup); + }); + + $group->orWhereGroup(static function ($nestedGroup): void { + \PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\WhereGroupBuilder', $nestedGroup); + }); +}); + +\PHPStan\Testing\assertType('Tempest\\Database\\Tests\\QueryStatements\\StubModel|null', $where->first()); +\PHPStan\Testing\assertType('array', $where->all()); + +$countBuilder = $queryBuilder->count(); +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\CountQueryBuilder', $countBuilder); + +$countWhere = $countBuilder->whereIn('id', [1, 2]); +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\CountQueryBuilder', $countWhere); + +$countFromSelect = CountQueryBuilder::fromQueryBuilder($where); +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\CountQueryBuilder', $countFromSelect); + +$updateBuilder = $queryBuilder->update(name: 'Rudy'); +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\UpdateQueryBuilder', $updateBuilder); + +$updateWhere = $updateBuilder->whereIn('id', [1, 2]); +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\UpdateQueryBuilder', $updateWhere); + +$updateFromSelect = UpdateQueryBuilder::fromQueryBuilder($where, name: 'Rudy'); +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\UpdateQueryBuilder', $updateFromSelect); + +$deleteBuilder = $queryBuilder->delete(); +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\DeleteQueryBuilder', $deleteBuilder); + +$deleteWhere = $deleteBuilder->whereIn('id', [1, 2]); +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\DeleteQueryBuilder', $deleteWhere); + +$deleteFromSelect = DeleteQueryBuilder::fromQueryBuilder($where); +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\DeleteQueryBuilder', $deleteFromSelect); + +$selectFromCount = SelectQueryBuilder::fromQueryBuilder($countBuilder); +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\SelectQueryBuilder', $selectFromCount); + +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\SelectQueryBuilder', StubModel::select()); +\PHPStan\Testing\assertType('Tempest\\Database\\Tests\\QueryStatements\\StubModel|null', StubModel::select()->where('id', 1)->first()); +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\SelectQueryBuilder', ChildStubModel::select()); +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\CountQueryBuilder', ChildStubModel::count()); +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\SelectQueryBuilder', ChildStubModel::find(id: 1)); +\PHPStan\Testing\assertType('Tempest\\Database\\Tests\\TypeInference\\ChildStubModel|null', ChildStubModel::findById(1)); +\PHPStan\Testing\assertType('Tempest\\Database\\Tests\\TypeInference\\ChildStubModel|null', ChildStubModel::get(1)); +\PHPStan\Testing\assertType('array', ChildStubModel::all()); + +$tableQueryBuilder = query('stub_models'); +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\QueryBuilder', $tableQueryBuilder); +\PHPStan\Testing\assertType('Tempest\\Database\\Builder\\QueryBuilders\\SelectQueryBuilder', $tableQueryBuilder->select()); +\PHPStan\Testing\assertType('object|null', $tableQueryBuilder->select()->where('id', 1)->first()); +\PHPStan\Testing\assertType('array', $tableQueryBuilder->select()->where('id', 1)->all()); diff --git a/packages/datetime/src/DateTime.php b/packages/datetime/src/DateTime.php index f1ab03f74b..830da8487d 100644 --- a/packages/datetime/src/DateTime.php +++ b/packages/datetime/src/DateTime.php @@ -171,28 +171,42 @@ public static function fromParts(Timezone $timezone, int $year, Month|int $month $seconds, ); - if ($seconds !== $calendar->get(IntlCalendar::FIELD_SECOND)) { - throw Exception\UnexpectedValueException::forSeconds($seconds, $calendar->get(IntlCalendar::FIELD_SECOND)); + $calendarYear = $calendar->get(IntlCalendar::FIELD_YEAR); + $calendarMonth = $calendar->get(IntlCalendar::FIELD_MONTH); + $calendarDay = $calendar->get(IntlCalendar::FIELD_DAY_OF_MONTH); + $calendarHours = $calendar->get(IntlCalendar::FIELD_HOUR_OF_DAY); + $calendarMinutes = $calendar->get(IntlCalendar::FIELD_MINUTE); + $calendarSeconds = $calendar->get(IntlCalendar::FIELD_SECOND); + + if ($calendarYear === false || $calendarMonth === false || $calendarDay === false || $calendarHours === false || $calendarMinutes === false || $calendarSeconds === false) { + throw new Exception\OverflowException(sprintf( + 'The year value "%d" exceeds the range supported by the calendar.', + $year, + )); } - if ($minutes !== $calendar->get(IntlCalendar::FIELD_MINUTE)) { - throw Exception\UnexpectedValueException::forMinutes($minutes, $calendar->get(IntlCalendar::FIELD_MINUTE)); + if ($seconds !== $calendarSeconds) { + throw Exception\UnexpectedValueException::forSeconds($seconds, $calendarSeconds); } - if ($hours !== $calendar->get(IntlCalendar::FIELD_HOUR_OF_DAY)) { - throw Exception\UnexpectedValueException::forHours($hours, $calendar->get(IntlCalendar::FIELD_HOUR_OF_DAY)); + if ($minutes !== $calendarMinutes) { + throw Exception\UnexpectedValueException::forMinutes($minutes, $calendarMinutes); } - if ($day !== $calendar->get(IntlCalendar::FIELD_DAY_OF_MONTH)) { - throw Exception\UnexpectedValueException::forDay($day, $calendar->get(IntlCalendar::FIELD_DAY_OF_MONTH)); + if ($hours !== $calendarHours) { + throw Exception\UnexpectedValueException::forHours($hours, $calendarHours); } - if ($month !== ($calendar->get(IntlCalendar::FIELD_MONTH) + 1)) { - throw Exception\UnexpectedValueException::forMonth($month, $calendar->get(IntlCalendar::FIELD_MONTH) + 1); + if ($day !== $calendarDay) { + throw Exception\UnexpectedValueException::forDay($day, $calendarDay); } - if ($year !== $calendar->get(IntlCalendar::FIELD_YEAR)) { - throw Exception\UnexpectedValueException::forYear($year, $calendar->get(IntlCalendar::FIELD_YEAR)); + if ($month !== ($calendarMonth + 1)) { + throw Exception\UnexpectedValueException::forMonth($month, $calendarMonth + 1); + } + + if ($year !== $calendarYear) { + throw Exception\UnexpectedValueException::forYear($year, $calendarYear); } $timestamp_in_seconds = (int) ($calendar->getTime() / (float) MILLISECONDS_PER_SECOND); @@ -263,9 +277,11 @@ public static function parse(NativeDateTimeInterface|TemporalInterface|string|in } if ($string instanceof NativeDateTimeInterface) { + $nativeTimezone = $string->getTimezone(); + return self::fromTimestamp( timestamp: Timestamp::fromParts($string->getTimestamp()), - timezone: $timezone ?? Timezone::tryFrom($string->getTimezone()?->getName() ?? ''), + timezone: $timezone ?? ($nativeTimezone instanceof \DateTimeZone ? Timezone::tryFrom($nativeTimezone->getName()) : null), ); } diff --git a/packages/datetime/src/DateTimeConvenienceMethods.php b/packages/datetime/src/DateTimeConvenienceMethods.php index 88388015ce..24536f7eed 100644 --- a/packages/datetime/src/DateTimeConvenienceMethods.php +++ b/packages/datetime/src/DateTimeConvenienceMethods.php @@ -338,8 +338,7 @@ public function isLeapYear(): bool /** * Adds a year to this date-time object, returning a new instance with the added year. * - * @throws Exception\UnderflowException If adding the years results in an arithmetic underflow. - * @throws Exception\OverflowException If adding the years results in an arithmetic overflow. + * @throws Exception\UnexpectedValueException If adding the year results in an arithmetic issue. */ public function plusYear(): static { @@ -359,8 +358,7 @@ public function plusYears(int $years): static /** * Subtracts a year from this date-time object, returning a new instance with the subtracted year. * - * @throws Exception\UnderflowException If subtracting the years results in an arithmetic underflow. - * @throws Exception\OverflowException If subtracting the years results in an arithmetic overflow. + * @throws Exception\UnexpectedValueException If subtracting the year results in an arithmetic issue. */ public function minusYear(): static { @@ -380,8 +378,7 @@ public function minusYears(int $years): static /** * Adds a month to this date-time object, returning a new instance with the added month. * - * @throws Exception\UnderflowException If adding the months results in an arithmetic underflow. - * @throws Exception\OverflowException If adding the months results in an arithmetic overflow. + * @throws Exception\UnexpectedValueException If adding the month results in an arithmetic issue. */ public function plusMonth(): static { @@ -424,8 +421,7 @@ public function plusMonths(int $months): static /** * Subtracts a month from this date-time object, returning a new instance with the subtracted month. * - * @throws Exception\UnderflowException If subtracting the months results in an arithmetic underflow. - * @throws Exception\OverflowException If subtracting the months results in an arithmetic overflow. + * @throws Exception\UnexpectedValueException If subtracting the month results in an arithmetic issue. */ public function minusMonth(): static { diff --git a/packages/datetime/src/TemporalConvenienceMethods.php b/packages/datetime/src/TemporalConvenienceMethods.php index d3c2362737..e024805030 100644 --- a/packages/datetime/src/TemporalConvenienceMethods.php +++ b/packages/datetime/src/TemporalConvenienceMethods.php @@ -558,7 +558,6 @@ public function __toString(): string /** * Stops the execution and dumps the current state of this temporal object. * - * @phpstan-ignore disallowed.function * @mago-expect lint:no-debug-symbols */ public function dd(): never diff --git a/packages/datetime/src/functions.php b/packages/datetime/src/functions.php index 0c4a9fc044..63233bcf7d 100644 --- a/packages/datetime/src/functions.php +++ b/packages/datetime/src/functions.php @@ -7,6 +7,7 @@ use IntlTimeZone; use RuntimeException; use Tempest\DateTime\DateStyle; +use Tempest\DateTime\Exception\OverflowException; use Tempest\DateTime\Exception\ParserException; use Tempest\DateTime\FormatPattern; use Tempest\DateTime\SecondsStyle; @@ -14,6 +15,7 @@ use Tempest\DateTime\TimeStyle; use Tempest\DateTime\Timezone; use Tempest\Intl\Locale; +use ValueError; use function hrtime; use function microtime; @@ -287,7 +289,16 @@ function create_intl_calendar_from_date_time( */ $calendar = IntlCalendar::createInstance(to_intl_timezone($timezone)); - $calendar->setDateTime($year, $month - 1, $day, $hours, $minutes, $seconds); + try { + $calendar->setDateTime($year, $month - 1, $day, $hours, $minutes, $seconds); + } catch (ValueError) { + throw new OverflowException(sprintf( + 'The year value "%d" exceeds the supported calendar range (%d to %d).', + $year, + -2_147_483_648, + 2_147_483_647, + )); + } return $calendar; } diff --git a/packages/datetime/tests/DateTimeTest.php b/packages/datetime/tests/DateTimeTest.php index 056b6f18bc..ba74285734 100644 --- a/packages/datetime/tests/DateTimeTest.php +++ b/packages/datetime/tests/DateTimeTest.php @@ -7,10 +7,12 @@ use DateTimeImmutable; use DateTimeZone; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Tempest\DateTime\DateStyle; use Tempest\DateTime\DateTime; +use Tempest\DateTime\Exception\OverflowException; use Tempest\DateTime\Exception\UnexpectedValueException; use Tempest\DateTime\FormatPattern; use Tempest\DateTime\Meridiem; @@ -1031,4 +1033,12 @@ public function test_timezone_considerations_for_same_methods(): void $this->assertTrue($utc->isSameYear($brussels)); $this->assertTrue($utc->isSameMonth($brussels)); } + + #[Test] + public function from_parts_with_year_exceeding_intl_calendar_range_throws_overflow(): void + { + $this->expectException(OverflowException::class); + + DateTime::fromParts(Timezone::UTC, PHP_INT_MAX, 1, 1); + } } diff --git a/packages/debug/src/TailDebugCommand.php b/packages/debug/src/TailDebugCommand.php index ae4338d78e..98397faa71 100644 --- a/packages/debug/src/TailDebugCommand.php +++ b/packages/debug/src/TailDebugCommand.php @@ -7,8 +7,6 @@ use Tempest\Console\Console; use Tempest\Console\ConsoleCommand; use Tempest\Console\Output\TailReader; -use Tempest\Container\Tag; -use Tempest\Highlight\Highlighter; use Tempest\Support\Filesystem; final readonly class TailDebugCommand @@ -16,8 +14,6 @@ public function __construct( private Console $console, private DebugConfig $debugConfig, - #[Tag('console')] - private Highlighter $highlighter, ) {} #[ConsoleCommand('tail:debug', description: 'Tails the debug log', aliases: ['debug:tail'])] diff --git a/packages/debug/tests/StacktraceTest.php b/packages/debug/tests/StacktraceTest.php index 332a371eea..6edf46f7be 100644 --- a/packages/debug/tests/StacktraceTest.php +++ b/packages/debug/tests/StacktraceTest.php @@ -22,7 +22,6 @@ public function creates_stacktrace_from_throwable(): void $this->assertSame('Test exception', $stacktrace->message); $this->assertSame(RuntimeException::class, $stacktrace->exceptionClass); $this->assertNotEmpty($stacktrace->frames); - $this->assertContainsOnlyInstancesOf(Frame::class, $stacktrace->frames); } #[Test] diff --git a/packages/event-bus/src/Testing/EventBusTester.php b/packages/event-bus/src/Testing/EventBusTester.php index b1409ecf45..668e4651b0 100644 --- a/packages/event-bus/src/Testing/EventBusTester.php +++ b/packages/event-bus/src/Testing/EventBusTester.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\Assert; use Tempest\Container\Container; use Tempest\EventBus\EventBus; +use Tempest\EventBus\EventBusConfig; use Tempest\Support\Str; final class EventBusTester @@ -24,7 +25,8 @@ public function __construct( public function recordEventDispatches(bool $preventHandling = false): self { $this->fakeEventBus = new FakeEventBus( - genericEventBus: $this->container->get(EventBus::class), + eventBus: $this->container->get(EventBus::class), + eventBusConfig: $this->container->get(EventBusConfig::class), preventHandling: $preventHandling, ); diff --git a/packages/event-bus/src/Testing/FakeEventBus.php b/packages/event-bus/src/Testing/FakeEventBus.php index 9003c08be0..dcc0962a52 100644 --- a/packages/event-bus/src/Testing/FakeEventBus.php +++ b/packages/event-bus/src/Testing/FakeEventBus.php @@ -5,7 +5,7 @@ use Closure; use Tempest\EventBus\CallableEventHandler; use Tempest\EventBus\EventBus; -use Tempest\EventBus\GenericEventBus; +use Tempest\EventBus\EventBusConfig; use UnitEnum; final class FakeEventBus implements EventBus @@ -15,11 +15,12 @@ final class FakeEventBus implements EventBus /** @var array> */ public array $handlers { - get => $this->genericEventBus->eventBusConfig->handlers; + get => $this->eventBusConfig->handlers; } public function __construct( - private(set) GenericEventBus $genericEventBus, + private(set) EventBus $eventBus, + private(set) EventBusConfig $eventBusConfig, public bool $preventHandling = true, ) {} @@ -28,12 +29,12 @@ public function dispatch(string|object $event): void $this->dispatched[] = $event; if ($this->preventHandling === false) { - $this->genericEventBus->dispatch($event); + $this->eventBus->dispatch($event); } } public function listen(Closure $handler, string|UnitEnum|null $event = null): void { - $this->genericEventBus->listen($handler, $event); + $this->eventBus->listen($handler, $event); } } diff --git a/packages/event-bus/tests/EventBusTest.php b/packages/event-bus/tests/EventBusTest.php index 3f47529ba3..f667779a9a 100644 --- a/packages/event-bus/tests/EventBusTest.php +++ b/packages/event-bus/tests/EventBusTest.php @@ -209,6 +209,7 @@ public function test_closure_based_handlers_using_listen_method_and_enums(): voi $eventBus->dispatch(EventEnum::TWO); + /** @var bool $hasHappened */ $this->assertTrue($hasHappened); } } diff --git a/packages/generation/src/Php/ClassManipulator.php b/packages/generation/src/Php/ClassManipulator.php index ba848b881e..b227a794f5 100644 --- a/packages/generation/src/Php/ClassManipulator.php +++ b/packages/generation/src/Php/ClassManipulator.php @@ -18,13 +18,10 @@ final class ClassManipulator public function __construct(string|ReflectionClass $source) { if (is_file($source)) { - /** @phpstan-ignore-next-line */ $this->classType = ClassType::fromCode(Filesystem\read_file($source)); } elseif (is_string($source)) { - /** @phpstan-ignore-next-line */ $this->classType = ClassType::from($source, withBodies: true); } else { - /** @phpstan-ignore-next-line */ $this->classType = ClassType::from($source->getName(), withBodies: true); } diff --git a/packages/generation/src/TypeScript/TypeResolvers/ScalarTypeResolver.php b/packages/generation/src/TypeScript/TypeResolvers/ScalarTypeResolver.php index 73765ef03a..ae88f7df35 100644 --- a/packages/generation/src/TypeScript/TypeResolvers/ScalarTypeResolver.php +++ b/packages/generation/src/TypeScript/TypeResolvers/ScalarTypeResolver.php @@ -13,17 +13,22 @@ #[Priority(Priority::LOW)] final class ScalarTypeResolver implements TypeResolver { + private const array SCALAR_TYPE_MAP = [ + 'string' => 'string', + 'int' => 'number', + 'float' => 'number', + 'bool' => 'boolean', + ]; + public function canResolve(TypeReflector $type): bool { - return $type->isBuiltIn() && in_array($type->getName(), ['string', 'int', 'float', 'bool'], strict: true); + return $type->isBuiltIn() && isset(self::SCALAR_TYPE_MAP[$type->getName()]); } public function resolve(TypeReflector $type, TypeScriptGenerator $generator): ResolvedType { - return new ResolvedType(match ($type->getName()) { - 'string' => 'string', - 'int', 'float' => 'number', - 'bool' => 'boolean', - }); + $type = self::SCALAR_TYPE_MAP[$type->getName()] ?? throw new \LogicException(sprintf('Unsupported scalar type "%s".', $type->getName())); + + return new ResolvedType($type); } } diff --git a/packages/generation/src/TypeScript/TypeScriptOutput.php b/packages/generation/src/TypeScript/TypeScriptOutput.php index 2c80408807..75ee3a1a9e 100644 --- a/packages/generation/src/TypeScript/TypeScriptOutput.php +++ b/packages/generation/src/TypeScript/TypeScriptOutput.php @@ -10,7 +10,7 @@ final readonly class TypeScriptOutput { /** - * @param array $namespaces Type definitions grouped by namespace + * @param array> $namespaces Type definitions grouped by namespace * @param array $imports Additional import statements to include */ public function __construct( @@ -21,13 +21,13 @@ public function __construct( /** * Gets all type definitions across all namespaces. * - * @return TypeDefinition[] + * @return array */ public function getAllDefinitions(): array { $definitions = []; - foreach ($this->namespaces as $namespace => $namespaceDefinitions) { + foreach ($this->namespaces as $namespaceDefinitions) { $definitions = [...$definitions, ...$namespaceDefinitions]; } @@ -47,7 +47,7 @@ public function getNamespaces(): array /** * Gets definitions for a specific namespace. * - * @return TypeDefinition[] + * @return array */ public function getDefinitionsForNamespace(string $namespace): array { diff --git a/packages/http/src/Cookie/Cookie.php b/packages/http/src/Cookie/Cookie.php index 71ff00fd6c..36f235faa7 100644 --- a/packages/http/src/Cookie/Cookie.php +++ b/packages/http/src/Cookie/Cookie.php @@ -17,8 +17,8 @@ final class Cookie implements Stringable * @param null|int $maxAge The maximum age of the cookie in seconds. This is an alternative to the expiration date. If set, the cookie will expire after the specified number of seconds. * @param null|string $domain This specifies the domain for which the cookie is valid. If set, the cookie will be sent to this domain and its subdomains. If `⁠null`, it defaults to the current domain. * @param null|string $path The URL path that must exist in the requested URL for the cookie to be sent. If set, the cookie will only be sent for requests to this path and its subdirectories. - * @param null|bool $secure When `true`, this cookie is only transmitted over secure connections. - * @param null|bool $httpOnly When `true`, this cookie will not be accessible using JavaScript. + * @param bool $secure When `true`, this cookie is only transmitted over secure connections. + * @param bool $httpOnly When `true`, this cookie will not be accessible using JavaScript. * @param null|SameSite $sameSite See {@see \Tempest\Http\Cookie\SameSite}. */ public function __construct( @@ -30,7 +30,7 @@ public function __construct( public ?string $path = '/', public bool $secure = true, public bool $httpOnly = false, - public SameSite $sameSite = SameSite::LAX, + public ?SameSite $sameSite = SameSite::LAX, ) {} public function withValue(string $value): self @@ -116,6 +116,7 @@ public static function createFromString(string $string): self } if ($attributeName === 'max-age') { + $cookie['max-age'] = (int) $attributeValue; $cookie['expires'] = time() + (int) $attributeValue; } } diff --git a/packages/http/src/IsRequest.php b/packages/http/src/IsRequest.php index 1ee0020fb6..53408abb37 100644 --- a/packages/http/src/IsRequest.php +++ b/packages/http/src/IsRequest.php @@ -140,7 +140,7 @@ public function accepts(ContentType ...$contentTypes): bool { $header = $this->headers->get(name: 'accept') ?? ''; - /** @var array{mediaType:string,subType:string} */ + /** @var list */ $acceptedMediaTypes = []; foreach (str($header)->explode(separator: ',') as $acceptedType) { diff --git a/packages/http/src/Session/Managers/FileSessionManager.php b/packages/http/src/Session/Managers/FileSessionManager.php index af332393a6..6cfe992ed2 100644 --- a/packages/http/src/Session/Managers/FileSessionManager.php +++ b/packages/http/src/Session/Managers/FileSessionManager.php @@ -5,8 +5,8 @@ namespace Tempest\Http\Session\Managers; use Tempest\Clock\Clock; +use Tempest\Http\Session\Config\FileSessionConfig; use Tempest\Http\Session\Session; -use Tempest\Http\Session\SessionConfig; use Tempest\Http\Session\SessionCreated; use Tempest\Http\Session\SessionDeleted; use Tempest\Http\Session\SessionId; @@ -21,7 +21,7 @@ { public function __construct( private Clock $clock, - private SessionConfig $sessionConfig, + private FileSessionConfig $sessionConfig, // TODO: rename to $config, see RedisSessionManager and DatabaseSessionManager ) {} public function getOrCreate(SessionId $id): Session diff --git a/packages/icon/src/Icon.php b/packages/icon/src/Icon.php index c97a0b3c37..8efcbf076b 100644 --- a/packages/icon/src/Icon.php +++ b/packages/icon/src/Icon.php @@ -40,7 +40,10 @@ public function render(string $icon): ?string expiresAt: $this->iconConfig->expiresAfter, ); - if ($this->iconCache->get("icon-failure-{$collection}-{$iconName}")) { + /** @var bool $failed */ + $failed = $this->iconCache->get("icon-failure-{$collection}-{$iconName}"); + + if ($failed) { $this->iconCache->delete("icon-{$collection}-{$iconName}"); } diff --git a/packages/intl/bin/plural-rules.php b/packages/intl/bin/plural-rules.php index 19de138307..0a79e7795a 100755 --- a/packages/intl/bin/plural-rules.php +++ b/packages/intl/bin/plural-rules.php @@ -32,22 +32,26 @@ public function generate(): string $output .= ' * Generated on: ' . date('Y-m-d H:i:s') . "\n"; $output .= " */\n"; $output .= "final class {$this->className}\n{\n"; - $output .= $this->generateHelperMethods(); $pluralRules = $this->data['supplemental']['plurals-type-cardinal'] ?? []; + $languageMethods = ''; + foreach ($pluralRules as $locale => $rules) { - $output .= $this->generateLanguageMethod($locale, $rules); + $languageMethods .= $this->generateLanguageMethod($locale, $rules); } + $output .= $this->generateHelperMethods(str_contains($languageMethods, 'self::matchesValues(')); + $output .= $languageMethods; + $output .= $this->generateDispatcherMethod(array_keys($pluralRules)); $output .= "}\n"; return $output; } - private function generateHelperMethods(): string + private function generateHelperMethods(bool $includeMatchesValues): string { - return <<<'PHP' + $helpers = <<<'PHP' /** * Extracts the integer part of a number. */ @@ -63,7 +67,7 @@ private static function getVisibleFractionalDigits(float|int $n): int { $str = (string) $n; - if (!str_contains($str, '.')) { + if (! str_contains($str, '.')) { return 0; } @@ -77,7 +81,7 @@ private static function getFractionalDigits(float|int $n): int { $str = (string) $n; - if (!str_contains($str, '.')) { + if (! str_contains($str, '.')) { return 0; } @@ -126,6 +130,21 @@ private static function inRange(int|float $value, int|float $start, int|float $e return $value >= $start && $value <= $end; } + /** + * Checks whether two numeric values are equal. + */ + private static function isEqual(int|float $left, int|float $right): bool + { + return $left === $right; + } + + PHP; + + if (! $includeMatchesValues) { + return $helpers; + } + + return $helpers . <<<'PHP' /** * Checks if number matches any value in comma-separated list. */ @@ -152,6 +171,7 @@ private static function matchesValues(int|float $value, string $values): bool return true; } } + return false; } @@ -184,6 +204,8 @@ private function generateLanguageMethod(string $locale, array $rules): string } } + $excludedEqualities = []; + foreach ($sortedRules as $category => $rule) { if ($category === 'other') { $output .= " return '{$category}';\n"; @@ -191,9 +213,16 @@ private function generateLanguageMethod(string $locale, array $rules): string } if ($condition = $this->parseRule($rule)) { + $condition = $this->removeExcludedNotEquals($condition, $excludedEqualities); + $output .= " if ({$condition}) {\n"; $output .= " return '{$category}';\n"; $output .= " }\n\n"; + + $excludedEqualities = array_values(array_unique([ + ...$excludedEqualities, + ...$this->extractPureEquals($condition), + ])); } } @@ -202,6 +231,56 @@ private function generateLanguageMethod(string $locale, array $rules): string return $output; } + /** + * @return string[] + */ + private function extractPureEquals(string $condition): array + { + if (str_contains($condition, '&&')) { + return []; + } + + preg_match_all('/self::isEqual\(([^,]+),\s*(\d+(?:\.\d+)?)\)/', $condition, $matches, PREG_SET_ORDER); + + if ($matches === []) { + return []; + } + + $stripped = $condition; + $equalities = []; + + foreach ($matches as $match) { + $stripped = str_replace($match[0], '', $stripped); + $equalities[] = 'self::isEqual(' . trim($match[1]) . ", {$match[2]})"; + } + + $stripped = preg_replace('/[()\s]/', '', $stripped); + $stripped = str_replace('||', '', $stripped); + + if ($stripped !== '') { + return []; + } + + return $equalities; + } + + /** + * @param string[] $excludedEqualities + */ + private function removeExcludedNotEquals(string $condition, array $excludedEqualities): string + { + foreach ($excludedEqualities as $equality) { + $negatedEquality = preg_quote("!{$equality}", '/'); + + $condition = preg_replace('/\(\s*' . $negatedEquality . '\s*\)\s*&&\s*/', '', $condition) ?? $condition; + $condition = preg_replace('/\s*&&\s*\(\s*' . $negatedEquality . '\s*\)/', '', $condition) ?? $condition; + $condition = preg_replace('/' . $negatedEquality . '\s*&&\s*/', '', $condition) ?? $condition; + $condition = preg_replace('/\s*&&\s*' . $negatedEquality . '/', '', $condition) ?? $condition; + } + + return $condition; + } + private function parseRule(string $rule): string { $rulePart = trim(explode('@', $rule)[0]); @@ -262,7 +341,9 @@ private function parseValueCondition(string $varExpression, string $operator, st $isNegative = $operator === '!=='; if (preg_match('/^\d+(?:\.\d+)?$/', $values)) { - return "{$varExpression} {$operator} {$values}"; + $condition = "self::isEqual({$varExpression}, {$values})"; + + return $isNegative ? "!{$condition}" : $condition; } if (preg_match('/^(\d+(?:\.\d+)?)\.\.(\d+(?:\.\d+)?)$/', $values, $matches)) { @@ -290,7 +371,7 @@ private function parseValueCondition(string $varExpression, string $operator, st $conditions[] = "self::inRange({$varExpression}, {$start}, {$end})"; } } else { - $conditions[] = "{$varExpression} === {$part}"; + $conditions[] = "self::isEqual({$varExpression}, {$part})"; } } diff --git a/packages/intl/src/Catalog/GenericCatalog.php b/packages/intl/src/Catalog/GenericCatalog.php index 01bdbc682d..34aa05d1f7 100644 --- a/packages/intl/src/Catalog/GenericCatalog.php +++ b/packages/intl/src/Catalog/GenericCatalog.php @@ -8,7 +8,7 @@ final class GenericCatalog implements Catalog { /** - * @var array $catalog + * @param array $catalog */ public function __construct( private array $catalog = [], diff --git a/packages/intl/src/IntlInsightsProvider.php b/packages/intl/src/IntlInsightsProvider.php index 7da6edf3fa..404cce751d 100644 --- a/packages/intl/src/IntlInsightsProvider.php +++ b/packages/intl/src/IntlInsightsProvider.php @@ -4,6 +4,7 @@ use Tempest\Core\Insight; use Tempest\Core\InsightsProvider; +use Tempest\Core\InsightType; use function Tempest\Support\arr; @@ -21,7 +22,7 @@ public function getInsights(): array 'Current locale' => $this->intlConfig->currentLocale->getDisplayLanguage(), 'Fallback locale' => $this->intlConfig->fallbackLocale->getDisplayLanguage(), 'Translation files' => (string) arr($this->intlConfig->translationMessagePaths)->flatten()->count(), - 'Intl extension' => extension_loaded('intl') ? new Insight('ENABLED', Insight::SUCCESS) : new Insight('DISABLED', Insight::WARNING), + 'Intl extension' => extension_loaded('intl') ? new Insight('ENABLED', InsightType::SUCCESS) : new Insight('DISABLED', InsightType::WARNING), ]; } } diff --git a/packages/intl/src/MessageFormat/Formatter/MessageFormatter.php b/packages/intl/src/MessageFormat/Formatter/MessageFormatter.php index 01dc72f0fe..c0acb0f83f 100644 --- a/packages/intl/src/MessageFormat/Formatter/MessageFormatter.php +++ b/packages/intl/src/MessageFormat/Formatter/MessageFormatter.php @@ -31,7 +31,6 @@ use Tempest\Intl\MessageFormat\Parser\Node\Variable; use Tempest\Intl\MessageFormat\SelectorFunction; use Tempest\Intl\MessageFormat\StandaloneMarkupFormatter; -use Tempest\Support\Arr; use function Tempest\Support\arr; @@ -102,18 +101,22 @@ private function formatMessage(MessageNode $message): string identifier: $variableName, value: $this->variables[$variableName]->value, selector: $this->getSelectorFunction((string) $expression->function->identifier), - formatter: $this->getFormattingFunction((string) $expression->function?->identifier), + formatter: $this->getFormattingFunction((string) $expression->function->identifier), parameters: $this->evaluateOptions($expression->function->options), ); } } elseif ($declaration instanceof LocalDeclaration) { $variableName = $declaration->variable->name->name; + $functionName = $declaration->expression->function + ? (string) $declaration->expression->function->identifier + : null; + $localVariables[$variableName] = new LocalVariable( identifier: $variableName, value: $this->evaluateExpression($declaration->expression)->value, - selector: $this->getSelectorFunction($declaration->expression->function?->identifier), - formatter: $this->getFormattingFunction($declaration->expression->function?->identifier), + selector: $this->getSelectorFunction($functionName), + formatter: $this->getFormattingFunction($functionName), parameters: $declaration->expression->attributes, ); } @@ -344,7 +347,7 @@ private function parseLocalVariables(array $variables): array private function formatMarkup(Markup $markup): string { $tag = (string) $markup->identifier; - $options = Arr\map_with_keys($markup->options, fn (Option $option) => yield $option->identifier->name => $option->value->value); + $options = $this->evaluateOptions($markup->options); if ($markup->type === MarkupType::STANDALONE) { if (is_null($formatter = $this->getStandaloneMarkupFormatter($tag))) { @@ -358,11 +361,11 @@ private function formatMarkup(Markup $markup): string return ''; } - return match ($markup->type) { - MarkupType::OPEN => $formatter->formatOpenTag($tag, $options), - MarkupType::CLOSE => $formatter->formatCloseTag($tag), - default => '', - }; + if ($markup->type === MarkupType::OPEN) { + return $formatter->formatOpenTag($tag, $options); + } + + return $formatter->formatCloseTag($tag); } private function getMarkupFormatter(?string $tag): ?MarkupFormatter diff --git a/packages/intl/src/MessageFormat/Parser/MessageFormatParser.php b/packages/intl/src/MessageFormat/Parser/MessageFormatParser.php index fa59c59280..95f288065e 100644 --- a/packages/intl/src/MessageFormat/Parser/MessageFormatParser.php +++ b/packages/intl/src/MessageFormat/Parser/MessageFormatParser.php @@ -129,7 +129,7 @@ private function parseInputDeclaration(): InputDeclaration $optional = (bool) array_find( array: $expression->function->options ?? [], - callback: fn (Option $option) => $option->identifier->name === 'default' && ! is_null($option->value->value), + callback: fn (Option $option) => $option->identifier->name === 'default', ); return new InputDeclaration($expression, $optional); diff --git a/packages/intl/src/MessageFormat/Parser/Node/ComplexBody/Matcher.php b/packages/intl/src/MessageFormat/Parser/Node/ComplexBody/Matcher.php index 50a28aa3b7..016ca1f971 100644 --- a/packages/intl/src/MessageFormat/Parser/Node/ComplexBody/Matcher.php +++ b/packages/intl/src/MessageFormat/Parser/Node/ComplexBody/Matcher.php @@ -3,6 +3,7 @@ namespace Tempest\Intl\MessageFormat\Parser\Node\ComplexBody; use Tempest\Intl\MessageFormat\Parser\Node\Pattern\Pattern; +use Tempest\Intl\MessageFormat\Parser\Node\Variable; final readonly class Matcher implements ComplexBody { diff --git a/packages/intl/src/MessageFormat/Parser/Node/ComplexBody/Variant.php b/packages/intl/src/MessageFormat/Parser/Node/ComplexBody/Variant.php index 24538b2a10..d8380954e5 100644 --- a/packages/intl/src/MessageFormat/Parser/Node/ComplexBody/Variant.php +++ b/packages/intl/src/MessageFormat/Parser/Node/ComplexBody/Variant.php @@ -2,6 +2,7 @@ namespace Tempest\Intl\MessageFormat\Parser\Node\ComplexBody; +use Tempest\Intl\MessageFormat\Parser\Node\Key\Key; use Tempest\Intl\MessageFormat\Parser\Node\Node; use Tempest\Intl\MessageFormat\Parser\Node\Pattern\QuotedPattern; diff --git a/packages/intl/src/MessageFormat/Parser/Node/ComplexMessage.php b/packages/intl/src/MessageFormat/Parser/Node/ComplexMessage.php index 58de213e9c..46142d1a08 100644 --- a/packages/intl/src/MessageFormat/Parser/Node/ComplexMessage.php +++ b/packages/intl/src/MessageFormat/Parser/Node/ComplexMessage.php @@ -3,6 +3,7 @@ namespace Tempest\Intl\MessageFormat\Parser\Node; use Tempest\Intl\MessageFormat\Parser\Node\ComplexBody\ComplexBody; +use Tempest\Intl\MessageFormat\Parser\Node\Declaration\Declaration; final class ComplexMessage extends MessageNode { diff --git a/packages/intl/src/MessageFormat/Parser/Node/Markup/Markup.php b/packages/intl/src/MessageFormat/Parser/Node/Markup/Markup.php index 938a85fe3b..d245c02231 100644 --- a/packages/intl/src/MessageFormat/Parser/Node/Markup/Markup.php +++ b/packages/intl/src/MessageFormat/Parser/Node/Markup/Markup.php @@ -2,14 +2,16 @@ namespace Tempest\Intl\MessageFormat\Parser\Node\Markup; +use Tempest\Intl\MessageFormat\Parser\Node\Expression\Attribute; +use Tempest\Intl\MessageFormat\Parser\Node\Expression\Option; use Tempest\Intl\MessageFormat\Parser\Node\Identifier; use Tempest\Intl\MessageFormat\Parser\Node\Pattern\Placeholder; final readonly class Markup implements Placeholder { /** - * @param (Option)[] $options - * @param (Attribute)[] $attributes + * @param Option[] $options + * @param Attribute[] $attributes */ public function __construct( public MarkupType $type, diff --git a/packages/intl/src/Number/functions.php b/packages/intl/src/Number/functions.php index 3652c1cd25..e67d018634 100644 --- a/packages/intl/src/Number/functions.php +++ b/packages/intl/src/Number/functions.php @@ -172,7 +172,7 @@ function to_human_readable(int|float $number, int $precision = 0, ?int $maxPreci return sprintf('%s' . end($units), namespace\to_human_readable($number / 1e15, $precision, $maxPrecision, $units)); } - $numberExponent = Math\floor(Math\log($number, base: 10)); + $numberExponent = (int) Math\floor(Math\log($number, base: 10)); $displayExponent = $numberExponent - ($numberExponent % 3); $number /= 10 ** $displayExponent; diff --git a/packages/intl/src/PluralRules/PluralRulesMatcher.php b/packages/intl/src/PluralRules/PluralRulesMatcher.php index 09e52266b7..eb89ced577 100644 --- a/packages/intl/src/PluralRules/PluralRulesMatcher.php +++ b/packages/intl/src/PluralRules/PluralRulesMatcher.php @@ -6,7 +6,7 @@ /** * This file was auto-generated using the plural rules CLDR dataset. - * Generated on: 2025-06-21 14:21:38 + * Generated on: 2026-02-15 08:37:06 */ final class PluralRulesMatcher { @@ -89,32 +89,11 @@ private static function inRange(int|float $value, int|float $start, int|float $e } /** - * Checks if number matches any value in comma-separated list. + * Checks whether two numeric values are equal. */ - private static function matchesValues(int|float $value, string $values): bool + private static function isEqual(int|float $left, int|float $right): bool { - $parts = explode(',', $values); - - foreach ($parts as $part) { - $part = trim($part); - - if (str_contains($part, '~')) { - [$start, $end] = explode('~', $part); - - if (self::inRange($value, (float) trim($start), (float) trim($end))) { - return true; - } - } elseif (str_contains($part, '..')) { - [$start, $end] = explode('..', $part); - - if (self::inRange($value, (float) trim($start), (float) trim($end))) { - return true; - } - } elseif ((float) $part === (float) $value) { - return true; - } - } - return false; + return $left === $right; } /** @@ -128,7 +107,7 @@ private static function getPluralCategoryAf(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -164,7 +143,7 @@ private static function getPluralCategoryAm(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 0 || $n === 1) { + if (self::isEqual($i, 0) || self::isEqual($n, 1)) { return 'one'; } @@ -182,7 +161,7 @@ private static function getPluralCategoryAn(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -200,15 +179,15 @@ private static function getPluralCategoryAr(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 0) { + if (self::isEqual($n, 0)) { return 'zero'; } - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } - if ($n === 2) { + if (self::isEqual($n, 2)) { return 'two'; } @@ -234,15 +213,15 @@ private static function getPluralCategoryArs(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 0) { + if (self::isEqual($n, 0)) { return 'zero'; } - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } - if ($n === 2) { + if (self::isEqual($n, 2)) { return 'two'; } @@ -268,7 +247,7 @@ private static function getPluralCategoryAs(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 0 || $n === 1) { + if (self::isEqual($i, 0) || self::isEqual($n, 1)) { return 'one'; } @@ -286,7 +265,7 @@ private static function getPluralCategoryAsa(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -304,7 +283,7 @@ private static function getPluralCategoryAst(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } @@ -322,7 +301,7 @@ private static function getPluralCategoryAz(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -340,7 +319,7 @@ private static function getPluralCategoryBal(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -358,7 +337,7 @@ private static function getPluralCategoryBe(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($n % 10) === 1 && ($n % 100) !== 11) { + if (self::isEqual($n % 10, 1) && ! self::isEqual($n % 100, 11)) { return 'one'; } @@ -366,7 +345,7 @@ private static function getPluralCategoryBe(float|int $n): string return 'few'; } - if (($n % 10) === 0 || self::inRange($n % 10, 5, 9) || self::inRange($n % 100, 11, 14)) { + if (self::isEqual($n % 10, 0) || self::inRange($n % 10, 5, 9) || self::inRange($n % 100, 11, 14)) { return 'many'; } @@ -384,7 +363,7 @@ private static function getPluralCategoryBem(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -402,7 +381,7 @@ private static function getPluralCategoryBez(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -420,7 +399,7 @@ private static function getPluralCategoryBg(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -456,11 +435,11 @@ private static function getPluralCategoryBlo(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 0) { + if (self::isEqual($n, 0)) { return 'zero'; } - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -492,7 +471,7 @@ private static function getPluralCategoryBn(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 0 || $n === 1) { + if (self::isEqual($i, 0) || self::isEqual($n, 1)) { return 'one'; } @@ -524,19 +503,22 @@ private static function getPluralCategoryBr(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($n % 10) === 1 && ! (($n % 100) === 11 || ($n % 100) === 71 || ($n % 100) === 91)) { + if (self::isEqual($n % 10, 1) && ! (self::isEqual($n % 100, 11) || self::isEqual($n % 100, 71) || self::isEqual($n % 100, 91))) { return 'one'; } - if (($n % 10) === 2 && ! (($n % 100) === 12 || ($n % 100) === 72 || ($n % 100) === 92)) { + if (self::isEqual($n % 10, 2) && ! (self::isEqual($n % 100, 12) || self::isEqual($n % 100, 72) || self::isEqual($n % 100, 92))) { return 'two'; } - if ((self::inRange($n % 10, 3, 4) || ($n % 10) === 9) && ! (self::inRange($n % 100, 10, 19) || self::inRange($n % 100, 70, 79) || self::inRange($n % 100, 90, 99))) { + if ( + (self::inRange($n % 10, 3, 4) || self::isEqual($n % 10, 9)) + && ! (self::inRange($n % 100, 10, 19) || self::inRange($n % 100, 70, 79) || self::inRange($n % 100, 90, 99)) + ) { return 'few'; } - if ($n !== 0 && ($n % 1000000) === 0) { + if (! self::isEqual($n, 0) && self::isEqual($n % 1000000, 0)) { return 'many'; } @@ -554,7 +536,7 @@ private static function getPluralCategoryBrx(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -572,11 +554,11 @@ private static function getPluralCategoryBs(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($v === 0 && ($i % 10) === 1 && ($i % 100) !== 11 || ($f % 10) === 1 && ($f % 100) !== 11) { + if (self::isEqual($v, 0) && self::isEqual($i % 10, 1) && ! self::isEqual($i % 100, 11) || self::isEqual($f % 10, 1) && ! self::isEqual($f % 100, 11)) { return 'one'; } - if ($v === 0 && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14) || self::inRange($f % 10, 2, 4) && ! self::inRange($f % 100, 12, 14)) { + if (self::isEqual($v, 0) && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14) || self::inRange($f % 10, 2, 4) && ! self::inRange($f % 100, 12, 14)) { return 'few'; } @@ -594,11 +576,11 @@ private static function getPluralCategoryCa(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } - if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { + if (self::isEqual($e, 0) && ! self::isEqual($i, 0) && self::isEqual($i % 1000000, 0) && self::isEqual($v, 0) || ! self::inRange($e, 0, 5)) { return 'many'; } @@ -616,7 +598,7 @@ private static function getPluralCategoryCe(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -635,9 +617,9 @@ private static function getPluralCategoryCeb(float|int $n): string $e = self::getExponent($n); if ( - $v === 0 && ($i === 1 || $i === 2 || $i === 3) - || $v === 0 && ! (($i % 10) === 4 || ($i % 10) === 6 || ($i % 10) === 9) - || $v !== 0 && ! (($f % 10) === 4 || ($f % 10) === 6 || ($f % 10) === 9) + self::isEqual($v, 0) && (self::isEqual($i, 1) || self::isEqual($i, 2) || self::isEqual($i, 3)) + || self::isEqual($v, 0) && ! (self::isEqual($i % 10, 4) || self::isEqual($i % 10, 6) || self::isEqual($i % 10, 9)) + || ! self::isEqual($v, 0) && ! (self::isEqual($f % 10, 4) || self::isEqual($f % 10, 6) || self::isEqual($f % 10, 9)) ) { return 'one'; } @@ -656,7 +638,7 @@ private static function getPluralCategoryCgg(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -674,7 +656,7 @@ private static function getPluralCategoryChr(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -692,7 +674,7 @@ private static function getPluralCategoryCkb(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -710,15 +692,15 @@ private static function getPluralCategoryCs(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } - if (self::inRange($i, 2, 4) && $v === 0) { + if (self::inRange($i, 2, 4) && self::isEqual($v, 0)) { return 'few'; } - if ($v !== 0) { + if (! self::isEqual($v, 0)) { return 'many'; } @@ -743,6 +725,28 @@ private static function getPluralCategoryCsw(float|int $n): string return 'other'; } + /** + * Gets the plural category for the cv locale. + */ + private static function getPluralCategoryCv(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (self::isEqual($n, 0)) { + return 'zero'; + } + + if (self::isEqual($n, 1)) { + return 'one'; + } + + return 'other'; + } + /** * Gets the plural category for the cy locale. */ @@ -754,23 +758,23 @@ private static function getPluralCategoryCy(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 0) { + if (self::isEqual($n, 0)) { return 'zero'; } - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } - if ($n === 2) { + if (self::isEqual($n, 2)) { return 'two'; } - if ($n === 3) { + if (self::isEqual($n, 3)) { return 'few'; } - if ($n === 6) { + if (self::isEqual($n, 6)) { return 'many'; } @@ -788,7 +792,7 @@ private static function getPluralCategoryDa(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1 || $t !== 0 && ($i === 0 || $i === 1)) { + if (self::isEqual($n, 1) || ! self::isEqual($t, 0) && (self::isEqual($i, 0) || self::isEqual($i, 1))) { return 'one'; } @@ -806,7 +810,7 @@ private static function getPluralCategoryDe(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } @@ -824,7 +828,7 @@ private static function getPluralCategoryDoi(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 0 || $n === 1) { + if (self::isEqual($i, 0) || self::isEqual($n, 1)) { return 'one'; } @@ -842,15 +846,15 @@ private static function getPluralCategoryDsb(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($v === 0 && ($i % 100) === 1 || ($f % 100) === 1) { + if (self::isEqual($v, 0) && self::isEqual($i % 100, 1) || self::isEqual($f % 100, 1)) { return 'one'; } - if ($v === 0 && ($i % 100) === 2 || ($f % 100) === 2) { + if (self::isEqual($v, 0) && self::isEqual($i % 100, 2) || self::isEqual($f % 100, 2)) { return 'two'; } - if ($v === 0 && self::inRange($i % 100, 3, 4) || self::inRange($f % 100, 3, 4)) { + if (self::isEqual($v, 0) && self::inRange($i % 100, 3, 4) || self::inRange($f % 100, 3, 4)) { return 'few'; } @@ -868,7 +872,7 @@ private static function getPluralCategoryDv(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -900,7 +904,7 @@ private static function getPluralCategoryEe(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -918,7 +922,7 @@ private static function getPluralCategoryEl(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -936,7 +940,7 @@ private static function getPluralCategoryEn(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } @@ -954,7 +958,7 @@ private static function getPluralCategoryEo(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -972,11 +976,11 @@ private static function getPluralCategoryEs(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } - if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { + if (self::isEqual($e, 0) && ! self::isEqual($i, 0) && self::isEqual($i % 1000000, 0) && self::isEqual($v, 0) || ! self::inRange($e, 0, 5)) { return 'many'; } @@ -994,7 +998,7 @@ private static function getPluralCategoryEt(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } @@ -1012,7 +1016,7 @@ private static function getPluralCategoryEu(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -1030,7 +1034,7 @@ private static function getPluralCategoryFa(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 0 || $n === 1) { + if (self::isEqual($i, 0) || self::isEqual($n, 1)) { return 'one'; } @@ -1048,7 +1052,7 @@ private static function getPluralCategoryFf(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 0 || $i === 1) { + if (self::isEqual($i, 0) || self::isEqual($i, 1)) { return 'one'; } @@ -1066,7 +1070,7 @@ private static function getPluralCategoryFi(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } @@ -1085,9 +1089,9 @@ private static function getPluralCategoryFil(float|int $n): string $e = self::getExponent($n); if ( - $v === 0 && ($i === 1 || $i === 2 || $i === 3) - || $v === 0 && ! (($i % 10) === 4 || ($i % 10) === 6 || ($i % 10) === 9) - || $v !== 0 && ! (($f % 10) === 4 || ($f % 10) === 6 || ($f % 10) === 9) + self::isEqual($v, 0) && (self::isEqual($i, 1) || self::isEqual($i, 2) || self::isEqual($i, 3)) + || self::isEqual($v, 0) && ! (self::isEqual($i % 10, 4) || self::isEqual($i % 10, 6) || self::isEqual($i % 10, 9)) + || ! self::isEqual($v, 0) && ! (self::isEqual($f % 10, 4) || self::isEqual($f % 10, 6) || self::isEqual($f % 10, 9)) ) { return 'one'; } @@ -1106,7 +1110,7 @@ private static function getPluralCategoryFo(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -1124,11 +1128,11 @@ private static function getPluralCategoryFr(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 0 || $i === 1) { + if (self::isEqual($i, 0) || self::isEqual($i, 1)) { return 'one'; } - if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { + if (self::isEqual($e, 0) && self::isEqual($i % 1000000, 0) && self::isEqual($v, 0) || ! self::inRange($e, 0, 5)) { return 'many'; } @@ -1146,7 +1150,7 @@ private static function getPluralCategoryFur(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -1164,7 +1168,7 @@ private static function getPluralCategoryFy(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } @@ -1182,11 +1186,11 @@ private static function getPluralCategoryGa(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } - if ($n === 2) { + if (self::isEqual($n, 2)) { return 'two'; } @@ -1212,11 +1216,11 @@ private static function getPluralCategoryGd(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1 || $n === 11) { + if (self::isEqual($n, 1) || self::isEqual($n, 11)) { return 'one'; } - if ($n === 2 || $n === 12) { + if (self::isEqual($n, 2) || self::isEqual($n, 12)) { return 'two'; } @@ -1238,7 +1242,7 @@ private static function getPluralCategoryGl(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } @@ -1256,7 +1260,7 @@ private static function getPluralCategoryGsw(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -1274,7 +1278,7 @@ private static function getPluralCategoryGu(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 0 || $n === 1) { + if (self::isEqual($i, 0) || self::isEqual($n, 1)) { return 'one'; } @@ -1310,19 +1314,22 @@ private static function getPluralCategoryGv(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($v === 0 && ($i % 10) === 1) { + if (self::isEqual($v, 0) && self::isEqual($i % 10, 1)) { return 'one'; } - if ($v === 0 && ($i % 10) === 2) { + if (self::isEqual($v, 0) && self::isEqual($i % 10, 2)) { return 'two'; } - if ($v === 0 && (($i % 100) === 0 || ($i % 100) === 20 || ($i % 100) === 40 || ($i % 100) === 60 || ($i % 100) === 80)) { + if ( + self::isEqual($v, 0) + && (self::isEqual($i % 100, 0) || self::isEqual($i % 100, 20) || self::isEqual($i % 100, 40) || self::isEqual($i % 100, 60) || self::isEqual($i % 100, 80)) + ) { return 'few'; } - if ($v !== 0) { + if (! self::isEqual($v, 0)) { return 'many'; } @@ -1340,7 +1347,7 @@ private static function getPluralCategoryHa(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -1358,7 +1365,7 @@ private static function getPluralCategoryHaw(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -1376,11 +1383,11 @@ private static function getPluralCategoryHe(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0 || $i === 0 && $v !== 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0) || self::isEqual($i, 0) && ! self::isEqual($v, 0)) { return 'one'; } - if ($i === 2 && $v === 0) { + if (self::isEqual($i, 2) && self::isEqual($v, 0)) { return 'two'; } @@ -1398,7 +1405,7 @@ private static function getPluralCategoryHi(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 0 || $n === 1) { + if (self::isEqual($i, 0) || self::isEqual($n, 1)) { return 'one'; } @@ -1430,11 +1437,11 @@ private static function getPluralCategoryHr(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($v === 0 && ($i % 10) === 1 && ($i % 100) !== 11 || ($f % 10) === 1 && ($f % 100) !== 11) { + if (self::isEqual($v, 0) && self::isEqual($i % 10, 1) && ! self::isEqual($i % 100, 11) || self::isEqual($f % 10, 1) && ! self::isEqual($f % 100, 11)) { return 'one'; } - if ($v === 0 && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14) || self::inRange($f % 10, 2, 4) && ! self::inRange($f % 100, 12, 14)) { + if (self::isEqual($v, 0) && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14) || self::inRange($f % 10, 2, 4) && ! self::inRange($f % 100, 12, 14)) { return 'few'; } @@ -1452,15 +1459,15 @@ private static function getPluralCategoryHsb(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($v === 0 && ($i % 100) === 1 || ($f % 100) === 1) { + if (self::isEqual($v, 0) && self::isEqual($i % 100, 1) || self::isEqual($f % 100, 1)) { return 'one'; } - if ($v === 0 && ($i % 100) === 2 || ($f % 100) === 2) { + if (self::isEqual($v, 0) && self::isEqual($i % 100, 2) || self::isEqual($f % 100, 2)) { return 'two'; } - if ($v === 0 && self::inRange($i % 100, 3, 4) || self::inRange($f % 100, 3, 4)) { + if (self::isEqual($v, 0) && self::inRange($i % 100, 3, 4) || self::inRange($f % 100, 3, 4)) { return 'few'; } @@ -1478,7 +1485,7 @@ private static function getPluralCategoryHu(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -1496,7 +1503,7 @@ private static function getPluralCategoryHy(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 0 || $i === 1) { + if (self::isEqual($i, 0) || self::isEqual($i, 1)) { return 'one'; } @@ -1514,7 +1521,7 @@ private static function getPluralCategoryIa(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } @@ -1535,6 +1542,24 @@ private static function getPluralCategoryId(float|int $n): string return 'other'; } + /** + * Gets the plural category for the ie locale. + */ + private static function getPluralCategoryIe(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { + return 'one'; + } + + return 'other'; + } + /** * Gets the plural category for the ig locale. */ @@ -1574,7 +1599,7 @@ private static function getPluralCategoryIo(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } @@ -1592,7 +1617,7 @@ private static function getPluralCategoryIs(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($t === 0 && ($i % 10) === 1 && ($i % 100) !== 11 || ($t % 10) === 1 && ($t % 100) !== 11) { + if (self::isEqual($t, 0) && self::isEqual($i % 10, 1) && ! self::isEqual($i % 100, 11) || self::isEqual($t % 10, 1) && ! self::isEqual($t % 100, 11)) { return 'one'; } @@ -1610,11 +1635,11 @@ private static function getPluralCategoryIt(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } - if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { + if (self::isEqual($e, 0) && ! self::isEqual($i, 0) && self::isEqual($i % 1000000, 0) && self::isEqual($v, 0) || ! self::inRange($e, 0, 5)) { return 'many'; } @@ -1632,11 +1657,11 @@ private static function getPluralCategoryIu(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } - if ($n === 2) { + if (self::isEqual($n, 2)) { return 'two'; } @@ -1682,7 +1707,7 @@ private static function getPluralCategoryJgo(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -1700,7 +1725,7 @@ private static function getPluralCategoryJmc(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -1746,7 +1771,7 @@ private static function getPluralCategoryKa(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -1764,7 +1789,7 @@ private static function getPluralCategoryKab(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 0 || $i === 1) { + if (self::isEqual($i, 0) || self::isEqual($i, 1)) { return 'one'; } @@ -1782,7 +1807,7 @@ private static function getPluralCategoryKaj(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -1800,7 +1825,7 @@ private static function getPluralCategoryKcg(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -1846,7 +1871,7 @@ private static function getPluralCategoryKk(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -1864,7 +1889,7 @@ private static function getPluralCategoryKkj(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -1882,7 +1907,7 @@ private static function getPluralCategoryKl(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -1914,7 +1939,7 @@ private static function getPluralCategoryKn(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 0 || $n === 1) { + if (self::isEqual($i, 0) || self::isEqual($n, 1)) { return 'one'; } @@ -1935,6 +1960,42 @@ private static function getPluralCategoryKo(float|int $n): string return 'other'; } + /** + * Gets the plural category for the kok locale. + */ + private static function getPluralCategoryKok(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (self::isEqual($i, 0) || self::isEqual($n, 1)) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the kok-Latn locale. + */ + private static function getPluralCategoryKok_Latn(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (self::isEqual($i, 0) || self::isEqual($n, 1)) { + return 'one'; + } + + return 'other'; + } + /** * Gets the plural category for the ks locale. */ @@ -1946,7 +2007,7 @@ private static function getPluralCategoryKs(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -1964,7 +2025,7 @@ private static function getPluralCategoryKsb(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -1982,11 +2043,11 @@ private static function getPluralCategoryKsh(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 0) { + if (self::isEqual($n, 0)) { return 'zero'; } - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2004,7 +2065,7 @@ private static function getPluralCategoryKu(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2022,31 +2083,32 @@ private static function getPluralCategoryKw(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 0) { + if (self::isEqual($n, 0)) { return 'zero'; } - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } if ( - ($n % 100) === 2 - || ($n % 100) === 22 - || ($n % 100) === 42 - || ($n % 100) === 62 - || ($n % 100) === 82 - || ($n % 1000) === 0 && (self::inRange($n % 100000, 1000, 20000) || ($n % 100000) === 40000 || ($n % 100000) === 60000 || ($n % 100000) === 80000) - || $n !== 0 && ($n % 1000000) === 100000 + self::isEqual($n % 100, 2) + || self::isEqual($n % 100, 22) + || self::isEqual($n % 100, 42) + || self::isEqual($n % 100, 62) + || self::isEqual($n % 100, 82) + || self::isEqual($n % 1000, 0) + && (self::inRange($n % 100000, 1000, 20000) || self::isEqual($n % 100000, 40000) || self::isEqual($n % 100000, 60000) || self::isEqual($n % 100000, 80000)) + || self::isEqual($n % 1000000, 100000) ) { return 'two'; } - if (($n % 100) === 3 || ($n % 100) === 23 || ($n % 100) === 43 || ($n % 100) === 63 || ($n % 100) === 83) { + if (self::isEqual($n % 100, 3) || self::isEqual($n % 100, 23) || self::isEqual($n % 100, 43) || self::isEqual($n % 100, 63) || self::isEqual($n % 100, 83)) { return 'few'; } - if ($n !== 1 && (($n % 100) === 1 || ($n % 100) === 21 || ($n % 100) === 41 || ($n % 100) === 61 || ($n % 100) === 81)) { + if (self::isEqual($n % 100, 1) || self::isEqual($n % 100, 21) || self::isEqual($n % 100, 41) || self::isEqual($n % 100, 61) || self::isEqual($n % 100, 81)) { return 'many'; } @@ -2064,7 +2126,7 @@ private static function getPluralCategoryKy(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2082,11 +2144,11 @@ private static function getPluralCategoryLag(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 0) { + if (self::isEqual($n, 0)) { return 'zero'; } - if (($i === 0 || $i === 1) && $n !== 0) { + if (self::isEqual($i, 0) || self::isEqual($i, 1)) { return 'one'; } @@ -2104,7 +2166,7 @@ private static function getPluralCategoryLb(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2122,7 +2184,7 @@ private static function getPluralCategoryLg(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2140,7 +2202,7 @@ private static function getPluralCategoryLij(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } @@ -2172,11 +2234,11 @@ private static function getPluralCategoryLld(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } - if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { + if (self::isEqual($e, 0) && ! self::isEqual($i, 0) && self::isEqual($i % 1000000, 0) && self::isEqual($v, 0) || ! self::inRange($e, 0, 5)) { return 'many'; } @@ -2226,7 +2288,7 @@ private static function getPluralCategoryLt(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($n % 10) === 1 && ! self::inRange($n % 100, 11, 19)) { + if (self::isEqual($n % 10, 1) && ! self::inRange($n % 100, 11, 19)) { return 'one'; } @@ -2234,7 +2296,7 @@ private static function getPluralCategoryLt(float|int $n): string return 'few'; } - if ($f !== 0) { + if (! self::isEqual($f, 0)) { return 'many'; } @@ -2252,11 +2314,15 @@ private static function getPluralCategoryLv(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($n % 10) === 0 || self::inRange($n % 100, 11, 19) || $v === 2 && self::inRange($f % 100, 11, 19)) { + if (self::isEqual($n % 10, 0) || self::inRange($n % 100, 11, 19) || self::isEqual($v, 2) && self::inRange($f % 100, 11, 19)) { return 'zero'; } - if (($n % 10) === 1 && ($n % 100) !== 11 || $v === 2 && ($f % 10) === 1 && ($f % 100) !== 11 || $v !== 2 && ($f % 10) === 1) { + if ( + self::isEqual($n % 10, 1) && ! self::isEqual($n % 100, 11) + || self::isEqual($v, 2) && self::isEqual($f % 10, 1) && ! self::isEqual($f % 100, 11) + || ! self::isEqual($v, 2) && self::isEqual($f % 10, 1) + ) { return 'one'; } @@ -2274,7 +2340,7 @@ private static function getPluralCategoryMas(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2310,7 +2376,7 @@ private static function getPluralCategoryMgo(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2328,7 +2394,7 @@ private static function getPluralCategoryMk(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($v === 0 && ($i % 10) === 1 && ($i % 100) !== 11 || ($f % 10) === 1 && ($f % 100) !== 11) { + if (self::isEqual($v, 0) && self::isEqual($i % 10, 1) && ! self::isEqual($i % 100, 11) || self::isEqual($f % 10, 1) && ! self::isEqual($f % 100, 11)) { return 'one'; } @@ -2346,7 +2412,7 @@ private static function getPluralCategoryMl(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2364,7 +2430,7 @@ private static function getPluralCategoryMn(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2382,11 +2448,11 @@ private static function getPluralCategoryMo(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } - if ($v !== 0 || $n === 0 || $n !== 1 && self::inRange($n % 100, 1, 19)) { + if (! self::isEqual($v, 0) || self::isEqual($n, 0) || ! self::isEqual($n, 1) && self::inRange($n % 100, 1, 19)) { return 'few'; } @@ -2404,7 +2470,7 @@ private static function getPluralCategoryMr(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2436,15 +2502,15 @@ private static function getPluralCategoryMt(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } - if ($n === 2) { + if (self::isEqual($n, 2)) { return 'two'; } - if ($n === 0 || self::inRange($n % 100, 3, 10)) { + if (self::isEqual($n, 0) || self::inRange($n % 100, 3, 10)) { return 'few'; } @@ -2480,7 +2546,7 @@ private static function getPluralCategoryNah(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2498,11 +2564,11 @@ private static function getPluralCategoryNaq(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } - if ($n === 2) { + if (self::isEqual($n, 2)) { return 'two'; } @@ -2520,7 +2586,7 @@ private static function getPluralCategoryNb(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2538,7 +2604,7 @@ private static function getPluralCategoryNd(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2556,7 +2622,7 @@ private static function getPluralCategoryNe(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2574,7 +2640,7 @@ private static function getPluralCategoryNl(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } @@ -2592,7 +2658,7 @@ private static function getPluralCategoryNn(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2610,7 +2676,7 @@ private static function getPluralCategoryNnh(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2628,7 +2694,7 @@ private static function getPluralCategoryNo(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2660,7 +2726,7 @@ private static function getPluralCategoryNr(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2696,7 +2762,7 @@ private static function getPluralCategoryNy(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2714,7 +2780,7 @@ private static function getPluralCategoryNyn(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2732,7 +2798,7 @@ private static function getPluralCategoryOm(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2750,7 +2816,7 @@ private static function getPluralCategoryOr(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2768,7 +2834,7 @@ private static function getPluralCategoryOs(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2818,7 +2884,7 @@ private static function getPluralCategoryPap(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2836,7 +2902,7 @@ private static function getPluralCategoryPcm(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 0 || $n === 1) { + if (self::isEqual($i, 0) || self::isEqual($n, 1)) { return 'one'; } @@ -2854,15 +2920,19 @@ private static function getPluralCategoryPl(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } - if ($v === 0 && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14)) { + if (self::isEqual($v, 0) && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14)) { return 'few'; } - if ($v === 0 && $i !== 1 && self::inRange($i % 10, 0, 1) || $v === 0 && self::inRange($i % 10, 5, 9) || $v === 0 && self::inRange($i % 100, 12, 14)) { + if ( + self::isEqual($v, 0) && ! self::isEqual($i, 1) && self::inRange($i % 10, 0, 1) + || self::isEqual($v, 0) && self::inRange($i % 10, 5, 9) + || self::isEqual($v, 0) && self::inRange($i % 100, 12, 14) + ) { return 'many'; } @@ -2880,11 +2950,15 @@ private static function getPluralCategoryPrg(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($n % 10) === 0 || self::inRange($n % 100, 11, 19) || $v === 2 && self::inRange($f % 100, 11, 19)) { + if (self::isEqual($n % 10, 0) || self::inRange($n % 100, 11, 19) || self::isEqual($v, 2) && self::inRange($f % 100, 11, 19)) { return 'zero'; } - if (($n % 10) === 1 && ($n % 100) !== 11 || $v === 2 && ($f % 10) === 1 && ($f % 100) !== 11 || $v !== 2 && ($f % 10) === 1) { + if ( + self::isEqual($n % 10, 1) && ! self::isEqual($n % 100, 11) + || self::isEqual($v, 2) && self::isEqual($f % 10, 1) && ! self::isEqual($f % 100, 11) + || ! self::isEqual($v, 2) && self::isEqual($f % 10, 1) + ) { return 'one'; } @@ -2902,7 +2976,7 @@ private static function getPluralCategoryPs(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2924,7 +2998,7 @@ private static function getPluralCategoryPt(float|int $n): string return 'one'; } - if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { + if (self::isEqual($e, 0) && ! self::isEqual($i, 0) && self::isEqual($i % 1000000, 0) && self::isEqual($v, 0) || ! self::inRange($e, 0, 5)) { return 'many'; } @@ -2942,11 +3016,11 @@ private static function getPluralCategoryPt_PT(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } - if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { + if (self::isEqual($e, 0) && ! self::isEqual($i, 0) && self::isEqual($i % 1000000, 0) && self::isEqual($v, 0) || ! self::inRange($e, 0, 5)) { return 'many'; } @@ -2964,7 +3038,7 @@ private static function getPluralCategoryRm(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -2982,11 +3056,11 @@ private static function getPluralCategoryRo(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } - if ($v !== 0 || $n === 0 || $n !== 1 && self::inRange($n % 100, 1, 19)) { + if (! self::isEqual($v, 0) || self::isEqual($n, 0) || ! self::isEqual($n, 1) && self::inRange($n % 100, 1, 19)) { return 'few'; } @@ -3004,7 +3078,7 @@ private static function getPluralCategoryRof(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3022,15 +3096,15 @@ private static function getPluralCategoryRu(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($v === 0 && ($i % 10) === 1 && ($i % 100) !== 11) { + if (self::isEqual($v, 0) && self::isEqual($i % 10, 1) && ! self::isEqual($i % 100, 11)) { return 'one'; } - if ($v === 0 && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14)) { + if (self::isEqual($v, 0) && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14)) { return 'few'; } - if ($v === 0 && ($i % 10) === 0 || $v === 0 && self::inRange($i % 10, 5, 9) || $v === 0 && self::inRange($i % 100, 11, 14)) { + if (self::isEqual($v, 0) && self::isEqual($i % 10, 0) || self::isEqual($v, 0) && self::inRange($i % 10, 5, 9) || self::isEqual($v, 0) && self::inRange($i % 100, 11, 14)) { return 'many'; } @@ -3048,7 +3122,7 @@ private static function getPluralCategoryRwk(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3080,7 +3154,7 @@ private static function getPluralCategorySaq(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3098,11 +3172,11 @@ private static function getPluralCategorySat(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } - if ($n === 2) { + if (self::isEqual($n, 2)) { return 'two'; } @@ -3120,7 +3194,7 @@ private static function getPluralCategorySc(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } @@ -3138,11 +3212,11 @@ private static function getPluralCategoryScn(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } - if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { + if (self::isEqual($e, 0) && ! self::isEqual($i, 0) && self::isEqual($i % 1000000, 0) && self::isEqual($v, 0) || ! self::inRange($e, 0, 5)) { return 'many'; } @@ -3160,7 +3234,7 @@ private static function getPluralCategorySd(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3178,7 +3252,7 @@ private static function getPluralCategorySdh(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3196,11 +3270,11 @@ private static function getPluralCategorySe(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } - if ($n === 2) { + if (self::isEqual($n, 2)) { return 'two'; } @@ -3218,7 +3292,7 @@ private static function getPluralCategorySeh(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3253,6 +3327,36 @@ private static function getPluralCategorySg(float|int $n): string return 'other'; } + /** + * Gets the plural category for the sgs locale. + */ + private static function getPluralCategorySgs(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (self::isEqual($n % 10, 1) && ! self::isEqual($n % 100, 11)) { + return 'one'; + } + + if (self::isEqual($n, 2)) { + return 'two'; + } + + if (self::inRange($n % 10, 2, 9) && ! self::inRange($n % 100, 11, 19)) { + return 'few'; + } + + if (! self::isEqual($f, 0)) { + return 'many'; + } + + return 'other'; + } + /** * Gets the plural category for the sh locale. */ @@ -3264,11 +3368,11 @@ private static function getPluralCategorySh(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($v === 0 && ($i % 10) === 1 && ($i % 100) !== 11 || ($f % 10) === 1 && ($f % 100) !== 11) { + if (self::isEqual($v, 0) && self::isEqual($i % 10, 1) && ! self::isEqual($i % 100, 11) || self::isEqual($f % 10, 1) && ! self::isEqual($f % 100, 11)) { return 'one'; } - if ($v === 0 && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14) || self::inRange($f % 10, 2, 4) && ! self::inRange($f % 100, 12, 14)) { + if (self::isEqual($v, 0) && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14) || self::inRange($f % 10, 2, 4) && ! self::inRange($f % 100, 12, 14)) { return 'few'; } @@ -3286,7 +3390,7 @@ private static function getPluralCategoryShi(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 0 || $n === 1) { + if (self::isEqual($i, 0) || self::isEqual($n, 1)) { return 'one'; } @@ -3308,7 +3412,7 @@ private static function getPluralCategorySi(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 0 || $n === 1 || $i === 0 && $f === 1) { + if (self::isEqual($n, 0) || self::isEqual($n, 1) || self::isEqual($i, 0) && self::isEqual($f, 1)) { return 'one'; } @@ -3326,15 +3430,15 @@ private static function getPluralCategorySk(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } - if (self::inRange($i, 2, 4) && $v === 0) { + if (self::inRange($i, 2, 4) && self::isEqual($v, 0)) { return 'few'; } - if ($v !== 0) { + if (! self::isEqual($v, 0)) { return 'many'; } @@ -3352,15 +3456,15 @@ private static function getPluralCategorySl(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($v === 0 && ($i % 100) === 1) { + if (self::isEqual($v, 0) && self::isEqual($i % 100, 1)) { return 'one'; } - if ($v === 0 && ($i % 100) === 2) { + if (self::isEqual($v, 0) && self::isEqual($i % 100, 2)) { return 'two'; } - if ($v === 0 && self::inRange($i % 100, 3, 4) || $v !== 0) { + if (self::isEqual($v, 0) && self::inRange($i % 100, 3, 4) || ! self::isEqual($v, 0)) { return 'few'; } @@ -3378,11 +3482,11 @@ private static function getPluralCategorySma(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } - if ($n === 2) { + if (self::isEqual($n, 2)) { return 'two'; } @@ -3400,11 +3504,11 @@ private static function getPluralCategorySmi(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } - if ($n === 2) { + if (self::isEqual($n, 2)) { return 'two'; } @@ -3422,11 +3526,11 @@ private static function getPluralCategorySmj(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } - if ($n === 2) { + if (self::isEqual($n, 2)) { return 'two'; } @@ -3444,11 +3548,11 @@ private static function getPluralCategorySmn(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } - if ($n === 2) { + if (self::isEqual($n, 2)) { return 'two'; } @@ -3466,11 +3570,11 @@ private static function getPluralCategorySms(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } - if ($n === 2) { + if (self::isEqual($n, 2)) { return 'two'; } @@ -3488,7 +3592,7 @@ private static function getPluralCategorySn(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3506,7 +3610,7 @@ private static function getPluralCategorySo(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3524,7 +3628,7 @@ private static function getPluralCategorySq(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3542,11 +3646,11 @@ private static function getPluralCategorySr(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($v === 0 && ($i % 10) === 1 && ($i % 100) !== 11 || ($f % 10) === 1 && ($f % 100) !== 11) { + if (self::isEqual($v, 0) && self::isEqual($i % 10, 1) && ! self::isEqual($i % 100, 11) || self::isEqual($f % 10, 1) && ! self::isEqual($f % 100, 11)) { return 'one'; } - if ($v === 0 && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14) || self::inRange($f % 10, 2, 4) && ! self::inRange($f % 100, 12, 14)) { + if (self::isEqual($v, 0) && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14) || self::inRange($f % 10, 2, 4) && ! self::inRange($f % 100, 12, 14)) { return 'few'; } @@ -3564,7 +3668,7 @@ private static function getPluralCategorySs(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3582,7 +3686,7 @@ private static function getPluralCategorySsy(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3600,7 +3704,7 @@ private static function getPluralCategorySt(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3632,7 +3736,7 @@ private static function getPluralCategorySv(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } @@ -3650,7 +3754,7 @@ private static function getPluralCategorySw(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } @@ -3668,7 +3772,7 @@ private static function getPluralCategorySyr(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3686,7 +3790,7 @@ private static function getPluralCategoryTa(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3704,7 +3808,7 @@ private static function getPluralCategoryTe(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3722,7 +3826,7 @@ private static function getPluralCategoryTeo(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3772,7 +3876,7 @@ private static function getPluralCategoryTig(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3790,7 +3894,7 @@ private static function getPluralCategoryTk(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3809,9 +3913,9 @@ private static function getPluralCategoryTl(float|int $n): string $e = self::getExponent($n); if ( - $v === 0 && ($i === 1 || $i === 2 || $i === 3) - || $v === 0 && ! (($i % 10) === 4 || ($i % 10) === 6 || ($i % 10) === 9) - || $v !== 0 && ! (($f % 10) === 4 || ($f % 10) === 6 || ($f % 10) === 9) + self::isEqual($v, 0) && (self::isEqual($i, 1) || self::isEqual($i, 2) || self::isEqual($i, 3)) + || self::isEqual($v, 0) && ! (self::isEqual($i % 10, 4) || self::isEqual($i % 10, 6) || self::isEqual($i % 10, 9)) + || ! self::isEqual($v, 0) && ! (self::isEqual($f % 10, 4) || self::isEqual($f % 10, 6) || self::isEqual($f % 10, 9)) ) { return 'one'; } @@ -3830,7 +3934,7 @@ private static function getPluralCategoryTn(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3876,7 +3980,7 @@ private static function getPluralCategoryTr(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3894,7 +3998,7 @@ private static function getPluralCategoryTs(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3930,7 +4034,7 @@ private static function getPluralCategoryUg(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -3948,15 +4052,15 @@ private static function getPluralCategoryUk(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($v === 0 && ($i % 10) === 1 && ($i % 100) !== 11) { + if (self::isEqual($v, 0) && self::isEqual($i % 10, 1) && ! self::isEqual($i % 100, 11)) { return 'one'; } - if ($v === 0 && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14)) { + if (self::isEqual($v, 0) && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14)) { return 'few'; } - if ($v === 0 && ($i % 10) === 0 || $v === 0 && self::inRange($i % 10, 5, 9) || $v === 0 && self::inRange($i % 100, 11, 14)) { + if (self::isEqual($v, 0) && self::isEqual($i % 10, 0) || self::isEqual($v, 0) && self::inRange($i % 10, 5, 9) || self::isEqual($v, 0) && self::inRange($i % 100, 11, 14)) { return 'many'; } @@ -3988,7 +4092,7 @@ private static function getPluralCategoryUr(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } @@ -4006,7 +4110,7 @@ private static function getPluralCategoryUz(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -4024,7 +4128,7 @@ private static function getPluralCategoryVe(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -4042,11 +4146,11 @@ private static function getPluralCategoryVec(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } - if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { + if (self::isEqual($e, 0) && ! self::isEqual($i, 0) && self::isEqual($i % 1000000, 0) && self::isEqual($v, 0) || ! self::inRange($e, 0, 5)) { return 'many'; } @@ -4078,7 +4182,7 @@ private static function getPluralCategoryVo(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -4096,7 +4200,7 @@ private static function getPluralCategoryVun(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -4132,7 +4236,7 @@ private static function getPluralCategoryWae(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -4164,7 +4268,7 @@ private static function getPluralCategoryXh(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -4182,7 +4286,7 @@ private static function getPluralCategoryXog(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($n === 1) { + if (self::isEqual($n, 1)) { return 'one'; } @@ -4200,7 +4304,7 @@ private static function getPluralCategoryYi(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 1 && $v === 0) { + if (self::isEqual($i, 1) && self::isEqual($v, 0)) { return 'one'; } @@ -4260,7 +4364,7 @@ private static function getPluralCategoryZu(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ($i === 0 || $n === 1) { + if (self::isEqual($i, 0) || self::isEqual($n, 1)) { return 'one'; } @@ -4304,6 +4408,7 @@ public static function getPluralCategory(Locale $locale, float|int $number): str 'ckb' => self::getPluralCategoryCkb($number), 'cs' => self::getPluralCategoryCs($number), 'csw' => self::getPluralCategoryCsw($number), + 'cv' => self::getPluralCategoryCv($number), 'cy' => self::getPluralCategoryCy($number), 'da' => self::getPluralCategoryDa($number), 'de' => self::getPluralCategoryDe($number), @@ -4344,6 +4449,7 @@ public static function getPluralCategory(Locale $locale, float|int $number): str 'hy' => self::getPluralCategoryHy($number), 'ia' => self::getPluralCategoryIa($number), 'id' => self::getPluralCategoryId($number), + 'ie' => self::getPluralCategoryIe($number), 'ig' => self::getPluralCategoryIg($number), 'ii' => self::getPluralCategoryIi($number), 'io' => self::getPluralCategoryIo($number), @@ -4368,6 +4474,8 @@ public static function getPluralCategory(Locale $locale, float|int $number): str 'km' => self::getPluralCategoryKm($number), 'kn' => self::getPluralCategoryKn($number), 'ko' => self::getPluralCategoryKo($number), + 'kok' => self::getPluralCategoryKok($number), + 'kok-Latn' => self::getPluralCategoryKok_Latn($number), 'ks' => self::getPluralCategoryKs($number), 'ksb' => self::getPluralCategoryKsb($number), 'ksh' => self::getPluralCategoryKsh($number), @@ -4437,6 +4545,7 @@ public static function getPluralCategory(Locale $locale, float|int $number): str 'seh' => self::getPluralCategorySeh($number), 'ses' => self::getPluralCategorySes($number), 'sg' => self::getPluralCategorySg($number), + 'sgs' => self::getPluralCategorySgs($number), 'sh' => self::getPluralCategorySh($number), 'shi' => self::getPluralCategoryShi($number), 'si' => self::getPluralCategorySi($number), @@ -4533,6 +4642,7 @@ public static function getSupportedLocales(): array 'ckb', 'cs', 'csw', + 'cv', 'cy', 'da', 'de', @@ -4573,6 +4683,7 @@ public static function getSupportedLocales(): array 'hy', 'ia', 'id', + 'ie', 'ig', 'ii', 'io', @@ -4597,6 +4708,8 @@ public static function getSupportedLocales(): array 'km', 'kn', 'ko', + 'kok', + 'kok-Latn', 'ks', 'ksb', 'ksh', @@ -4666,6 +4779,7 @@ public static function getSupportedLocales(): array 'seh', 'ses', 'sg', + 'sgs', 'sh', 'shi', 'si', diff --git a/packages/intl/tests/FormatterTest.php b/packages/intl/tests/FormatterTest.php index fce7c77d99..e7428c305c 100644 --- a/packages/intl/tests/FormatterTest.php +++ b/packages/intl/tests/FormatterTest.php @@ -420,8 +420,6 @@ private function createNumberFunction(): NumberFunction private function createDateTimeFunction(): DateTimeFunction { - return new DateTimeFunction( - new IntlConfig(Locale::default(), Locale::default()), - ); + return new DateTimeFunction(); } } diff --git a/packages/intl/tests/GenericTranslatorTest.php b/packages/intl/tests/GenericTranslatorTest.php index 2d04fc87d6..fcc76a9f8d 100644 --- a/packages/intl/tests/GenericTranslatorTest.php +++ b/packages/intl/tests/GenericTranslatorTest.php @@ -37,7 +37,7 @@ protected function setUp(): void formatter: new MessageFormatter([ new StringFunction(), new NumberFunction($this->config), - new DateTimeFunction($this->config), + new DateTimeFunction(), ]), ); } diff --git a/packages/intl/tests/ParserTest.php b/packages/intl/tests/ParserTest.php index 8a6eaf8d82..9860918397 100644 --- a/packages/intl/tests/ParserTest.php +++ b/packages/intl/tests/ParserTest.php @@ -4,9 +4,12 @@ use PHPUnit\Framework\TestCase; use Tempest\Intl\MessageFormat\Parser\MessageFormatParser; +use Tempest\Intl\MessageFormat\Parser\Node\ComplexBody\Matcher; use Tempest\Intl\MessageFormat\Parser\Node\ComplexMessage; +use Tempest\Intl\MessageFormat\Parser\Node\Declaration\InputDeclaration; use Tempest\Intl\MessageFormat\Parser\Node\Declaration\LocalDeclaration; use Tempest\Intl\MessageFormat\Parser\Node\Expression\VariableExpression; +use Tempest\Intl\MessageFormat\Parser\Node\Literal\Literal; use Tempest\Intl\MessageFormat\Parser\Node\Pattern\Pattern; use Tempest\Intl\MessageFormat\Parser\Node\Pattern\Text; use Tempest\Intl\MessageFormat\Parser\Node\SimpleMessage; @@ -35,11 +38,15 @@ public function test_local_declaration(): void $this->assertInstanceOf(Text::class, $ast->pattern->elements[0]); $this->assertInstanceOf(VariableExpression::class, $ast->pattern->elements[1]); $this->assertInstanceOf(LocalDeclaration::class, $ast->declarations[0]); + + $expression = $ast->declarations[0]->expression; + $this->assertInstanceOf(VariableExpression::class, $expression); + $this->assertSame('time', $ast->declarations[0]->variable->name->name); $this->assertSame('datetime', $ast->declarations[0]->expression->function->identifier->name); $this->assertSame('style', $ast->declarations[0]->expression->function->options[0]->identifier->name); $this->assertSame('medium', $ast->declarations[0]->expression->function->options[0]->value->value); - $this->assertSame('launch_date', $ast->declarations[0]->expression->variable->name->name); + $this->assertSame('launch_date', $expression->variable->name->name); } public function test_input_declaration(): void @@ -54,21 +61,44 @@ public function test_input_declaration(): void MF2)->parse(); $this->assertInstanceOf(ComplexMessage::class, $ast); - $this->assertSame('numDays', $ast->pattern->elements[0]->pattern->elements[0]->variable->name->name); - $this->assertSame(' one', $ast->pattern->elements[0]->pattern->elements[1]->value); - $this->assertSame('numDays', $ast->pattern->elements[1]->pattern->elements[0]->variable->name->name); - $this->assertSame(' two', $ast->pattern->elements[1]->pattern->elements[1]->value); - $this->assertSame('numDays', $ast->pattern->elements[2]->pattern->elements[0]->variable->name->name); - $this->assertSame(' three', $ast->pattern->elements[2]->pattern->elements[1]->value); + $this->assertInstanceOf(InputDeclaration::class, $ast->declarations[0]); + $this->assertSame('numDays', $ast->declarations[0]->expression->variable->name->name); + + $this->assertInstanceOf(Matcher::class, $ast->body); + + $firstVariant = $ast->body->variants[0]->pattern->pattern; + $this->assertInstanceOf(VariableExpression::class, $firstVariant->elements[0]); + $this->assertInstanceOf(Text::class, $firstVariant->elements[1]); + $this->assertSame('numDays', $firstVariant->elements[0]->variable->name->name); + $this->assertSame(' one', $firstVariant->elements[1]->value); + + $secondVariant = $ast->body->variants[1]->pattern->pattern; + $this->assertInstanceOf(VariableExpression::class, $secondVariant->elements[0]); + $this->assertInstanceOf(Text::class, $secondVariant->elements[1]); + $this->assertSame('numDays', $secondVariant->elements[0]->variable->name->name); + $this->assertSame(' two', $secondVariant->elements[1]->value); + + $thirdVariant = $ast->body->variants[2]->pattern->pattern; + $this->assertInstanceOf(VariableExpression::class, $thirdVariant->elements[0]); + $this->assertInstanceOf(Text::class, $thirdVariant->elements[1]); + $this->assertSame('numDays', $thirdVariant->elements[0]->variable->name->name); + $this->assertSame(' three', $thirdVariant->elements[1]->value); } public function test_function_with_option_quoted_literal(): void { - /** @var ComplexMessage $ast */ $ast = new MessageFormatParser(<<<'MF2' Today is {$today :datetime pattern=|yyyy/MM/dd|}. MF2)->parse(); - $this->assertSame('yyyy/MM/dd', $ast->pattern->elements[1]->function->options[0]->value->value); + $this->assertInstanceOf(SimpleMessage::class, $ast); + $this->assertInstanceOf(VariableExpression::class, $ast->pattern->elements[1]); + + $function = $ast->pattern->elements[1]->function; + $this->assertNotNull($function); + + $optionValue = $function->options[0]->value; + $this->assertInstanceOf(Literal::class, $optionValue); + $this->assertSame('yyyy/MM/dd', $optionValue->value); } } diff --git a/packages/kv-store/src/Redis/Config/RedisConfig.php b/packages/kv-store/src/Redis/Config/RedisConfig.php index ec63e7759c..5356b67fec 100644 --- a/packages/kv-store/src/Redis/Config/RedisConfig.php +++ b/packages/kv-store/src/Redis/Config/RedisConfig.php @@ -50,6 +50,11 @@ public function __construct( */ public bool $persistent = false, + /** + * Identity for the requested persistent connection. Useful when multiple persistent connections are needed. + */ + public ?string $persistentId = null, + /** * The maximum duration, in seconds, to wait for a connection to be established. */ diff --git a/packages/kv-store/src/Redis/RedisExtensionWasMissing.php b/packages/kv-store/src/Redis/RedisExtensionWasMissing.php index 0e595127bb..19a05154ae 100644 --- a/packages/kv-store/src/Redis/RedisExtensionWasMissing.php +++ b/packages/kv-store/src/Redis/RedisExtensionWasMissing.php @@ -13,6 +13,7 @@ public function __construct(string $fqcn) 'Redis client not found.' . match ($fqcn) { \Redis::class => ' You may be missing the `redis` extension.', Predis\Client::class => ' You may need to install the `predis/predis` package.', + default => ' Install the `redis` extension or the `predis/predis` package.', }, ); } diff --git a/packages/kv-store/src/Redis/RedisInsightsProvider.php b/packages/kv-store/src/Redis/RedisInsightsProvider.php index 90428cc9ee..7ef93de84f 100644 --- a/packages/kv-store/src/Redis/RedisInsightsProvider.php +++ b/packages/kv-store/src/Redis/RedisInsightsProvider.php @@ -6,6 +6,7 @@ use Tempest\Container\Container; use Tempest\Core\Insight; use Tempest\Core\InsightsProvider; +use Tempest\Core\InsightType; use Tempest\Support\Regex; final class RedisInsightsProvider implements InsightsProvider @@ -28,13 +29,13 @@ public function getInsights(): array 'Engine' => match (get_class($redis->getClient())) { \Redis::class => 'Redis extension', Predis\Client::class => 'Predis', - default => new Insight('None', Insight::WARNING), + default => new Insight('None', InsightType::WARNING), }, - 'Version' => $version ?: new Insight('Unknown', Insight::WARNING), + 'Version' => $version ?: new Insight('Unknown', InsightType::WARNING), ]; } catch (\Throwable) { return [ - 'Engine' => new Insight('Disconnected', Insight::ERROR), + 'Engine' => new Insight('Disconnected', InsightType::ERROR), ]; } } diff --git a/packages/kv-store/tests/PredisClientTest.php b/packages/kv-store/tests/PredisClientTest.php index d6f11def13..72ee2fa352 100644 --- a/packages/kv-store/tests/PredisClientTest.php +++ b/packages/kv-store/tests/PredisClientTest.php @@ -22,13 +22,13 @@ protected function configure(): void $this->redis = new PredisClient( client: new Predis\Client( - parameters: array_filter([ + parameters: [ 'scheme' => 'tcp', 'host' => '127.0.0.1', 'port' => 6379, 'database' => 6, 'timeout' => .2, - ]), + ], options: ['prefix' => 'tempest_test:'], ), ); diff --git a/packages/log/src/Channels/DailyLogChannel.php b/packages/log/src/Channels/DailyLogChannel.php index 938c165760..c6e6c71d26 100644 --- a/packages/log/src/Channels/DailyLogChannel.php +++ b/packages/log/src/Channels/DailyLogChannel.php @@ -39,7 +39,7 @@ public function getHandlers(Level $level): array return [ new RotatingFileHandler( filename: $this->path, - maxFiles: $this->maxFiles ?? 0, + maxFiles: $this->maxFiles, level: $level, bubble: $this->bubble, filePermission: $this->filePermission, diff --git a/packages/log/src/Config/DailyLogConfig.php b/packages/log/src/Config/DailyLogConfig.php index 281c7ef383..e6df856ac0 100644 --- a/packages/log/src/Config/DailyLogConfig.php +++ b/packages/log/src/Config/DailyLogConfig.php @@ -3,6 +3,7 @@ namespace Tempest\Log\Config; use Tempest\Log\Channels\DailyLogChannel; +use Tempest\Log\LogChannel; use Tempest\Log\LogConfig; use Tempest\Log\LogLevel; use UnitEnum; diff --git a/packages/log/src/Config/SimpleLogConfig.php b/packages/log/src/Config/SimpleLogConfig.php index 3d12dfcbad..ce9ceae012 100644 --- a/packages/log/src/Config/SimpleLogConfig.php +++ b/packages/log/src/Config/SimpleLogConfig.php @@ -3,6 +3,7 @@ namespace Tempest\Log\Config; use Tempest\Log\Channels\AppendLogChannel; +use Tempest\Log\LogChannel; use Tempest\Log\LogConfig; use Tempest\Log\LogLevel; use UnitEnum; diff --git a/packages/log/src/Config/SlackLogConfig.php b/packages/log/src/Config/SlackLogConfig.php index b4f471d6fc..c82fcef597 100644 --- a/packages/log/src/Config/SlackLogConfig.php +++ b/packages/log/src/Config/SlackLogConfig.php @@ -4,6 +4,7 @@ use Tempest\Log\Channels\Slack\PresentationMode; use Tempest\Log\Channels\SlackLogChannel; +use Tempest\Log\LogChannel; use Tempest\Log\LogConfig; use Tempest\Log\LogLevel; use UnitEnum; @@ -31,6 +32,7 @@ final class SlackLogConfig implements LogConfig * @param string|null $username The username to display as the sender of the message. * @param PresentationMode $mode The display mode for the Slack messages. * @param LogLevel $minimumLogLevel The minimum log level to record. + * @param array $channels Additional channels to include in the configuration. * @param null|string $prefix An optional prefix displayed in all log messages. By default, the current environment is used. * @param null|UnitEnum|string $tag An optional tag to identify the logger instance associated to this configuration. */ @@ -39,7 +41,8 @@ public function __construct( private(set) ?string $channelId = null, private(set) ?string $username = null, private(set) PresentationMode $mode = PresentationMode::INLINE, - private LogLevel $minimumLogLevel = LogLevel::DEBUG, + private(set) LogLevel $minimumLogLevel = LogLevel::DEBUG, + private(set) array $channels = [], private(set) ?string $prefix = null, private(set) null|UnitEnum|string $tag = null, ) {} diff --git a/packages/log/src/Config/SysLogConfig.php b/packages/log/src/Config/SysLogConfig.php index e1a72895f1..09bca80756 100644 --- a/packages/log/src/Config/SysLogConfig.php +++ b/packages/log/src/Config/SysLogConfig.php @@ -3,6 +3,7 @@ namespace Tempest\Log\Config; use Tempest\Log\Channels\SysLogChannel; +use Tempest\Log\LogChannel; use Tempest\Log\LogConfig; use Tempest\Log\LogLevel; use UnitEnum; diff --git a/packages/log/src/Config/WeeklyLogConfig.php b/packages/log/src/Config/WeeklyLogConfig.php index 3b57e3e077..93b87e0bf2 100644 --- a/packages/log/src/Config/WeeklyLogConfig.php +++ b/packages/log/src/Config/WeeklyLogConfig.php @@ -3,6 +3,7 @@ namespace Tempest\Log\Config; use Tempest\Log\Channels\WeeklyLogChannel; +use Tempest\Log\LogChannel; use Tempest\Log\LogConfig; use Tempest\Log\LogLevel; use UnitEnum; diff --git a/packages/mail/src/EmailToSymfonyEmailMapper.php b/packages/mail/src/EmailToSymfonyEmailMapper.php index 596df7703c..d6f862b62d 100644 --- a/packages/mail/src/EmailToSymfonyEmailMapper.php +++ b/packages/mail/src/EmailToSymfonyEmailMapper.php @@ -122,8 +122,7 @@ private function convertAddresses(null|string|array|EmailAddress $addresses): ar ->map(fn (string|EmailAddress|SymfonyAddress $address) => match (true) { $address instanceof SymfonyAddress => $address, $address instanceof EmailAddress => new SymfonyAddress($address->email, $address->name ?? ''), - is_string($address) => SymfonyAddress::create($address), - default => null, + default => SymfonyAddress::create($address), }) ->filter() ->toArray(); diff --git a/packages/mail/src/GenericEmail.php b/packages/mail/src/GenericEmail.php index f9ff225c74..594db08ac5 100644 --- a/packages/mail/src/GenericEmail.php +++ b/packages/mail/src/GenericEmail.php @@ -30,6 +30,7 @@ public function __construct( public null|string|array|EmailAddress $replyTo = null, public array $headers = [], public EmailPriority $priority = EmailPriority::NORMAL, + /** @var Attachment[] */ public array $attachments = [], ) {} } diff --git a/packages/mail/src/GenericMailer.php b/packages/mail/src/GenericMailer.php index bbe39026c6..efe1631c84 100644 --- a/packages/mail/src/GenericMailer.php +++ b/packages/mail/src/GenericMailer.php @@ -25,6 +25,6 @@ public function send(Email $email): void $this->transport->send($symfonyMail); - $this->eventBus?->dispatch(new EmailWasSent($email)); + $this->eventBus->dispatch(new EmailWasSent($email)); } } diff --git a/packages/mail/src/MailerConfig.php b/packages/mail/src/MailerConfig.php index e43da66b43..4cbfbb618e 100644 --- a/packages/mail/src/MailerConfig.php +++ b/packages/mail/src/MailerConfig.php @@ -2,7 +2,6 @@ namespace Tempest\Mail; -use Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Transport\TransportInterface; interface MailerConfig @@ -10,7 +9,7 @@ interface MailerConfig /** * The underlying Symfony transport class. * - * @param class-string + * @var class-string */ public string $transport { get; diff --git a/packages/mail/src/Testing/MailTester.php b/packages/mail/src/Testing/MailTester.php index c534347ab1..1d19951bcf 100644 --- a/packages/mail/src/Testing/MailTester.php +++ b/packages/mail/src/Testing/MailTester.php @@ -21,7 +21,6 @@ final class MailTester { - private ?Email $sentEmail = null; private ?SymfonyEmail $sentSymfonyEmail = null; public function __construct( @@ -32,7 +31,6 @@ public function send(Email $email): self { $this->mailer->send($email); - $this->sentEmail = $email; $this->sentSymfonyEmail = map($email)->with(EmailToSymfonyEmailMapper::class)->do(); return $this; @@ -385,8 +383,6 @@ public function assertAttached(string $filename, ?Closure $callback = null): sel $filename, Arr\join(Arr\map($attachments, fn (DataPart $attachment) => $attachment->getName())), )); - - return $this; } /** @@ -452,8 +448,7 @@ private function convertAddresses(null|string|array|EmailAddress $addresses): ar return match (true) { $address instanceof SymfonyAddress => $address->getAddress(), $address instanceof EmailAddress => $address->email, - is_string($address) => $address, - default => null, + default => $address, }; }) ->filter() diff --git a/packages/mail/src/mail.config.php b/packages/mail/src/mail.config.php index 2865a4b73a..4d74e68d79 100644 --- a/packages/mail/src/mail.config.php +++ b/packages/mail/src/mail.config.php @@ -15,10 +15,13 @@ ); } +$scheme = strtolower(env('MAIL_SMTP_SCHEME', default: 'smtp')); + return new SmtpMailerConfig( - scheme: match (strtolower(env('MAIL_SMTP_SCHEME', default: 'smtp'))) { + scheme: match ($scheme) { 'smtps' => SmtpScheme::SMTPS, 'smtp' => SmtpScheme::SMTP, + default => throw new InvalidArgumentException(sprintf('Unsupported SMTP scheme "%s". Supported schemes: smtp, smtps.', $scheme)), }, host: env('MAIL_SMTP_HOST', default: '127.0.0.1'), port: env('MAIL_SMTP_PORT', default: 2525), diff --git a/packages/mapper/src/Casters/DateTimeCaster.php b/packages/mapper/src/Casters/DateTimeCaster.php index ee1b116e3f..f6b42f4201 100644 --- a/packages/mapper/src/Casters/DateTimeCaster.php +++ b/packages/mapper/src/Casters/DateTimeCaster.php @@ -50,6 +50,10 @@ public function cast(mixed $input): ?DateTimeInterface } try { + if ($this->format !== FormatPattern::ISO8601 && is_string($input)) { + return DateTime::fromPattern($input, $this->format); + } + return DateTime::parse($input); } catch (\Throwable) { return null; diff --git a/packages/mapper/src/Mappers/ArrayToObjectMapper.php b/packages/mapper/src/Mappers/ArrayToObjectMapper.php index 7694cfee9d..78c7ef7a37 100644 --- a/packages/mapper/src/Mappers/ArrayToObjectMapper.php +++ b/packages/mapper/src/Mappers/ArrayToObjectMapper.php @@ -161,7 +161,7 @@ private function setChildParentRelation(object $parent, mixed $child, ClassRefle public function resolveValue(PropertyReflector $property, mixed $value): mixed { - $caster = $this->memoize((string) $property, function () use ($property, $value) { + $caster = $this->memoize((string) $property, function () use ($property) { return $this->casterFactory ->in($this->context) ->forProperty($property); diff --git a/packages/mapper/src/ObjectFactory.php b/packages/mapper/src/ObjectFactory.php index 22207e64d9..b0f71f5d62 100644 --- a/packages/mapper/src/ObjectFactory.php +++ b/packages/mapper/src/ObjectFactory.php @@ -374,12 +374,12 @@ private function mapWith(mixed $mapper, mixed $from, mixed $to): mixed */ private function resolveMappers(): array { + /** @var Mapper[] $mappers */ $mappers = []; $context = MappingContext::from($this->context); foreach ($this->config->mappers as $mapperClass) { - /** @var Mapper $mapper */ $mappers[] = $this->container->get($mapperClass, context: $context); } diff --git a/packages/mapper/src/SerializerFactory.php b/packages/mapper/src/SerializerFactory.php index 336d3f65a9..5917fd6d09 100644 --- a/packages/mapper/src/SerializerFactory.php +++ b/packages/mapper/src/SerializerFactory.php @@ -4,7 +4,6 @@ namespace Tempest\Mapper; -use Closure; use Tempest\Container\Container; use Tempest\Container\Singleton; use Tempest\Reflection\ClassReflector; @@ -83,11 +82,7 @@ public function forProperty(PropertyReflector $property): ?Serializer } } - $serializer = $this->resolveSerializer($serializerClass, $property); - - if ($serializer !== null) { - return $serializer; - } + return $this->resolveSerializer($serializerClass, $property); } return null; @@ -113,20 +108,16 @@ public function forValue(mixed $value): ?Serializer } } - $serializer = $this->resolveSerializer($serializerClass, $input); - - if ($serializer !== null) { - return $serializer; - } + return $this->resolveSerializer($serializerClass, $input); } return null; } /** - * @param Closure|class-string $serializerClass + * @param class-string $serializerClass */ - private function resolveSerializer(string $serializerClass, PropertyReflector|TypeReflector|string $input): ?Serializer + private function resolveSerializer(string $serializerClass, PropertyReflector|TypeReflector|string $input): Serializer { $context = MappingContext::from($this->context); @@ -138,7 +129,7 @@ private function resolveSerializer(string $serializerClass, PropertyReflector|Ty } /** - * @return array{class-string|Closure,int}[] + * @return array{class-string,int}[] */ private function resolveSerializers(): array { diff --git a/packages/process/src/InvokedProcess.php b/packages/process/src/InvokedProcess.php index d4a96f8a86..7465f258c3 100644 --- a/packages/process/src/InvokedProcess.php +++ b/packages/process/src/InvokedProcess.php @@ -50,7 +50,7 @@ public function stop(float|int|Duration $timeout = 10, ?int $signal = null): sel /** * Waits for the process to finish. * - * @param null|callable(OutputChannel,string) $output The callback receives the type of output (out or err) and some bytes from the output in real-time while writing the standard input to the process. It allows to have feedback from the independent process during execution. + * @param null|callable(OutputChannel, string): void $output The callback receives the type of output (out or err) and some bytes from the output in real-time while writing the standard input to the process. It allows to have feedback from the independent process during execution. */ public function wait(?callable $output = null): ProcessResult; } diff --git a/packages/process/src/OutputChannel.php b/packages/process/src/OutputChannel.php index 1eaea91b3b..fa3f546c6a 100644 --- a/packages/process/src/OutputChannel.php +++ b/packages/process/src/OutputChannel.php @@ -14,6 +14,7 @@ public static function fromSymfonyOutputType(string $type): self return match ($type) { Process::OUT => self::OUTPUT, Process::ERR => self::ERROR, + default => throw new \UnexpectedValueException(sprintf('Unsupported output type "%s".', $type)), }; } } diff --git a/packages/process/src/PendingProcess.php b/packages/process/src/PendingProcess.php index ba2f018f90..4fd6a2e118 100644 --- a/packages/process/src/PendingProcess.php +++ b/packages/process/src/PendingProcess.php @@ -11,12 +11,12 @@ final class PendingProcess { /** * @param array|string $command The command to run and its arguments listed as separate entries. - * @param null|Duration $timeout Sets the process timeout (max. runtime). - * @param null|Duration $idleTimeout Sets the process idle timeout (max. time since last output). - * @param null|string $path Working directory for the process. - * @param null|string $input Content that will be passed to the underlying process standard input. - * @param null|bool $quietly Disables fetching output and error output from the underlying process. - * @param null|bool $tty If set to `true`, forces enabling TTY mode. + * @param Duration|null $timeout Sets the process timeout (max. runtime). + * @param Duration|null $idleTimeout Sets the process idle timeout (max. time since last output). + * @param string|null $path Working directory for the process. + * @param string|null $input Content that will be passed to the underlying process standard input. + * @param bool $quietly Disables fetching output and error output from the underlying process. + * @param bool $tty If set to `true`, forces enabling TTY mode. * @param array $environment Environment variables to set for the process. * @param array $options Underlying `proc_open` options. */ diff --git a/packages/process/src/Testing/InvokedProcessDescription.php b/packages/process/src/Testing/InvokedProcessDescription.php index 1f3c2e650a..fa0a942d74 100644 --- a/packages/process/src/Testing/InvokedProcessDescription.php +++ b/packages/process/src/Testing/InvokedProcessDescription.php @@ -16,7 +16,7 @@ final class InvokedProcessDescription /** * The process output, in order it should be received. * - * @var string[] + * @var array */ public array $output = []; @@ -42,6 +42,8 @@ public function pid(int $pid): self /** * Describes a line of standard output in the order it should be received. + * + * @param string|array $output */ public function output(string|array $output): self { @@ -61,6 +63,8 @@ public function output(string|array $output): self /** * Describes a line of error output in the order it should be received. + * + * @param string|array $errorOutput */ public function errorOutput(string|array $errorOutput): self { diff --git a/packages/process/src/Testing/InvokedTestingProcess.php b/packages/process/src/Testing/InvokedTestingProcess.php index 723015b692..9a19383025 100644 --- a/packages/process/src/Testing/InvokedTestingProcess.php +++ b/packages/process/src/Testing/InvokedTestingProcess.php @@ -69,6 +69,8 @@ final class InvokedTestingProcess implements InvokedProcess /** * The general output handler callback. + * + * @var null|\Closure(OutputChannel, string): void */ private ?\Closure $outputHandler = null; @@ -95,11 +97,6 @@ final class InvokedTestingProcess implements InvokedProcess */ private int $nextErrorOutputIndex = 0; - /** - * The signals that have been received. - */ - private array $receivedSignals = []; - public function __construct( private readonly InvokedProcessDescription $description, ) {} @@ -108,8 +105,6 @@ public function signal(int $signal): self { $this->invokeOutputHandlerWithNextLineOfOutput(); - $this->receivedSignals[] = $signal; - return $this; } @@ -122,7 +117,11 @@ public function stop(float|int|Duration $timeout = 10, ?int $signal = null): sel public function wait(?callable $output = null): ProcessResult { - $this->outputHandler = $output ?: $this->outputHandler; + if ($output !== null) { + $this->outputHandler = $output instanceof \Closure + ? $output + : \Closure::fromCallable($output); + } if (! $this->outputHandler) { $this->remainingRunIterations = 0; @@ -203,7 +202,7 @@ private function invokeOutputHandlerWithNextLineOfOutput(): bool $this->nextOutputIndex = $i + 1; - return $currentOutput; + return true; } if ($currentOutput['type'] === OutputChannel::ERROR && $i >= $this->nextErrorOutputIndex) { @@ -211,7 +210,7 @@ private function invokeOutputHandlerWithNextLineOfOutput(): bool $this->nextErrorOutputIndex = $i + 1; - return $currentOutput; + return true; } } diff --git a/packages/process/src/Testing/ProcessTester.php b/packages/process/src/Testing/ProcessTester.php index 0d8fb4cdb1..ba75c8d330 100644 --- a/packages/process/src/Testing/ProcessTester.php +++ b/packages/process/src/Testing/ProcessTester.php @@ -104,12 +104,12 @@ public function disableProcessExecution(): void /** * Stops the process and dumps the recorded process executions. - * - * @mago-expect lint:no-debug-symbols */ public function debugExecutedProcesses(): never { - dd($this->executor->executions); + $this->ensureTestingSetUp(); + + throw new \RuntimeException(var_export($this->executor->executions, true)); } /** @@ -123,7 +123,7 @@ public function describe(): InvokedProcessDescription /** * Asserts that the given command has been ran. Alternatively, a callback may be passed. * - * @param (\Closure(ProcessResult,PendingProcess=):false|void)|string $command + * @param null|Closure(): mixed|Closure(ProcessResult): mixed|Closure(ProcessResult, PendingProcess): mixed $callback */ public function assertCommandRan(string $command, ?\Closure $callback = null): self { @@ -158,7 +158,7 @@ public function assertCommandRan(string $command, ?\Closure $callback = null): s /** * Asserts that the a command has been ran by the given callback. * - * @param \Closure(PendingProcess,ProcessResult=):false|void $callback + * @param Closure(PendingProcess): mixed|Closure(PendingProcess, ProcessResult): mixed $callback */ public function assertRan(\Closure $callback): self { @@ -186,7 +186,7 @@ public function assertRan(\Closure $callback): self /** * Asserts that the given command did not run. Alternatively, a callback may be passed. * - * @param (\Closure(PendingProcess,ProcessResult=):false|void)|string $command + * @param string|Closure(): mixed|Closure(PendingProcess): mixed|Closure(PendingProcess, ProcessResult): mixed $command */ public function assertCommandDidNotRun(string|\Closure $command): self { diff --git a/packages/process/src/Testing/TestingProcessExecutor.php b/packages/process/src/Testing/TestingProcessExecutor.php index 9f0308d958..0b5381e186 100644 --- a/packages/process/src/Testing/TestingProcessExecutor.php +++ b/packages/process/src/Testing/TestingProcessExecutor.php @@ -19,7 +19,7 @@ final class TestingProcessExecutor implements ProcessExecutor private(set) array $executions = []; /** - * @param array $mocks + * @param array $mocks */ public function __construct( private readonly GenericProcessExecutor $executor, diff --git a/packages/reflection/src/EnumReflector.php b/packages/reflection/src/EnumReflector.php index c5fdd35395..f1d204b569 100644 --- a/packages/reflection/src/EnumReflector.php +++ b/packages/reflection/src/EnumReflector.php @@ -21,15 +21,15 @@ final class EnumReflector implements Reflector private readonly PHPReflectionEnum $reflectionEnum; /** - * @param class-string|TEnumName|PHPReflectionEnum $reflectionEnum + * @param class-string|TEnumName|self|PHPReflectionEnum $reflectionEnum */ - public function __construct(string|object $reflectionEnum) + public function __construct(string|UnitEnum|self|PHPReflectionEnum $reflectionEnum) { if (is_string($reflectionEnum)) { $reflectionEnum = new PHPReflectionEnum($reflectionEnum); } elseif ($reflectionEnum instanceof self) { $reflectionEnum = $reflectionEnum->getReflection(); - } elseif (! $reflectionEnum instanceof PHPReflectionEnum) { + } elseif ($reflectionEnum instanceof UnitEnum) { $reflectionEnum = new PHPReflectionEnum($reflectionEnum); } diff --git a/packages/reflection/src/TypeReflector.php b/packages/reflection/src/TypeReflector.php index f208b3aa6a..739cfd4e58 100644 --- a/packages/reflection/src/TypeReflector.php +++ b/packages/reflection/src/TypeReflector.php @@ -171,6 +171,10 @@ public function isEnumCase(): bool public function asEnumCase(): PHPReflectionEnumUnitCase { + if (! $this->reflector instanceof PHPReflectionEnumUnitCase) { + throw new \LogicException(sprintf('Cannot get enum case from `%s`.', $this->definition)); + } + return $this->reflector; } diff --git a/packages/reflection/tests/EnumReflectorTest.php b/packages/reflection/tests/EnumReflectorTest.php index 398da38d16..2af7df7d43 100644 --- a/packages/reflection/tests/EnumReflectorTest.php +++ b/packages/reflection/tests/EnumReflectorTest.php @@ -197,13 +197,47 @@ public function constructor_with_enum_instance(): void $this->assertSame(TestUnitEnum::class, $reflector->getName()); } + #[Test] + public function constructor_with_backed_enum_instance(): void + { + $reflector = new EnumReflector(TestBackedEnum::ACTIVE); + + $this->assertSame(TestBackedEnum::class, $reflector->getName()); + $this->assertTrue($reflector->isBacked()); + $this->assertSame(TestBackedEnum::ACTIVE, $reflector->getCase('ACTIVE')); + } + + #[Test] + public function constructor_with_native_reflection_enum(): void + { + $reflectionEnum = new ReflectionEnum(TestBackedEnum::class); + $reflector = new EnumReflector($reflectionEnum); + + $this->assertSame(TestBackedEnum::class, $reflector->getName()); + $this->assertTrue($reflector->isBacked()); + $this->assertSame($reflectionEnum, $reflector->getReflection()); + } + #[Test] public function constructor_with_enum_reflector(): void + { + $reflector1 = new EnumReflector(TestBackedEnum::class); + $reflector2 = new EnumReflector($reflector1); + + $this->assertEquals($reflector1, $reflector2); + $this->assertTrue($reflector2->isBacked()); + $this->assertSame('string', $reflector2->getBackingType()?->getName()); + } + + #[Test] + public function constructor_with_unit_enum_reflector(): void { $reflector1 = new EnumReflector(TestUnitEnum::class); $reflector2 = new EnumReflector($reflector1); $this->assertEquals($reflector1, $reflector2); + $this->assertFalse($reflector2->isBacked()); + $this->assertNull($reflector2->getBackingType()); } #[Test] @@ -247,7 +281,7 @@ enum TestEnumWithInterface: string implements TestInterface } #[\Attribute] -class TestAttribute +final class TestAttribute { public function __construct( public string $value, diff --git a/packages/router/src/Exceptions/ConvertsToResponse.php b/packages/router/src/Exceptions/ConvertsToResponse.php index adf912c1c7..f49d1258d4 100644 --- a/packages/router/src/Exceptions/ConvertsToResponse.php +++ b/packages/router/src/Exceptions/ConvertsToResponse.php @@ -6,10 +6,8 @@ /** * Marks this exception class as one that can be converted to a response. - * - * @phpstan-require-extends \Throwable */ -interface ConvertsToResponse +interface ConvertsToResponse extends \Throwable { /** * Gets a response to be sent to the client. diff --git a/packages/router/src/Exceptions/DevelopmentException.php b/packages/router/src/Exceptions/DevelopmentException.php index c1d3608756..8ffa2d6da4 100644 --- a/packages/router/src/Exceptions/DevelopmentException.php +++ b/packages/router/src/Exceptions/DevelopmentException.php @@ -60,7 +60,7 @@ public function __construct(Throwable $throwable, Response $response, Request $r ], 'resources' => [ 'memoryPeakUsage' => memory_get_peak_usage(real_usage: true), - 'executionTimeMs' => (hrtime(as_number: true) - TEMPEST_START) / 1_000_000, + 'executionTimeMs' => $this->resolveExecutionTimeInMilliseconds(), ], 'versions' => [ 'php' => PHP_VERSION, @@ -71,6 +71,17 @@ public function __construct(Throwable $throwable, Response $response, Request $r ); } + private function resolveExecutionTimeInMilliseconds(): float + { + $tempestStart = defined('TEMPEST_START') ? constant('TEMPEST_START') : null; + + if (! is_int($tempestStart)) { + return 0.0; + } + + return (hrtime(as_number: true) - $tempestStart) / 1_000_000; + } + private function enhanceStacktraceForViewCompilation(ViewCompilationFailed $exception, Stacktrace $stacktrace): Stacktrace { $previous = $exception->getPrevious(); diff --git a/packages/router/src/GenericRouter.php b/packages/router/src/GenericRouter.php index 5c87319d2a..6bedfe0e0e 100644 --- a/packages/router/src/GenericRouter.php +++ b/packages/router/src/GenericRouter.php @@ -39,14 +39,12 @@ public function dispatch(Request|PsrRequest $request): Response private function getCallable(): HttpMiddlewareCallable { $callControllerAction = function (Request $_) { - $matchedRoute = $this->container->get(MatchedRoute::class); - - if ($matchedRoute === null) { - // At this point, the `MatchRouteMiddleware` should have run. - // If that's not the case, then someone messed up by clearing all HTTP middleware + if (! $this->container->has(MatchedRoute::class)) { throw new MatchedRouteCouldNotBeResolved(); } + $matchedRoute = $this->container->get(MatchedRoute::class); + $route = $matchedRoute->route; $response = $this->container->invoke( diff --git a/packages/router/src/Routing/Construction/DiscoveredRoute.php b/packages/router/src/Routing/Construction/DiscoveredRoute.php index 4ea2d27e29..be603c8821 100644 --- a/packages/router/src/Routing/Construction/DiscoveredRoute.php +++ b/packages/router/src/Routing/Construction/DiscoveredRoute.php @@ -34,7 +34,7 @@ public static function fromRoute(Route $route, array $decorators, MethodReflecto optionalParameters: $uri['optional'], middleware: $route->middleware, handler: $methodReflector, - without: $route->without ?? [], + without: $route->without, ); } @@ -49,6 +49,7 @@ private function __construct( /** @var class-string<\Tempest\Router\HttpMiddleware>[] */ public array $middleware, public MethodReflector $handler, + /** @var class-string<\Tempest\Router\HttpMiddleware>[] */ public array $without = [], ) { $this->isDynamic = $parameters !== []; diff --git a/packages/router/src/Static/StaticGenerateCommand.php b/packages/router/src/Static/StaticGenerateCommand.php index 4de293f5bf..ecef517c65 100644 --- a/packages/router/src/Static/StaticGenerateCommand.php +++ b/packages/router/src/Static/StaticGenerateCommand.php @@ -20,7 +20,6 @@ use Tempest\HttpClient\HttpClient; use Tempest\Intl; use Tempest\Router\DataProvider; -use Tempest\Router\RouteConfig; use Tempest\Router\Router; use Tempest\Router\Static\Exceptions\DeadLinksDetectedException; use Tempest\Router\Static\Exceptions\InvalidStatusCodeException; @@ -47,7 +46,6 @@ final class StaticGenerateCommand public function __construct( private readonly AppConfig $appConfig, - private readonly RouteConfig $routeConfig, private readonly Console $console, private readonly Kernel $kernel, private readonly Container $container, @@ -264,7 +262,7 @@ private function detectDeadLinks(string $uri, string $html, bool $checkExternal continue; } - if ($response?->status->isRedirect()) { + if ($response->status->isRedirect()) { $target = Arr\first($response->getHeader('Location')->values); } } while ($response?->status->isRedirect()); diff --git a/packages/router/src/UriGenerator.php b/packages/router/src/UriGenerator.php index 0a9bdcd710..5fa8630e33 100644 --- a/packages/router/src/UriGenerator.php +++ b/packages/router/src/UriGenerator.php @@ -216,7 +216,7 @@ private function normalizeActionToUri(array|string|MethodReflector $action): str if ($routes === []) { if (! class_exists($controllerClass)) { - throw ControllerActionDoesNotExist::controllerNotFound($controllerClass, $controllerMethod); + throw ControllerActionDoesNotExist::controllerNotFound($controllerClass); } if (! method_exists($controllerClass, $controllerMethod)) { diff --git a/packages/router/tests/FakeRouteBuilder.php b/packages/router/tests/FakeRouteBuilder.php index 1897a68431..1d041c7996 100644 --- a/packages/router/tests/FakeRouteBuilder.php +++ b/packages/router/tests/FakeRouteBuilder.php @@ -21,6 +21,7 @@ public function __construct( public string $uri = '/', /** @var class-string[] */ public array $middleware = [], + /** @var class-string[] */ public array $without = [], ) { $this->handler = new MethodReflector(new ReflectionMethod($this, 'handler')); diff --git a/packages/router/tests/FakeRouteBuilderWithOptionalParams.php b/packages/router/tests/FakeRouteBuilderWithOptionalParams.php index ed3356634e..ed03e644e8 100644 --- a/packages/router/tests/FakeRouteBuilderWithOptionalParams.php +++ b/packages/router/tests/FakeRouteBuilderWithOptionalParams.php @@ -21,6 +21,7 @@ public function __construct( public string $uri = '/', /** @var class-string[] */ public array $middleware = [], + /** @var class-string[] */ public array $without = [], private string $handlerMethod = 'handler', ) { diff --git a/packages/storage/src/Config/AzureStorageConfig.php b/packages/storage/src/Config/AzureStorageConfig.php index b91ca14256..66f5055a04 100644 --- a/packages/storage/src/Config/AzureStorageConfig.php +++ b/packages/storage/src/Config/AzureStorageConfig.php @@ -3,8 +3,8 @@ namespace Tempest\Storage\Config; use AzureOss\FlysystemAzureBlobStorage\AzureBlobStorageAdapter; +use AzureOss\Storage\Blob\BlobServiceClient; use League\Flysystem\FilesystemAdapter; -use MicrosoftAzure\Storage\Blob\BlobRestProxy; use UnitEnum; final class AzureStorageConfig implements StorageConfig @@ -41,8 +41,7 @@ public function __construct( public function createAdapter(): FilesystemAdapter { return new AzureBlobStorageAdapter( - client: BlobRestProxy::createBlobService($this->dsn), - container: $this->container, + containerClient: BlobServiceClient::fromConnectionString($this->dsn)->getContainerClient($this->container), prefix: $this->prefix, ); } diff --git a/packages/support/src/Arr/ImmutableArray.php b/packages/support/src/Arr/ImmutableArray.php index 6c11396497..84479a0408 100644 --- a/packages/support/src/Arr/ImmutableArray.php +++ b/packages/support/src/Arr/ImmutableArray.php @@ -13,6 +13,8 @@ final class ImmutableArray implements ArrayInterface { use IsIterable; + + /** @use ManipulatesArray */ use ManipulatesArray; /** diff --git a/packages/support/src/Arr/ManipulatesArray.php b/packages/support/src/Arr/ManipulatesArray.php index 633f4819ac..e038d9f17a 100644 --- a/packages/support/src/Arr/ManipulatesArray.php +++ b/packages/support/src/Arr/ManipulatesArray.php @@ -142,7 +142,7 @@ public function forget(string|int|array $keys): self * * @param TValue|array $values The values to remove. */ - public function removeValues(string|int|array $values): self + public function removeValues(mixed $values): self { return $this->createOrModify(remove_values($this->value, $values)); } @@ -347,9 +347,12 @@ public function equals(array|self $other): bool * Returns the first item in the instance that matches the given `$filter`. * If `$filter` is `null`, returns the first item. * + * @template TDefault + * * @param null|Closure(TValue $value, TKey $key): bool $filter + * @param TDefault $default * - * @return TValue + * @return TValue|TDefault */ public function first(?Closure $filter = null, mixed $default = null): mixed { @@ -360,9 +363,12 @@ public function first(?Closure $filter = null, mixed $default = null): mixed * Returns the last item in the instance that matches the given `$filter`. * If `$filter` is `null`, returns the last item. * + * @template TDefault + * * @param null|Closure(TValue $value, TKey $key): bool $filter + * @param TDefault $default * - * @return TValue + * @return TValue|TDefault */ public function last(?Closure $filter = null, mixed $default = null): mixed { @@ -476,7 +482,7 @@ public function filter(?Closure $filter = null): self /** * Applies the given callback to all items of the instance. * - * @param Closure(mixed $value, mixed $key): void $each + * @param Closure(TValue, TKey): mixed $each */ public function each(Closure $each): self { @@ -561,7 +567,7 @@ public function hasKey(int|string $key): bool /** * Asserts whether the instance contains the specified value. * - * @param TValue: bool $search + * @param TValue|Closure(TValue, TKey): bool $search */ public function hasValue(mixed $search): bool { @@ -804,7 +810,7 @@ public function tap(Closure $callback): self */ public function dump(mixed ...$dumps): self { - lw($this->value, ...$dumps); + $this->debugLog([$this->value, ...$dumps]); return $this; } @@ -814,7 +820,24 @@ public function dump(mixed ...$dumps): self */ public function dd(mixed ...$dd): void { - ld($this->value, ...$dd); + $this->debugLog([$this->value, ...$dd], terminate: true); + } + + private function debugLog(array $items, bool $terminate = false): void + { + $debugClass = \Tempest\Debug\Debug::class; + + if (class_exists($debugClass)) { + $debugClass::resolve()->log($items); + } else { + foreach ($items as $item) { + error_log(print_r($item, true)); + } + } + + if ($terminate) { + exit(1); + } } /** diff --git a/packages/support/src/Arr/MutableArray.php b/packages/support/src/Arr/MutableArray.php index 2940468255..56f7b5ee50 100644 --- a/packages/support/src/Arr/MutableArray.php +++ b/packages/support/src/Arr/MutableArray.php @@ -13,6 +13,8 @@ final class MutableArray implements ArrayInterface { use IsIterable; + + /** @use ManipulatesArray */ use ManipulatesArray; /** diff --git a/packages/support/src/Arr/functions.php b/packages/support/src/Arr/functions.php index 4a919e2c24..96c97c720a 100644 --- a/packages/support/src/Arr/functions.php +++ b/packages/support/src/Arr/functions.php @@ -102,9 +102,13 @@ function reduce(iterable $array, callable $callback, mixed $initial = null): mix * * @template TKey of array-key * @template TValue + * @template TDefault * * @param array $array - * @param array-key $key + * @param TKey $key + * @param TDefault $default + * + * @return TValue|TDefault */ function pull(array &$array, string|int $key, mixed $default = null): mixed { @@ -155,7 +159,7 @@ function remove_keys(iterable $array, string|int|array $keys): array * @param TValue|array $values The values to remove. * @return array */ -function remove_values(array $array, string|int|array $values): array +function remove_values(array $array, mixed $values): array { $array = to_array($array); @@ -193,7 +197,7 @@ function forget_keys(array &$array, string|int|array $keys): array * @param TValue|array $values The values to remove. * @return array */ -function forget_values(array &$array, string|int|array $values): array +function forget_values(array &$array, mixed $values): array { $values = is_array($values) ? $values : [$values]; @@ -231,10 +235,14 @@ function is_associative(iterable $array): bool * @template TValue * * @param iterable $array - * @param int $number The number of random values to get. + * @param positive-int $number The number of random values to get. * @param bool $preserveKey Whether to include the keys of the original array. * - * @return array|mixed The random values, or a single value if `$number` is 1. + * @return ( + * $number is 1 + * ? TValue + * : ($preserveKey is true ? ImmutableArray : ImmutableArray) + * ) The random values, or a single value if `$number` is 1. */ function random(iterable $array, int $number = 1, bool $preserveKey = false): mixed { @@ -272,8 +280,21 @@ function random(iterable $array, int $number = 1, bool $preserveKey = false): mi * Retrieves values from a given key in each sub-array of the current array. * Optionally, you can pass a second parameter to also get the keys following the same pattern. * + * @template TItem of array + * @template TValueKey of string + * @template TIndexKey of string + * + * @param iterable $array * @param string $value The key to assign the values from, support dot notation. * @param string|null $key The key to assign the keys from, support dot notation. + * @phpstan-param TValueKey $value + * @phpstan-param TIndexKey|null $key + * + * @return ( + * $key is null + * ? list<(TValueKey is key-of ? TItem[TValueKey]|null : mixed)> + * : array ? TItem[TValueKey]|null : mixed)> + * ) */ function pluck(iterable $array, string $value, ?string $key = null): array { @@ -308,9 +329,12 @@ function pluck(iterable $array, string $value, ?string $key = null): array * * @template TKey of array-key * @template TValue + * @template TPrepended * * @param iterable $array - * @param TValue $values + * @param TPrepended $values + * + * @return array */ function prepend(iterable $array, mixed ...$values): array { @@ -328,9 +352,12 @@ function prepend(iterable $array, mixed ...$values): array * * @template TKey of array-key * @template TValue + * @template TAppended * * @param iterable $array - * @param TValue $values + * @param TAppended $values + * + * @return array */ function append(iterable $array, mixed ...$values): array { @@ -348,9 +375,12 @@ function append(iterable $array, mixed ...$values): array * * @template TKey of array-key * @template TValue + * @template TPushed * * @param iterable $array - * @param TValue $value + * @param TPushed $value + * + * @return array */ function push(iterable $array, mixed $value): array { @@ -362,6 +392,15 @@ function push(iterable $array, mixed $value): array /** * Pads the array to the specified size with a value. + * + * @template TKey of array-key + * @template TValue + * @template TPad + * + * @param iterable $array + * @param TPad $value + * + * @return array */ function pad(iterable $array, int $size, mixed $value): array { @@ -389,8 +428,14 @@ function flip(iterable $array): array /** * Returns a new array with only unique items from the original array. * - * @param string|null|Closure $key The key to use as the uniqueness criteria in nested arrays. + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param string|null|Closure(TValue, array): mixed $key The key to use as the uniqueness criteria in nested arrays. * @param bool $shouldBeStrict Whether the comparison should be strict, only used when giving a key parameter. + * + * @return array|list */ function unique(iterable $array, null|Closure|string $key = null, bool $shouldBeStrict = false): array { @@ -439,6 +484,8 @@ function unique(iterable $array, null|Closure|string $key = null, bool $shouldBe * * @param iterable $array * @param array ...$arrays + * + * @return array */ function diff(iterable $array, array ...$arrays): array { @@ -453,6 +500,8 @@ function diff(iterable $array, array ...$arrays): array * * @param iterable $array * @param array ...$arrays + * + * @return array */ function diff_keys(iterable $array, array ...$arrays): array { @@ -467,6 +516,8 @@ function diff_keys(iterable $array, array ...$arrays): array * * @param iterable $array * @param array ...$arrays + * + * @return array */ function intersect(iterable $array, array ...$arrays): array { @@ -481,6 +532,8 @@ function intersect(iterable $array, array ...$arrays): array * * @param iterable $array * @param array ...$arrays + * + * @return array */ function intersect_keys(iterable $array, array ...$arrays): array { @@ -495,6 +548,8 @@ function intersect_keys(iterable $array, array ...$arrays): array * * @param iterable $array * @param array ...$arrays The arrays to merge. + * + * @return array */ function merge(iterable $array, iterable ...$arrays): array { @@ -544,11 +599,13 @@ function equals(iterable $array, iterable $other): bool * * @template TKey of array-key * @template TValue + * @template TDefault * * @param iterable $array * @param null|Closure(TValue $value, TKey $key): bool $filter + * @param TDefault $default * - * @return TValue + * @return TValue|TDefault */ function first(iterable $array, ?Closure $filter = null, mixed $default = null): mixed { @@ -593,11 +650,13 @@ function at(iterable $array, int $index, mixed $default = null): mixed * * @template TKey of array-key * @template TValue + * @template TDefault * * @param iterable $array * @param null|Closure(TValue $value, TKey $key): bool $filter + * @param TDefault $default * - * @return TValue + * @return TValue|TDefault */ function last(iterable $array, ?Closure $filter = null, mixed $default = null): mixed { @@ -617,7 +676,14 @@ function last(iterable $array, ?Closure $filter = null, mixed $default = null): /** * Returns a copy of the given array without the last value. * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array * @param mixed $value The popped value will be stored in this variable. + * @param-out TValue|null $value + * + * @return list */ function pop(iterable $array, mixed &$value = null): array { @@ -630,7 +696,14 @@ function pop(iterable $array, mixed &$value = null): array /** * Returns a copy of the given array without the first value. * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array * @param mixed $value The unshifted value will be stored in this variable + * @param-out TValue|null $value + * + * @return list */ function unshift(iterable $array, mixed &$value = null): array { @@ -642,6 +715,13 @@ function unshift(iterable $array, mixed &$value = null): array /** * Returns a copy of the given array in reverse order. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * + * @return array */ function reverse(iterable $array): array { @@ -666,18 +746,38 @@ function implode(iterable $array, string $glue): ImmutableString /** * Returns a copy of the given array with the keys of this array as values. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * + * @return list */ function keys(iterable $array): array { - return array_keys(to_array($array)); + /** @var list $result */ + $result = array_keys(to_array($array)); + + return $result; } /** * Returns a copy of the given array without its keys. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * + * @return list */ function values(iterable $array): array { - return array_values(to_array($array)); + /** @var list $result */ + $result = array_values(to_array($array)); + + return $result; } /** @@ -689,6 +789,8 @@ function values(iterable $array): array * * @param iterable $array * @param null|Closure(TValue $value, TKey $key): bool $filter + * + * @return array */ function filter(iterable $array, ?Closure $filter = null): array { @@ -711,7 +813,9 @@ function filter(iterable $array, ?Closure $filter = null): array * @template TValue * * @param iterable $array - * @param Closure(TKey $value, TValue $key): void $each + * @param Closure(TValue $value, TKey $key): mixed $each + * + * @return array */ function each(iterable $array, Closure $each): array { @@ -759,9 +863,13 @@ function map(iterable $array, Closure $map): array * * @template TKey of array-key * @template TValue + * @template TMapKey of array-key + * @template TMapValue * * @param iterable $array - * @param Closure(TValue $value, TKey $key): Generator $map + * @param Closure(TValue $value, TKey $key): Generator $map + * + * @return array */ function map_with_keys(iterable $array, Closure $map): array { @@ -770,7 +878,6 @@ function map_with_keys(iterable $array, Closure $map): array foreach (to_array($array) as $key => $value) { $generator = $map($value, $key); - // @phpstan-ignore instanceof.alwaysTrue if (! $generator instanceof Generator) { throw new MapWithKeysDidNotUseAGenerator(); } @@ -884,6 +991,13 @@ function every(iterable $array, ?Closure $callback = null): bool /** * Returns a copy of the array with the given `$value` associated to the given `$key`. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * + * @return array */ function set_by_key(iterable $array, string $key, mixed $value): array { @@ -921,6 +1035,13 @@ function set_by_key(iterable $array, string $key, mixed $value): array /** * Returns a copy of the array that converts the dot-notated keys to a set of nested arrays. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * + * @return array */ function undot(iterable $array): array { @@ -953,6 +1074,13 @@ function undot(iterable $array): array /** * Returns a copy of the array that converts nested arrays to a single-dimension dot-notation array. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * + * @return array */ function dot(iterable $array, string $prefix = ''): array { @@ -1002,6 +1130,12 @@ function join(iterable $array, string $glue = ', ', ?string $finalGlue = ' and ' * ```php * flatten(['foo', ['bar', 'baz']]); // ['foo', 'bar', 'baz'] * ``` + * + * @template TFlattenValue + * + * @param iterable> $array + * + * @return list */ function flatten(iterable $array, int|float $depth = INF): array { @@ -1054,8 +1188,12 @@ function flatten(iterable $array, int|float $depth = INF): array * * @template TKey of array-key * @template TValue + * @template TGroupKey of array-key + * * @param iterable $array - * @param Closure(TValue, TKey): array-key $keyExtracor + * @param Closure(TValue, TKey): TGroupKey $keyExtracor + * + * @return array> */ function group_by(iterable $array, Closure $keyExtracor): array { @@ -1080,9 +1218,9 @@ function group_by(iterable $array, Closure $keyExtracor): array * @template TValue * * @param iterable $array - * @param Closure(TValue,TKey): TMapValue[] $map + * @param Closure(TValue,TKey): array $map * - * @return array + * @return list */ function flat_map(iterable $array, Closure $map, int|float $depth = 1): array { @@ -1095,7 +1233,13 @@ function flat_map(iterable $array, Closure $map, int|float $depth = 1): array * @see Tempest\Mapper\map() * * @template T + * @template TKey of array-key + * @template TValue + * + * @param iterable $array * @param class-string $to + * + * @return array */ function map_to(iterable $array, string $to): array { @@ -1139,7 +1283,7 @@ function sort(iterable $array, bool $desc = false, ?bool $preserveKeys = null, i * @template TValue * * @param iterable $array - * @param \Closure(TValue $a, TValue $b) $callback The function to use for comparing values. It should accept two parameters and return an integer less than, equal to, or greater than zero if the first argument is considered to be respectively less than, equal to, or greater than the second. + * @param \Closure(TValue, TValue): int $callback The function to use for comparing values. * @param bool|null $preserveKeys Preserves array keys if `true`; reindexes numerically if `false`. Defaults to `null`, which auto-detects preservation based on array type (associative or list). * @return array Key type depends on whether array keys are preserved or not. */ @@ -1183,7 +1327,7 @@ function sort_keys(iterable $array, bool $desc = false, int $flags = SORT_REGULA * @template TValue * * @param iterable $array - * @param callable $callback The function to use for comparing keys. It should accept two parameters + * @param callable(TKey, TKey): int $callback The function to use for comparing keys. It should accept two parameters * and return an integer less than, equal to, or greater than zero if the * first argument is considered to be respectively less than, equal to, or * greater than the second. @@ -1205,6 +1349,13 @@ function sort_keys_by_callback(iterable $array, callable $callback): array * ```php * slice([1, 2, 3, 4, 5], 2); // [3, 4, 5] * ``` + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * + * @return list */ function slice(iterable $array, int $offset, ?int $length = null): array { @@ -1263,10 +1414,6 @@ function range(int|float $start, int|float $end, int|float|null $step = null): a $result = []; - /** - * @var int|float $start - * @var int|float $step - */ for ($i = $start; $i <= $end; $i += $step) { $result[] = $i; } @@ -1285,10 +1432,6 @@ function range(int|float $start, int|float $end, int|float|null $step = null): a $result = []; - /** - * @var int|float $start - * @var int|float $step - */ for ($i = $start; $i >= $end; $i += $step) { $result[] = $i; } @@ -1304,7 +1447,7 @@ function range(int|float $start, int|float $end, int|float|null $step = null): a * @param iterable $iterable * @param (Closure(T): bool) $predicate * - * @return array{0: array, 1: array} + * @return array{0: list, 1: list} */ function partition(iterable $iterable, Closure $predicate): array { @@ -1353,12 +1496,24 @@ function wrap(mixed $input = []): array * Converts various data structures to a PHP array. * As opposed to `{@see \Tempest\Support\Arr\wrap}`, this function converts {@see Traversable} and {@see Countable} instances to arrays. * - * @param mixed $input Any value that can be converted to an array: - * - Arrays are returned as-is - * - Scalar values are wrapped in an array - * - Traversable objects are converted using `{@see iterator_to_array}` - * - {@see Countable} objects are converted to arrays - * - {@see null} becomes an empty array + * @template TKey of array-key + * @template TValue + * + * @param null|array|ArrayInterface|Traversable|Countable|scalar|object $input Any value that can be converted to an array: + * + * @return ( + * $input is null ? array{} : + * ($input is array ? array : + * ($input is ArrayInterface ? array : + * ($input is Traversable ? array : array))) + * ) + * + * Supported input shapes: + * - Arrays are returned as-is + * - Scalar values are wrapped in an array + * - Traversable objects are converted using `{@see iterator_to_array}` + * - {@see Countable} objects are converted to arrays + * - {@see null} becomes an empty array */ function to_array(mixed $input): array { diff --git a/packages/support/src/Conditions/HasConditions.php b/packages/support/src/Conditions/HasConditions.php index 431b52e77b..2e31e59a17 100644 --- a/packages/support/src/Conditions/HasConditions.php +++ b/packages/support/src/Conditions/HasConditions.php @@ -12,9 +12,9 @@ trait HasConditions * Applies the given `$callback` if the `$condition` is true. * * @param mixed|Closure(static): bool $condition - * @param Closure(static): self $callback + * @param Closure(static): mixed $callback */ - public function when(mixed $condition, Closure $callback): self + public function when(mixed $condition, Closure $callback): static { if ($condition instanceof Closure) { $condition = $condition($this); @@ -31,9 +31,9 @@ public function when(mixed $condition, Closure $callback): self * Applies the given `$callback` if the `$condition` is false. * * @param mixed|Closure(static): bool $condition - * @param Closure(static): self $callback + * @param Closure(static): mixed $callback */ - public function unless(mixed $condition, Closure $callback): self + public function unless(mixed $condition, Closure $callback): static { if ($condition instanceof Closure) { $condition = $condition($this); diff --git a/packages/support/src/Filesystem/functions.php b/packages/support/src/Filesystem/functions.php index f2ca7d980e..f395bfb9fd 100644 --- a/packages/support/src/Filesystem/functions.php +++ b/packages/support/src/Filesystem/functions.php @@ -268,10 +268,6 @@ function create_temporary_directory(?string $prefix = null): string $temporaryDirectory = sys_get_temp_dir(); $uniqueDirectory = $temporaryDirectory . '/' . uniqid(prefix: $prefix ?? ''); - if ($uniqueDirectory === false) { - throw new Exceptions\RuntimeException('Failed to create a temporary directory.'); - } - namespace\ensure_directory_exists($uniqueDirectory); namespace\ensure_directory_empty($uniqueDirectory); diff --git a/packages/support/src/JavaScript/DependencyInstaller.php b/packages/support/src/JavaScript/DependencyInstaller.php index a19950864e..762c687557 100644 --- a/packages/support/src/JavaScript/DependencyInstaller.php +++ b/packages/support/src/JavaScript/DependencyInstaller.php @@ -22,6 +22,8 @@ public function __construct( /** * Installs the specified JavaScript dependencies. * The package manager will be detected from the lockfile present in `$cwd`. If none found, it will be prompted to the user. + * + * @param non-empty-string|list $dependencies */ public function installDependencies(string $cwd, string|array $dependencies, bool $dev = false): void { @@ -40,6 +42,8 @@ public function installDependencies(string $cwd, string|array $dependencies, boo /** * Installs dependencies without interacting with the console. + * + * @param non-empty-string|list $dependencies */ public function silentlyInstallDependencies(string $cwd, string|array $dependencies, bool $dev = false, PackageManager $defaultPackageManager = PackageManager::NPM): void { @@ -55,19 +59,27 @@ public function silentlyInstallDependencies(string $cwd, string|array $dependenc /** * Gets the `Process` instance that will install the specified dependencies. + * + * @param non-empty-string|list $dependencies */ private function getInstallProcess(PackageManager $packageManager, string $cwd, string|array $dependencies, bool $dev = false): Process { + /** @var list $dependencies */ + $dependencies = array_values(wrap($dependencies)); + + /** @var list $command */ + $command = array_values(array_filter( + [ + $packageManager->getBinaryName(), + $packageManager->getInstallCommand(), + $dev ? '-D' : null, + ...$dependencies, + ], + fn (?string $arg): bool => $arg !== null, + )); + return new Process( - array_filter( - [ - $packageManager->getBinaryName(), - $packageManager->getInstallCommand(), - $dev ? '-D' : null, - ...wrap($dependencies), - ], - fn (?string $arg): bool => $arg !== null, - ), + $command, $cwd, ); } diff --git a/packages/support/src/JavaScript/PackageManager.php b/packages/support/src/JavaScript/PackageManager.php index a244b4c864..f354f743f6 100644 --- a/packages/support/src/JavaScript/PackageManager.php +++ b/packages/support/src/JavaScript/PackageManager.php @@ -17,6 +17,7 @@ enum PackageManager: string case YARN = 'yarn'; case NPM = 'npm'; + /** @return list */ public function getLockFiles(): array { return match ($this) { @@ -61,7 +62,9 @@ public static function detect(string $cwd): ?self { return array_find( array: PackageManager::cases(), - callback: fn ($packageManager) => array_any($packageManager->getLockFiles(), fn ($lockFile) => Filesystem\is_file($cwd . '/' . $lockFile)), + callback: fn (PackageManager $packageManager): bool => array_any($packageManager->getLockFiles(), fn (string $lockFile): bool => Filesystem\is_file($cwd + . '/' + . $lockFile)), ); } } diff --git a/packages/support/src/Math/functions.php b/packages/support/src/Math/functions.php index 722bf80528..9fbc603730 100644 --- a/packages/support/src/Math/functions.php +++ b/packages/support/src/Math/functions.php @@ -47,11 +47,7 @@ function sqrt(float $number): float /** * Returns the absolute value of the given number. * - * @template T of int|float - * - * @param T $number - * - * @return T + * @return ($number is int ? int<0, max> : float) */ function abs(int|float $number): int|float { diff --git a/packages/support/src/Paginator/PaginatedData.php b/packages/support/src/Paginator/PaginatedData.php index 4a77f44c22..9ddce19573 100644 --- a/packages/support/src/Paginator/PaginatedData.php +++ b/packages/support/src/Paginator/PaginatedData.php @@ -11,6 +11,7 @@ final class PaginatedData implements JsonSerializable { /** * @param array $data + * @param list $pageRange */ public function __construct( public array $data, @@ -41,7 +42,7 @@ public function __construct( /** * @template U - * @param callable(mixed): U $callback + * @param callable(T): U $callback * @return PaginatedData */ public function map(callable $callback): self @@ -62,6 +63,25 @@ public function map(callable $callback): self ); } + /** + * @return array{ + * data: array, + * pagination: array{ + * current_page: int, + * total_pages: int, + * total_items: int, + * items_per_page: int, + * offset: int, + * limit: int, + * has_next: bool, + * has_previous: bool, + * next_page: ?int, + * previous_page: ?int, + * page_range: list, + * count: int + * } + * } + */ public function toArray(): array { return [ @@ -83,6 +103,25 @@ public function toArray(): array ]; } + /** + * @return array{ + * data: array, + * pagination: array{ + * current_page: int, + * total_pages: int, + * total_items: int, + * items_per_page: int, + * offset: int, + * limit: int, + * has_next: bool, + * has_previous: bool, + * next_page: ?int, + * previous_page: ?int, + * page_range: list, + * count: int + * } + * } + */ public function jsonSerialize(): array { return $this->toArray(); diff --git a/packages/support/src/Paginator/Paginator.php b/packages/support/src/Paginator/Paginator.php index 44e23adb7d..df810a20e4 100644 --- a/packages/support/src/Paginator/Paginator.php +++ b/packages/support/src/Paginator/Paginator.php @@ -67,6 +67,7 @@ public function __construct( get => $this->totalPages > 0 ? $this->totalPages : null; } + /** @var list */ public array $pageRange { get => $this->calculatePageRange(); } @@ -128,6 +129,7 @@ public function paginateWith(callable $callback): PaginatedData return $this->paginate($callback($this->limit, $this->offset)); } + /** @return list */ private function calculatePageRange(): array { if ($this->totalPages <= $this->maxLinks) { diff --git a/packages/support/src/Path/Path.php b/packages/support/src/Path/Path.php index 11b0875b98..f5f3206209 100644 --- a/packages/support/src/Path/Path.php +++ b/packages/support/src/Path/Path.php @@ -29,6 +29,8 @@ protected function createOrModify(Stringable|string $string): self /** * Returns information about the path. See {@see pathinfo()}. + * + * @return ($flags is PATHINFO_ALL ? array{dirname: string, basename: string, extension?: string, filename: string} : string) */ public function info(int $flags = PATHINFO_ALL): string|array { @@ -77,10 +79,18 @@ public function extension(): self /** * Appends a glob and returns an immutable array with the resulting paths. + * + * @return ImmutableArray */ public function glob(string $pattern): ImmutableArray { - return new ImmutableArray(glob(namespace\normalize($this->value, $pattern))); + $paths = glob(namespace\normalize($this->value, $pattern)); + + if ($paths === false) { + return new ImmutableArray(); + } + + return new ImmutableArray($paths); } /** diff --git a/packages/support/src/Random/functions.php b/packages/support/src/Random/functions.php index c47fb998a3..bd04943cf7 100644 --- a/packages/support/src/Random/functions.php +++ b/packages/support/src/Random/functions.php @@ -23,6 +23,8 @@ * @param int<0, max> $length The length of the string to generate. * * @throws InvalidArgumentException If $alphabet length is outside the [2^1, 2^56] range. + * + * @return ($length is 0 ? '' : non-empty-string) */ function secure_string(int $length, ?string $alphabet = null): string { diff --git a/packages/support/src/Regex/functions.php b/packages/support/src/Regex/functions.php index 21591270d3..5305d59713 100644 --- a/packages/support/src/Regex/functions.php +++ b/packages/support/src/Regex/functions.php @@ -22,6 +22,7 @@ * * @param non-empty-string $pattern The pattern to match against. * @param 0|2|256|512|768 $flags + * @return array|array>|list> */ function get_matches(Stringable|string $subject, Stringable|string $pattern, bool $global = false, int $flags = 0, int $offset = 0): array { @@ -61,6 +62,7 @@ function get_matches(Stringable|string $subject, Stringable|string $pattern, boo * Returns the specified matches of `$pattern` in `$subject`. * * @param non-empty-string $pattern The pattern to match against. + * @return list> */ function get_all_matches( Stringable|string $subject, @@ -68,6 +70,7 @@ function get_all_matches( Stringable|string|int|array $matches = 0, int $offset = 0, ): array { + /** @var list> $result */ $result = get_matches($subject, $pattern, true, PREG_SET_ORDER, $offset); return arr($result) @@ -78,8 +81,22 @@ function get_all_matches( /** * Returns the specified match of `$pattern` in `$subject`. If no match is specified, returns the whole matching array. * + * @template TMatch of null|array|Stringable|int|string + * @template TDefault + * * @param non-empty-string $pattern The pattern to match against. + * @param TMatch $match + * @param TDefault $default * @param 0|256|512|768 $flags + * @return ( + * TMatch is null + * ? array + * : ( + * TMatch is array + * ? array + * : string|array{0: string|null, 1: int}|null|TDefault + * ) + * ) */ function get_match( Stringable|string $subject, @@ -88,7 +105,7 @@ function get_match( mixed $default = null, int $flags = 0, int $offset = 0, -): null|int|string|array { +): mixed { $result = get_matches($subject, $pattern, false, $flags, $offset); if ($match === null) { diff --git a/packages/support/src/Str/ManipulatesString.php b/packages/support/src/Str/ManipulatesString.php index 784ea71d90..140f4e72cb 100644 --- a/packages/support/src/Str/ManipulatesString.php +++ b/packages/support/src/Str/ManipulatesString.php @@ -255,6 +255,8 @@ public function sentence(): self /** * Returns an array of words from the current string. + * + * @return ImmutableArray */ public function words(): ImmutableArray { @@ -451,6 +453,8 @@ public function unwrap(string|Stringable $before, string|Stringable|null $after /** * Extracts an excerpt from the instance. + * + * @return ($asArray is true ? ImmutableArray : self) */ public function excerpt(int $from, int $to, bool $asArray = false): self|ImmutableArray { @@ -650,6 +654,8 @@ public function padRight(int $totalLength, string $padString = ' '): self /** * Chunks the instance into parts of the specified `$length`. + * + * @return ImmutableArray */ public function chunk(int $length): ImmutableArray { @@ -658,6 +664,8 @@ public function chunk(int $length): ImmutableArray /** * Explodes the string into an {@see \Tempest\Support\Arr\ImmutableArray} instance by a separator. + * + * @return ImmutableArray */ public function explode(string $separator = ' ', int $limit = PHP_INT_MAX): ImmutableArray { @@ -704,12 +712,15 @@ public function replaceRegex(array|string $regex, array|string|callable $replace * str('10-abc')->match('/(?\d+-)/', match: 'id'); // 10 * ``` * + * @template TDefault + * * @param non-empty-string $pattern The regular expression to match on - * @param string|int $match The group number or name to retrieve - * @param mixed $default The default value to return if no match is found + * @param array|string|int $match The group number, name, or list of groups to retrieve + * @param TDefault $default The default value to return if no match is found * @param 0|256|512|768 $flags + * @return ($match is array ? array : string|array{0: string|null, 1: int}|null|TDefault) */ - public function match(string $pattern, array|Stringable|int|string $match = 1, mixed $default = null, int $flags = 0, int $offset = 0): null|int|string|array + public function match(string $pattern, array|Stringable|int|string $match = 1, mixed $default = null, int $flags = 0, int $offset = 0): mixed { return Regex\get_match($this->value, $pattern, $match, $default, $flags, $offset); } @@ -718,6 +729,7 @@ public function match(string $pattern, array|Stringable|int|string $match = 1, m * Gets all portions of the instance that match the given regular expression. * * @param non-empty-string $pattern The regular expression to match on + * @return ImmutableArray> */ public function matchAll(Stringable|string $pattern, array|Stringable|int|string $matches = 0, int $offset = 0): ImmutableArray { @@ -847,7 +859,7 @@ public function tap(Closure $callback): self */ public function dd(mixed ...$dd): void { - ld($this->value, ...$dd); + $this->debugLog([$this->value, ...$dd], terminate: true); } /** @@ -855,13 +867,32 @@ public function dd(mixed ...$dd): void */ public function dump(mixed ...$dumps): self { - lw($this->value, ...$dumps); + $this->debugLog([$this->value, ...$dumps]); return $this; } + private function debugLog(array $items, bool $terminate = false): void + { + $debugClass = \Tempest\Debug\Debug::class; + + if (class_exists($debugClass)) { + $debugClass::resolve()->log($items); + } else { + foreach ($items as $item) { + error_log(print_r($item, true)); + } + } + + if ($terminate) { + exit(1); + } + } + /** * Decodes the JSON string and returns an array helper instance. + * + * @return ImmutableArray */ public function decodeJson(): ImmutableArray { @@ -892,8 +923,11 @@ public function __toString(): string return $this->value; } - public static function __set_state(array $array): object + /** + * @param array{value: string} $array + */ + public static function __set_state(array $array): static { - return new self($array['value']); + return new static($array['value']); } } diff --git a/packages/support/tests/Conditions/HasConditionsTest.php b/packages/support/tests/Conditions/HasConditionsTest.php index 406bcb8447..ee994570e3 100644 --- a/packages/support/tests/Conditions/HasConditionsTest.php +++ b/packages/support/tests/Conditions/HasConditionsTest.php @@ -20,7 +20,7 @@ public function test_when(): void public bool $value = false; }; - $class->when(true, fn ($c) => $c->value = true); // @phpstan-ignore-line + $class->when(true, fn ($c) => $c->value = true); $this->assertTrue($class->value); } @@ -33,7 +33,7 @@ public function test_when_with_callback(): void public bool $value = false; }; - $class->when(fn () => true, fn ($c) => $c->value = true); // @phpstan-ignore-line + $class->when(fn () => true, fn ($c) => $c->value = true); $this->assertTrue($class->value); } @@ -46,7 +46,7 @@ public function test_unless(): void public bool $value = false; }; - $class->unless(true, fn ($c) => $c->value = true); // @phpstan-ignore-line + $class->unless(true, fn ($c) => $c->value = true); $this->assertFalse($class->value); } @@ -59,7 +59,7 @@ public function test_unless_with_callback(): void public bool $value = false; }; - $class->unless(fn () => true, fn ($c) => $c->value = true); // @phpstan-ignore-line + $class->unless(fn () => true, fn ($c) => $c->value = true); $this->assertFalse($class->value); } @@ -80,7 +80,7 @@ public function append(string $string): self } }; - $class->when(true, function ($c): void { // @phpstan-ignore-line + $class->when(true, function ($c): void { $c->append('bar'); }); diff --git a/packages/upgrade/src/Tempest2/MigrationRector.php b/packages/upgrade/src/Tempest2/MigrationRector.php index 414cfe2c4e..d0c36722f4 100644 --- a/packages/upgrade/src/Tempest2/MigrationRector.php +++ b/packages/upgrade/src/Tempest2/MigrationRector.php @@ -16,10 +16,10 @@ public function getNodeTypes(): array ]; } - public function refactor(Node $node): void + public function refactor(Node $node): ?int { if (! $node instanceof Node\Stmt\Class_) { - return; + return null; } // Check whether this class implements Tempest\Database\DatabaseMigration @@ -31,7 +31,7 @@ public function refactor(Node $node): void ); if ($implementsDatabaseMigration === null) { - return; + return null; } // Unset the old interface @@ -84,5 +84,7 @@ public function refactor(Node $node): void } $node->implements = $implements; + + return null; } } diff --git a/packages/upgrade/src/Tempest28/WriteableRouteRector.php b/packages/upgrade/src/Tempest28/WriteableRouteRector.php index 05d4c658f6..630a8055e3 100644 --- a/packages/upgrade/src/Tempest28/WriteableRouteRector.php +++ b/packages/upgrade/src/Tempest28/WriteableRouteRector.php @@ -16,10 +16,10 @@ public function getNodeTypes(): array ]; } - public function refactor(Node $node): void + public function refactor(Node $node): ?int { if (! $node instanceof Node\Stmt\Class_) { - return; + return null; } // Check whether this class implements Tempest\Router\Route @@ -31,13 +31,15 @@ public function refactor(Node $node): void ); if ($implementsRoute === null) { - return; + return null; } if (! $node->isReadonly()) { - return; + return null; } $node->flags &= ~Modifiers::READONLY; + + return null; } } diff --git a/packages/upgrade/src/Tempest3/UpdateExceptionProcessorRector.php b/packages/upgrade/src/Tempest3/UpdateExceptionProcessorRector.php index 26bc1f9ab3..7bb7dc54a4 100644 --- a/packages/upgrade/src/Tempest3/UpdateExceptionProcessorRector.php +++ b/packages/upgrade/src/Tempest3/UpdateExceptionProcessorRector.php @@ -17,7 +17,7 @@ public function getNodeTypes(): array ]; } - public function refactor(Node $node): void + public function refactor(Node $node): ?int { if ($node instanceof Node\UseItem) { $name = $node->name->toString(); @@ -26,11 +26,11 @@ public function refactor(Node $node): void $node->name = new Node\Name('Tempest\Core\Exceptions\ExceptionReporter'); } - return; + return null; } if (! $node instanceof Node\Stmt\Class_) { - return; + return null; } $implements = $node->implements; @@ -41,7 +41,7 @@ public function refactor(Node $node): void ); if ($implementsExceptionProcessor === null) { - return; + return null; } $implements[$implementsExceptionProcessor] = new Node\Name('\Tempest\Core\Exceptions\ExceptionReporter'); @@ -57,5 +57,7 @@ public function refactor(Node $node): void break; } } + + return null; } } diff --git a/packages/upgrade/src/Tempest3/UpdateHasContextRector.php b/packages/upgrade/src/Tempest3/UpdateHasContextRector.php index 2547057a01..20c115c46d 100644 --- a/packages/upgrade/src/Tempest3/UpdateHasContextRector.php +++ b/packages/upgrade/src/Tempest3/UpdateHasContextRector.php @@ -15,7 +15,7 @@ public function getNodeTypes(): array ]; } - public function refactor(Node $node): void + public function refactor(Node $node): ?int { if ($node instanceof Node\UseItem) { $name = $node->name->toString(); @@ -24,11 +24,11 @@ public function refactor(Node $node): void $node->name = new Node\Name('Tempest\Core\ProvidesContext'); } - return; + return null; } if (! $node instanceof Node\Stmt\Class_) { - return; + return null; } $implements = $node->implements; @@ -39,10 +39,12 @@ public function refactor(Node $node): void ); if ($implementsHasContext === null) { - return; + return null; } $implements[$implementsHasContext] = new Node\Name('\Tempest\Core\ProvidesContext'); $node->implements = $implements; + + return null; } } diff --git a/packages/upgrade/tests/RectorTester.php b/packages/upgrade/tests/RectorTester.php index 1bb8fecbfc..b4925df5d0 100644 --- a/packages/upgrade/tests/RectorTester.php +++ b/packages/upgrade/tests/RectorTester.php @@ -96,12 +96,4 @@ private function cleanDiff(string $diff): string return trim($diff); } - - /** - * @mago-expect lint:no-debug-symbols - */ - public function dd(): never - { - dd($this->actual); - } } diff --git a/packages/validation/src/Exceptions/ValidationFailed.php b/packages/validation/src/Exceptions/ValidationFailed.php index 400cb303a1..fa6ee4ac4b 100644 --- a/packages/validation/src/Exceptions/ValidationFailed.php +++ b/packages/validation/src/Exceptions/ValidationFailed.php @@ -13,7 +13,7 @@ final class ValidationFailed extends Exception * @template TKey of array-key * * @param array $failingRules - * @param array $errorMessages + * @param array> $errorMessages * @param class-string|null $targetClass */ public function __construct( diff --git a/packages/validation/src/Validator.php b/packages/validation/src/Validator.php index eca86d2518..7c692884f3 100644 --- a/packages/validation/src/Validator.php +++ b/packages/validation/src/Validator.php @@ -22,6 +22,10 @@ use function Tempest\Support\arr; use function Tempest\Support\str; +/** + * @phpstan-type ValidationClosure Closure(mixed $value): (bool|string|null) + * @phpstan-type ValidationRule Rule|ValidationClosure|false|null + */ #[Singleton] final readonly class Validator { @@ -56,7 +60,7 @@ public function validateObject(object $object): void /** * Creates a {@see ValidationFailed} exception from the given rule failures, populated with error messages. * - * @param array $failingRules + * @param array> $failingRules * @param class-string|null $targetClass */ public function createValidationFailureException(array $failingRules, null|object|string $subject = null, ?string $targetClass = null): ValidationFailed @@ -75,7 +79,7 @@ public function createValidationFailureException(array $failingRules, null|objec * Validates the specified `$values` for the corresponding public properties on the specified `$class`, using built-in PHP types and attribute rules. * * @param ClassReflector|class-string $class - * @return array + * @return array> */ public function validateValuesForClass(ClassReflector|string $class, ?array $values, string $prefix = ''): array { @@ -126,7 +130,7 @@ class: $property->getType()->asClass(), /** * Validates `$value` against the specified `$property`, using built-in PHP types and attribute rules. * - * @return FailingRule[] + * @return list */ public function validateValueForProperty(PropertyReflector $property, mixed $value): array { @@ -160,8 +164,8 @@ public function validateValueForProperty(PropertyReflector $property, mixed $val /** * Validates the specified `$value` against the specified set of `$rules`. If a rule is a closure, it may return a string as a validation error. * - * @param Rule|array|(Closure(mixed $value):string|false) $rules - * @return FailingRule[] + * @param Rule|ValidationClosure|array $rules + * @return list */ public function validateValue(mixed $value, Closure|Rule|array $rules): array { @@ -187,10 +191,10 @@ public function validateValue(mixed $value, Closure|Rule|array $rules): array * The `$rules` array is expected to have the same keys as `$values`, associated with instance of {@see Tempest\Validation\Rule}. * If `$rules` doesn't contain a key for a value, it will not be validated. * - * @param array $values - * @param array $rules + * @param iterable $values + * @param array> $rules * - * @return Rule[] + * @return array> */ public function validateValues(iterable $values, array $rules): array { diff --git a/packages/validation/tests/ValidatorTest.php b/packages/validation/tests/ValidatorTest.php index 8b47f9117b..6631364a90 100644 --- a/packages/validation/tests/ValidatorTest.php +++ b/packages/validation/tests/ValidatorTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase; use Tempest\Reflection\ClassReflector; use Tempest\Validation\Exceptions\ValidationFailed; +use Tempest\Validation\HasErrorMessage; use Tempest\Validation\Rules\IsBoolean; use Tempest\Validation\Rules\IsEmail; use Tempest\Validation\Rules\IsEnum; @@ -63,8 +64,11 @@ public function test_closure_fails_with_string_response(): void return 'I expected b'; }); + $rule = $failingRules[0]->rule; + $this->assertCount(1, $failingRules); - $this->assertSame('I expected b', $failingRules[0]->rule->message); + $this->assertInstanceOf(HasErrorMessage::class, $rule); + $this->assertSame('I expected b', $rule->getErrorMessage()); } public function test_closure_passes_with_null_response(): void diff --git a/packages/view/src/Components/x-component.view.php b/packages/view/src/Components/x-component.view.php index fdf0baa252..c35181f9d2 100644 --- a/packages/view/src/Components/x-component.view.php +++ b/packages/view/src/Components/x-component.view.php @@ -22,7 +22,9 @@ HTML, $is, $attributeString, $content, $is); -$html = get(TempestViewRenderer::class)->render(view($template, ...$this->data)); +$data = $scopedVariables ?? $_data ?? []; +$data = is_array($data) ? $data : []; +$html = get(TempestViewRenderer::class)->render(view($template, ...$data)); ?> {!! $html !!} diff --git a/packages/view/src/Components/x-icon.view.php b/packages/view/src/Components/x-icon.view.php index 67905ea976..3a3515ae2c 100644 --- a/packages/view/src/Components/x-icon.view.php +++ b/packages/view/src/Components/x-icon.view.php @@ -1,6 +1,6 @@ wrappingElement; - } - - public function compile(): string - { - // if ($this->unwrap(ViewComponentElement::class)) { - return $this->wrappingElement->compile(); - // } - - $localVariableName = str($this->name)->ltrim(':')->camel()->toString(); - $isExpression = str_starts_with($this->name, ':'); - $value = $this->value ?? ''; - - // We'll declare the variable in PHP right before the actual element - $variableDeclaration = sprintf( - '$_%sIsLocal = $_%sIsLocal ?? isset($%s) === false; $%s ??= %s ?? null;', - $localVariableName, - $localVariableName, - $localVariableName, - $localVariableName, - $isExpression - ? ($value ?: 'null') - : var_export($value, return: true), - ); - - // And we'll remove it right after the element, this way we've created a "local scope" - // where the variable is only available to that specific element. - $variableRemoval = sprintf( - 'if ($_%sIsLocal ?? null) { unset($%s); }; unset($_%sIsLocal)', - $localVariableName, - $localVariableName, - $localVariableName, - ); - - return sprintf( - ' -%s - -', - $variableDeclaration, - $this->wrappingElement->compile(), - $variableRemoval, - ); - } -} diff --git a/packages/view/src/Elements/RawConditionalAttribute.php b/packages/view/src/Elements/RawConditionalAttribute.php index 8475b9fe7f..c31f431955 100644 --- a/packages/view/src/Elements/RawConditionalAttribute.php +++ b/packages/view/src/Elements/RawConditionalAttribute.php @@ -1,11 +1,13 @@ directory); if ($path->isDirectory()) { - /** @phpstan-ignore-next-line */ $path->glob('/*.php')->each(fn (string $file) => unlink($file)); Filesystem\delete_directory($this->directory); diff --git a/packages/view/tests/ViewCachePoolTest.php b/packages/view/tests/ViewCachePoolTest.php index 53fd84e791..bb87c8c71f 100644 --- a/packages/view/tests/ViewCachePoolTest.php +++ b/packages/view/tests/ViewCachePoolTest.php @@ -33,7 +33,6 @@ protected function tearDown(): void $directory = path(self::DIRECTORY); if ($directory->isDirectory()) { - /** @phpstan-ignore-next-line */ $directory->glob('/*.php')->each(fn (string $file) => unlink($file)); rmdir(self::DIRECTORY); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8366ae2fac..9858fc847b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,2 +1,112 @@ parameters: - ignoreErrors: [] \ No newline at end of file + ignoreErrors: + - + identifier: argument.named + paths: + - src/Tempest/Framework/Testing/* + - tests/Integration/**/*Test.php + - packages/auth/src/OAuth/Testing/* + - packages/**/src/Testing/* + - packages/core/src/Exceptions/ExceptionTester.php + - packages/**/tests/**/*.php + - packages/**/tests/*.php + - + identifier: smaller.alwaysFalse + path: packages/support/src/Arr/functions.php + count: 1 + # Runtime guard: positive-int type guarantees $number >= 1, but we keep the check for safety + - + identifier: instanceof.alwaysTrue + path: packages/support/src/Arr/functions.php + count: 1 + # Runtime guard: closure return type guarantees Generator, but we validate for a clear error message + - + identifier: disallowed.function + path: packages/clock/src/MockClock.php + count: 1 + # Intentional: dd() is the purpose of MockClock::dd() + - + identifier: disallowed.function + path: packages/debug/src/DOMDebug.php + count: 1 + # Intentional: lw() is the purpose of DOMDebug::dump() + - + identifier: disallowed.function + path: packages/debug/src/functions.php + count: 2 + # Intentional: dd() delegates to ld() and dump() delegates to lw() + - + identifier: disallowed.function + path: packages/core/src/Composer.php + count: 1 + # Intentional: exec() is required for running composer commands + - + identifier: disallowed.function + path: packages/console/src/Terminal/Terminal.php + count: 3 + # Intentional: exec() is required for TTY terminal operations (stty, tput) + - + identifier: disallowed.function + path: packages/datetime/src/TemporalConvenienceMethods.php + count: 2 + # Intentional: dd() is the purpose of TemporalInterface::dd() + - + identifier: identical.alwaysFalse + path: packages/datetime/src/DateTime.php + count: 6 + # Runtime guard: IntlCalendar::get() returns false at runtime for extreme year values + - + identifier: booleanOr.alwaysFalse + path: packages/datetime/src/DateTime.php + count: 5 + # Runtime guard: IntlCalendar::get() returns false at runtime for extreme year values + - + identifier: instanceof.alwaysTrue + path: packages/datetime/src/DateTime.php + count: 1 + # Runtime guard: DateTimeInterface::getTimezone() can return false at runtime + - + identifier: disallowed.function + path: packages/console/src/Testing/ConsoleTester.php + count: 1 + # Intentional: ld() is the purpose of ConsoleTester::dd() + - + identifier: disallowed.function + path: src/Tempest/Framework/Commands/ConfigShowCommand.php + count: 1 + # Intentional: lw() used for debug output when available + - + identifier: disallowed.function + path: tests/Integration/Console/Fixtures/LogDebugCommand.php + count: 1 + # Intentional: lw() is the purpose of this test fixture + - + identifier: disallowed.function + path: packages/support/tests/Filesystem/UnixFunctionsTest.php + count: 1 + # Intentional: exec() restores permissions recursively during Unix test cleanup + - + identifier: requireOnce.fileNotFound + path: packages/support/tests/Fixtures/Phar/normalize_path.php + count: 1 + # Intentional: require_once target exists only inside generated Phar archive at runtime + - + identifier: function.alreadyNarrowedType + path: packages/console/src/GenericConsole.php + count: 1 + # Runtime guard: PHPDoc narrows string to class-string, but is_a() check is needed at runtime + - + identifier: argument.type + path: packages/validation/tests/Rules/IsEnumTest.php + count: 1 + # Intentional: constructor accepts only enum class-strings; this test verifies runtime rejection path for invalid input + - + identifier: staticMethod.alreadyNarrowedType + path: packages/process/src/Testing/ProcessTester.php + count: 3 + # Intentional: explicit assertTrue() calls keep callback-only branches counted as assertions in tests + - + identifier: method.alreadyNarrowedType + path: tests/Integration/Database/GroupedWhereMethodsTest.php + count: 1 + # Intentional: assertion keeps explicit null-check semantics in grouped-where integration test diff --git a/phpstan.neon.dist b/phpstan.neon.dist index d2dc5f775b..c7f22f37ed 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,39 +1,79 @@ includes: - - phpstan-baseline.neon - - vendor/spaze/phpstan-disallowed-calls/extension.neon - - vendor/phpat/phpat/extension.neon + - phpstan-baseline.neon + - vendor/spaze/phpstan-disallowed-calls/extension.neon + - vendor/phpat/phpat/extension.neon services: - - - class: Tests\Tempest\Architecture\ArchitectureTestCase - tags: - - phpat.test + - + class: Tests\Tempest\Architecture\ArchitectureTestCase + tags: + - phpat.test + - + class: Tests\Tempest\PHPStan\LazyReadWritePropertiesExtension + tags: + - phpstan.properties.readWriteExtension + - + class: Tests\Tempest\PHPStan\QueryFunctionDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension parameters: - level: 5 - tmpDir: .cache/phpstan - tips: - treatPhpDocTypesAsCertain: false - excludePaths: - - tests/Integration/View/blade/cache/**.php - paths: - - src - - tests - ignoreErrors: - - - identifier: argument.named - - - message: '#.*exec*#' - path: packages/console/src/Terminal/Terminal.php + level: 5 + tmpDir: .cache/phpstan + tips: + treatPhpDocTypesAsCertain: false + paths: + - src + - packages/auth + - packages/cache + - packages/clock + - packages/command-bus + - packages/console + - packages/container + - packages/core + - packages/cryptography + - packages/database + - packages/datetime + - packages/debug + - packages/discovery + - packages/event-bus + - packages/generation + - packages/http + - packages/http-client + - packages/icon + - packages/intl + - packages/kv-store + - packages/log + - packages/mail + - packages/mapper + - packages/process + - packages/reflection + - packages/router + - packages/storage + - packages/support + - packages/upgrade + - packages/validation + - packages/view + - packages/vite + - packages/vite-plugin-tempest + - tests + excludePaths: + analyse: + - packages/upgrade/tests/*/Fixtures/*.php + reportUnmatchedIgnoredErrors: true - disallowedFunctionCalls: - - - function: 'exec()' - - - function: 'eval()' - - - function: 'dd()' - - - function: 'dump()' - - - function: 'phpinfo()' - - - function: 'var_dump()' + disallowedFunctionCalls: + - + function: 'exec()' + - + function: 'eval()' + - + function: 'dd()' + - + function: 'dump()' + - + function: 'ld()' + - + function: 'lw()' + - + function: 'phpinfo()' + - + function: 'var_dump()' diff --git a/tests/Integration/Auth/Authentication/SessionAuthenticatorTest.php b/tests/Integration/Auth/Authentication/SessionAuthenticatorTest.php index e20251574e..c23d41d08c 100644 --- a/tests/Integration/Auth/Authentication/SessionAuthenticatorTest.php +++ b/tests/Integration/Auth/Authentication/SessionAuthenticatorTest.php @@ -23,7 +23,6 @@ use Tempest\Http\Session\Config\FileSessionConfig; use Tempest\Http\Session\Managers\FileSessionManager; use Tempest\Http\Session\Session; -use Tempest\Http\Session\SessionConfig; use Tempest\Http\Session\SessionManager; use Tempest\Support\Filesystem; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; @@ -47,7 +46,7 @@ protected function configure(): void $this->container->config(new AuthConfig(authenticatables: [User::class])); $this->container->singleton(SessionManager::class, fn () => new FileSessionManager( $this->container->get(Clock::class), - $this->container->get(SessionConfig::class), + $this->container->get(FileSessionConfig::class), )); $this->database->migrate(CreateMigrationsTable::class, CreateUsersTableMigration::class, CreateApiKeysTableMigration::class); diff --git a/tests/Integration/Console/Commands/MakeConfigCommandTest.php b/tests/Integration/Console/Commands/MakeConfigCommandTest.php index 0f0766b346..66ebae005a 100644 --- a/tests/Integration/Console/Commands/MakeConfigCommandTest.php +++ b/tests/Integration/Console/Commands/MakeConfigCommandTest.php @@ -11,7 +11,7 @@ use Tempest\Console\Enums\ConfigType; use Tempest\Database\Config\MysqlConfig; use Tempest\EventBus\EventBusConfig; -use Tempest\Log\LogConfig; +use Tempest\Log\Config\SimpleLogConfig; use Tempest\Support\Namespace\Psr4Namespace; use Tempest\View\Renderers\BladeConfig; use Tempest\View\Renderers\TwigConfig; @@ -89,7 +89,7 @@ public static function config_type_provider(): array ], 'log_config' => [ 'configType' => ConfigType::LOG, - 'expectedConfigClass' => LogConfig::class, + 'expectedConfigClass' => SimpleLogConfig::class, ], 'console_config' => [ 'configType' => ConfigType::CONSOLE, diff --git a/tests/Integration/Framework/PHPStan/LazyReadWritePropertiesExtensionTest.php b/tests/Integration/Framework/PHPStan/LazyReadWritePropertiesExtensionTest.php new file mode 100644 index 0000000000..3b3d107ab7 --- /dev/null +++ b/tests/Integration/Framework/PHPStan/LazyReadWritePropertiesExtensionTest.php @@ -0,0 +1,64 @@ +createPropertyWithAttributes($this->createAttribute(Lazy::class)); + + $this->assertTrue($extension->isInitialized($property, 'property')); + } + + #[Test] + public function marks_properties_with_inject_attribute_as_initialized(): void + { + $extension = new LazyReadWritePropertiesExtension(); + $property = $this->createPropertyWithAttributes($this->createAttribute(Inject::class)); + + $this->assertTrue($extension->isInitialized($property, 'property')); + } + + #[Test] + public function does_not_mark_other_properties_as_initialized(): void + { + $extension = new LazyReadWritePropertiesExtension(); + $property = $this->createPropertyWithAttributes($this->createAttribute('Some\\Other\\Attribute')); + + $this->assertFalse($extension->isInitialized($property, 'property')); + } + + private function createPropertyWithAttributes(object ...$attributes): object + { + $property = $this->createStub(ExtendedPropertyReflection::class); + $property->method('getAttributes')->willReturn($attributes); + + return $property; + } + + private function createAttribute(string $name): object + { + return new readonly class($name) { + public function __construct( + private string $name, + ) {} + + public function getName(): string + { + return $this->name; + } + }; + } +} diff --git a/tests/Integration/Http/FileSessionTest.php b/tests/Integration/Http/FileSessionTest.php index 8fef58cdf0..eb87ddb835 100644 --- a/tests/Integration/Http/FileSessionTest.php +++ b/tests/Integration/Http/FileSessionTest.php @@ -13,7 +13,6 @@ use Tempest\Http\Session\Config\FileSessionConfig; use Tempest\Http\Session\Managers\FileSessionManager; use Tempest\Http\Session\Session; -use Tempest\Http\Session\SessionConfig; use Tempest\Http\Session\SessionCreated; use Tempest\Http\Session\SessionDeleted; use Tempest\Http\Session\SessionId; @@ -52,7 +51,7 @@ protected function configure(): void $this->container->singleton(SessionManager::class, fn () => new FileSessionManager( $this->container->get(Clock::class), - $this->container->get(SessionConfig::class), + $this->container->get(FileSessionConfig::class), )); } diff --git a/tests/Integration/Mapper/Fixtures/ObjectWithConfiguredTempestDateTimeFormat.php b/tests/Integration/Mapper/Fixtures/ObjectWithConfiguredTempestDateTimeFormat.php new file mode 100644 index 0000000000..f99c8fbb9b --- /dev/null +++ b/tests/Integration/Mapper/Fixtures/ObjectWithConfiguredTempestDateTimeFormat.php @@ -0,0 +1,16 @@ +assertSame('2025-03-02', $fromArray->nativeDate->format('Y-m-d')); } + #[Test] + public function map_uses_configured_format_for_tempest_datetime(): void + { + $object = map([ + 'date' => '01/12/2024 10:10:10', + ])->to(ObjectWithConfiguredTempestDateTimeFormat::class); + + $this->assertSame('2024-12-01 10:10:10', $object->date->format('yyyy-MM-dd HH:mm:ss')); + } + public function test_multiple_map_from_source(): void { $object = map(['name' => 'Guillaume'])->to(ObjectWithMultipleMapFrom::class); diff --git a/tests/Integration/Mapper/Mappers/ArrayToObjectMapperTestCase.php b/tests/Integration/Mapper/Mappers/ArrayToObjectMapperTestCase.php index abcaa8e977..be123c3e6e 100644 --- a/tests/Integration/Mapper/Mappers/ArrayToObjectMapperTestCase.php +++ b/tests/Integration/Mapper/Mappers/ArrayToObjectMapperTestCase.php @@ -60,7 +60,7 @@ public function test_default_values(): void $object = map([])->to(ObjectWithDefaultValues::class); $this->assertSame('a', $object->a); - $this->assertSame(null, $object->b); + $this->assertNull($object->b); } public function test_built_in_casters(): void @@ -82,7 +82,7 @@ public function test_built_in_casters(): void $this->assertSame('2024-01-01 10:10:10', $object->dateTime->format('Y-m-d H:i:s')); $this->assertSame('2024-12-01 10:10:10', $object->dateTimeWithFormat->format('Y-m-d H:i:s')); $this->assertNull($object->nullableDateTimeImmutable); - $this->assertSame(false, $object->bool); + $this->assertFalse($object->bool); $this->assertSame(0.1, $object->float); $this->assertSame(1, $object->int); } diff --git a/tests/Integration/View/ViewCacheTest.php b/tests/Integration/View/ViewCacheTest.php index a594a6166b..0a805812fd 100644 --- a/tests/Integration/View/ViewCacheTest.php +++ b/tests/Integration/View/ViewCacheTest.php @@ -37,7 +37,6 @@ protected function tearDown(): void $directory = path(self::DIRECTORY); if ($directory->isDirectory()) { - /** @phpstan-ignore-next-line */ $directory->glob('/*.php')->each(fn (string $file) => unlink($file)); rmdir(self::DIRECTORY); diff --git a/tests/Integration/View/ViewComponentTest.php b/tests/Integration/View/ViewComponentTest.php index 4da32987b1..6ef4dbeb38 100644 --- a/tests/Integration/View/ViewComponentTest.php +++ b/tests/Integration/View/ViewComponentTest.php @@ -6,6 +6,7 @@ use Generator; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; use Tempest\Core\Environment; use Tempest\Http\Session\FormSession; use Tempest\Validation\FailingRule; @@ -872,6 +873,22 @@ public function test_dynamic_view_component_with_slot(): void $this->assertSnippetsMatch('
test
', $html); } + #[Test] + public function dynamic_view_component_keeps_foreach_scope(): void + { + $this->view->registerViewComponent('x-test', '
'); + + $html = $this->view->render( + <<<'HTML' + {{ $item }} + HTML, + name: 'x-test', + items: ['a', 'b'], + ); + + $this->assertSnippetsMatch('
a
b
', $html); + } + public function test_nested_slots(): void { $this->view->registerViewComponent('x-a', ''); diff --git a/tests/PHPStan/LazyReadWritePropertiesExtension.php b/tests/PHPStan/LazyReadWritePropertiesExtension.php new file mode 100644 index 0000000000..d96609a51d --- /dev/null +++ b/tests/PHPStan/LazyReadWritePropertiesExtension.php @@ -0,0 +1,36 @@ +getAttributes() as $attribute) { + $attributeName = ltrim($attribute->getName(), '\\'); + + if ($attributeName === Lazy::class || $attributeName === Inject::class) { + return true; + } + } + + return false; + } +} diff --git a/tests/PHPStan/QueryFunctionDynamicReturnTypeExtension.php b/tests/PHPStan/QueryFunctionDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..1675a5872c --- /dev/null +++ b/tests/PHPStan/QueryFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,48 @@ +getName() === 'Tempest\\Database\\query'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) !== 1) { + return null; + } + + $constantStrings = $scope + ->getType($functionCall->getArgs()[0]->value) + ->getConstantStrings(); + + if (count($constantStrings) !== 1) { + return null; + } + + if ($this->reflectionProvider->hasClass($constantStrings[0]->getValue())) { + return null; + } + + return new GenericObjectType(QueryBuilder::class, [new ObjectWithoutClassType()]); + } +}