diff --git a/bin/release b/bin/release index 293575f1d5..6b6b086fd2 100755 --- a/bin/release +++ b/bin/release @@ -18,11 +18,10 @@ use Composer\Semver\VersionParser; use Tempest\Console\Console; use Tempest\Console\ConsoleApplication; use Tempest\Console\Exceptions\InterruptException; -use Tempest\Http\Response; use Tempest\HttpClient\HttpClient; use Tempest\Support\Json; +use Tempest\Container; -use function Tempest\get; use function Tempest\Support\arr; use function Tempest\Support\str; @@ -178,7 +177,7 @@ function updateBranchProtection(bool $enabled): void $token = Tempest\env('RELEASE_GITHUB_TOKEN'); $uri = "https://api.github.com/repos/tempestphp/tempest-framework/rulesets/{$ruleset}"; - $httpClient = Tempest\get(HttpClient::class); + $httpClient = Container\get(HttpClient::class); $response = $httpClient->put( uri: $uri, headers: ['Authorization' => "Bearer {$token}"], @@ -196,7 +195,7 @@ function triggerSubsplit(): void $token = Tempest\env('RELEASE_GITHUB_TOKEN'); $uri = 'https://api.github.com/repos/tempestphp/tempest-framework/actions/workflows/subsplit-packages.yml/dispatches'; - $httpClient = Tempest\get(HttpClient::class); + $httpClient = Container\get(HttpClient::class); $response = $httpClient->post( uri: $uri, @@ -346,7 +345,7 @@ try { performPreReleaseChecks('origin', 'main'); - $console = get(Console::class); + $console = Container\get(Console::class); $console->writeln(); $console->info(sprintf('Current version is %s.', $current = getCurrentVersion())); diff --git a/docs/1-essentials/05-container.md b/docs/1-essentials/05-container.md index 81154551b3..71e12affae 100644 --- a/docs/1-essentials/05-container.md +++ b/docs/1-essentials/05-container.md @@ -47,10 +47,10 @@ The `{php}\Tempest\invoke()` function serves the same purpose when the container ### Locating a dependency -There are situations where it may not be possible to inject a dependency on a constructor. To work around this, Tempest provides the `{php}\Tempest\get()` function, which can resolve an object from the container. +There are situations where it may not be possible to inject a dependency on a constructor. To work around this, Tempest provides the `{php}\Tempest\Container\get()` function, which can resolve an object from the container. ```php -use function Tempest\get; +use function Tempest\Container\get; $config = get(AppConfig::class); ``` diff --git a/packages/clock/src/functions.php b/packages/clock/src/functions.php index ede8871ba6..2f2a6fad72 100644 --- a/packages/clock/src/functions.php +++ b/packages/clock/src/functions.php @@ -4,7 +4,7 @@ use Tempest\DateTime\DateTimeInterface; -use function Tempest\get; +use function Tempest\Container\get; /** * Get the current date and time as a {@see \Tempest\DateTime\DateTimeInterface} object. diff --git a/packages/command-bus/src/functions.php b/packages/command-bus/src/functions.php index 72aeb7aa0c..e9dfbceda2 100644 --- a/packages/command-bus/src/functions.php +++ b/packages/command-bus/src/functions.php @@ -2,16 +2,17 @@ declare(strict_types=1); -namespace Tempest { - use Tempest\CommandBus\CommandBus; +namespace Tempest\CommandBus; - /** - * Dispatches the given `$command` to the {@see CommandBus}, triggering all associated command handlers. - */ - function command(object $command): void - { - $commandBus = get(CommandBus::class); +use Tempest\CommandBus\CommandBus; +use Tempest\Container; - $commandBus->dispatch($command); - } +/** + * Dispatches the given `$command` to the {@see CommandBus}, triggering all associated command handlers. + */ +function command(object $command): void +{ + $commandBus = Container\get(CommandBus::class); + + $commandBus->dispatch($command); } diff --git a/packages/console/src/Scheduler/GenericScheduler.php b/packages/console/src/Scheduler/GenericScheduler.php index 1cfc370caf..b98c1bec84 100644 --- a/packages/console/src/Scheduler/GenericScheduler.php +++ b/packages/console/src/Scheduler/GenericScheduler.php @@ -10,7 +10,7 @@ use Tempest\Process\ProcessExecutor; use Tempest\Support\Filesystem; -use function Tempest\event; +use function Tempest\EventBus\event; use function Tempest\internal_storage_path; final readonly class GenericScheduler implements Scheduler diff --git a/packages/container/src/functions.php b/packages/container/src/functions.php index 4ceadcb0a5..32eee772ba 100644 --- a/packages/container/src/functions.php +++ b/packages/container/src/functions.php @@ -2,42 +2,42 @@ declare(strict_types=1); -namespace Tempest { - use Tempest\Container\GenericContainer; - use Tempest\Reflection\FunctionReflector; - use Tempest\Reflection\MethodReflector; +namespace Tempest\Container; - /** - * Retrieves an instance of the specified `$className` from the container. - * - * @template TClassName of object - * @param class-string $className - * @return TClassName - */ - function get(string $className, ?string $tag = null, mixed ...$params): object - { - $container = GenericContainer::instance(); +use Tempest\Container\GenericContainer; +use Tempest\Reflection\FunctionReflector; +use Tempest\Reflection\MethodReflector; - return $container->get($className, $tag, ...$params); - } +/** + * Retrieves an instance of the specified `$className` from the container. + * + * @template TClassName of object + * @param class-string $className + * @return TClassName + */ +function get(string $className, ?string $tag = null, mixed ...$params): object +{ + $container = GenericContainer::instance(); - /** - * Invokes the given method, function, callable or invokable class from the container. If no named parameters are specified, they will be resolved from the container. - * - * #### Examples - * ```php - * \Tempest\invoke(function (MyService $service) { - * $service->execute(); - * }); - * ``` - * ```php - * \Tempest\invoke(MyService::class, key: $apiKey); - * ``` - */ - function invoke(MethodReflector|FunctionReflector|callable|string $callable, mixed ...$params): mixed - { - $container = GenericContainer::instance(); + return $container->get($className, $tag, ...$params); +} + +/** + * Invokes the given method, function, callable or invokable class from the container. If no named parameters are specified, they will be resolved from the container. + * + * #### Examples + * ```php + * \Tempest\invoke(function (MyService $service) { + * $service->execute(); + * }); + * ``` + * ```php + * \Tempest\invoke(MyService::class, key: $apiKey); + * ``` + */ +function invoke(MethodReflector|FunctionReflector|callable|string $callable, mixed ...$params): mixed +{ + $container = GenericContainer::instance(); - return $container->invoke($callable, ...$params); - } + return $container->invoke($callable, ...$params); } diff --git a/packages/container/tests/ContainerTest.php b/packages/container/tests/ContainerTest.php index 9f8552a8d3..effa97435e 100644 --- a/packages/container/tests/ContainerTest.php +++ b/packages/container/tests/ContainerTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\TestCase; use ReflectionClass; +use Tempest\Container; use Tempest\Container\Exceptions\CircularDependencyEncountered; use Tempest\Container\Exceptions\DecoratorDidNotImplementInterface; use Tempest\Container\Exceptions\DependencyCouldNotBeAutowired; @@ -65,7 +66,7 @@ use Tempest\Container\Tests\Fixtures\UnionTypesClass; use Tempest\Reflection\ClassReflector; -use function Tempest\reflect; +use function Tempest\Reflection\reflect; /** * @internal @@ -411,7 +412,7 @@ public function test_invoke_closure_with_function(): void GenericContainer::setInstance($container = new GenericContainer()); $container->singleton(SingletonClass::class, fn () => new SingletonClass()); - $result = \Tempest\invoke(fn (SingletonClass $class) => $class::class); + $result = Container\invoke(fn (SingletonClass $class) => $class::class); $this->assertEquals(SingletonClass::class, $result); } diff --git a/packages/core/src/functions.php b/packages/core/src/functions.php index e38a8387a6..3e93e0267e 100644 --- a/packages/core/src/functions.php +++ b/packages/core/src/functions.php @@ -2,104 +2,105 @@ declare(strict_types=1); -namespace Tempest { - use Closure; - use Stringable; - use Tempest\Core\Composer; - use Tempest\Core\DeferredTasks; - use Tempest\Core\EnvironmentVariableValidationFailed; - use Tempest\Core\Kernel; - use Tempest\Intl\Translator; - use Tempest\Support\Namespace\PathCouldNotBeMappedToNamespace; - use Tempest\Validation\Rule; - use Tempest\Validation\Validator; +namespace Tempest; - use function Tempest\Support\Namespace\to_psr4_namespace; - use function Tempest\Support\Path\to_absolute_path; +use Closure; +use Stringable; +use Tempest\Container; +use Tempest\Core\Composer; +use Tempest\Core\DeferredTasks; +use Tempest\Core\EnvironmentVariableValidationFailed; +use Tempest\Core\Kernel; +use Tempest\Intl\Translator; +use Tempest\Support\Namespace\PathCouldNotBeMappedToNamespace; +use Tempest\Validation\Rule; +use Tempest\Validation\Validator; - /** - * Creates an absolute path scoped to the root of the project. - */ - function root_path(Stringable|string ...$parts): string - { - return to_absolute_path(get(Kernel::class)->root, ...$parts); - } +use function Tempest\Support\Namespace\to_psr4_namespace; +use function Tempest\Support\Path\to_absolute_path; - /** - * Creates an absolute path scoped to the main directory of the project. - */ - function src_path(Stringable|string ...$parts): string - { - return root_path(get(Composer::class)->mainNamespace->path, ...$parts); - } +/** + * Creates an absolute path scoped to the root of the project. + */ +function root_path(Stringable|string ...$parts): string +{ + return to_absolute_path(Container\get(Kernel::class)->root, ...$parts); +} - /** - * Creates an absolute path scoped to the framework's internal storage directory. - */ - function internal_storage_path(Stringable|string ...$parts): string - { - return to_absolute_path(get(Kernel::class)->internalStorage, ...$parts); - } +/** + * Creates an absolute path scoped to the main directory of the project. + */ +function src_path(Stringable|string ...$parts): string +{ + return root_path(Container\get(Composer::class)->mainNamespace->path, ...$parts); +} - /** - * Converts the given path to a registered namespace. The path is expected to be absolute, or relative to the root of the project. - * - * @throws PathCouldNotBeMappedToNamespace If the path cannot be mapped to registered namespace - */ - function registered_namespace(Stringable|string ...$parts): string - { - return to_psr4_namespace(get(Composer::class)->namespaces, root_path(...$parts), root: root_path()); - } +/** + * Creates an absolute path scoped to the framework's internal storage directory. + */ +function internal_storage_path(Stringable|string ...$parts): string +{ + return to_absolute_path(Container\get(Kernel::class)->internalStorage, ...$parts); +} - /** - * Converts the given path to the main namespace. The path is expected to be absolute, or relative to the root of the project. - * - * @throws PathCouldNotBeMappedToNamespace If the path cannot be mapped to the main namespace - */ - function src_namespace(Stringable|string ...$parts): string - { - return to_psr4_namespace(get(Composer::class)->mainNamespace, root_path(...$parts), root: root_path()); - } +/** + * Converts the given path to a registered namespace. The path is expected to be absolute, or relative to the root of the project. + * + * @throws PathCouldNotBeMappedToNamespace If the path cannot be mapped to registered namespace + */ +function registered_namespace(Stringable|string ...$parts): string +{ + return to_psr4_namespace(Container\get(Composer::class)->namespaces, root_path(...$parts), root: root_path()); +} - /** - * Retrieves the given `$key` from the environment variables. If `$key` is not defined, `$default` is returned instead. - * - * @param Rule[] $rules Optional validation rules for the value of this environment variable. If one of the rules don't pass, an exception is thrown, preventing the application from booting. - */ - function env(string $key, mixed $default = null, array $rules = []): mixed - { - $value = getenv($key); - $value = match (is_string($value) ? mb_strtolower($value) : $value) { - 'true' => true, - 'false' => false, - false, 'null', '' => $default, - default => $value, - }; +/** + * Converts the given path to the main namespace. The path is expected to be absolute, or relative to the root of the project. + * + * @throws PathCouldNotBeMappedToNamespace If the path cannot be mapped to the main namespace + */ +function src_namespace(Stringable|string ...$parts): string +{ + return to_psr4_namespace(Container\get(Composer::class)->mainNamespace, root_path(...$parts), root: root_path()); +} - if ($rules === [] || ! class_exists(Validator::class) || ! interface_exists(Translator::class)) { - return $value; - } +/** + * Retrieves the given `$key` from the environment variables. If `$key` is not defined, `$default` is returned instead. + * + * @param Rule[] $rules Optional validation rules for the value of this environment variable. If one of the rules don't pass, an exception is thrown, preventing the application from booting. + */ +function env(string $key, mixed $default = null, array $rules = []): mixed +{ + $value = getenv($key); + $value = match (is_string($value) ? mb_strtolower($value) : $value) { + 'true' => true, + 'false' => false, + false, 'null', '' => $default, + default => $value, + }; - $validator = get(Validator::class); - $failures = $validator->validateValue($value, $rules); + if ($rules === [] || ! class_exists(Validator::class) || ! interface_exists(Translator::class)) { + return $value; + } - if ($failures === []) { - return $value; - } + $validator = Container\get(Validator::class); + $failures = $validator->validateValue($value, $rules); - throw new EnvironmentVariableValidationFailed( - name: $key, - value: $value, - failingRules: $failures, - validator: $validator, - ); + if ($failures === []) { + return $value; } - /** - * Defer a task, will be run after a request has been sent or a command has executed - */ - function defer(Closure $closure): void - { - get(DeferredTasks::class)->add($closure); - } + throw new EnvironmentVariableValidationFailed( + name: $key, + value: $value, + failingRules: $failures, + validator: $validator, + ); +} + +/** + * Defer a task, will be run after a request has been sent or a command has executed + */ +function defer(Closure $closure): void +{ + Container\get(DeferredTasks::class)->add($closure); } diff --git a/packages/database/src/Builder/ModelInspector.php b/packages/database/src/Builder/ModelInspector.php index 5c7f76093b..7b349e3a85 100644 --- a/packages/database/src/Builder/ModelInspector.php +++ b/packages/database/src/Builder/ModelInspector.php @@ -23,8 +23,8 @@ use Tempest\Validation\SkipValidation; use Tempest\Validation\Validator; +use function Tempest\Container\get; use function Tempest\Database\inspect; -use function Tempest\get; use function Tempest\Support\arr; use function Tempest\Support\str; diff --git a/packages/database/src/Builder/QueryBuilders/InsertQueryBuilder.php b/packages/database/src/Builder/QueryBuilders/InsertQueryBuilder.php index cde01ae3e7..3710eae0e2 100644 --- a/packages/database/src/Builder/QueryBuilders/InsertQueryBuilder.php +++ b/packages/database/src/Builder/QueryBuilders/InsertQueryBuilder.php @@ -24,8 +24,8 @@ use Tempest\Support\Random; use Tempest\Support\Str\ImmutableString; +use function Tempest\Container\get; use function Tempest\Database\inspect; -use function Tempest\get; use function Tempest\Support\str; /** diff --git a/packages/database/src/Builder/QueryBuilders/QueryBuilder.php b/packages/database/src/Builder/QueryBuilders/QueryBuilder.php index 079ca098c6..9394d6f87b 100644 --- a/packages/database/src/Builder/QueryBuilders/QueryBuilder.php +++ b/packages/database/src/Builder/QueryBuilders/QueryBuilder.php @@ -6,9 +6,9 @@ use Tempest\Database\PrimaryKey; use Tempest\Mapper\SerializerFactory; +use function Tempest\Container\get; use function Tempest\Database\inspect; use function Tempest\Database\query; -use function Tempest\get; use function Tempest\Mapper\make; use function Tempest\Support\arr; diff --git a/packages/database/src/Builder/QueryBuilders/SelectQueryBuilder.php b/packages/database/src/Builder/QueryBuilders/SelectQueryBuilder.php index d563d74f98..b348653c55 100644 --- a/packages/database/src/Builder/QueryBuilders/SelectQueryBuilder.php +++ b/packages/database/src/Builder/QueryBuilders/SelectQueryBuilder.php @@ -29,8 +29,8 @@ use Tempest\Support\Paginator\Paginator; use Tempest\Support\Str\ImmutableString; +use function Tempest\Container\get; use function Tempest\Database\inspect; -use function Tempest\get; use function Tempest\Mapper\map; /** diff --git a/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php b/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php index 795184ac2d..b0ebd8fd46 100644 --- a/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php +++ b/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php @@ -24,8 +24,8 @@ use Tempest\Support\Conditions\HasConditions; use Tempest\Support\Str\ImmutableString; +use function Tempest\Container\get; use function Tempest\Database\inspect; -use function Tempest\get; /** * @template TModel of object diff --git a/packages/database/src/Migrations/MigrationManager.php b/packages/database/src/Migrations/MigrationManager.php index 1e1b43b626..530ac36d6c 100644 --- a/packages/database/src/Migrations/MigrationManager.php +++ b/packages/database/src/Migrations/MigrationManager.php @@ -24,7 +24,7 @@ use function Tempest\Database\inspect; use function Tempest\Database\query; -use function Tempest\event; +use function Tempest\EventBus\event; final class MigrationManager { diff --git a/packages/database/src/Query.php b/packages/database/src/Query.php index 73bac2bfab..958ee743de 100644 --- a/packages/database/src/Query.php +++ b/packages/database/src/Query.php @@ -7,7 +7,7 @@ use Tempest\Database\Config\DatabaseDialect; use Tempest\Support\Str\ImmutableString; -use function Tempest\get; +use function Tempest\Container\get; /** * A database query that can be executed. diff --git a/packages/database/src/functions.php b/packages/database/src/functions.php index af93d2bc90..c91b2ff343 100644 --- a/packages/database/src/functions.php +++ b/packages/database/src/functions.php @@ -1,31 +1,31 @@ |string|TModel $model - * @return QueryBuilder - */ - function query(string|object $model): QueryBuilder - { - return new QueryBuilder($model); - } +use Tempest\Database\Builder\ModelInspector; +use Tempest\Database\Builder\QueryBuilders\QueryBuilder; - /** - * Inspects the given model or table name to provide database insights. - * - * @template TModel of object - * @param class-string|string|TModel $model - * @return ModelInspector - * @internal - */ - function inspect(string|object $model): ModelInspector - { - return ModelInspector::forModel($model); - } +/** + * Creates a new query builder instance for the given model or table name. + * + * @template TModel of object + * @param class-string|string|TModel $model + * @return QueryBuilder + */ +function query(string|object $model): QueryBuilder +{ + return new QueryBuilder($model); +} + +/** + * Inspects the given model or table name to provide database insights. + * + * @template TModel of object + * @param class-string|string|TModel $model + * @return ModelInspector + * @internal + */ +function inspect(string|object $model): ModelInspector +{ + return ModelInspector::forModel($model); } diff --git a/packages/datetime/src/functions.php b/packages/datetime/src/functions.php index 8d97238d01..0c4a9fc044 100644 --- a/packages/datetime/src/functions.php +++ b/packages/datetime/src/functions.php @@ -1,293 +1,293 @@ getSeconds(); + $nanoseconds = $timestamp->getNanoseconds(); - $seconds = $timestamp->getSeconds(); - $nanoseconds = $timestamp->getNanoseconds(); + // Intl formatter cannot handle nanoseconds and microseconds, do it manually instead. + $fraction = substr((string) $nanoseconds, 0, $secondsStyle->value); - // Intl formatter cannot handle nanoseconds and microseconds, do it manually instead. - $fraction = substr((string) $nanoseconds, 0, $secondsStyle->value); + if ($fraction !== '') { + $fraction = '.' . $fraction; + } - if ($fraction !== '') { - $fraction = '.' . $fraction; - } + $pattern = match ($useZ) { + true => "yyyy-MM-dd'T'HH:mm:ss@ZZZZZ", + false => "yyyy-MM-dd'T'HH:mm:ss@xxx", + }; - $pattern = match ($useZ) { - true => "yyyy-MM-dd'T'HH:mm:ss@ZZZZZ", - false => "yyyy-MM-dd'T'HH:mm:ss@xxx", - }; + $formatter = namespace\create_intl_date_formatter( + pattern: $pattern, + timezone: $timezone, + ); - $formatter = namespace\create_intl_date_formatter( - pattern: $pattern, - timezone: $timezone, - ); + $rfcString = $formatter->format($seconds); - $rfcString = $formatter->format($seconds); + return str_replace('@', $fraction, $rfcString); +} - return str_replace('@', $fraction, $rfcString); +/** + * @internal + */ +function create_intl_date_formatter( + ?DateStyle $dateStyle = null, + ?TimeStyle $timeStyle = null, + null|FormatPattern|string $pattern = null, + ?Timezone $timezone = null, + ?Locale $locale = null, +): IntlDateFormatter { + if ($pattern instanceof FormatPattern) { + $pattern = $pattern->value; } + $dateStyle ??= DateStyle::default(); + $timeStyle ??= TimeStyle::default(); + $locale ??= current_locale(); + $timezone ??= Timezone::default(); + + return new IntlDateFormatter( + locale: $locale->value, + dateType: match ($dateStyle) { + DateStyle::NONE => IntlDateFormatter::NONE, + DateStyle::SHORT => IntlDateFormatter::SHORT, + DateStyle::MEDIUM => IntlDateFormatter::MEDIUM, + DateStyle::LONG => IntlDateFormatter::LONG, + DateStyle::FULL => IntlDateFormatter::FULL, + DateStyle::RELATIVE_SHORT => IntlDateFormatter::RELATIVE_SHORT, + DateStyle::RELATIVE_MEDIUM => IntlDateFormatter::RELATIVE_MEDIUM, + DateStyle::RELATIVE_LONG => IntlDateFormatter::RELATIVE_LONG, + DateStyle::RELATIVE_FULL => IntlDateFormatter::RELATIVE_FULL, + }, + timeType: match ($timeStyle) { + TimeStyle::NONE => IntlDateFormatter::NONE, + TimeStyle::SHORT => IntlDateFormatter::SHORT, + TimeStyle::MEDIUM => IntlDateFormatter::MEDIUM, + TimeStyle::LONG => IntlDateFormatter::LONG, + TimeStyle::FULL => IntlDateFormatter::FULL, + }, + timezone: namespace\to_intl_timezone($timezone), + calendar: IntlDateFormatter::GREGORIAN, + pattern: $pattern, + ); +} + +/** + * @internal + */ +function default_timezone(): Timezone +{ /** - * @internal + * `date_default_timezone_get` function might return any of the "Others" timezones + * mentioned in PHP doc: https://www.php.net/manual/en/timezones.others.php. + * + * Those timezones are not supported by Tempest (aside from UTC), as they are considered "legacy". */ - function create_intl_date_formatter( - ?DateStyle $dateStyle = null, - ?TimeStyle $timeStyle = null, - null|FormatPattern|string $pattern = null, - ?Timezone $timezone = null, - ?Locale $locale = null, - ): IntlDateFormatter { - if ($pattern instanceof FormatPattern) { - $pattern = $pattern->value; - } + $timezoneId = date_default_timezone_get(); - $dateStyle ??= DateStyle::default(); - $timeStyle ??= TimeStyle::default(); - $locale ??= current_locale(); - $timezone ??= Timezone::default(); - - return new IntlDateFormatter( - locale: $locale->value, - dateType: match ($dateStyle) { - DateStyle::NONE => IntlDateFormatter::NONE, - DateStyle::SHORT => IntlDateFormatter::SHORT, - DateStyle::MEDIUM => IntlDateFormatter::MEDIUM, - DateStyle::LONG => IntlDateFormatter::LONG, - DateStyle::FULL => IntlDateFormatter::FULL, - DateStyle::RELATIVE_SHORT => IntlDateFormatter::RELATIVE_SHORT, - DateStyle::RELATIVE_MEDIUM => IntlDateFormatter::RELATIVE_MEDIUM, - DateStyle::RELATIVE_LONG => IntlDateFormatter::RELATIVE_LONG, - DateStyle::RELATIVE_FULL => IntlDateFormatter::RELATIVE_FULL, - }, - timeType: match ($timeStyle) { - TimeStyle::NONE => IntlDateFormatter::NONE, - TimeStyle::SHORT => IntlDateFormatter::SHORT, - TimeStyle::MEDIUM => IntlDateFormatter::MEDIUM, - TimeStyle::LONG => IntlDateFormatter::LONG, - TimeStyle::FULL => IntlDateFormatter::FULL, - }, - timezone: namespace\to_intl_timezone($timezone), - calendar: IntlDateFormatter::GREGORIAN, - pattern: $pattern, - ); - } + return Timezone::tryFrom($timezoneId) ?? Timezone::UTC; +} +/** + * @return array{int, int} + * + * @internal + */ +function high_resolution_time(): array +{ /** - * @internal + * @var null|array{int, int} $offset */ - function default_timezone(): Timezone - { - /** - * `date_default_timezone_get` function might return any of the "Others" timezones - * mentioned in PHP doc: https://www.php.net/manual/en/timezones.others.php. - * - * Those timezones are not supported by Tempest (aside from UTC), as they are considered "legacy". - */ - $timezoneId = date_default_timezone_get(); - - return Timezone::tryFrom($timezoneId) ?? Timezone::UTC; - } + static $offset = null; - /** - * @return array{int, int} - * - * @internal - */ - function high_resolution_time(): array - { - /** - * @var null|array{int, int} $offset - */ - static $offset = null; - - if ($offset === null) { - $offset = hrtime(); - - if ($offset === false) { // @phpstan-ignore-line identical.alwaysFalse - throw new \RuntimeException('The system does not provide a monotonic timer.'); - } - - $time = system_time(); - - $offset = [ - $time[0] - $offset[0], - $time[1] - $offset[1], - ]; + if ($offset === null) { + $offset = hrtime(); + + if ($offset === false) { // @phpstan-ignore-line identical.alwaysFalse + throw new \RuntimeException('The system does not provide a monotonic timer.'); } - [$secondsOffset, $nanosecondsOffset] = $offset; - [$seconds, $nanoseconds] = hrtime(); + $time = system_time(); - $nanosecondsAdjusted = $nanoseconds + $nanosecondsOffset; + $offset = [ + $time[0] - $offset[0], + $time[1] - $offset[1], + ]; + } - if ($nanosecondsAdjusted >= NANOSECONDS_PER_SECOND) { - ++$seconds; - $nanosecondsAdjusted -= NANOSECONDS_PER_SECOND; - } elseif ($nanosecondsAdjusted < 0) { - --$seconds; - $nanosecondsAdjusted += NANOSECONDS_PER_SECOND; - } + [$secondsOffset, $nanosecondsOffset] = $offset; + [$seconds, $nanoseconds] = hrtime(); - $seconds += $secondsOffset; - $nanoseconds = $nanosecondsAdjusted; + $nanosecondsAdjusted = $nanoseconds + $nanosecondsOffset; - return [$seconds, $nanoseconds]; + if ($nanosecondsAdjusted >= NANOSECONDS_PER_SECOND) { + ++$seconds; + $nanosecondsAdjusted -= NANOSECONDS_PER_SECOND; + } elseif ($nanosecondsAdjusted < 0) { + --$seconds; + $nanosecondsAdjusted += NANOSECONDS_PER_SECOND; } - /** - * @internal - */ - function intl_parse( - string $rawString, - ?DateStyle $dateStyle = null, - ?TimeStyle $timeStyle = null, - null|FormatPattern|string $pattern = null, - ?Timezone $timezone = null, - ?Locale $locale = null, - ): int { - $formatter = namespace\create_intl_date_formatter($dateStyle, $timeStyle, $pattern, $timezone, $locale); - - $timestamp = $formatter->parse($rawString); - - if ($timestamp === false) { - // Only show pattern in the exception if it was provided. - if (null !== $pattern) { - $formatter_pattern = $pattern instanceof FormatPattern ? $pattern->value : $pattern; - - throw new ParserException(sprintf( - "Unable to interpret '%s' as a valid date/time using pattern '%s'.", - $rawString, - $formatter_pattern, - )); - } - - throw new ParserException("Unable to interpret '{$rawString}' as a valid date/time."); + $seconds += $secondsOffset; + $nanoseconds = $nanosecondsAdjusted; + + return [$seconds, $nanoseconds]; +} + +/** + * @internal + */ +function intl_parse( + string $rawString, + ?DateStyle $dateStyle = null, + ?TimeStyle $timeStyle = null, + null|FormatPattern|string $pattern = null, + ?Timezone $timezone = null, + ?Locale $locale = null, +): int { + $formatter = namespace\create_intl_date_formatter($dateStyle, $timeStyle, $pattern, $timezone, $locale); + + $timestamp = $formatter->parse($rawString); + + if ($timestamp === false) { + // Only show pattern in the exception if it was provided. + if (null !== $pattern) { + $formatter_pattern = $pattern instanceof FormatPattern ? $pattern->value : $pattern; + + throw new ParserException(sprintf( + "Unable to interpret '%s' as a valid date/time using pattern '%s'.", + $rawString, + $formatter_pattern, + )); } - return (int) $timestamp; + throw new ParserException("Unable to interpret '{$rawString}' as a valid date/time."); } - /** - * @return array{int, int} - * - * @internal - */ - function system_time(): array - { - $time = microtime(); + return (int) $timestamp; +} - $parts = explode(' ', $time); - $seconds = (int) $parts[1]; - $nanoseconds = (int) ((float) $parts[0] * (float) NANOSECONDS_PER_SECOND); +/** + * @return array{int, int} + * + * @internal + */ +function system_time(): array +{ + $time = microtime(); - return [$seconds, $nanoseconds]; - } + $parts = explode(' ', $time); + $seconds = (int) $parts[1]; + $nanoseconds = (int) ((float) $parts[0] * (float) NANOSECONDS_PER_SECOND); - /** - * @internal - */ - function to_intl_timezone(Timezone $timezone): IntlTimeZone - { - $value = $timezone->value; + return [$seconds, $nanoseconds]; +} - if (str_starts_with($value, '+') || str_starts_with($value, '-')) { - $value = 'GMT' . $value; - } +/** + * @internal + */ +function to_intl_timezone(Timezone $timezone): IntlTimeZone +{ + $value = $timezone->value; - $tz = IntlTimeZone::createTimeZone($value); + if (str_starts_with($value, '+') || str_starts_with($value, '-')) { + $value = 'GMT' . $value; + } - if ($tz === null) { // @phpstan-ignore-line identical.alwaysFalse - throw new \RuntimeException(sprintf( - 'Failed to create intl timezone from timezone "%s" ("%s" / "%s").', - $timezone->name, - $timezone->value, - $value, - )); - } + $tz = IntlTimeZone::createTimeZone($value); - if ($tz->getID() === 'Etc/Unknown' && $tz->getRawOffset() === 0) { - throw new \RuntimeException(sprintf( - 'Failed to create a valid intl timezone, unknown timezone "%s" ("%s" / "%s") given.', - $timezone->name, - $timezone->value, - $value, - )); - } + if ($tz === null) { // @phpstan-ignore-line identical.alwaysFalse + throw new \RuntimeException(sprintf( + 'Failed to create intl timezone from timezone "%s" ("%s" / "%s").', + $timezone->name, + $timezone->value, + $value, + )); + } - return $tz; + if ($tz->getID() === 'Etc/Unknown' && $tz->getRawOffset() === 0) { + throw new \RuntimeException(sprintf( + 'Failed to create a valid intl timezone, unknown timezone "%s" ("%s" / "%s") given.', + $timezone->name, + $timezone->value, + $value, + )); } + return $tz; +} + +/** + * @internal + */ +function create_intl_calendar_from_date_time( + Timezone $timezone, + int $year, + int $month, + int $day, + int $hours, + int $minutes, + int $seconds, +): IntlCalendar { /** - * @internal + * @var IntlCalendar $calendar */ - function create_intl_calendar_from_date_time( - Timezone $timezone, - int $year, - int $month, - int $day, - int $hours, - int $minutes, - int $seconds, - ): IntlCalendar { - /** - * @var IntlCalendar $calendar - */ - $calendar = IntlCalendar::createInstance(to_intl_timezone($timezone)); - - $calendar->setDateTime($year, $month - 1, $day, $hours, $minutes, $seconds); - - return $calendar; - } + $calendar = IntlCalendar::createInstance(to_intl_timezone($timezone)); + + $calendar->setDateTime($year, $month - 1, $day, $hours, $minutes, $seconds); + + return $calendar; } diff --git a/packages/debug/src/functions.php b/packages/debug/src/functions.php index c390d290de..3aa00a0cdd 100644 --- a/packages/debug/src/functions.php +++ b/packages/debug/src/functions.php @@ -2,75 +2,73 @@ declare(strict_types=1); -namespace { - use Tempest\Debug\Debug; +use Tempest\Debug\Debug; - if (! function_exists('lw')) { - /** - * Writes the given `$input` to the logs, and dumps it. - * @see \Tempest\Debug\Debug::log() - */ - function lw(mixed ...$input): void - { - Debug::resolve()->log($input); - } +if (! function_exists('lw')) { + /** + * Writes the given `$input` to the logs, and dumps it. + * @see \Tempest\Debug\Debug::log() + */ + function lw(mixed ...$input): void + { + Debug::resolve()->log($input); } +} - if (! function_exists('ld')) { - /** - * Writes the given `$input` to the logs, dumps it, and stops the execution of the script. - * @see \Tempest\Debug\Debug::log() - */ - function ld(mixed ...$input): never - { - Debug::resolve()->log($input); - die(); - } +if (! function_exists('ld')) { + /** + * Writes the given `$input` to the logs, dumps it, and stops the execution of the script. + * @see \Tempest\Debug\Debug::log() + */ + function ld(mixed ...$input): never + { + Debug::resolve()->log($input); + die(); } +} - if (! function_exists('ll')) { - /** - * Writes the given `$input` to the logs. - * @see \Tempest\Debug\Debug::log() - */ - function ll(mixed ...$input): void - { - Debug::resolve()->log($input, writeToOut: false); - } +if (! function_exists('ll')) { + /** + * Writes the given `$input` to the logs. + * @see \Tempest\Debug\Debug::log() + */ + function ll(mixed ...$input): void + { + Debug::resolve()->log($input, writeToOut: false); } +} - if (! function_exists('le')) { - /** - * Emits a `ItemsDebugged` event. - * @see \Tempest\Debug\Debug::log() - */ - function le(mixed ...$input): void - { - Debug::resolve()->log($input, writeToOut: false, writeToLog: false); - } +if (! function_exists('le')) { + /** + * Emits a `ItemsDebugged` event. + * @see \Tempest\Debug\Debug::log() + */ + function le(mixed ...$input): void + { + Debug::resolve()->log($input, writeToOut: false, writeToLog: false); } +} - if (! function_exists('dd')) { - /** - * Writes the given `$input` to the logs, dumps it, and stops the execution of the script. - * @see ld() - * @see \Tempest\Debug\Debug::log() - */ - function dd(mixed ...$input): never - { - ld(...$input); - } +if (! function_exists('dd')) { + /** + * Writes the given `$input` to the logs, dumps it, and stops the execution of the script. + * @see ld() + * @see \Tempest\Debug\Debug::log() + */ + function dd(mixed ...$input): never + { + ld(...$input); } +} - if (! function_exists('dump')) { - /** - * Writes the given `$input` to the logs, and dumps it. - * @see lw() - * @see \Tempest\Debug\Debug::log() - */ - function dump(mixed ...$input): void - { - lw(...$input); - } +if (! function_exists('dump')) { + /** + * Writes the given `$input` to the logs, and dumps it. + * @see lw() + * @see \Tempest\Debug\Debug::log() + */ + function dump(mixed ...$input): void + { + lw(...$input); } } diff --git a/packages/event-bus/src/GenericEventBus.php b/packages/event-bus/src/GenericEventBus.php index 5212f0bc1d..28499f1558 100644 --- a/packages/event-bus/src/GenericEventBus.php +++ b/packages/event-bus/src/GenericEventBus.php @@ -9,7 +9,7 @@ use Tempest\Support\Str; use UnitEnum; -use function Tempest\reflect; +use function Tempest\Reflection\reflect; final readonly class GenericEventBus implements EventBus { diff --git a/packages/event-bus/src/functions.php b/packages/event-bus/src/functions.php index 4b49355f17..3869d85c16 100644 --- a/packages/event-bus/src/functions.php +++ b/packages/event-bus/src/functions.php @@ -2,28 +2,29 @@ declare(strict_types=1); -namespace Tempest { - use Closure; - use Tempest\EventBus\EventBus; - use Tempest\EventBus\EventBusConfig; +namespace Tempest\EventBus; - /** - * Dispatches the given `$event`, triggering all associated event listeners. - */ - function event(string|object $event): void - { - $eventBus = get(EventBus::class); +use Closure; +use Tempest\Container; +use Tempest\EventBus\EventBus; +use Tempest\EventBus\EventBusConfig; - $eventBus->dispatch($event); - } +/** + * Dispatches the given `$event`, triggering all associated event listeners. + */ +function event(string|object $event): void +{ + $eventBus = Container\get(EventBus::class); - /** - * Registers a closure-based event listener for the given `$event`. - */ - function listen(Closure $handler, ?string $event = null): void - { - $config = get(EventBusConfig::class); + $eventBus->dispatch($event); +} + +/** + * Registers a closure-based event listener for the given `$event`. + */ +function listen(Closure $handler, ?string $event = null): void +{ + $config = Container\get(EventBusConfig::class); - $config->addClosureHandler($handler, $event); - } + $config->addClosureHandler($handler, $event); } diff --git a/packages/event-bus/tests/EventBusTest.php b/packages/event-bus/tests/EventBusTest.php index 0a8c5c041c..bce5c7621f 100644 --- a/packages/event-bus/tests/EventBusTest.php +++ b/packages/event-bus/tests/EventBusTest.php @@ -22,8 +22,8 @@ use Tempest\EventBus\Tests\Fixtures\MyService; use Tempest\Reflection\MethodReflector; -use function Tempest\get; -use function Tempest\listen; +use function Tempest\Container\get; +use function Tempest\EventBus\listen; /** * @internal diff --git a/packages/http/src/IsRequest.php b/packages/http/src/IsRequest.php index a9cc811da8..1ee0020fb6 100644 --- a/packages/http/src/IsRequest.php +++ b/packages/http/src/IsRequest.php @@ -8,7 +8,7 @@ use Tempest\Http\Session\Session; use Tempest\Validation\SkipValidation; -use function Tempest\get; +use function Tempest\Container\get; use function Tempest\Support\Arr\get_by_key; use function Tempest\Support\Arr\has_key; use function Tempest\Support\str; diff --git a/packages/http/src/IsResponse.php b/packages/http/src/IsResponse.php index 105206e2c3..0eda90fb79 100644 --- a/packages/http/src/IsResponse.php +++ b/packages/http/src/IsResponse.php @@ -12,7 +12,7 @@ use Tempest\View\View; use UnitEnum; -use function Tempest\get; +use function Tempest\Container\get; /** @phpstan-require-implements \Tempest\Http\Response */ trait IsResponse diff --git a/packages/http/src/Responses/Back.php b/packages/http/src/Responses/Back.php index e26ef8bb01..db427bad9a 100644 --- a/packages/http/src/Responses/Back.php +++ b/packages/http/src/Responses/Back.php @@ -10,7 +10,7 @@ use Tempest\Http\Session\PreviousUrl; use Tempest\Http\Status; -use function Tempest\get; +use function Tempest\Container\get; /** * This response is not fit for stateless requests. diff --git a/packages/http/src/Session/Managers/DatabaseSessionManager.php b/packages/http/src/Session/Managers/DatabaseSessionManager.php index 273c795146..788eaf5643 100644 --- a/packages/http/src/Session/Managers/DatabaseSessionManager.php +++ b/packages/http/src/Session/Managers/DatabaseSessionManager.php @@ -14,7 +14,7 @@ use Tempest\Http\Session\SessionManager; use function Tempest\Database\query; -use function Tempest\event; +use function Tempest\EventBus\event; final readonly class DatabaseSessionManager implements SessionManager { diff --git a/packages/http/src/Session/Managers/FileSessionManager.php b/packages/http/src/Session/Managers/FileSessionManager.php index 5933359ca2..af332393a6 100644 --- a/packages/http/src/Session/Managers/FileSessionManager.php +++ b/packages/http/src/Session/Managers/FileSessionManager.php @@ -14,7 +14,7 @@ use Tempest\Support\Filesystem; use Throwable; -use function Tempest\event; +use function Tempest\EventBus\event; use function Tempest\internal_storage_path; final readonly class FileSessionManager implements SessionManager diff --git a/packages/http/src/Session/Managers/RedisSessionManager.php b/packages/http/src/Session/Managers/RedisSessionManager.php index c9344802c4..a2450ff4cc 100644 --- a/packages/http/src/Session/Managers/RedisSessionManager.php +++ b/packages/http/src/Session/Managers/RedisSessionManager.php @@ -15,7 +15,7 @@ use Tempest\Support\Str; use Throwable; -use function Tempest\event; +use function Tempest\EventBus\event; final readonly class RedisSessionManager implements SessionManager { diff --git a/packages/icon/src/functions.php b/packages/icon/src/functions.php index 90e004c047..bf26b93859 100644 --- a/packages/icon/src/functions.php +++ b/packages/icon/src/functions.php @@ -2,7 +2,7 @@ namespace Tempest\Icon; -use function Tempest\get; +use function Tempest\Container\get; /** * Renders an icon as an SVG snippet. If the icon is not cached, it will be diff --git a/packages/intl/bin/plural-rules.php b/packages/intl/bin/plural-rules.php index f413c49043..19de138307 100755 --- a/packages/intl/bin/plural-rules.php +++ b/packages/intl/bin/plural-rules.php @@ -13,7 +13,7 @@ use Tempest\Support\Filesystem; use Tempest\Support\Json; -use function Tempest\get; +use function Tempest\Container\get; final class PluralRulesMatcherGenerator { diff --git a/packages/intl/src/functions.php b/packages/intl/src/functions.php index a37226b75d..768d7ce0da 100644 --- a/packages/intl/src/functions.php +++ b/packages/intl/src/functions.php @@ -9,7 +9,7 @@ use Tempest\Intl\Pluralizer\Pluralizer; use Tempest\Intl\Translator; -use function Tempest\get; +use function Tempest\Container\get; /** * Translates the given key with optional arguments. diff --git a/packages/mail/src/Testing/TestingMailer.php b/packages/mail/src/Testing/TestingMailer.php index d51b92da3f..82ecdf3196 100644 --- a/packages/mail/src/Testing/TestingMailer.php +++ b/packages/mail/src/Testing/TestingMailer.php @@ -7,7 +7,7 @@ use Tempest\Mail\EmailWasSent; use Tempest\Mail\Mailer; -use function Tempest\get; +use function Tempest\Container\get; final class TestingMailer implements Mailer { diff --git a/packages/mapper/src/functions.php b/packages/mapper/src/functions.php index 42b8f359ba..59aeb2f297 100644 --- a/packages/mapper/src/functions.php +++ b/packages/mapper/src/functions.php @@ -4,7 +4,7 @@ namespace Tempest\Mapper; -use Tempest; +use Tempest\Container; use Tempest\Mapper\ObjectFactory; /** @@ -24,7 +24,7 @@ */ function make(object|string $objectOrClass): ObjectFactory { - return Tempest\get(ObjectFactory::class)->forClass($objectOrClass); + return Container\get(ObjectFactory::class)->forClass($objectOrClass); } /** @@ -40,5 +40,5 @@ function make(object|string $objectOrClass): ObjectFactory */ function map(mixed $data): ObjectFactory { - return Tempest\get(ObjectFactory::class)->withData($data); + return Container\get(ObjectFactory::class)->withData($data); } diff --git a/packages/reflection/src/functions.php b/packages/reflection/src/functions.php index e663ed9e8f..20f07a96eb 100644 --- a/packages/reflection/src/functions.php +++ b/packages/reflection/src/functions.php @@ -2,29 +2,29 @@ declare(strict_types=1); -namespace Tempest { - use ReflectionClass as PHPReflectionClass; - use ReflectionProperty as PHPReflectionProperty; - use Tempest\Reflection\ClassReflector; - use Tempest\Reflection\PropertyReflector; +namespace Tempest\Reflection; - /** - * Creates a new {@see Reflector} instance based on the given `$classOrProperty`. - */ - function reflect(mixed $classOrProperty, ?string $propertyName = null): ClassReflector|PropertyReflector - { - if ($classOrProperty instanceof PHPReflectionClass) { - return new ClassReflector($classOrProperty); - } +use ReflectionClass as PHPReflectionClass; +use ReflectionProperty as PHPReflectionProperty; +use Tempest\Reflection\ClassReflector; +use Tempest\Reflection\PropertyReflector; - if ($classOrProperty instanceof PHPReflectionProperty) { - return new PropertyReflector($classOrProperty); - } +/** + * Creates a new {@see Reflector} instance based on the given `$classOrProperty`. + */ +function reflect(mixed $classOrProperty, ?string $propertyName = null): ClassReflector|PropertyReflector +{ + if ($classOrProperty instanceof PHPReflectionClass) { + return new ClassReflector($classOrProperty); + } - if ($propertyName !== null) { - return new PropertyReflector(new PHPReflectionProperty($classOrProperty, $propertyName)); - } + if ($classOrProperty instanceof PHPReflectionProperty) { + return new PropertyReflector($classOrProperty); + } - return new ClassReflector($classOrProperty); + if ($propertyName !== null) { + return new PropertyReflector(new PHPReflectionProperty($classOrProperty, $propertyName)); } + + return new ClassReflector($classOrProperty); } diff --git a/packages/router/src/functions.php b/packages/router/src/functions.php index 33848a8aaa..083c5f8379 100644 --- a/packages/router/src/functions.php +++ b/packages/router/src/functions.php @@ -9,7 +9,7 @@ use Tempest\Reflection\MethodReflector; use Tempest\Router\UriGenerator; -use function Tempest\get; +use function Tempest\Container\get; /** * Creates a valid URI to the given `$action`. diff --git a/packages/storage/src/Config/CustomStorageConfig.php b/packages/storage/src/Config/CustomStorageConfig.php index bf735b7c1c..a4bb6d64f0 100644 --- a/packages/storage/src/Config/CustomStorageConfig.php +++ b/packages/storage/src/Config/CustomStorageConfig.php @@ -5,7 +5,7 @@ use League\Flysystem\FilesystemAdapter; use UnitEnum; -use function Tempest\get; +use function Tempest\Container\get; final class CustomStorageConfig implements StorageConfig { diff --git a/packages/support/src/Arr/functions.php b/packages/support/src/Arr/functions.php index 3efc4cf86c..f320b1d4b5 100644 --- a/packages/support/src/Arr/functions.php +++ b/packages/support/src/Arr/functions.php @@ -2,1284 +2,1263 @@ declare(strict_types=1); -namespace Tempest\Support\Arr { - use Closure; - use Countable; - use Generator; - use InvalidArgumentException; - use LogicException; - use Random\Randomizer; - use Tempest\Mapper; - use Tempest\Support\Str\ImmutableString; - use Traversable; - - use function sort as php_sort; - - /** - * Finds a value in the array and return the corresponding key if successful. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param (Closure(TValue, TKey): bool)|mixed $value The value to search for, a {@see Closure} will find the first item that returns true. - * @param bool $strict Whether to use strict comparison. - * - * @return array-key|null The key for `$value` if found, `null` otherwise. - */ - function find_key(iterable $array, mixed $value, bool $strict = false): int|string|null - { - $array = to_array($array); - - if (! $value instanceof Closure) { - $search = array_search($value, $array, $strict); - - return $search === false ? null : $search; // Keep empty values but convert false to null - } - - return array_find_key($array, static fn ($item, $key) => $value($item, $key) === true); +namespace Tempest\Support\Arr; + +use Closure; +use Countable; +use Generator; +use InvalidArgumentException; +use LogicException; +use Random\Randomizer; +use Tempest\Mapper; +use Tempest\Support\Str\ImmutableString; +use Traversable; + +use function sort as php_sort; + +/** + * Finds a value in the array and return the corresponding key if successful. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param (Closure(TValue, TKey): bool)|mixed $value The value to search for, a {@see Closure} will find the first item that returns true. + * @param bool $strict Whether to use strict comparison. + * + * @return array-key|null The key for `$value` if found, `null` otherwise. + */ +function find_key(iterable $array, mixed $value, bool $strict = false): int|string|null +{ + $array = to_array($array); + + if (! $value instanceof Closure) { + $search = array_search($value, $array, $strict); + + return $search === false ? null : $search; // Keep empty values but convert false to null } - /** - * Chunks the array into chunks of the given size. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param int $size The size of each chunk. - * @param bool $preserveKeys Whether to preserve the keys of the original array. - * - * @return array> - */ - function chunk(iterable $array, int $size, bool $preserveKeys = true): array - { - $array = to_array($array); - - if ($size <= 0) { - return []; - } - - $chunks = []; - foreach (array_chunk($array, $size, $preserveKeys) as $chunk) { - $chunks[] = $chunk; - } + return array_find_key($array, static fn ($item, $key) => $value($item, $key) === true); +} - return $chunks; +/** + * Chunks the array into chunks of the given size. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param int $size The size of each chunk. + * @param bool $preserveKeys Whether to preserve the keys of the original array. + * + * @return array> + */ +function chunk(iterable $array, int $size, bool $preserveKeys = true): array +{ + $array = to_array($array); + + if ($size <= 0) { + return []; } - /** - * Reduces the array to a single value using a callback. - * - * @template TKey of array-key - * @template TValue - * - * @template TReduceInitial - * @template TReduceReturnType - * - * @param iterable $array - * @param callable(TReduceInitial|TReduceReturnType, TValue, TKey): TReduceReturnType $callback - * @param TReduceInitial $initial - * - * @return TReduceReturnType - */ - function reduce(iterable $array, callable $callback, mixed $initial = null): mixed - { - $array = to_array($array); - - $result = $initial; - - foreach ($array as $key => $value) { - $result = $callback($result, $value, $key); - } - - return $result; + $chunks = []; + foreach (array_chunk($array, $size, $preserveKeys) as $chunk) { + $chunks[] = $chunk; } - /** - * Gets a value by its key from the array and remove it. Mutates the array. - * - * @template TKey of array-key - * @template TValue - * - * @param array $array - * @param array-key $key - */ - function pull(array &$array, string|int $key, mixed $default = null): mixed - { - $value = get_by_key($array, $key, $default); - $array = namespace\forget_keys($array, $key); + return $chunks; +} - return $value; +/** + * Reduces the array to a single value using a callback. + * + * @template TKey of array-key + * @template TValue + * + * @template TReduceInitial + * @template TReduceReturnType + * + * @param iterable $array + * @param callable(TReduceInitial|TReduceReturnType, TValue, TKey): TReduceReturnType $callback + * @param TReduceInitial $initial + * + * @return TReduceReturnType + */ +function reduce(iterable $array, callable $callback, mixed $initial = null): mixed +{ + $array = to_array($array); + + $result = $initial; + + foreach ($array as $key => $value) { + $result = $callback($result, $value, $key); } - /** - * Shuffles the array. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @return array - */ - function shuffle(iterable $array): array - { - return new Randomizer()->shuffleArray(to_array($array)); - } + return $result; +} - /** - * Removes the specified keys from the array. The array is not mutated. - * - * @template TKey of array-key - * @template TValue - * - * @param array $array - * @param array-key|array $keys The keys of the items to remove. - * @return array - */ - function remove_keys(iterable $array, string|int|array $keys): array - { - $array = to_array($array); +/** + * Gets a value by its key from the array and remove it. Mutates the array. + * + * @template TKey of array-key + * @template TValue + * + * @param array $array + * @param array-key $key + */ +function pull(array &$array, string|int $key, mixed $default = null): mixed +{ + $value = get_by_key($array, $key, $default); + $array = namespace\forget_keys($array, $key); + + return $value; +} - return namespace\forget_keys($array, $keys); - } +/** + * Shuffles the array. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @return array + */ +function shuffle(iterable $array): array +{ + return new Randomizer()->shuffleArray(to_array($array)); +} - /** - * Removes the specified values from the array. The array is not mutated. - * - * @template TKey of array-key - * @template TValue - * - * @param array $array - * @param TValue|array $values The values to remove. - * @return array - */ - function remove_values(array $array, string|int|array $values): array - { - $array = to_array($array); +/** + * Removes the specified keys from the array. The array is not mutated. + * + * @template TKey of array-key + * @template TValue + * + * @param array $array + * @param array-key|array $keys The keys of the items to remove. + * @return array + */ +function remove_keys(iterable $array, string|int|array $keys): array +{ + $array = to_array($array); + + return namespace\forget_keys($array, $keys); +} + +/** + * Removes the specified values from the array. The array is not mutated. + * + * @template TKey of array-key + * @template TValue + * + * @param array $array + * @param TValue|array $values The values to remove. + * @return array + */ +function remove_values(array $array, string|int|array $values): array +{ + $array = to_array($array); + + return namespace\forget_values($array, $values); +} - return namespace\forget_values($array, $values); +/** + * Removes the specified keys from the array. The array is mutated. + * + * @template TKey of array-key + * @template TValue + * + * @param array $array + * @param array-key|array $keys The keys of the items to remove. + * @return array + */ +function forget_keys(array &$array, string|int|array $keys): array +{ + $keys = is_array($keys) ? $keys : [$keys]; + + foreach ($keys as $key) { + unset($array[$key]); } - /** - * Removes the specified keys from the array. The array is mutated. - * - * @template TKey of array-key - * @template TValue - * - * @param array $array - * @param array-key|array $keys The keys of the items to remove. - * @return array - */ - function forget_keys(array &$array, string|int|array $keys): array - { - $keys = is_array($keys) ? $keys : [$keys]; + return $array; +} - foreach ($keys as $key) { +/** + * Removes the specified values from the array. The array is mutated. + * + * @template TKey of array-key + * @template TValue + * + * @param array $array + * @param TValue|array $values The values to remove. + * @return array + */ +function forget_values(array &$array, string|int|array $values): array +{ + $values = is_array($values) ? $values : [$values]; + + foreach ($values as $value) { + if (! is_null($key = array_find_key($array, fn (mixed $match) => $value === $match))) { unset($array[$key]); } - - return $array; } - /** - * Removes the specified values from the array. The array is mutated. - * - * @template TKey of array-key - * @template TValue - * - * @param array $array - * @param TValue|array $values The values to remove. - * @return array - */ - function forget_values(array &$array, string|int|array $values): array - { - $values = is_array($values) ? $values : [$values]; + return $array; +} - foreach ($values as $value) { - if (! is_null($key = array_find_key($array, fn (mixed $match) => $value === $match))) { - unset($array[$key]); - } - } +/** + * Asserts whether the array is a list. + * An array is a list if its keys consist of consecutive numbers. + */ +function is_list(iterable $array): bool +{ + return array_is_list(to_array($array)); +} - return $array; - } +/** + * Asserts whether the array is a associative. + * An array is associative if its keys do not consist of consecutive numbers. + */ +function is_associative(iterable $array): bool +{ + return ! is_list(to_array($array)); +} - /** - * Asserts whether the array is a list. - * An array is a list if its keys consist of consecutive numbers. - */ - function is_list(iterable $array): bool - { - return array_is_list(to_array($array)); +/** + * Gets one or a specified number of random values from the array. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param 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. + */ +function random(iterable $array, int $number = 1, bool $preserveKey = false): mixed +{ + $array = to_array($array); + + $count = count($array); + + if ($number > $count) { + throw new InvalidArgumentException("Cannot retrieve {$number} items from an array of {$count} items."); } - /** - * Asserts whether the array is a associative. - * An array is associative if its keys do not consist of consecutive numbers. - */ - function is_associative(iterable $array): bool - { - return ! is_list(to_array($array)); + if ($number < 1) { + throw new InvalidArgumentException("Random value only accepts positive integers, {$number} requested."); } - /** - * Gets one or a specified number of random values from the array. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param 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. - */ - function random(iterable $array, int $number = 1, bool $preserveKey = false): mixed - { - $array = to_array($array); - - $count = count($array); + $keys = new Randomizer()->pickArrayKeys($array, $number); - if ($number > $count) { - throw new InvalidArgumentException("Cannot retrieve {$number} items from an array of {$count} items."); - } - - if ($number < 1) { - throw new InvalidArgumentException("Random value only accepts positive integers, {$number} requested."); - } - - $keys = new Randomizer()->pickArrayKeys($array, $number); - - $randomValues = []; - foreach ($keys as $key) { - $preserveKey - ? ($randomValues[$key] = $array[$key]) - : ($randomValues[] = $array[$key]); - } - - if ($preserveKey === false) { - shuffle($randomValues); - } - - return count($randomValues) > 1 - ? new ImmutableArray($randomValues) - : $randomValues[0]; + $randomValues = []; + foreach ($keys as $key) { + $preserveKey + ? ($randomValues[$key] = $array[$key]) + : ($randomValues[] = $array[$key]); } - /** - * 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. - * - * @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. - */ - function pluck(iterable $array, string $value, ?string $key = null): array - { - $array = to_array($array); - - $results = []; - - foreach ($array as $item) { - if (! is_array($item)) { - continue; - } - - $itemValue = get_by_key($item, $value); - - /** - * Perform basic pluck if no key is given. - * Otherwise, also pluck the key as well. - */ - if (is_null($key)) { - $results[] = $itemValue; - } else { - $itemKey = get_by_key($item, $key); - $results[$itemKey] = $itemValue; - } - } - - return $results; + if ($preserveKey === false) { + shuffle($randomValues); } - /** - * Returns a new array with the specified values prepended. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param TValue $values - */ - function prepend(iterable $array, mixed ...$values): array - { - $array = to_array($array); + return count($randomValues) > 1 + ? new ImmutableArray($randomValues) + : $randomValues[0]; +} - foreach (array_reverse($values) as $value) { - $array = [$value, ...$array]; +/** + * 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. + * + * @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. + */ +function pluck(iterable $array, string $value, ?string $key = null): array +{ + $array = to_array($array); + + $results = []; + + foreach ($array as $item) { + if (! is_array($item)) { + continue; } - return $array; - } + $itemValue = get_by_key($item, $value); - /** - * Appends the specified values to the array. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param TValue $values - */ - function append(iterable $array, mixed ...$values): array - { - $array = to_array($array); - - foreach ($values as $value) { - $array = [...$array, $value]; + /** + * Perform basic pluck if no key is given. + * Otherwise, also pluck the key as well. + */ + if (is_null($key)) { + $results[] = $itemValue; + } else { + $itemKey = get_by_key($item, $key); + $results[$itemKey] = $itemValue; } - - return $array; - } - - /** - * Appends the specified value to the array. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param TValue $value - */ - function push(iterable $array, mixed $value): array - { - $array = to_array($array); - $array[] = $value; - - return $array; } - /** - * Pads the array to the specified size with a value. - */ - function pad(iterable $array, int $size, mixed $value): array - { - $array = to_array($array); + return $results; +} - return array_pad($array, $size, $value); +/** + * Returns a new array with the specified values prepended. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param TValue $values + */ +function prepend(iterable $array, mixed ...$values): array +{ + $array = to_array($array); + + foreach (array_reverse($values) as $value) { + $array = [$value, ...$array]; } - /** - * Reverses the keys and values of the array. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @return array - */ - function flip(iterable $array): array - { - $array = to_array($array); + return $array; +} - return array_flip($array); +/** + * Appends the specified values to the array. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param TValue $values + */ +function append(iterable $array, mixed ...$values): array +{ + $array = to_array($array); + + foreach ($values as $value) { + $array = [...$array, $value]; } - /** - * 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. - * @param bool $shouldBeStrict Whether the comparison should be strict, only used when giving a key parameter. - */ - function unique(iterable $array, null|Closure|string $key = null, bool $shouldBeStrict = false): array - { - $array = to_array($array); - - if (is_null($key) && $shouldBeStrict === false) { - return array_unique($array, flags: SORT_REGULAR); - } - - $uniqueItems = []; - $uniqueFilteredValues = []; - - foreach ($array as $item) { - // Ensure we don't check raw values with key filter - if (! is_null($key) && ! is_array($item) && ! $key instanceof Closure) { - continue; - } - - $filterValue = match ($key instanceof Closure) { - true => $key($item, $array), - false => is_array($item) - ? get_by_key($item, $key) - : $item, - }; - - if (is_null($filterValue)) { - continue; - } - - if (in_array($filterValue, $uniqueFilteredValues, strict: $shouldBeStrict)) { - continue; - } - - $uniqueItems[] = $item; - $uniqueFilteredValues[] = $filterValue; - } + return $array; +} - return $uniqueItems; - } +/** + * Appends the specified value to the array. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param TValue $value + */ +function push(iterable $array, mixed $value): array +{ + $array = to_array($array); + $array[] = $value; + + return $array; +} - /** - * Returns a copy of the given array with only the items that are not present in any of the given arrays. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param array ...$arrays - */ - function diff(iterable $array, array ...$arrays): array - { - return array_diff(to_array($array), ...$arrays); - } +/** + * Pads the array to the specified size with a value. + */ +function pad(iterable $array, int $size, mixed $value): array +{ + $array = to_array($array); - /** - * Returns a new array with only the items whose keys are not present in any of the given arrays. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param array ...$arrays - */ - function diff_keys(iterable $array, array ...$arrays): array - { - return array_diff_key(to_array($array), ...$arrays); - } + return array_pad($array, $size, $value); +} - /** - * Returns a copy of the given array with only the items that are present in all of the given arrays. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param array ...$arrays - */ - function intersect(iterable $array, array ...$arrays): array - { - return array_intersect(to_array($array), ...$arrays); - } +/** + * Reverses the keys and values of the array. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @return array + */ +function flip(iterable $array): array +{ + $array = to_array($array); + + return array_flip($array); +} - /** - * Returns a copy of the given array with only the items whose keys are present in all of the given arrays. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param array ...$arrays - */ - function intersect_keys(iterable $array, array ...$arrays): array - { - return array_intersect_key(to_array($array), ...$arrays); +/** + * 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. + * @param bool $shouldBeStrict Whether the comparison should be strict, only used when giving a key parameter. + */ +function unique(iterable $array, null|Closure|string $key = null, bool $shouldBeStrict = false): array +{ + $array = to_array($array); + + if (is_null($key) && $shouldBeStrict === false) { + return array_unique($array, flags: SORT_REGULAR); } - /** - * Merges the array with the given arrays. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param array ...$arrays The arrays to merge. - */ - function merge(iterable $array, iterable ...$arrays): array - { - return array_merge(to_array($array), ...array_map(to_array(...), $arrays)); - } + $uniqueItems = []; + $uniqueFilteredValues = []; - /** - * Creates a new array with this current array values as keys and the given values as values. - * - * @template TCombineValue - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param iterable $values - * - * @return array - */ - function combine(iterable $array, iterable $values): array - { - $array = to_array($array); - $values = to_array($values); - - if (count($array) !== count($values)) { - throw new InvalidArgumentException( - sprintf('Cannot combine arrays of different lengths (%d keys vs %d values)', count($array), count($values)), - ); + foreach ($array as $item) { + // Ensure we don't check raw values with key filter + if (! is_null($key) && ! is_array($item) && ! $key instanceof Closure) { + continue; } - return array_combine($array, $values); - } - - /** - * Asserts whether the given `$array` is equal to `$other` array. - */ - function equals(iterable $array, iterable $other): bool - { - $array = to_array($array); - $other = to_array($other); - - return $array === $other; - } - - /** - * Returns the first item in the array that matches the given `$filter`. - * If `$filter` is `null`, returns the first item. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param null|Closure(TValue $value, TKey $key): bool $filter - * - * @return TValue - */ - function first(iterable $array, ?Closure $filter = null, mixed $default = null): mixed - { - $array = to_array($array); + $filterValue = match ($key instanceof Closure) { + true => $key($item, $array), + false => is_array($item) + ? get_by_key($item, $key) + : $item, + }; - if ($array === []) { - return $default; + if (is_null($filterValue)) { + continue; } - if ($filter === null) { - return $array[array_key_first($array)] ?? $default; + if (in_array($filterValue, $uniqueFilteredValues, strict: $shouldBeStrict)) { + continue; } - return array_find($array, static fn ($value, $key) => $filter($value, $key)) ?? $default; + $uniqueItems[] = $item; + $uniqueFilteredValues[] = $filterValue; } - /** - * Returns the item at the given index in the specified array. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * - * @return TValue - */ - function at(iterable $array, int $index, mixed $default = null): mixed - { - $array = to_array($array); + return $uniqueItems; +} - if ($index < 0) { - $index = abs($index) - 1; - $array = namespace\reverse($array); - } +/** + * Returns a copy of the given array with only the items that are not present in any of the given arrays. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param array ...$arrays + */ +function diff(iterable $array, array ...$arrays): array +{ + return array_diff(to_array($array), ...$arrays); +} - return namespace\get_by_key(array_values($array), key: $index, default: $default); - } +/** + * Returns a new array with only the items whose keys are not present in any of the given arrays. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param array ...$arrays + */ +function diff_keys(iterable $array, array ...$arrays): array +{ + return array_diff_key(to_array($array), ...$arrays); +} - /** - * Returns the last item in the array that matches the given `$filter`. - * If `$filter` is `null`, returns the last item. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param null|Closure(TValue $value, TKey $key): bool $filter - * - * @return TValue - */ - function last(iterable $array, ?Closure $filter = null, mixed $default = null): mixed - { - $array = to_array($array); +/** + * Returns a copy of the given array with only the items that are present in all of the given arrays. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param array ...$arrays + */ +function intersect(iterable $array, array ...$arrays): array +{ + return array_intersect(to_array($array), ...$arrays); +} - if ($array === []) { - return $default; - } +/** + * Returns a copy of the given array with only the items whose keys are present in all of the given arrays. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param array ...$arrays + */ +function intersect_keys(iterable $array, array ...$arrays): array +{ + return array_intersect_key(to_array($array), ...$arrays); +} - if ($filter === null) { - return $array[array_key_last($array)] ?? $default; - } +/** + * Merges the array with the given arrays. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param array ...$arrays The arrays to merge. + */ +function merge(iterable $array, iterable ...$arrays): array +{ + return array_merge(to_array($array), ...array_map(to_array(...), $arrays)); +} - return array_find(namespace\reverse($array), static fn ($value, $key) => $filter($value, $key)) ?? $default; +/** + * Creates a new array with this current array values as keys and the given values as values. + * + * @template TCombineValue + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param iterable $values + * + * @return array + */ +function combine(iterable $array, iterable $values): array +{ + $array = to_array($array); + $values = to_array($values); + + if (count($array) !== count($values)) { + throw new InvalidArgumentException( + sprintf('Cannot combine arrays of different lengths (%d keys vs %d values)', count($array), count($values)), + ); } - /** - * Returns a copy of the given array without the last value. - * - * @param mixed $value The popped value will be stored in this variable. - */ - function pop(iterable $array, mixed &$value = null): array - { - $array = to_array($array); - $value = namespace\last($array); + return array_combine($array, $values); +} - return array_slice($array, 0, -1); - } +/** + * Asserts whether the given `$array` is equal to `$other` array. + */ +function equals(iterable $array, iterable $other): bool +{ + $array = to_array($array); + $other = to_array($other); - /** - * Returns a copy of the given array without the first value. - * - * @param mixed $value The unshifted value will be stored in this variable - */ - function unshift(iterable $array, mixed &$value = null): array - { - $array = to_array($array); - $value = namespace\first($array); + return $array === $other; +} - return array_slice($array, 1); +/** + * Returns the first item in the array that matches the given `$filter`. + * If `$filter` is `null`, returns the first item. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param null|Closure(TValue $value, TKey $key): bool $filter + * + * @return TValue + */ +function first(iterable $array, ?Closure $filter = null, mixed $default = null): mixed +{ + $array = to_array($array); + + if ($array === []) { + return $default; } - /** - * Returns a copy of the given array in reverse order. - */ - function reverse(iterable $array): array - { - return array_reverse(to_array($array)); + if ($filter === null) { + return $array[array_key_first($array)] ?? $default; } - /** - * Asserts whether the array is empty. - */ - function is_empty(iterable $array): bool - { - return to_array($array) === []; - } + return array_find($array, static fn ($value, $key) => $filter($value, $key)) ?? $default; +} - /** - * Returns an instance of {@see \Tempest\Support\Str\ImmutableString} with the values of the array joined with the given `$glue`. - */ - function implode(iterable $array, string $glue): ImmutableString - { - return new ImmutableString(\implode($glue, to_array($array))); +/** + * Returns the item at the given index in the specified array. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * + * @return TValue + */ +function at(iterable $array, int $index, mixed $default = null): mixed +{ + $array = to_array($array); + + if ($index < 0) { + $index = abs($index) - 1; + $array = namespace\reverse($array); } - /** - * Returns a copy of the given array with the keys of this array as values. - */ - function keys(iterable $array): array - { - return array_keys(to_array($array)); - } + return namespace\get_by_key(array_values($array), key: $index, default: $default); +} - /** - * Returns a copy of the given array without its keys. - */ - function values(iterable $array): array - { - return array_values(to_array($array)); +/** + * Returns the last item in the array that matches the given `$filter`. + * If `$filter` is `null`, returns the last item. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param null|Closure(TValue $value, TKey $key): bool $filter + * + * @return TValue + */ +function last(iterable $array, ?Closure $filter = null, mixed $default = null): mixed +{ + $array = to_array($array); + + if ($array === []) { + return $default; } - /** - * Returns a copy of the given array with only the items that pass the given `$filter`. - * If `$filter` is `null`, the new array will contain only values that are not `false` or `null`. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param null|Closure(TValue $value, TKey $key): bool $filter - */ - function filter(iterable $array, ?Closure $filter = null): array - { - $result = []; - $filter ??= static fn (mixed $value, mixed $_) => ! in_array($value, [false, null], strict: true); - - foreach (to_array($array) as $key => $value) { - if ($filter($value, $key)) { - $result[$key] = $value; - } - } - - return $result; + if ($filter === null) { + return $array[array_key_last($array)] ?? $default; } - /** - * Applies the given callback to all items of the array. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param Closure(TKey $value, TValue $key): void $each - */ - function each(iterable $array, Closure $each): array - { - $array = to_array($array); - - foreach ($array as $key => $value) { - $each($value, $key); - } + return array_find(namespace\reverse($array), static fn ($value, $key) => $filter($value, $key)) ?? $default; +} - return $array; - } +/** + * Returns a copy of the given array without the last value. + * + * @param mixed $value The popped value will be stored in this variable. + */ +function pop(iterable $array, mixed &$value = null): array +{ + $array = to_array($array); + $value = namespace\last($array); + + return array_slice($array, 0, -1); +} - /** - * Returns a copy of the given array with each item transformed by the given callback. - * - * @template TMapValue - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param Closure(TValue, TKey): TMapValue $map - * - * @return array - */ - function map_iterable(iterable $array, Closure $map): array - { - $result = []; +/** + * Returns a copy of the given array without the first value. + * + * @param mixed $value The unshifted value will be stored in this variable + */ +function unshift(iterable $array, mixed &$value = null): array +{ + $array = to_array($array); + $value = namespace\first($array); + + return array_slice($array, 1); +} - foreach (to_array($array) as $key => $value) { - $result[$key] = $map($value, $key); - } +/** + * Returns a copy of the given array in reverse order. + */ +function reverse(iterable $array): array +{ + return array_reverse(to_array($array)); +} - return $result; - } +/** + * Asserts whether the array is empty. + */ +function is_empty(iterable $array): bool +{ + return to_array($array) === []; +} - /** - * Returns a copy of the given array with each item transformed by the given callback. - * The callback must return a generator, associating a key and a value. - * - * ### Example - * ```php - * map_with_keys(['a', 'b'], fn (mixed $value, mixed $key) => yield $key => $value); - * ``` - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param Closure(TValue $value, TKey $key): Generator $map - */ - function map_with_keys(iterable $array, Closure $map): array - { - $result = []; +/** + * Returns an instance of {@see \Tempest\Support\Str\ImmutableString} with the values of the array joined with the given `$glue`. + */ +function implode(iterable $array, string $glue): ImmutableString +{ + return new ImmutableString(\implode($glue, to_array($array))); +} - foreach (to_array($array) as $key => $value) { - $generator = $map($value, $key); +/** + * Returns a copy of the given array with the keys of this array as values. + */ +function keys(iterable $array): array +{ + return array_keys(to_array($array)); +} - // @phpstan-ignore instanceof.alwaysTrue - if (! $generator instanceof Generator) { - throw new MapWithKeysDidNotUseAGenerator(); - } +/** + * Returns a copy of the given array without its keys. + */ +function values(iterable $array): array +{ + return array_values(to_array($array)); +} - $result[$generator->key()] = $generator->current(); +/** + * Returns a copy of the given array with only the items that pass the given `$filter`. + * If `$filter` is `null`, the new array will contain only values that are not `false` or `null`. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param null|Closure(TValue $value, TKey $key): bool $filter + */ +function filter(iterable $array, ?Closure $filter = null): array +{ + $result = []; + $filter ??= static fn (mixed $value, mixed $_) => ! in_array($value, [false, null], strict: true); + + foreach (to_array($array) as $key => $value) { + if ($filter($value, $key)) { + $result[$key] = $value; } - - return $result; } - /** - * Gets the value identified by the specified `$key`, or `$default` if no such value exists. - * @return mixed|ImmutableArray - */ - function get_by_key(iterable $array, int|string $key, mixed $default = null): mixed - { - $value = to_array($array); - - if (isset($value[$key])) { - return is_array($value[$key]) - ? new ImmutableArray($value[$key]) - : $value[$key]; - } + return $result; +} - $keys = is_int($key) - ? [$key] - : explode('.', $key); +/** + * Applies the given callback to all items of the array. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param Closure(TKey $value, TValue $key): void $each + */ +function each(iterable $array, Closure $each): array +{ + $array = to_array($array); + + foreach ($array as $key => $value) { + $each($value, $key); + } - foreach ($keys as $key) { - if (! is_array($value) && ! $value instanceof \ArrayAccess) { - return $default; - } + return $array; +} - if (! isset($value[$key])) { - return $default; - } +/** + * Returns a copy of the given array with each item transformed by the given callback. + * + * @template TMapValue + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param Closure(TValue, TKey): TMapValue $map + * + * @return array + */ +function map_iterable(iterable $array, Closure $map): array +{ + $result = []; + + foreach (to_array($array) as $key => $value) { + $result[$key] = $map($value, $key); + } - $value = $value[$key]; - } + return $result; +} - if (is_array($value)) { - return new ImmutableArray($value); +/** + * Returns a copy of the given array with each item transformed by the given callback. + * The callback must return a generator, associating a key and a value. + * + * ### Example + * ```php + * map_with_keys(['a', 'b'], fn (mixed $value, mixed $key) => yield $key => $value); + * ``` + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param Closure(TValue $value, TKey $key): Generator $map + */ +function map_with_keys(iterable $array, Closure $map): array +{ + $result = []; + + foreach (to_array($array) as $key => $value) { + $generator = $map($value, $key); + + // @phpstan-ignore instanceof.alwaysTrue + if (! $generator instanceof Generator) { + throw new MapWithKeysDidNotUseAGenerator(); } - return $value; + $result[$generator->key()] = $generator->current(); } - /** - * Asserts whether a value identified by the specified `$key` exists. Dot notation is supported. - */ - function has_key(iterable $array, int|string $key): bool - { - $array = to_array($array); - - if (isset($array[$key])) { - return true; - } + return $result; +} - $keys = is_int($key) - ? [$key] - : explode('.', $key); +/** + * Gets the value identified by the specified `$key`, or `$default` if no such value exists. + * @return mixed|ImmutableArray + */ +function get_by_key(iterable $array, int|string $key, mixed $default = null): mixed +{ + $value = to_array($array); + + if (isset($value[$key])) { + return is_array($value[$key]) + ? new ImmutableArray($value[$key]) + : $value[$key]; + } - foreach ($keys as $key) { - if (! isset($array[$key])) { - return false; - } + $keys = is_int($key) + ? [$key] + : explode('.', $key); - $array = &$array[$key]; + foreach ($keys as $key) { + if (! is_array($value) && ! $value instanceof \ArrayAccess) { + return $default; } - return true; - } + if (! isset($value[$key])) { + return $default; + } - /** - * Asserts whether the given array contains a value that can be identified by `$search`. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param TValue|Closure(TValue, TKey): bool $search - */ - function contains(iterable $array, mixed $search): bool - { - $array = to_array($array); - $search = $search instanceof Closure - ? $search - : static fn (mixed $value) => $value === $search; - - return array_any($array, static fn ($value, $key) => $search($value, $key)); + $value = $value[$key]; } - /** - * Asserts whether all items in the given array pass the given `$callback`. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param Closure(TValue, TKey): bool $callback - * - * @return bool If the collection is empty, returns `true`. - */ - function every(iterable $array, ?Closure $callback = null): bool - { - $array = to_array($array); - $callback ??= static fn (mixed $value) => ! is_null($value); - - return array_all($array, static fn (mixed $value, int|string $key) => $callback($value, $key)); + if (is_array($value)) { + return new ImmutableArray($value); } - /** - * Returns a copy of the array with the given `$value` associated to the given `$key`. - */ - function set_by_key(iterable $array, string $key, mixed $value): array - { - $array = to_array($array); - $current = &$array; - $keys = explode('.', $key); + return $value; +} - foreach ($keys as $i => $key) { - // If this is the last key in dot notation, we don't - // need to go through the next steps. - if (count($keys) === 1) { - break; - } +/** + * Asserts whether a value identified by the specified `$key` exists. Dot notation is supported. + */ +function has_key(iterable $array, int|string $key): bool +{ + $array = to_array($array); - // Remove the current key from our keys array - // so that later we can use the first value - // from that array as our key. - unset($keys[$i]); + if (isset($array[$key])) { + return true; + } - // If we know this key is not an array, make it one. - if (! isset($current[$key]) || ! is_array($current[$key])) { - $current[$key] = []; - } + $keys = is_int($key) + ? [$key] + : explode('.', $key); - // Set the context to this key. - $current = &$current[$key]; + foreach ($keys as $key) { + if (! isset($array[$key])) { + return false; } - // Pull the first key out of the array - // and use it to set the value. - $current[array_shift($keys)] = $value; - - return $array; + $array = &$array[$key]; } - /** - * Returns a copy of the array that converts the dot-notated keys to a set of nested arrays. - */ - function undot(iterable $array): array - { - $array = to_array($array); - - $unwrapValue = function (string|int $key, mixed $value) { - if (is_int($key)) { - return [$key => $value]; - } - - $keys = explode('.', $key); + return true; +} - for ($i = array_key_last($keys); $i >= 0; $i--) { - $currentKey = $keys[$i]; +/** + * Asserts whether the given array contains a value that can be identified by `$search`. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param TValue|Closure(TValue, TKey): bool $search + */ +function contains(iterable $array, mixed $search): bool +{ + $array = to_array($array); + $search = $search instanceof Closure + ? $search + : static fn (mixed $value) => $value === $search; + + return array_any($array, static fn ($value, $key) => $search($value, $key)); +} - $value = [$currentKey => $value]; - } +/** + * Asserts whether all items in the given array pass the given `$callback`. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param Closure(TValue, TKey): bool $callback + * + * @return bool If the collection is empty, returns `true`. + */ +function every(iterable $array, ?Closure $callback = null): bool +{ + $array = to_array($array); + $callback ??= static fn (mixed $value) => ! is_null($value); + + return array_all($array, static fn (mixed $value, int|string $key) => $callback($value, $key)); +} - return $value; - }; +/** + * Returns a copy of the array with the given `$value` associated to the given `$key`. + */ +function set_by_key(iterable $array, string $key, mixed $value): array +{ + $array = to_array($array); + $current = &$array; + $keys = explode('.', $key); + + foreach ($keys as $i => $key) { + // If this is the last key in dot notation, we don't + // need to go through the next steps. + if (count($keys) === 1) { + break; + } - $unwrapped = []; + // Remove the current key from our keys array + // so that later we can use the first value + // from that array as our key. + unset($keys[$i]); - foreach ($array as $key => $value) { - $unwrapped[] = $unwrapValue($key, $value); + // If we know this key is not an array, make it one. + if (! isset($current[$key]) || ! is_array($current[$key])) { + $current[$key] = []; } - return array_merge_recursive(...$unwrapped); + // Set the context to this key. + $current = &$current[$key]; } - /** - * Returns a copy of the array that converts nested arrays to a single-dimension dot-notation array. - */ - function dot(iterable $array, string $prefix = ''): array - { - $array = to_array($array); + // Pull the first key out of the array + // and use it to set the value. + $current[array_shift($keys)] = $value; - $result = []; + return $array; +} - foreach ($array as $key => $value) { - if (is_array($value)) { - $result = [...$result, ...dot($value, $prefix . $key . '.')]; - } else { - $result[$prefix . $key] = $value; - } - } +/** + * Returns a copy of the array that converts the dot-notated keys to a set of nested arrays. + */ +function undot(iterable $array): array +{ + $array = to_array($array); - return $result; - } + $unwrapValue = function (string|int $key, mixed $value) { + if (is_int($key)) { + return [$key => $value]; + } - /** - * Joins all values using the specified `$glue`. The last item of the string is separated by `$finalGlue`. - */ - function join(iterable $array, string $glue = ', ', ?string $finalGlue = ' and '): ImmutableString - { - $array = to_array($array); + $keys = explode('.', $key); - if ($finalGlue === '' || is_null($finalGlue)) { - return namespace\implode($array, $glue); - } + for ($i = array_key_last($keys); $i >= 0; $i--) { + $currentKey = $keys[$i]; - if (namespace\is_empty($array)) { - return new ImmutableString(''); + $value = [$currentKey => $value]; } - $parts = namespace\pop($array, $last); + return $value; + }; - if (! namespace\is_empty($parts)) { - return namespace\implode($parts, $glue)->append($finalGlue, $last); - } + $unwrapped = []; - return new ImmutableString($last); + foreach ($array as $key => $value) { + $unwrapped[] = $unwrapValue($key, $value); } - /** - * Returns a copy of the array flattened to a single level, or until the specified `$depth` is reached. - * - * ### Example - * ```php - * flatten(['foo', ['bar', 'baz']]); // ['foo', 'bar', 'baz'] - * ``` - */ - function flatten(iterable $array, int|float $depth = INF): array - { - $array = to_array($array); - $result = []; - - foreach ($array as $item) { - if (! is_array($item)) { - $result[] = $item; + return array_merge_recursive(...$unwrapped); +} - continue; - } +/** + * Returns a copy of the array that converts nested arrays to a single-dimension dot-notation array. + */ +function dot(iterable $array, string $prefix = ''): array +{ + $array = to_array($array); - $values = $depth === 1 - ? namespace\values($item) - : namespace\flatten($item, $depth - 1); + $result = []; - foreach ($values as $value) { - $result[] = $value; - } + foreach ($array as $key => $value) { + if (is_array($value)) { + $result = [...$result, ...dot($value, $prefix . $key . '.')]; + } else { + $result[$prefix . $key] = $value; } - - return $result; } - /** - * Returns a copy of the array grouped by the result of the given `$keyExtractor`. - * The keys of the resulting array are the values returned by the `$keyExtractor`. - * - * ### Example - * ```php - * group_by( - * [ - * ['country' => 'france', 'continent' => 'europe'], - * ['country' => 'Sweden', 'continent' => 'europe'], - * ['country' => 'USA', 'continent' => 'america'] - * ], - * fn($item) => $item['continent'] - * ); - * // [ - * // 'europe' => [ - * // ['country' => 'france', 'continent' => 'europe'], - * // ['country' => 'Sweden', 'continent' => 'europe'] - * // ], - * // 'america' => [ - * // ['country' => 'USA', 'continent' => 'america'] - * // ] - * // ] - * ``` - * - * @template TKey of array-key - * @template TValue - * @param iterable $array - * @param Closure(TValue, TKey): array-key $keyExtracor - */ - function group_by(iterable $array, Closure $keyExtracor): array - { - $array = to_array($array); - - $result = []; - - foreach ($array as $key => $item) { - $key = $keyExtracor($item, $key); + return $result; +} - $result[$key][] = $item; - } +/** + * Joins all values using the specified `$glue`. The last item of the string is separated by `$finalGlue`. + */ +function join(iterable $array, string $glue = ', ', ?string $finalGlue = ' and '): ImmutableString +{ + $array = to_array($array); - return $result; + if ($finalGlue === '' || is_null($finalGlue)) { + return namespace\implode($array, $glue); } - /** - * Returns a copy of the given array, with each item transformed by the given callback, then flattens it by the specified depth. - * - * @template TMapValue - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param Closure(TValue,TKey): TMapValue[] $map - * - * @return array - */ - function flat_map(iterable $array, Closure $map, int|float $depth = 1): array - { - return namespace\flatten(namespace\map_iterable(to_array($array), $map), $depth); + if (namespace\is_empty($array)) { + return new ImmutableString(''); } - /** - * Returns a new array with the value of the given array mapped to the given object. - * - * @see Tempest\Mapper\map() - * - * @template T - * @param class-string $to - */ - function map_to(iterable $array, string $to): array - { - return Mapper\map(to_array($array))->collection()->to($to); - } + $parts = namespace\pop($array, $last); - /** - * Returns a copy of the given array sorted by its values. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param bool $desc Sorts in descending order if `true`; defaults to `false` (ascending). - * @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). - * @param int $flags Sorting flags to define comparison behavior, defaulting to `SORT_REGULAR`. - * @return array Key type depends on whether array keys are preserved or not. - */ - function sort(iterable $array, bool $desc = false, ?bool $preserveKeys = null, int $flags = SORT_REGULAR): array - { - $array = to_array($array); + if (! namespace\is_empty($parts)) { + return namespace\implode($parts, $glue)->append($finalGlue, $last); + } - if ($preserveKeys === null) { - $preserveKeys = is_associative($array); - } + return new ImmutableString($last); +} - if ($preserveKeys) { - $desc ? arsort($array, $flags) : asort($array, $flags); - } else { - $desc ? rsort($array, $flags) : php_sort($array, $flags); +/** + * Returns a copy of the array flattened to a single level, or until the specified `$depth` is reached. + * + * ### Example + * ```php + * flatten(['foo', ['bar', 'baz']]); // ['foo', 'bar', 'baz'] + * ``` + */ +function flatten(iterable $array, int|float $depth = INF): array +{ + $array = to_array($array); + $result = []; + + foreach ($array as $item) { + if (! is_array($item)) { + $result[] = $item; + + continue; } - return $array; - } - - /** - * Returns a copy of the given array sorted by its values using a callback function. - * - * @template TKey of array-key - * @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 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. - */ - function sort_by_callback(iterable $array, callable $callback, ?bool $preserveKeys = null): array - { - $array = to_array($array); + $values = $depth === 1 + ? namespace\values($item) + : namespace\flatten($item, $depth - 1); - if ($preserveKeys === null) { - $preserveKeys = is_associative($array); + foreach ($values as $value) { + $result[] = $value; } + } - $preserveKeys ? uasort($array, $callback) : usort($array, $callback); + return $result; +} - return $array; +/** + * Returns a copy of the array grouped by the result of the given `$keyExtractor`. + * The keys of the resulting array are the values returned by the `$keyExtractor`. + * + * ### Example + * ```php + * group_by( + * [ + * ['country' => 'france', 'continent' => 'europe'], + * ['country' => 'Sweden', 'continent' => 'europe'], + * ['country' => 'USA', 'continent' => 'america'] + * ], + * fn($item) => $item['continent'] + * ); + * // [ + * // 'europe' => [ + * // ['country' => 'france', 'continent' => 'europe'], + * // ['country' => 'Sweden', 'continent' => 'europe'] + * // ], + * // 'america' => [ + * // ['country' => 'USA', 'continent' => 'america'] + * // ] + * // ] + * ``` + * + * @template TKey of array-key + * @template TValue + * @param iterable $array + * @param Closure(TValue, TKey): array-key $keyExtracor + */ +function group_by(iterable $array, Closure $keyExtracor): array +{ + $array = to_array($array); + + $result = []; + + foreach ($array as $key => $item) { + $key = $keyExtracor($item, $key); + + $result[$key][] = $item; } - /** - * Returns a copy of the given array sorted by its keys. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param bool $desc Sorts in descending order if `true`; defaults to `false` (ascending). - * @param int $flags Sorting flags to define comparison behavior, defaulting to `SORT_REGULAR`. - * @return array - */ - function sort_keys(iterable $array, bool $desc = false, int $flags = SORT_REGULAR): array - { - $array = to_array($array); - - $desc ? krsort($array, $flags) : ksort($array, $flags); + return $result; +} - return $array; - } +/** + * Returns a copy of the given array, with each item transformed by the given callback, then flattens it by the specified depth. + * + * @template TMapValue + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param Closure(TValue,TKey): TMapValue[] $map + * + * @return array + */ +function flat_map(iterable $array, Closure $map, int|float $depth = 1): array +{ + return namespace\flatten(namespace\map_iterable(to_array($array), $map), $depth); +} - /** - * Returns a copy of the given array sorted by its keys using a callback function. - * - * @template TKey of array-key - * @template TValue - * - * @param iterable $array - * @param callable $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. - * @return array - */ - function sort_keys_by_callback(iterable $array, callable $callback): array - { - $array = to_array($array); +/** + * Returns a new array with the value of the given array mapped to the given object. + * + * @see Tempest\Mapper\map() + * + * @template T + * @param class-string $to + */ +function map_to(iterable $array, string $to): array +{ + return Mapper\map(to_array($array))->collection()->to($to); +} - uksort($array, $callback); +/** + * Returns a copy of the given array sorted by its values. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param bool $desc Sorts in descending order if `true`; defaults to `false` (ascending). + * @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). + * @param int $flags Sorting flags to define comparison behavior, defaulting to `SORT_REGULAR`. + * @return array Key type depends on whether array keys are preserved or not. + */ +function sort(iterable $array, bool $desc = false, ?bool $preserveKeys = null, int $flags = SORT_REGULAR): array +{ + $array = to_array($array); + + if ($preserveKeys === null) { + $preserveKeys = is_associative($array); + } - return $array; + if ($preserveKeys) { + $desc ? arsort($array, $flags) : asort($array, $flags); + } else { + $desc ? rsort($array, $flags) : php_sort($array, $flags); } - /** - * Extracts a part of the array. - * - * ### Example - * ```php - * slice([1, 2, 3, 4, 5], 2); // [3, 4, 5] - * ``` - */ - function slice(iterable $array, int $offset, ?int $length = null): array - { - $array = to_array($array); - $length ??= count($array) - $offset; + return $array; +} - return array_slice($array, $offset, $length); +/** + * Returns a copy of the given array sorted by its values using a callback function. + * + * @template TKey of array-key + * @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 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. + */ +function sort_by_callback(iterable $array, callable $callback, ?bool $preserveKeys = null): array +{ + $array = to_array($array); + + if ($preserveKeys === null) { + $preserveKeys = is_associative($array); } - /** - * Returns a new list containing the range of numbers from `$start` to `$end` - * inclusive, with the step between elements being `$step` if provided, or 1 by - * default. - * - * If `$start > $end`, it returns a descending range instead of an empty one. - * - * Examples: - * - * range(0, 5) - * => array(0, 1, 2, 3, 4, 5) - * - * range(5, 0) - * => array(5, 4, 3, 2, 1, 0) - * - * range(0.0, 3.0, 0.5) - * => array(0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0) - * - * range(3.0, 0.0, -0.5) - * => array(3.0, 2.5, 2.0, 1.5, 1.0, 0.5, 0.0) - * - * @template T of int|float - * - * @param T $start - * @param T $end - * @param T|null $step - * - * @throws LogicException If $start < $end, and $step is negative. - * - * @return non-empty-list - */ - function range(int|float $start, int|float $end, int|float|null $step = null): array - { - if ((float) $start === (float) $end) { - return [$start]; - } + $preserveKeys ? uasort($array, $callback) : usort($array, $callback); - if ($start < $end) { - if (null === $step) { - /** @var T $step */ - $step = 1; - } + return $array; +} - if ($step < 0) { - throw new LogicException('If $end is greater than $start, then $step must be positive or null.'); - } +/** + * Returns a copy of the given array sorted by its keys. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param bool $desc Sorts in descending order if `true`; defaults to `false` (ascending). + * @param int $flags Sorting flags to define comparison behavior, defaulting to `SORT_REGULAR`. + * @return array + */ +function sort_keys(iterable $array, bool $desc = false, int $flags = SORT_REGULAR): array +{ + $array = to_array($array); + + $desc ? krsort($array, $flags) : ksort($array, $flags); + + return $array; +} - $result = []; +/** + * Returns a copy of the given array sorted by its keys using a callback function. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $array + * @param callable $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. + * @return array + */ +function sort_keys_by_callback(iterable $array, callable $callback): array +{ + $array = to_array($array); + + uksort($array, $callback); + + return $array; +} - /** - * @var int|float $start - * @var int|float $step - */ - for ($i = $start; $i <= $end; $i += $step) { - $result[] = $i; - } +/** + * Extracts a part of the array. + * + * ### Example + * ```php + * slice([1, 2, 3, 4, 5], 2); // [3, 4, 5] + * ``` + */ +function slice(iterable $array, int $offset, ?int $length = null): array +{ + $array = to_array($array); + $length ??= count($array) - $offset; + + return array_slice($array, $offset, $length); +} - return $result; - } +/** + * Returns a new list containing the range of numbers from `$start` to `$end` + * inclusive, with the step between elements being `$step` if provided, or 1 by + * default. + * + * If `$start > $end`, it returns a descending range instead of an empty one. + * + * Examples: + * + * range(0, 5) + * => array(0, 1, 2, 3, 4, 5) + * + * range(5, 0) + * => array(5, 4, 3, 2, 1, 0) + * + * range(0.0, 3.0, 0.5) + * => array(0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0) + * + * range(3.0, 0.0, -0.5) + * => array(3.0, 2.5, 2.0, 1.5, 1.0, 0.5, 0.0) + * + * @template T of int|float + * + * @param T $start + * @param T $end + * @param T|null $step + * + * @throws LogicException If $start < $end, and $step is negative. + * + * @return non-empty-list + */ +function range(int|float $start, int|float $end, int|float|null $step = null): array +{ + if ((float) $start === (float) $end) { + return [$start]; + } + if ($start < $end) { if (null === $step) { /** @var T $step */ - $step = -1; + $step = 1; } - if ($step > 0) { - throw new LogicException('If $start is greater than $end, then $step must be negative or null.'); + if ($step < 0) { + throw new LogicException('If $end is greater than $start, then $step must be positive or null.'); } $result = []; @@ -1288,109 +1267,130 @@ function range(int|float $start, int|float $end, int|float|null $step = null): a * @var int|float $start * @var int|float $step */ - for ($i = $start; $i >= $end; $i += $step) { + for ($i = $start; $i <= $end; $i += $step) { $result[] = $i; } return $result; } + if (null === $step) { + /** @var T $step */ + $step = -1; + } + + if ($step > 0) { + throw new LogicException('If $start is greater than $end, then $step must be negative or null.'); + } + + $result = []; + /** - * Returns a pair containing lists for which the given predicate returned `true` and `false`, respectively. - * - * @template T - * - * @param iterable $iterable - * @param (Closure(T): bool) $predicate - * - * @return array{0: array, 1: array} + * @var int|float $start + * @var int|float $step */ - function partition(iterable $iterable, Closure $predicate): array - { - $success = []; - $failure = []; - - foreach ($iterable as $value) { - if ($predicate($value)) { - $success[] = $value; - continue; - } + for ($i = $start; $i >= $end; $i += $step) { + $result[] = $i; + } + + return $result; +} - $failure[] = $value; +/** + * Returns a pair containing lists for which the given predicate returned `true` and `false`, respectively. + * + * @template T + * + * @param iterable $iterable + * @param (Closure(T): bool) $predicate + * + * @return array{0: array, 1: array} + */ +function partition(iterable $iterable, Closure $predicate): array +{ + $success = []; + $failure = []; + + foreach ($iterable as $value) { + if ($predicate($value)) { + $success[] = $value; + continue; } - return [$success, $failure]; + $failure[] = $value; } - /** - * Wraps the specified `$input` into an array. If the `$input` is already an array, it is returned. - * As opposed to {@see \Tempest\Support\Arr\to_array}, this function does not convert {@see Traversable} and {@see Countable} instances to arrays. - * - * @template TKey of array-key - * @template TValue - * @param null|array|ArrayInterface|TValue $input - * @return array - */ - function wrap(mixed $input = []): array - { - if (is_array($input)) { - return $input; - } + return [$success, $failure]; +} - if ($input instanceof ArrayInterface) { - return $input->toArray(); - } +/** + * Wraps the specified `$input` into an array. If the `$input` is already an array, it is returned. + * As opposed to {@see \Tempest\Support\Arr\to_array}, this function does not convert {@see Traversable} and {@see Countable} instances to arrays. + * + * @template TKey of array-key + * @template TValue + * @param null|array|ArrayInterface|TValue $input + * @return array + */ +function wrap(mixed $input = []): array +{ + if (is_array($input)) { + return $input; + } - if ($input === null) { - return []; - } + if ($input instanceof ArrayInterface) { + return $input->toArray(); + } - return [$input]; + if ($input === null) { + return []; } - /** - * 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 - */ - function to_array(mixed $input): array - { - if (is_array($input)) { - return $input; - } + return [$input]; +} - if ($input instanceof ArrayInterface) { - return $input->toArray(); - } +/** + * 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 + */ +function to_array(mixed $input): array +{ + if (is_array($input)) { + return $input; + } - if ($input instanceof Traversable) { - return iterator_to_array($input); - } + if ($input instanceof ArrayInterface) { + return $input->toArray(); + } - if ($input instanceof Countable) { - $count = count($input); - $result = []; + if ($input instanceof Traversable) { + return iterator_to_array($input); + } - for ($i = 0; $i < $count; $i++) { - if (isset($input[$i])) { - $result[$i] = $input[$i]; - } - } + if ($input instanceof Countable) { + $count = count($input); + $result = []; - return $result; + for ($i = 0; $i < $count; $i++) { + if (isset($input[$i])) { + $result[$i] = $input[$i]; + } } - // Scalar values (string, int, float, bool) and objects are wrapped - if (is_scalar($input) || is_object($input)) { - return [$input]; - } + return $result; + } - return []; + // Scalar values (string, int, float, bool) and objects are wrapped + if (is_scalar($input) || is_object($input)) { + return [$input]; } + + return []; } diff --git a/packages/support/src/Comparison/functions.php b/packages/support/src/Comparison/functions.php index af48429702..7b248da655 100644 --- a/packages/support/src/Comparison/functions.php +++ b/packages/support/src/Comparison/functions.php @@ -1,109 +1,109 @@ value; - } +namespace Tempest\Support\Comparison; - /** - * @template T - * - * @param T $a - * @param T $b - */ - function not_equal(mixed $a, mixed $b): bool - { - return compare($a, $b) !== Order::EQUAL; - } +/** + * @template T + * + * @param T $a + * @param T $b + * + * This method can be used as a sorter callback function for Comparable items. + * + * Vec\sort($list, Comparable\sort(...)) + */ +function sort(mixed $a, mixed $b): int +{ + return compare($a, $b)->value; +} - /** - * @template T - * - * @param T $a - * @param T $b - */ - function less(mixed $a, mixed $b): bool - { - return compare($a, $b) === Order::LESS; - } +/** + * @template T + * + * @param T $a + * @param T $b + */ +function not_equal(mixed $a, mixed $b): bool +{ + return compare($a, $b) !== Order::EQUAL; +} - /** - * @template T - * - * @param T $a - * @param T $b - */ - function less_or_equal(mixed $a, mixed $b): bool - { - $order = compare($a, $b); +/** + * @template T + * + * @param T $a + * @param T $b + */ +function less(mixed $a, mixed $b): bool +{ + return compare($a, $b) === Order::LESS; +} - return $order === Order::EQUAL || $order === Order::LESS; - } +/** + * @template T + * + * @param T $a + * @param T $b + */ +function less_or_equal(mixed $a, mixed $b): bool +{ + $order = compare($a, $b); - /** - * @template T - * - * @param T $a - * @param T $b - */ - function greater(mixed $a, mixed $b): bool - { - return compare($a, $b) === Order::GREATER; - } + return $order === Order::EQUAL || $order === Order::LESS; +} - /** - * @template T - * - * @param T $a - * @param T $b - */ - function greater_or_equal(mixed $a, mixed $b): bool - { - $order = compare($a, $b); +/** + * @template T + * + * @param T $a + * @param T $b + */ +function greater(mixed $a, mixed $b): bool +{ + return compare($a, $b) === Order::GREATER; +} - return $order === Order::EQUAL || $order === Order::GREATER; - } +/** + * @template T + * + * @param T $a + * @param T $b + */ +function greater_or_equal(mixed $a, mixed $b): bool +{ + $order = compare($a, $b); - /** - * @template T - * - * @param T $a - * @param T $b - */ - function equal(mixed $a, mixed $b): bool - { - return compare($a, $b) === Order::EQUAL; - } + return $order === Order::EQUAL || $order === Order::GREATER; +} - /** - * @template T - * - * @param T $a - * @param T $b - * - * This function can compare 2 values of a similar type. - * When the type happens to be mixed or never, it will fall back to PHP's internal comparison rules: - * - * @link https://www.php.net/manual/en/language.operators.comparison.php - * @link https://www.php.net/manual/en/types.comparisons.php - */ - function compare(mixed $a, mixed $b): Order - { - if ($a instanceof Comparable) { - return $a->compare($b); - } +/** + * @template T + * + * @param T $a + * @param T $b + */ +function equal(mixed $a, mixed $b): bool +{ + return compare($a, $b) === Order::EQUAL; +} - return Order::from($a <=> $b); +/** + * @template T + * + * @param T $a + * @param T $b + * + * This function can compare 2 values of a similar type. + * When the type happens to be mixed or never, it will fall back to PHP's internal comparison rules: + * + * @link https://www.php.net/manual/en/language.operators.comparison.php + * @link https://www.php.net/manual/en/types.comparisons.php + */ +function compare(mixed $a, mixed $b): Order +{ + if ($a instanceof Comparable) { + return $a->compare($b); } + + return Order::from($a <=> $b); } diff --git a/packages/support/src/Html/functions.php b/packages/support/src/Html/functions.php index 7e2bae244e..1dde0cb19f 100644 --- a/packages/support/src/Html/functions.php +++ b/packages/support/src/Html/functions.php @@ -2,206 +2,206 @@ declare(strict_types=1); -namespace Tempest\Support\Html { - use Stringable; +namespace Tempest\Support\Html; - use function Tempest\Support\arr; +use Stringable; - /** - * Determines whether the specified HTML tag is a void tag. - * @see https://developer.mozilla.org/en-US/docs/Glossary/Void_element - */ - function is_void_tag(Stringable|string $tag): bool - { - return in_array( +use function Tempest\Support\arr; + +/** + * Determines whether the specified HTML tag is a void tag. + * @see https://developer.mozilla.org/en-US/docs/Glossary/Void_element + */ +function is_void_tag(Stringable|string $tag): bool +{ + return in_array( + (string) $tag, + [ + 'area', + 'base', + 'br', + 'col', + 'embed', + 'hr', + 'img', + 'input', + 'link', + 'meta', + 'param', + 'source', + 'track', + 'wbr', + ], + strict: true, + ); +} + +/** + * Determines whether the specified HTML tag is known HTML tag. + * @see https://developer.mozilla.org/en-US/docs/Glossary/Tag + */ +function is_html_tag(Stringable|string $tag): bool +{ + return ( + is_void_tag($tag) + || in_array( (string) $tag, [ + 'a', + 'abbr', + 'acronym', + 'address', + 'applet', 'area', + 'article', + 'aside', + 'audio', + 'b', 'base', + 'basefont', + 'bdi', + 'bdo', + 'big', + 'blockquote', + 'body', 'br', + 'button', + 'canvas', + 'caption', + 'center', + 'cite', + 'code', 'col', + 'colgroup', + 'data', + 'datalist', + 'dd', + 'del', + 'details', + 'dfn', + 'dialog', + 'dir', + 'div', + 'dl', + 'dt', + 'em', 'embed', + 'fieldset', + 'figcaption', + 'figure', + 'font', + 'footer', + 'form', + 'frame', + 'frameset', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'head', + 'header', + 'hgroup', 'hr', + 'html', + 'i', + 'iframe', 'img', 'input', + 'ins', + 'kbd', + 'label', + 'legend', + 'li', 'link', + 'main', + 'map', + 'mark', + 'menu', 'meta', + 'meter', + 'nav', + 'noframes', + 'noscript', + 'object', + 'ol', + 'optgroup', + 'option', + 'output', + 'p', 'param', + 'picture', + 'pre', + 'progress', + 'q', + 'rp', + 'rt', + 'ruby', + 's', + 'samp', + 'script', + 'search', + 'section', + 'select', + 'small', 'source', + 'span', + 'strike', + 'strong', + 'style', + 'sub', + 'summary', + 'sup', + 'svg', + 'table', + 'tbody', + 'td', + 'template', + 'textarea', + 'tfoot', + 'th', + 'thead', + 'time', + 'title', + 'tr', 'track', + 'tt', + 'u', + 'ul', + 'var', + 'video', 'wbr', ], strict: true, - ); - } - - /** - * Determines whether the specified HTML tag is known HTML tag. - * @see https://developer.mozilla.org/en-US/docs/Glossary/Tag - */ - function is_html_tag(Stringable|string $tag): bool - { - return ( - is_void_tag($tag) - || in_array( - (string) $tag, - [ - 'a', - 'abbr', - 'acronym', - 'address', - 'applet', - 'area', - 'article', - 'aside', - 'audio', - 'b', - 'base', - 'basefont', - 'bdi', - 'bdo', - 'big', - 'blockquote', - 'body', - 'br', - 'button', - 'canvas', - 'caption', - 'center', - 'cite', - 'code', - 'col', - 'colgroup', - 'data', - 'datalist', - 'dd', - 'del', - 'details', - 'dfn', - 'dialog', - 'dir', - 'div', - 'dl', - 'dt', - 'em', - 'embed', - 'fieldset', - 'figcaption', - 'figure', - 'font', - 'footer', - 'form', - 'frame', - 'frameset', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'head', - 'header', - 'hgroup', - 'hr', - 'html', - 'i', - 'iframe', - 'img', - 'input', - 'ins', - 'kbd', - 'label', - 'legend', - 'li', - 'link', - 'main', - 'map', - 'mark', - 'menu', - 'meta', - 'meter', - 'nav', - 'noframes', - 'noscript', - 'object', - 'ol', - 'optgroup', - 'option', - 'output', - 'p', - 'param', - 'picture', - 'pre', - 'progress', - 'q', - 'rp', - 'rt', - 'ruby', - 's', - 'samp', - 'script', - 'search', - 'section', - 'select', - 'small', - 'source', - 'span', - 'strike', - 'strong', - 'style', - 'sub', - 'summary', - 'sup', - 'svg', - 'table', - 'tbody', - 'td', - 'template', - 'textarea', - 'tfoot', - 'th', - 'thead', - 'time', - 'title', - 'tr', - 'track', - 'tt', - 'u', - 'ul', - 'var', - 'video', - 'wbr', - ], - strict: true, - ) - ); - } - - function format_attributes(array $attributes = []): string - { - return $attributes = arr($attributes) - ->filter(fn (mixed $value) => ! in_array($value, [false, null], strict: true)) - ->map(fn (mixed $value, int|string $key) => $value === true ? $key : $key . '="' . $value . '"') - ->values() - ->implode(' ') - ->when( - condition: fn ($string) => $string->length() !== 0, - callback: fn ($string) => $string->prepend(' '), - ) - ->toString(); - } + ) + ); +} - /** - * Creates an HTML tag with the specified optional attributes and content. - */ - function create_tag(string $tag, array $attributes = [], ?string $content = null): HtmlString - { - $attributes = namespace\format_attributes($attributes); +function format_attributes(array $attributes = []): string +{ + return $attributes = arr($attributes) + ->filter(fn (mixed $value) => ! in_array($value, [false, null], strict: true)) + ->map(fn (mixed $value, int|string $key) => $value === true ? $key : $key . '="' . $value . '"') + ->values() + ->implode(' ') + ->when( + condition: fn ($string) => $string->length() !== 0, + callback: fn ($string) => $string->prepend(' '), + ) + ->toString(); +} - if ($content || ! is_void_tag($tag)) { - return new HtmlString(sprintf('<%s%s>%s', $tag, $attributes, $content ?? '', $tag)); - } +/** + * Creates an HTML tag with the specified optional attributes and content. + */ +function create_tag(string $tag, array $attributes = [], ?string $content = null): HtmlString +{ + $attributes = namespace\format_attributes($attributes); - return new HtmlString(sprintf('<%s%s />', $tag, $attributes)); + if ($content || ! is_void_tag($tag)) { + return new HtmlString(sprintf('<%s%s>%s', $tag, $attributes, $content ?? '', $tag)); } + + return new HtmlString(sprintf('<%s%s />', $tag, $attributes)); } diff --git a/packages/support/src/Math/functions.php b/packages/support/src/Math/functions.php index 51420afe99..722bf80528 100644 --- a/packages/support/src/Math/functions.php +++ b/packages/support/src/Math/functions.php @@ -1,578 +1,578 @@ $fromBase - * @param int<2, 36> $toBase - * - * @throws Exception\InvalidArgumentException If the given value is invalid. - */ - function base_convert(string $value, int $fromBase, int $toBase): string - { - $fromAlphabet = mb_substr(Str\ALPHABET_ALPHANUMERIC, 0, $fromBase); - $resultDecimal = '0'; - $placeValue = bcpow((string) $fromBase, (string) (strlen($value) - 1)); - - foreach (str_split($value) as $digit) { - $digitNumeric = stripos($fromAlphabet, $digit); - - if (false === $digitNumeric) { - throw new Exception\InvalidArgumentException(sprintf('Invalid digit %s in base %d', $digit, $fromBase)); - } - - $resultDecimal = bcadd($resultDecimal, bcmul((string) $digitNumeric, $placeValue)); - $placeValue = bcdiv($placeValue, (string) $fromBase); - } +/** + * Returns the arc tangent of the given coordinates. + */ +function atan2(float $y, float $x): float +{ + return php_atan2($y, $x); +} - if (10 === $toBase) { - return $resultDecimal; +/** + * Converts the given string in base `$from_base` to base `$to_base`, assuming + * letters a-z are used for digits for bases greater than 10. The conversion is + * done to arbitrary precision. + * + * @param non-empty-string $value + * @param int<2, 36> $fromBase + * @param int<2, 36> $toBase + * + * @throws Exception\InvalidArgumentException If the given value is invalid. + */ +function base_convert(string $value, int $fromBase, int $toBase): string +{ + $fromAlphabet = mb_substr(Str\ALPHABET_ALPHANUMERIC, 0, $fromBase); + $resultDecimal = '0'; + $placeValue = bcpow((string) $fromBase, (string) (strlen($value) - 1)); + + foreach (str_split($value) as $digit) { + $digitNumeric = stripos($fromAlphabet, $digit); + + if (false === $digitNumeric) { + throw new Exception\InvalidArgumentException(sprintf('Invalid digit %s in base %d', $digit, $fromBase)); } - $toAlphabet = mb_substr(Str\ALPHABET_ALPHANUMERIC, 0, $toBase); - $result = ''; - - do { - $result = $toAlphabet[(int) bcmod($resultDecimal, (string) $toBase)] . $result; - $resultDecimal = bcdiv($resultDecimal, (string) $toBase); - } while (bccomp($resultDecimal, '0') > 0); - - return $result; + $resultDecimal = bcadd($resultDecimal, bcmul((string) $digitNumeric, $placeValue)); + $placeValue = bcdiv($placeValue, (string) $fromBase); } - /** - * Return the smallest integer value greater than or equal to the given number. - */ - function ceil(float $number): float - { - return php_ceil($number); + if (10 === $toBase) { + return $resultDecimal; } - /** - * Returns the given number clamped to the given range. - * - * @template T of float|int - * - * @param T $number - * @param T $min - * @param T $max - * - * @throws Exception\InvalidArgumentException If $min is bigger than $max - * - * @return T - */ - function clamp(int|float $number, int|float $min, int|float $max): int|float - { - if ($max < $min) { - throw new Exception\InvalidArgumentException('Expected $min to be lower or equal to $max.'); - } + $toAlphabet = mb_substr(Str\ALPHABET_ALPHANUMERIC, 0, $toBase); + $result = ''; - if ($number < $min) { - return $min; - } + do { + $result = $toAlphabet[(int) bcmod($resultDecimal, (string) $toBase)] . $result; + $resultDecimal = bcdiv($resultDecimal, (string) $toBase); + } while (bccomp($resultDecimal, '0') > 0); - if ($number > $max) { - return $max; - } + return $result; +} - return $number; - } +/** + * Return the smallest integer value greater than or equal to the given number. + */ +function ceil(float $number): float +{ + return php_ceil($number); +} - /** - * Return the cosine of the given number. - */ - function cos(float $number): float - { - return php_cos($number); +/** + * Returns the given number clamped to the given range. + * + * @template T of float|int + * + * @param T $number + * @param T $min + * @param T $max + * + * @throws Exception\InvalidArgumentException If $min is bigger than $max + * + * @return T + */ +function clamp(int|float $number, int|float $min, int|float $max): int|float +{ + if ($max < $min) { + throw new Exception\InvalidArgumentException('Expected $min to be lower or equal to $max.'); } - /** - * Returns the result of integer division of the given numerator by the given denominator. - * - * @throws Exception\ArithmeticException If the $numerator is Math\INT64_MIN and the $denominator is -1. - * @throws Exception\DivisionByZeroException If the $denominator is 0. - */ - function div(int $numerator, int $denominator): int - { - try { - return intdiv($numerator, $denominator); - } catch (DivisionByZeroError $error) { - throw new Exception\DivisionByZeroException(sprintf('%s.', $error->getMessage()), $error->getCode(), $error); - } catch (ArithmeticError $error) { - throw new Exception\ArithmeticException( - 'Division of Math\INT64_MIN by -1 is not an integer.', - $error->getCode(), - $error, - ); - } + if ($number < $min) { + return $min; } - /** - * Returns the exponential of the given number. - */ - function exp(float $number): float - { - return php_exp($number); + if ($number > $max) { + return $max; } - /** - * Return the largest integer value less then or equal to the given number. - */ - function floor(float $number): float - { - return php_floor($number); - } + return $number; +} - /** - * Converts the given string in base `$from_base` to an integer, assuming letters a-z - * are used for digits when `$from_base` > 10. - * - * @param non-empty-string $number - * @param int<2, 36> $fromBase - * - * @throws Exception\InvalidArgumentException If $number contains an invalid digit in base $from_base - * @throws Exception\OverflowException In case of an integer overflow - */ - function from_base(string $number, int $fromBase): int - { - $limit = div(INT64_MAX, $fromBase); - $result = 0; - - foreach (str_split($number) as $digit) { - $oval = ord($digit); - - // Branches sorted by guesstimated frequency of use. */ - if (/* '0' - '9' */ $oval <= 57 && $oval >= 48) { - $dval = $oval - 48; - } elseif (/* 'a' - 'z' */ $oval >= 97 && $oval <= 122) { - $dval = $oval - 87; - } elseif (/* 'A' - 'Z' */ $oval >= 65 && $oval <= 90) { - $dval = $oval - 55; - } else { - $dval = 99; - } - - if ($fromBase < $dval) { - throw new Exception\InvalidArgumentException(sprintf('Invalid digit %s in base %d', $digit, $fromBase)); - } - - $oldval = $result; - $result = ($fromBase * $result) + $dval; - if ($oldval > $limit || $oldval > $result) { - throw new Exception\OverflowException(sprintf('Unexpected integer overflow parsing %s from base %d', $number, $fromBase)); - } - } +/** + * Return the cosine of the given number. + */ +function cos(float $number): float +{ + return php_cos($number); +} - return $result; +/** + * Returns the result of integer division of the given numerator by the given denominator. + * + * @throws Exception\ArithmeticException If the $numerator is Math\INT64_MIN and the $denominator is -1. + * @throws Exception\DivisionByZeroException If the $denominator is 0. + */ +function div(int $numerator, int $denominator): int +{ + try { + return intdiv($numerator, $denominator); + } catch (DivisionByZeroError $error) { + throw new Exception\DivisionByZeroException(sprintf('%s.', $error->getMessage()), $error->getCode(), $error); + } catch (ArithmeticError $error) { + throw new Exception\ArithmeticException( + 'Division of Math\INT64_MIN by -1 is not an integer.', + $error->getCode(), + $error, + ); } +} - /** - * Converts the given non-negative number into the given base, using letters a-z - * for digits when then given base is > 10. - * - * @param int<0, max> $number - * @param int<2, 36> $base - * - * @return non-empty-string - */ - function to_base(int $number, int $base): string - { - $result = ''; - - do { - $quotient = div($number, $base); - $result = Str\ALPHABET_ALPHANUMERIC[$number - ($quotient * $base)] . $result; - $number = $quotient; - } while (0 !== $number); - - return $result; - } +/** + * Returns the exponential of the given number. + */ +function exp(float $number): float +{ + return php_exp($number); +} - /** - * Returns the logarithm of the given number. - * - * @throws Exception\InvalidArgumentException If $number or $base are negative, or $base is equal to 1.0. - */ - function log(float $number, ?float $base = null): float - { - if ($number <= 0) { - throw new Exception\InvalidArgumentException('$number must be positive.'); - } +/** + * Return the largest integer value less then or equal to the given number. + */ +function floor(float $number): float +{ + return php_floor($number); +} - if (null === $base) { - return php_log($number); +/** + * Converts the given string in base `$from_base` to an integer, assuming letters a-z + * are used for digits when `$from_base` > 10. + * + * @param non-empty-string $number + * @param int<2, 36> $fromBase + * + * @throws Exception\InvalidArgumentException If $number contains an invalid digit in base $from_base + * @throws Exception\OverflowException In case of an integer overflow + */ +function from_base(string $number, int $fromBase): int +{ + $limit = div(INT64_MAX, $fromBase); + $result = 0; + + foreach (str_split($number) as $digit) { + $oval = ord($digit); + + // Branches sorted by guesstimated frequency of use. */ + if (/* '0' - '9' */ $oval <= 57 && $oval >= 48) { + $dval = $oval - 48; + } elseif (/* 'a' - 'z' */ $oval >= 97 && $oval <= 122) { + $dval = $oval - 87; + } elseif (/* 'A' - 'Z' */ $oval >= 65 && $oval <= 90) { + $dval = $oval - 55; + } else { + $dval = 99; } - if ($base <= 0) { - throw new Exception\InvalidArgumentException('$base must be positive.'); + if ($fromBase < $dval) { + throw new Exception\InvalidArgumentException(sprintf('Invalid digit %s in base %d', $digit, $fromBase)); } - if ($base === 1.0) { - throw new Exception\InvalidArgumentException('Logarithm undefined for $base of 1.0.'); + $oldval = $result; + $result = ($fromBase * $result) + $dval; + if ($oldval > $limit || $oldval > $result) { + throw new Exception\OverflowException(sprintf('Unexpected integer overflow parsing %s from base %d', $number, $fromBase)); } - - return php_log($number, $base); } - /** - * Returns the largest element of the given iterable, or null if the - * iterable is empty. - * - * The value for comparison is determined by the given function. - * - * In the case of duplicate values, later values overwrite previous ones. - * - * @template T - * - * @param iterable $numbers - * @param (Closure(T): numeric) $numericFunction - * - * @return T|null - */ - function max_by(iterable $numbers, Closure $numericFunction): mixed - { - $max = null; - $maxNum = null; - - foreach ($numbers as $value) { - $valueNum = $numericFunction($value); - if (null === $maxNum || $valueNum >= $maxNum) { - $max = $value; - $maxNum = $valueNum; - } - } - - return $max; - } + return $result; +} - /** - * Returns the largest element of the given list, or null if the array is empty. - * - * @template T of int|float - * - * @param array $numbers - * - * @return ($numbers is non-empty-list ? T : null) - */ - function max(array $numbers): null|int|float - { - $max = null; - - foreach ($numbers as $number) { - if (null === $max || $number > $max) { - $max = $number; - } - } +/** + * Converts the given non-negative number into the given base, using letters a-z + * for digits when then given base is > 10. + * + * @param int<0, max> $number + * @param int<2, 36> $base + * + * @return non-empty-string + */ +function to_base(int $number, int $base): string +{ + $result = ''; + + do { + $quotient = div($number, $base); + $result = Str\ALPHABET_ALPHANUMERIC[$number - ($quotient * $base)] . $result; + $number = $quotient; + } while (0 !== $number); + + return $result; +} - return $max; +/** + * Returns the logarithm of the given number. + * + * @throws Exception\InvalidArgumentException If $number or $base are negative, or $base is equal to 1.0. + */ +function log(float $number, ?float $base = null): float +{ + if ($number <= 0) { + throw new Exception\InvalidArgumentException('$number must be positive.'); } - /** - * Returns the largest number of all the given numbers. - * - * @template T of int|float - * - * @param T $first - * @param T $second - * @param T ...$rest - * - * @return T - */ - function maxva(int|float $first, int|float $second, int|float ...$rest): int|float - { - $max = \max($first, $second); - - foreach ($rest as $number) { - if ($number > $max) { - $max = $number; - } - } + if (null === $base) { + return php_log($number); + } - return $max; + if ($base <= 0) { + throw new Exception\InvalidArgumentException('$base must be positive.'); } - /** - * Returns the arithmetic mean of the given numbers in the list. - * - * Return null if the given list is empty. - * - * @param array $numbers - * - * @return ($numbers is non-empty-list ? float : null) - */ - function mean(array $numbers): ?float - { - $count = (float) count($numbers); - - if (0.0 === $count) { - return null; - } + if ($base === 1.0) { + throw new Exception\InvalidArgumentException('Logarithm undefined for $base of 1.0.'); + } - $mean = 0.0; + return php_log($number, $base); +} - foreach ($numbers as $number) { - $mean += (float) $number / $count; +/** + * Returns the largest element of the given iterable, or null if the + * iterable is empty. + * + * The value for comparison is determined by the given function. + * + * In the case of duplicate values, later values overwrite previous ones. + * + * @template T + * + * @param iterable $numbers + * @param (Closure(T): numeric) $numericFunction + * + * @return T|null + */ +function max_by(iterable $numbers, Closure $numericFunction): mixed +{ + $max = null; + $maxNum = null; + + foreach ($numbers as $value) { + $valueNum = $numericFunction($value); + if (null === $maxNum || $valueNum >= $maxNum) { + $max = $value; + $maxNum = $valueNum; } - - return $mean; } - /** - * Returns the median of the given numbers in the list. - * - * Returns null if the given iterable is empty. - * - * @param array $numbers - * - * @return ($numbers is non-empty-list ? float : null) - */ - function median(array $numbers): ?float - { - sort($numbers); - $count = count($numbers); - - if (0 === $count) { - return null; + return $max; +} + +/** + * Returns the largest element of the given list, or null if the array is empty. + * + * @template T of int|float + * + * @param array $numbers + * + * @return ($numbers is non-empty-list ? T : null) + */ +function max(array $numbers): null|int|float +{ + $max = null; + + foreach ($numbers as $number) { + if (null === $max || $number > $max) { + $max = $number; } + } - $middleIndex = div($count, 2); + return $max; +} - if (0 === ($count % 2)) { - return mean([$numbers[$middleIndex], $numbers[$middleIndex - 1]]); +/** + * Returns the largest number of all the given numbers. + * + * @template T of int|float + * + * @param T $first + * @param T $second + * @param T ...$rest + * + * @return T + */ +function maxva(int|float $first, int|float $second, int|float ...$rest): int|float +{ + $max = \max($first, $second); + + foreach ($rest as $number) { + if ($number > $max) { + $max = $number; } + } - return (float) $numbers[$middleIndex]; + return $max; +} + +/** + * Returns the arithmetic mean of the given numbers in the list. + * + * Return null if the given list is empty. + * + * @param array $numbers + * + * @return ($numbers is non-empty-list ? float : null) + */ +function mean(array $numbers): ?float +{ + $count = (float) count($numbers); + + if (0.0 === $count) { + return null; } - /** - * Returns the smallest element of the given iterable, or null if the - * iterable is empty. - * - * The value for comparison is determined by the given function. - * - * In the case of duplicate values, later values overwrite previous ones. - * - * @template T - * - * @param iterable $numbers - * @param (Closure(T): numeric) $numericFunction - * - * @return T|null - */ - function min_by(iterable $numbers, Closure $numericFunction): mixed - { - $min = null; - $minNum = null; - - foreach ($numbers as $value) { - $valueNum = $numericFunction($value); - - if (null === $minNum || $valueNum <= $minNum) { - $min = $value; - $minNum = $valueNum; - } - } + $mean = 0.0; - return $min; + foreach ($numbers as $number) { + $mean += (float) $number / $count; } - /** - * Returns the smallest element of the given list, or null if the - * list is empty. - * - * @template T of int|float - * - * @param array $numbers - * - * @return ($numbers is non-empty-list ? T : null) - */ - function min(array $numbers): null|float|int - { - $min = null; - - foreach ($numbers as $number) { - if (null === $min || $number < $min) { - $min = $number; - } - } + return $mean; +} - return $min; +/** + * Returns the median of the given numbers in the list. + * + * Returns null if the given iterable is empty. + * + * @param array $numbers + * + * @return ($numbers is non-empty-list ? float : null) + */ +function median(array $numbers): ?float +{ + sort($numbers); + $count = count($numbers); + + if (0 === $count) { + return null; } - /** - * Returns the smallest number of all the given numbers. - * - * @template T of int|float - * - * @param T $first - * @param T $second - * @param T ...$rest - * - * @return T - */ - function minva(int|float $first, int|float $second, int|float ...$rest): int|float - { - $min = \min($first, $second); - - foreach ($rest as $number) { - if ($number < $min) { - $min = $number; - } - } + $middleIndex = div($count, 2); - return $min; + if (0 === ($count % 2)) { + return mean([$numbers[$middleIndex], $numbers[$middleIndex - 1]]); } - /** - * Returns the given number rounded to the specified precision. - * - * A positive precision rounds to the nearest decimal place whereas a negative precision - * rounds to the nearest power of ten. - * - * For example, a precision of 1 rounds to the nearest tenth whereas a precision of -1 rounds to the nearst nearest. - */ - function round(float $number, int $precision = 0): float - { - return php_round($number, $precision); + return (float) $numbers[$middleIndex]; +} + +/** + * Returns the smallest element of the given iterable, or null if the + * iterable is empty. + * + * The value for comparison is determined by the given function. + * + * In the case of duplicate values, later values overwrite previous ones. + * + * @template T + * + * @param iterable $numbers + * @param (Closure(T): numeric) $numericFunction + * + * @return T|null + */ +function min_by(iterable $numbers, Closure $numericFunction): mixed +{ + $min = null; + $minNum = null; + + foreach ($numbers as $value) { + $valueNum = $numericFunction($value); + + if (null === $minNum || $valueNum <= $minNum) { + $min = $value; + $minNum = $valueNum; + } } - /** - * Returns the sine of the given number. - */ - function sin(float $number): float - { - return php_sin($number); + return $min; +} + +/** + * Returns the smallest element of the given list, or null if the + * list is empty. + * + * @template T of int|float + * + * @param array $numbers + * + * @return ($numbers is non-empty-list ? T : null) + */ +function min(array $numbers): null|float|int +{ + $min = null; + + foreach ($numbers as $number) { + if (null === $min || $number < $min) { + $min = $number; + } } - /** - * Returns the sum of all the given numbers. - * - * @param array $numbers - */ - function sum_floats(array $numbers): float - { - $result = 0.0; - - foreach ($numbers as $number) { - $result += (float) $number; + return $min; +} + +/** + * Returns the smallest number of all the given numbers. + * + * @template T of int|float + * + * @param T $first + * @param T $second + * @param T ...$rest + * + * @return T + */ +function minva(int|float $first, int|float $second, int|float ...$rest): int|float +{ + $min = \min($first, $second); + + foreach ($rest as $number) { + if ($number < $min) { + $min = $number; } + } + + return $min; +} + +/** + * Returns the given number rounded to the specified precision. + * + * A positive precision rounds to the nearest decimal place whereas a negative precision + * rounds to the nearest power of ten. + * + * For example, a precision of 1 rounds to the nearest tenth whereas a precision of -1 rounds to the nearst nearest. + */ +function round(float $number, int $precision = 0): float +{ + return php_round($number, $precision); +} - return $result; +/** + * Returns the sine of the given number. + */ +function sin(float $number): float +{ + return php_sin($number); +} + +/** + * Returns the sum of all the given numbers. + * + * @param array $numbers + */ +function sum_floats(array $numbers): float +{ + $result = 0.0; + + foreach ($numbers as $number) { + $result += (float) $number; } - /** - * Returns the sum of all the given numbers. - * - * @param array $numbers - */ - function sum(array $numbers): int - { - $result = 0; - - foreach ($numbers as $number) { - $result += $number; - } + return $result; +} - return $result; +/** + * Returns the sum of all the given numbers. + * + * @param array $numbers + */ +function sum(array $numbers): int +{ + $result = 0; + + foreach ($numbers as $number) { + $result += $number; } + + return $result; } diff --git a/packages/support/src/Path/functions.php b/packages/support/src/Path/functions.php index f05fd46b7c..f5035c494f 100644 --- a/packages/support/src/Path/functions.php +++ b/packages/support/src/Path/functions.php @@ -2,155 +2,155 @@ declare(strict_types=1); -namespace Tempest\Support\Path { - use Stringable; - - use function Tempest\Support\Regex\matches; - use function Tempest\Support\Str\ends_with; - use function Tempest\Support\Str\starts_with; - - /** - * Determines whether the given path is a relative path. The path is not checked against the filesystem. - */ - function is_relative_path(null|Stringable|string ...$parts): bool - { - return ! namespace\is_absolute_path(...$parts); - } +namespace Tempest\Support\Path; - /** - * Converts the given absolute path to a path relative to `$from`. - * If the given path is not an absolute path, it is assumed to already by relative to `$from` and will be returned as-is. - */ - function to_relative_path(null|Stringable|string $from, Stringable|string ...$parts): string - { - $path = namespace\normalize(...$parts); - $from = $from === null ? '' : (string) $from; - - if (is_relative_path($path)) { - return $path; - } +use Stringable; - $from = rtrim($from, '/'); - $path = rtrim($path, '/'); +use function Tempest\Support\Regex\matches; +use function Tempest\Support\Str\ends_with; +use function Tempest\Support\Str\starts_with; - $fromParts = explode('/', $from); - $pathParts = explode('/', $path); +/** + * Determines whether the given path is a relative path. The path is not checked against the filesystem. + */ +function is_relative_path(null|Stringable|string ...$parts): bool +{ + return ! namespace\is_absolute_path(...$parts); +} - while ($fromParts !== [] && $pathParts !== [] && $fromParts[0] === $pathParts[0]) { - array_shift($fromParts); - array_shift($pathParts); - } +/** + * Converts the given absolute path to a path relative to `$from`. + * If the given path is not an absolute path, it is assumed to already by relative to `$from` and will be returned as-is. + */ +function to_relative_path(null|Stringable|string $from, Stringable|string ...$parts): string +{ + $path = namespace\normalize(...$parts); + $from = $from === null ? '' : (string) $from; - $upDirs = count($fromParts); - $relativePath = str_repeat('../', $upDirs) . implode('/', $pathParts); + if (is_relative_path($path)) { + return $path; + } + + $from = rtrim($from, '/'); + $path = rtrim($path, '/'); - return $relativePath === '' ? '.' : $relativePath; + $fromParts = explode('/', $from); + $pathParts = explode('/', $path); + + while ($fromParts !== [] && $pathParts !== [] && $fromParts[0] === $pathParts[0]) { + array_shift($fromParts); + array_shift($pathParts); } - /** - * Determines whether the given path is an absolute path. The path is not checked against the filesystem. - */ - function is_absolute_path(null|Stringable|string ...$parts): bool - { - $path = namespace\normalize(...$parts); + $upDirs = count($fromParts); + $relativePath = str_repeat('../', $upDirs) . implode('/', $pathParts); - if (strlen($path) === 0 || '.' === $path[0]) { - return false; - } + return $relativePath === '' ? '.' : $relativePath; +} - if (preg_match('#^[a-zA-Z]:/#', $path)) { - return true; - } +/** + * Determines whether the given path is an absolute path. The path is not checked against the filesystem. + */ +function is_absolute_path(null|Stringable|string ...$parts): bool +{ + $path = namespace\normalize(...$parts); - return '/' === $path[0]; + if (strlen($path) === 0 || '.' === $path[0]) { + return false; } - /** - * Converts the given path to an absolute path. - */ - function to_absolute_path(Stringable|string $cwd, null|Stringable|string ...$parts): string - { - $cwd = namespace\normalize($cwd); - $path = namespace\normalize(...$parts); + if (preg_match('#^[a-zA-Z]:/#', $path)) { + return true; + } - if (starts_with($path, $cwd) && namespace\is_absolute_path($path)) { - return $path; - } + return '/' === $path[0]; +} - $segments = explode('/', namespace\normalize($cwd, $path)); - $resolved = []; +/** + * Converts the given path to an absolute path. + */ +function to_absolute_path(Stringable|string $cwd, null|Stringable|string ...$parts): string +{ + $cwd = namespace\normalize($cwd); + $path = namespace\normalize(...$parts); - foreach ($segments as $part) { - if ($part === '') { - continue; - } + if (starts_with($path, $cwd) && namespace\is_absolute_path($path)) { + return $path; + } - if ($part === '.') { - continue; - } + $segments = explode('/', namespace\normalize($cwd, $path)); + $resolved = []; - if ($part === '..') { - if ($resolved !== []) { - array_pop($resolved); - } - } else { - $resolved[] = $part; - } + foreach ($segments as $part) { + if ($part === '') { + continue; } - $absolutePath = namespace\normalize(...$resolved); + if ($part === '.') { + continue; + } - if (matches($cwd, '#^[a-zA-Z]:/#')) { - return $absolutePath; + if ($part === '..') { + if ($resolved !== []) { + array_pop($resolved); + } + } else { + $resolved[] = $part; } + } + + $absolutePath = namespace\normalize(...$resolved); - return '/' . $absolutePath; + if (matches($cwd, '#^[a-zA-Z]:/#')) { + return $absolutePath; } - /** - * Normalizes the given path to use forward-slashes. - */ - function normalize(null|Stringable|string ...$paths): string - { - if ($paths === []) { - return ''; - } + return '/' . $absolutePath; +} - $paths = array_map( - fn (null|Stringable|string $path) => $path === null ? '' : (string) $path, - $paths, - ); +/** + * Normalizes the given path to use forward-slashes. + */ +function normalize(null|Stringable|string ...$paths): string +{ + if ($paths === []) { + return ''; + } - // Split paths items on forward and backward slashes - $parts = array_reduce($paths, fn (array $carry, string $part) => [...$carry, ...explode('/', $part)], []); - $parts = array_reduce($parts, fn (array $carry, string $part) => [...$carry, ...explode('\\', $part)], []); + $paths = array_map( + fn (null|Stringable|string $path) => $path === null ? '' : (string) $path, + $paths, + ); - // Trim forward and backward slashes - $parts = array_map(fn (string $part) => trim($part, '/\\'), $parts); - $parts = array_filter($parts, fn (string $part) => $part !== ''); + // Split paths items on forward and backward slashes + $parts = array_reduce($paths, fn (array $carry, string $part) => [...$carry, ...explode('/', $part)], []); + $parts = array_reduce($parts, fn (array $carry, string $part) => [...$carry, ...explode('\\', $part)], []); - // Glue parts together - $path = implode('/', $parts); + // Trim forward and backward slashes + $parts = array_map(fn (string $part) => trim($part, '/\\'), $parts); + $parts = array_filter($parts, fn (string $part) => $part !== ''); - // Add / if first entry starts with forward- or backward slash - $firstEntry = $paths[0]; + // Glue parts together + $path = implode('/', $parts); - if (starts_with($firstEntry, ['/', '\\'])) { - $path = '/' . $path; - } + // Add / if first entry starts with forward- or backward slash + $firstEntry = $paths[0]; - // Add / if last entry ends with forward- or backward slash - $lastEntry = $paths[count($paths) - 1]; + if (starts_with($firstEntry, ['/', '\\'])) { + $path = '/' . $path; + } - if ((count($paths) > 1 || strlen($lastEntry) > 1) && ends_with($lastEntry, ['/', '\\'])) { - $path .= '/'; - } + // Add / if last entry ends with forward- or backward slash + $lastEntry = $paths[count($paths) - 1]; - // Restore virtual phar prefix - if (str_starts_with($path, 'phar:')) { - $path = str_replace('phar:', 'phar://', $path); - } + if ((count($paths) > 1 || strlen($lastEntry) > 1) && ends_with($lastEntry, ['/', '\\'])) { + $path .= '/'; + } - return $path; + // Restore virtual phar prefix + if (str_starts_with($path, 'phar:')) { + $path = str_replace('phar:', 'phar://', $path); } + + return $path; } diff --git a/packages/support/src/Regex/functions.php b/packages/support/src/Regex/functions.php index ab24e7a999..21591270d3 100644 --- a/packages/support/src/Regex/functions.php +++ b/packages/support/src/Regex/functions.php @@ -2,136 +2,129 @@ declare(strict_types=1); -namespace Tempest\Support\Regex { - use Closure; - use RuntimeException; - use Stringable; - - use function Tempest\Support\arr; - use function Tempest\Support\Arr\filter; - use function Tempest\Support\Arr\first; - use function Tempest\Support\Arr\get_by_key; - use function Tempest\Support\Arr\wrap; - use function Tempest\Support\Str\starts_with; - use function Tempest\Support\Str\strip_end; - use function Tempest\Support\Str\strip_start; - - /** - * Returns portions of the `$subject` that match the given `$pattern`. If `$global` is set to `true`, returns all matches. Otherwise, only returns the first one. - * - * @param non-empty-string $pattern The pattern to match against. - * @param 0|2|256|512|768 $flags - */ - function get_matches(Stringable|string $subject, Stringable|string $pattern, bool $global = false, int $flags = 0, int $offset = 0): array - { - if (str_ends_with($pattern, 'g')) { - $global = true; - $pattern = strip_end($pattern, 'g'); - } - - return call_preg($global ? 'preg_match_all' : 'preg_match', static function () use ($subject, $pattern, $global, $flags, $offset): array { - $matches = []; - $result = match ($global) { - true => preg_match_all( - (string) $pattern, - (string) $subject, - $matches, - $flags, - $offset, - ), - false => preg_match( - (string) $pattern, - (string) $subject, - $matches, - $flags, - $offset, - ), - }; - - if ($result === false || $result === 0) { - return []; - } - - return $matches; - }); - } - - /** - * Returns the specified matches of `$pattern` in `$subject`. - * - * @param non-empty-string $pattern The pattern to match against. - */ - function get_all_matches( - Stringable|string $subject, - Stringable|string $pattern, - Stringable|string|int|array $matches = 0, - int $offset = 0, - ): array { - $result = get_matches($subject, $pattern, true, PREG_SET_ORDER, $offset); - - return arr($result) - ->map(fn (array $result) => filter($result, fn ($_, string|int $key) => in_array($key, wrap($matches), strict: false))) - ->toArray(); +namespace Tempest\Support\Regex; + +use Closure; +use RuntimeException; +use Stringable; + +use function Tempest\Support\arr; +use function Tempest\Support\Arr\filter; +use function Tempest\Support\Arr\first; +use function Tempest\Support\Arr\get_by_key; +use function Tempest\Support\Arr\wrap; +use function Tempest\Support\Str\starts_with; +use function Tempest\Support\Str\strip_end; +use function Tempest\Support\Str\strip_start; + +/** + * Returns portions of the `$subject` that match the given `$pattern`. If `$global` is set to `true`, returns all matches. Otherwise, only returns the first one. + * + * @param non-empty-string $pattern The pattern to match against. + * @param 0|2|256|512|768 $flags + */ +function get_matches(Stringable|string $subject, Stringable|string $pattern, bool $global = false, int $flags = 0, int $offset = 0): array +{ + if (str_ends_with($pattern, 'g')) { + $global = true; + $pattern = strip_end($pattern, 'g'); } - /** - * Returns the specified match of `$pattern` in `$subject`. If no match is specified, returns the whole matching array. - * - * @param non-empty-string $pattern The pattern to match against. - * @param 0|256|512|768 $flags - */ - function get_match( - Stringable|string $subject, - Stringable|string $pattern, - null|array|Stringable|int|string $match = null, - mixed $default = null, - int $flags = 0, - int $offset = 0, - ): null|int|string|array { - $result = get_matches($subject, $pattern, false, $flags, $offset); - - if ($match === null) { - return $result; + return call_preg($global ? 'preg_match_all' : 'preg_match', static function () use ($subject, $pattern, $global, $flags, $offset): array { + $matches = []; + $result = match ($global) { + true => preg_match_all( + (string) $pattern, + (string) $subject, + $matches, + $flags, + $offset, + ), + false => preg_match( + (string) $pattern, + (string) $subject, + $matches, + $flags, + $offset, + ), + }; + + if ($result === false || $result === 0) { + return []; } - if (is_array($match)) { - return arr($result) - ->filter(fn ($_, string|int $key) => in_array($key, $match, strict: false)) - ->mapWithKeys(fn (array $matches, string|int $key) => yield $key => first($matches)) - ->toArray(); - } + return $matches; + }); +} + +/** + * Returns the specified matches of `$pattern` in `$subject`. + * + * @param non-empty-string $pattern The pattern to match against. + */ +function get_all_matches( + Stringable|string $subject, + Stringable|string $pattern, + Stringable|string|int|array $matches = 0, + int $offset = 0, +): array { + $result = get_matches($subject, $pattern, true, PREG_SET_ORDER, $offset); + + return arr($result) + ->map(fn (array $result) => filter($result, fn ($_, string|int $key) => in_array($key, wrap($matches), strict: false))) + ->toArray(); +} - return get_by_key($result, $match, $default); +/** + * Returns the specified match of `$pattern` in `$subject`. If no match is specified, returns the whole matching array. + * + * @param non-empty-string $pattern The pattern to match against. + * @param 0|256|512|768 $flags + */ +function get_match( + Stringable|string $subject, + Stringable|string $pattern, + null|array|Stringable|int|string $match = null, + mixed $default = null, + int $flags = 0, + int $offset = 0, +): null|int|string|array { + $result = get_matches($subject, $pattern, false, $flags, $offset); + + if ($match === null) { + return $result; } - /** - * Determines if $subject matches the given $pattern. - * - * @param non-empty-string $pattern The pattern to match against. - */ - function matches(string $subject, string $pattern, int $offset = 0): bool - { - return call_preg('preg_match', static fn (): int|false => preg_match($pattern, $subject, offset: $offset)) === 1; + if (is_array($match)) { + return arr($result) + ->filter(fn ($_, string|int $key) => in_array($key, $match, strict: false)) + ->mapWithKeys(fn (array $matches, string|int $key) => yield $key => first($matches)) + ->toArray(); } - /** - * Returns the '$haystack' string with all occurrences of `$pattern` replaced by `$replacement`. - * - * @param non-empty-string $pattern The pattern to search for. - * @param null|positive-int $limit The maximum possible replacements for $pattern within $haystack. - */ - function replace(array|string $haystack, array|string $pattern, Closure|array|string $replacement, ?int $limit = null): string - { - if ($replacement instanceof Closure) { - return (string) call_preg('preg_replace_callback', static fn (): ?string => preg_replace_callback( - $pattern, - $replacement, - $haystack, - $limit ?? -1, - )); - } + return get_by_key($result, $match, $default); +} - return (string) call_preg('preg_replace', static fn (): ?string => preg_replace( +/** + * Determines if $subject matches the given $pattern. + * + * @param non-empty-string $pattern The pattern to match against. + */ +function matches(string $subject, string $pattern, int $offset = 0): bool +{ + return call_preg('preg_match', static fn (): int|false => preg_match($pattern, $subject, offset: $offset)) === 1; +} + +/** + * Returns the '$haystack' string with all occurrences of `$pattern` replaced by `$replacement`. + * + * @param non-empty-string $pattern The pattern to search for. + * @param null|positive-int $limit The maximum possible replacements for $pattern within $haystack. + */ +function replace(array|string $haystack, array|string $pattern, Closure|array|string $replacement, ?int $limit = null): string +{ + if ($replacement instanceof Closure) { + return (string) call_preg('preg_replace_callback', static fn (): ?string => preg_replace_callback( $pattern, $replacement, $haystack, @@ -139,76 +132,83 @@ function replace(array|string $haystack, array|string $pattern, Closure|array|st )); } - /** - * Returns the '$haystack' string with all occurrences of the keys of - * '$replacements' (patterns) replaced by the corresponding values. - * - * @param array $replacements An array where the keys are regular expression patterns, and the values are the replacements. - * @param null|positive-int $limit The maximum possible replacements for each pattern in $haystack. - */ - function replace_every(string $haystack, array $replacements, ?int $limit = null): string - { - return (string) call_preg('preg_replace', static fn (): ?string => preg_replace( - array_keys($replacements), - array_values($replacements), - $haystack, - $limit ?? -1, - )); - } + return (string) call_preg('preg_replace', static fn (): ?string => preg_replace( + $pattern, + $replacement, + $haystack, + $limit ?? -1, + )); +} - /** - * @return null|array{message: string, code: int, pattern_message: null|string} - * @internal - */ - function get_preg_error(string $function): ?array - { - $code = preg_last_error(); - if ($code === PREG_NO_ERROR) { - return null; - } +/** + * Returns the '$haystack' string with all occurrences of the keys of + * '$replacements' (patterns) replaced by the corresponding values. + * + * @param array $replacements An array where the keys are regular expression patterns, and the values are the replacements. + * @param null|positive-int $limit The maximum possible replacements for each pattern in $haystack. + */ +function replace_every(string $haystack, array $replacements, ?int $limit = null): string +{ + return (string) call_preg('preg_replace', static fn (): ?string => preg_replace( + array_keys($replacements), + array_values($replacements), + $haystack, + $limit ?? -1, + )); +} - $messages = [ - PREG_INTERNAL_ERROR => 'Internal error', - PREG_BAD_UTF8_ERROR => 'Malformed UTF-8 characters, possibly incorrectly encoded', - PREG_BAD_UTF8_OFFSET_ERROR => 'The offset did not correspond to the beginning of a valid UTF-8 code point', - PREG_BACKTRACK_LIMIT_ERROR => 'Backtrack limit exhausted', - PREG_RECURSION_LIMIT_ERROR => 'Recursion limit exhausted', - PREG_JIT_STACKLIMIT_ERROR => 'JIT stack limit exhausted', - ]; - - $message = $messages[$code] ?? 'Unknown error'; - $result = ['message' => $message, 'code' => $code, 'pattern_message' => null]; - $error = error_get_last(); - - if ($error !== null && starts_with($error['message'], $function)) { - $result['pattern_message'] = strip_start($error['message'], sprintf('%s(): ', $function)); - } +/** + * @return null|array{message: string, code: int, pattern_message: null|string} + * @internal + */ +function get_preg_error(string $function): ?array +{ + $code = preg_last_error(); + if ($code === PREG_NO_ERROR) { + return null; + } - return $result; + $messages = [ + PREG_INTERNAL_ERROR => 'Internal error', + PREG_BAD_UTF8_ERROR => 'Malformed UTF-8 characters, possibly incorrectly encoded', + PREG_BAD_UTF8_OFFSET_ERROR => 'The offset did not correspond to the beginning of a valid UTF-8 code point', + PREG_BACKTRACK_LIMIT_ERROR => 'Backtrack limit exhausted', + PREG_RECURSION_LIMIT_ERROR => 'Recursion limit exhausted', + PREG_JIT_STACKLIMIT_ERROR => 'JIT stack limit exhausted', + ]; + + $message = $messages[$code] ?? 'Unknown error'; + $result = ['message' => $message, 'code' => $code, 'pattern_message' => null]; + $error = error_get_last(); + + if ($error !== null && starts_with($error['message'], $function)) { + $result['pattern_message'] = strip_start($error['message'], sprintf('%s(): ', $function)); } - /** - * @template T - * - * @param non-empty-string $function - * @param Closure(): T $closure - * - * @return T - * @internal - */ - function call_preg(string $function, Closure $closure): mixed - { - error_clear_last(); - $result = @$closure(); - - if ($error = get_preg_error($function)) { - if ($error['pattern_message'] !== null) { - throw new InvalidPatternException($error['pattern_message'], $error['code']); - } - - throw new RuntimeException($error['message'], $error['code']); + return $result; +} + +/** + * @template T + * + * @param non-empty-string $function + * @param Closure(): T $closure + * + * @return T + * @internal + */ +function call_preg(string $function, Closure $closure): mixed +{ + error_clear_last(); + $result = @$closure(); + + if ($error = get_preg_error($function)) { + if ($error['pattern_message'] !== null) { + throw new InvalidPatternException($error['pattern_message'], $error['code']); } - return $result; + throw new RuntimeException($error['message'], $error['code']); } + + return $result; } diff --git a/packages/support/src/Str/functions.php b/packages/support/src/Str/functions.php index e56f30a22c..5842b86a7c 100644 --- a/packages/support/src/Str/functions.php +++ b/packages/support/src/Str/functions.php @@ -2,933 +2,933 @@ declare(strict_types=1); -namespace Tempest\Support\Str { - use BackedEnum; - use Stringable; - use Tempest\Support\Arr; - use UnitEnum; - use voku\helper\ASCII; - - use function levenshtein as php_levenshtein; - use function metaphone as php_metaphone; - use function strip_tags as php_strip_tags; - use function Tempest\Support\arr; - - /** - * Converts the given string to title case. - */ - function to_title_case(Stringable|string $string): string - { - return mb_convert_case((string) $string, mode: MB_CASE_TITLE, encoding: 'UTF-8'); - } - - /** - * Converts the given string to a naive sentence case. This doesn't detect proper nouns and proper adjectives that should stay capitalized. - */ - function to_sentence_case(Stringable|string $string): string - { - $words = array_map( - callback: fn (string $string) => to_lower_case($string), - array: to_words($string), - ); - - return upper_first(implode(' ', $words)); - } - - /** - * Converts the given string to lower case. - */ - function to_lower_case(Stringable|string $string): string - { - return mb_strtolower((string) $string, encoding: 'UTF-8'); - } - - /** - * Converts the given string to upper case. - */ - function to_upper_case(Stringable|string $string): string - { - return mb_strtoupper((string) $string, encoding: 'UTF-8'); - } - - /** - * Converts the given string to snake case. - * - * @mago-expect lint:require-preg-quote-delimiter - */ - function to_snake_case(Stringable|string $string, Stringable|string $delimiter = '_'): string - { - $string = (string) $string; - $delimiter = (string) $delimiter; - - if (ctype_lower($string)) { - return $string; - } +namespace Tempest\Support\Str; + +use BackedEnum; +use Stringable; +use Tempest\Support\Arr; +use UnitEnum; +use voku\helper\ASCII; + +use function levenshtein as php_levenshtein; +use function metaphone as php_metaphone; +use function strip_tags as php_strip_tags; +use function Tempest\Support\arr; + +/** + * Converts the given string to title case. + */ +function to_title_case(Stringable|string $string): string +{ + return mb_convert_case((string) $string, mode: MB_CASE_TITLE, encoding: 'UTF-8'); +} - $string = preg_replace('/(?<=\p{Ll}|\p{N})(\p{Lu})/u', $delimiter . '$1', $string); - $string = preg_replace('/(?<=\p{Lu})(\p{Lu}\p{Ll})/u', $delimiter . '$1', $string); - $string = preg_replace('![^' . preg_quote($delimiter) . '\pL\pN\s]+!u', $delimiter, mb_strtolower($string, 'UTF-8')); - $string = preg_replace('/\s+/u', $delimiter, $string); - $string = trim($string, $delimiter); +/** + * Converts the given string to a naive sentence case. This doesn't detect proper nouns and proper adjectives that should stay capitalized. + */ +function to_sentence_case(Stringable|string $string): string +{ + $words = array_map( + callback: fn (string $string) => to_lower_case($string), + array: to_words($string), + ); + + return upper_first(implode(' ', $words)); +} - return namespace\deduplicate($string, $delimiter); - } - - /** - * Returns an array of words from the specified string. - * This is more accurate than {@see str_word_count()}. - */ - function to_words(Stringable|string $string): array - { - // Remove 'words' that don't consist of alphanumerical characters or punctuation - $words = trim(preg_replace("#[^(\w|\d|\'|\"|\.|\!|\?|;|,|\\|\/|\-|:|\&|@)]+#", ' ', (string) $string)); - // Remove one-letter 'words' that consist only of punctuation - $words = trim(preg_replace("#\s*[(\'|\"|\.|\!|\?|;|,|\\|\/|\-|:|\&|@)]\s*#", ' ', $words)); - - return array_values(array_filter(explode(' ', namespace\deduplicate($words)))); - } - - /** - * Counts the number of words in the given string. - */ - function word_count(Stringable|string $string): int - { - return count(to_words($string)); - } - - /** - * Converts the given string to kebab case. - */ - function to_kebab_case(Stringable|string $string): string - { - return to_snake_case((string) $string, delimiter: '-'); - } - - /** - * Converts the given string to pascal case. - */ - function to_pascal_case(Stringable|string $string): string - { - $string = (string) $string; - $words = explode(' ', str_replace(['-', '_'], ' ', $string)); - $studlyWords = array_map(mb_ucfirst(...), $words); - - return implode('', $studlyWords); - } - - /** - * Converts the given string to camel case. - */ - function to_camel_case(Stringable|string $string): string - { - return lcfirst(to_pascal_case((string) $string)); - } - - /** - * Converts the given string to an URL-safe slug. - * - * @param bool $replaceSymbols Adds some more replacements e.g. "£" with "pound". - */ - function to_slug(Stringable|string $string, Stringable|string $separator = '-', array $replacements = [], bool $replaceSymbols = true): string - { - return ASCII::to_slugify((string) $string, (string) $separator, replacements: $replacements, replace_extra_symbols: $replaceSymbols); - } - - /** - * Transliterates the given string to ASCII. - * - * @param string $language Language of the source string. Defaults to english. - */ - function to_ascii(Stringable|string $string, Stringable|string $language = 'en'): string - { - return ASCII::to_ascii((string) $string, (string) $language, replace_single_chars_only: false); - } - - /** - * Checks whether the given string is valid ASCII. - */ - function is_ascii(Stringable|string $string): bool - { - return ASCII::is_ascii((string) $string); - } - - /** - * Changes the case of the first letter to uppercase. - */ - function upper_first(Stringable|string $string): string - { - return mb_ucfirst((string) $string); - } - - /** - * Changes the case of the first letter to lowercase. - */ - function lower_first(Stringable|string $string): string - { - return mb_lcfirst((string) $string); - } - - /** - * Replaces consecutive instances of a given character with a single character. - */ - function deduplicate(Stringable|string $string, Stringable|string|iterable $characters = ' '): string - { - $string = (string) $string; - - foreach (Arr\wrap($characters) as $character) { - $string = preg_replace('/' . preg_quote($character, '/') . '+/u', $character, $string); - } +/** + * Converts the given string to lower case. + */ +function to_lower_case(Stringable|string $string): string +{ + return mb_strtolower((string) $string, encoding: 'UTF-8'); +} +/** + * Converts the given string to upper case. + */ +function to_upper_case(Stringable|string $string): string +{ + return mb_strtoupper((string) $string, encoding: 'UTF-8'); +} + +/** + * Converts the given string to snake case. + * + * @mago-expect lint:require-preg-quote-delimiter + */ +function to_snake_case(Stringable|string $string, Stringable|string $delimiter = '_'): string +{ + $string = (string) $string; + $delimiter = (string) $delimiter; + + if (ctype_lower($string)) { return $string; } - /** - * Ensures the given string starts with the specified `$prefix`. - */ - function ensure_starts_with(Stringable|string $string, Stringable|string $prefix): string - { - return $prefix . preg_replace('/^(?:' . preg_quote($prefix, '/') . ')+/u', replacement: '', subject: (string) $string); - } + $string = preg_replace('/(?<=\p{Ll}|\p{N})(\p{Lu})/u', $delimiter . '$1', $string); + $string = preg_replace('/(?<=\p{Lu})(\p{Lu}\p{Ll})/u', $delimiter . '$1', $string); + $string = preg_replace('![^' . preg_quote($delimiter) . '\pL\pN\s]+!u', $delimiter, mb_strtolower($string, 'UTF-8')); + $string = preg_replace('/\s+/u', $delimiter, $string); + $string = trim($string, $delimiter); - /** - * Ensures the given string ends with the specified `$cap`. - */ - function ensure_ends_with(Stringable|string $string, Stringable|string $cap): string - { - return preg_replace('/(?:' . preg_quote((string) $cap, '/') . ')+$/u', replacement: '', subject: (string) $string) . $cap; - } + return namespace\deduplicate($string, $delimiter); +} + +/** + * Returns an array of words from the specified string. + * This is more accurate than {@see str_word_count()}. + */ +function to_words(Stringable|string $string): array +{ + // Remove 'words' that don't consist of alphanumerical characters or punctuation + $words = trim(preg_replace("#[^(\w|\d|\'|\"|\.|\!|\?|;|,|\\|\/|\-|:|\&|@)]+#", ' ', (string) $string)); + // Remove one-letter 'words' that consist only of punctuation + $words = trim(preg_replace("#\s*[(\'|\"|\.|\!|\?|;|,|\\|\/|\-|:|\&|@)]\s*#", ' ', $words)); + + return array_values(array_filter(explode(' ', namespace\deduplicate($words)))); +} - /** - * Returns the remainder of the string after the first occurrence of the given value. - */ - function after_first(Stringable|string $string, Stringable|string|array $search): string - { - $string = (string) $string; - $search = normalize_string($search); +/** + * Counts the number of words in the given string. + */ +function word_count(Stringable|string $string): int +{ + return count(to_words($string)); +} - if ($search === '' || $search === []) { - return $string; - } +/** + * Converts the given string to kebab case. + */ +function to_kebab_case(Stringable|string $string): string +{ + return to_snake_case((string) $string, delimiter: '-'); +} + +/** + * Converts the given string to pascal case. + */ +function to_pascal_case(Stringable|string $string): string +{ + $string = (string) $string; + $words = explode(' ', str_replace(['-', '_'], ' ', $string)); + $studlyWords = array_map(mb_ucfirst(...), $words); - $nearestPosition = mb_strlen($string); // Initialize with a large value - $foundSearch = ''; + return implode('', $studlyWords); +} - foreach (Arr\wrap($search) as $term) { - $position = mb_strpos($string, $term); +/** + * Converts the given string to camel case. + */ +function to_camel_case(Stringable|string $string): string +{ + return lcfirst(to_pascal_case((string) $string)); +} - if ($position !== false && $position < $nearestPosition) { - $nearestPosition = $position; - $foundSearch = $term; - } - } +/** + * Converts the given string to an URL-safe slug. + * + * @param bool $replaceSymbols Adds some more replacements e.g. "£" with "pound". + */ +function to_slug(Stringable|string $string, Stringable|string $separator = '-', array $replacements = [], bool $replaceSymbols = true): string +{ + return ASCII::to_slugify((string) $string, (string) $separator, replacements: $replacements, replace_extra_symbols: $replaceSymbols); +} - if ($nearestPosition === mb_strlen($string)) { - return $string; - } +/** + * Transliterates the given string to ASCII. + * + * @param string $language Language of the source string. Defaults to english. + */ +function to_ascii(Stringable|string $string, Stringable|string $language = 'en'): string +{ + return ASCII::to_ascii((string) $string, (string) $language, replace_single_chars_only: false); +} + +/** + * Checks whether the given string is valid ASCII. + */ +function is_ascii(Stringable|string $string): bool +{ + return ASCII::is_ascii((string) $string); +} + +/** + * Changes the case of the first letter to uppercase. + */ +function upper_first(Stringable|string $string): string +{ + return mb_ucfirst((string) $string); +} + +/** + * Changes the case of the first letter to lowercase. + */ +function lower_first(Stringable|string $string): string +{ + return mb_lcfirst((string) $string); +} + +/** + * Replaces consecutive instances of a given character with a single character. + */ +function deduplicate(Stringable|string $string, Stringable|string|iterable $characters = ' '): string +{ + $string = (string) $string; - return mb_substr($string, $nearestPosition + mb_strlen($foundSearch)); + foreach (Arr\wrap($characters) as $character) { + $string = preg_replace('/' . preg_quote($character, '/') . '+/u', $character, $string); } - /** - * Returns the remainder of the string after the last occurrence of the given value. - */ - function after_last(Stringable|string $string, Stringable|string|array $search): string - { - $string = (string) $string; - $search = normalize_string($search); + return $string; +} - if ($search === '' || $search === []) { - return $string; - } +/** + * Ensures the given string starts with the specified `$prefix`. + */ +function ensure_starts_with(Stringable|string $string, Stringable|string $prefix): string +{ + return $prefix . preg_replace('/^(?:' . preg_quote($prefix, '/') . ')+/u', replacement: '', subject: (string) $string); +} - $farthestPosition = -1; - $foundSearch = null; +/** + * Ensures the given string ends with the specified `$cap`. + */ +function ensure_ends_with(Stringable|string $string, Stringable|string $cap): string +{ + return preg_replace('/(?:' . preg_quote((string) $cap, '/') . ')+$/u', replacement: '', subject: (string) $string) . $cap; +} - foreach (Arr\wrap($search) as $term) { - $position = mb_strrpos($string, $term); +/** + * Returns the remainder of the string after the first occurrence of the given value. + */ +function after_first(Stringable|string $string, Stringable|string|array $search): string +{ + $string = (string) $string; + $search = normalize_string($search); - if ($position !== false && $position > $farthestPosition) { - $farthestPosition = $position; - $foundSearch = $term; - } - } + if ($search === '' || $search === []) { + return $string; + } - if ($farthestPosition === -1 || $foundSearch === null) { - return $string; + $nearestPosition = mb_strlen($string); // Initialize with a large value + $foundSearch = ''; + + foreach (Arr\wrap($search) as $term) { + $position = mb_strpos($string, $term); + + if ($position !== false && $position < $nearestPosition) { + $nearestPosition = $position; + $foundSearch = $term; } + } - return mb_substr($string, $farthestPosition + mb_strlen($foundSearch)); + if ($nearestPosition === mb_strlen($string)) { + return $string; } - /** - * Returns the portion of the string before the first occurrence of the given value. - */ - function before_first(Stringable|string $string, Stringable|string|array $search): string - { - $string = (string) $string; - $search = normalize_string($search); + return mb_substr($string, $nearestPosition + mb_strlen($foundSearch)); +} - if ($search === '' || $search === []) { - return $string; - } +/** + * Returns the remainder of the string after the last occurrence of the given value. + */ +function after_last(Stringable|string $string, Stringable|string|array $search): string +{ + $string = (string) $string; + $search = normalize_string($search); - $nearestPosition = mb_strlen($string); + if ($search === '' || $search === []) { + return $string; + } - foreach (Arr\wrap($search) as $char) { - $position = mb_strpos($string, $char); + $farthestPosition = -1; + $foundSearch = null; - if ($position !== false && $position < $nearestPosition) { - $nearestPosition = $position; - } - } + foreach (Arr\wrap($search) as $term) { + $position = mb_strrpos($string, $term); - if ($nearestPosition === mb_strlen($string)) { - return $string; + if ($position !== false && $position > $farthestPosition) { + $farthestPosition = $position; + $foundSearch = $term; } + } - return mb_substr($string, start: 0, length: $nearestPosition); + if ($farthestPosition === -1 || $foundSearch === null) { + return $string; } - /** - * Returns the portion of the string before the last occurrence of the given value. - */ - function before_last(Stringable|string $string, Stringable|string|array $search): string - { - $string = (string) $string; - $search = normalize_string($search); + return mb_substr($string, $farthestPosition + mb_strlen($foundSearch)); +} - if ($search === '' || $search === []) { - return $string; - } +/** + * Returns the portion of the string before the first occurrence of the given value. + */ +function before_first(Stringable|string $string, Stringable|string|array $search): string +{ + $string = (string) $string; + $search = normalize_string($search); - $farthestPosition = -1; + if ($search === '' || $search === []) { + return $string; + } - foreach (Arr\wrap($search) as $char) { - $position = mb_strrpos($string, $char); + $nearestPosition = mb_strlen($string); - if ($position !== false && $position > $farthestPosition) { - $farthestPosition = $position; - } - } + foreach (Arr\wrap($search) as $char) { + $position = mb_strpos($string, $char); - if ($farthestPosition === -1) { - return $string; + if ($position !== false && $position < $nearestPosition) { + $nearestPosition = $position; } - - return mb_substr($string, start: 0, length: $farthestPosition); } - /** - * Returns the multi-bytes length of the string. - */ - function length(Stringable|string $string): int - { - return mb_strlen((string) $string); + if ($nearestPosition === mb_strlen($string)) { + return $string; } - /** - * Returns the base name of the string, assuming the string is a class name. - */ - function class_basename(Stringable|string $string): string - { - return basename(str_replace('\\', '/', (string) $string)); + return mb_substr($string, start: 0, length: $nearestPosition); +} + +/** + * Returns the portion of the string before the last occurrence of the given value. + */ +function before_last(Stringable|string $string, Stringable|string|array $search): string +{ + $string = (string) $string; + $search = normalize_string($search); + + if ($search === '' || $search === []) { + return $string; } - /** - * Asserts whether the string starts with one of the given needles. - */ - function starts_with(Stringable|string $string, Stringable|string|array $needles): bool - { - $string = (string) $string; + $farthestPosition = -1; + + foreach (Arr\wrap($search) as $char) { + $position = mb_strrpos($string, $char); - if (! is_array($needles)) { - $needles = [$needles]; + if ($position !== false && $position > $farthestPosition) { + $farthestPosition = $position; } + } - return array_any($needles, fn ($needle) => str_starts_with($string, (string) $needle)); + if ($farthestPosition === -1) { + return $string; } - /** - * Asserts whether the string ends with one of the given `$needles`. - */ - function ends_with(Stringable|string $string, Stringable|string|array $needles): bool - { - $string = (string) $string; + return mb_substr($string, start: 0, length: $farthestPosition); +} - if (! is_array($needles)) { - $needles = [$needles]; - } +/** + * Returns the multi-bytes length of the string. + */ +function length(Stringable|string $string): int +{ + return mb_strlen((string) $string); +} + +/** + * Returns the base name of the string, assuming the string is a class name. + */ +function class_basename(Stringable|string $string): string +{ + return basename(str_replace('\\', '/', (string) $string)); +} + +/** + * Asserts whether the string starts with one of the given needles. + */ +function starts_with(Stringable|string $string, Stringable|string|array $needles): bool +{ + $string = (string) $string; + + if (! is_array($needles)) { + $needles = [$needles]; + } - return array_any($needles, static fn ($needle) => str_ends_with($string, (string) $needle)); + return array_any($needles, fn ($needle) => str_starts_with($string, (string) $needle)); +} + +/** + * Asserts whether the string ends with one of the given `$needles`. + */ +function ends_with(Stringable|string $string, Stringable|string|array $needles): bool +{ + $string = (string) $string; + + if (! is_array($needles)) { + $needles = [$needles]; } - /** - * Replaces the first occurrence of `$search` with `$replace`. - */ - function replace_first(Stringable|string $string, array|Stringable|string $search, Stringable|string $replace): string - { - $string = (string) $string; - $search = normalize_string($search); + return array_any($needles, static fn ($needle) => str_ends_with($string, (string) $needle)); +} - foreach (Arr\wrap($search) as $item) { - if ($item === '') { - continue; - } +/** + * Replaces the first occurrence of `$search` with `$replace`. + */ +function replace_first(Stringable|string $string, array|Stringable|string $search, Stringable|string $replace): string +{ + $string = (string) $string; + $search = normalize_string($search); - $position = strpos($string, (string) $item); + foreach (Arr\wrap($search) as $item) { + if ($item === '') { + continue; + } - if ($position === false) { - continue; - } + $position = strpos($string, (string) $item); - return substr_replace($string, $replace, $position, strlen($item)); + if ($position === false) { + continue; } - return $string; + return substr_replace($string, $replace, $position, strlen($item)); } - /** - * Replaces the last occurrence of `$search` with `$replace`. - */ - function replace_last(Stringable|string $string, array|Stringable|string $search, Stringable|string $replace): string - { - $string = (string) $string; - $search = normalize_string($search); + return $string; +} - foreach (Arr\wrap($search) as $item) { - if ($item === '') { - continue; - } +/** + * Replaces the last occurrence of `$search` with `$replace`. + */ +function replace_last(Stringable|string $string, array|Stringable|string $search, Stringable|string $replace): string +{ + $string = (string) $string; + $search = normalize_string($search); - $position = strrpos($string, (string) $item); + foreach (Arr\wrap($search) as $item) { + if ($item === '') { + continue; + } - if ($position === false) { - continue; - } + $position = strrpos($string, (string) $item); - return substr_replace($string, $replace, $position, strlen($item)); + if ($position === false) { + continue; } - return $string; + return substr_replace($string, $replace, $position, strlen($item)); } - /** - * Replaces `$search` with `$replace` if `$search` is at the end of the string. - */ - function replace_end(Stringable|string $string, array|Stringable|string $search, Stringable|string $replace): string - { - $string = (string) $string; - $search = normalize_string($search); + return $string; +} - foreach (Arr\wrap($search) as $item) { - if ($item === '') { - continue; - } +/** + * Replaces `$search` with `$replace` if `$search` is at the end of the string. + */ +function replace_end(Stringable|string $string, array|Stringable|string $search, Stringable|string $replace): string +{ + $string = (string) $string; + $search = normalize_string($search); - if (! ends_with($string, $item)) { - continue; - } + foreach (Arr\wrap($search) as $item) { + if ($item === '') { + continue; + } - return replace_last($string, $item, $replace); + if (! ends_with($string, $item)) { + continue; } - return $string; + return replace_last($string, $item, $replace); } - /** - * Replaces `$search` with `$replace` if `$search` is at the start of the string. - */ - function replace_start(Stringable|string $string, array|Stringable|string $search, Stringable|string $replace): string - { - $string = (string) $string; + return $string; +} - foreach (Arr\wrap($search) as $item) { - if ($item === '') { - continue; - } +/** + * Replaces `$search` with `$replace` if `$search` is at the start of the string. + */ +function replace_start(Stringable|string $string, array|Stringable|string $search, Stringable|string $replace): string +{ + $string = (string) $string; - if (! starts_with($string, $item)) { - continue; - } + foreach (Arr\wrap($search) as $item) { + if ($item === '') { + continue; + } - return replace_first($string, $item, $replace); + if (! starts_with($string, $item)) { + continue; } - return $string; + return replace_first($string, $item, $replace); } - /** - * Strips the specified `$prefix` from the start of the string. - */ - function strip_start(Stringable|string $string, array|Stringable|string $prefix): string - { - return replace_start($string, $prefix, ''); - } + return $string; +} - /** - * Strips the specified `$suffix` from the end of the string. - */ - function strip_end(Stringable|string $string, array|Stringable|string $suffix): string - { - return replace_end($string, $suffix, ''); +/** + * Strips the specified `$prefix` from the start of the string. + */ +function strip_start(Stringable|string $string, array|Stringable|string $prefix): string +{ + return replace_start($string, $prefix, ''); +} + +/** + * Strips the specified `$suffix` from the end of the string. + */ +function strip_end(Stringable|string $string, array|Stringable|string $suffix): string +{ + return replace_end($string, $suffix, ''); +} + +/** + * Replaces the portion of the specified `$length` at the specified `$position` with the specified `$replace`. + */ +function replace_at(Stringable|string $string, int $position, int $length, Stringable|string $replace): string +{ + $string = (string) $string; + + if ($length < 0) { + $position += $length; + $length = abs($length); } - /** - * Replaces the portion of the specified `$length` at the specified `$position` with the specified `$replace`. - */ - function replace_at(Stringable|string $string, int $position, int $length, Stringable|string $replace): string - { - $string = (string) $string; + return substr_replace($string, (string) $replace, $position, $length); +} - if ($length < 0) { - $position += $length; - $length = abs($length); - } +/** + * Returns the '$haystack' string with all occurrences of the keys of `$replacements` replaced by the corresponding values. + * + * @param array $replacements + */ +function replace_every(Stringable|string $haystack, array $replacements): string +{ + $string = (string) $haystack; - return substr_replace($string, (string) $replace, $position, $length); + foreach ($replacements as $needle => $replacement) { + $string = namespace\replace($string, $needle, (string) $replacement); } - /** - * Returns the '$haystack' string with all occurrences of the keys of `$replacements` replaced by the corresponding values. - * - * @param array $replacements - */ - function replace_every(Stringable|string $haystack, array $replacements): string - { - $string = (string) $haystack; + return $string; +} - foreach ($replacements as $needle => $replacement) { - $string = namespace\replace($string, $needle, (string) $replacement); - } +/** + * Appends the given strings to the string. + */ +function append(Stringable|string $string, string|Stringable ...$append): string +{ + return $string . implode('', $append); +} + +/** + * Prepends the given strings to the string. + */ +function prepend(Stringable|string $string, string|Stringable ...$prepend): string +{ + return implode('', $prepend) . $string; +} +/** + * Returns the portion of the string between the widest possible instances of the given strings. + */ +function between(Stringable|string $string, string|Stringable $from, string|Stringable $to): string +{ + $string = (string) $string; + $from = normalize_string($from); + $to = normalize_string($to); + + if ($from === '' || $to === '') { return $string; } - /** - * Appends the given strings to the string. - */ - function append(Stringable|string $string, string|Stringable ...$append): string - { - return $string . implode('', $append); - } + return before_last(after_first($string, $from), $to); +} - /** - * Prepends the given strings to the string. - */ - function prepend(Stringable|string $string, string|Stringable ...$prepend): string - { - return implode('', $prepend) . $string; - } +/** + * Wraps the string with the given string. If `$after` is specified, it will be appended instead of `$before`. + */ +function wrap(Stringable|string $string, string|Stringable $before, string|Stringable|null $after = null): string +{ + return $before . $string . ($after ??= $before); +} - /** - * Returns the portion of the string between the widest possible instances of the given strings. - */ - function between(Stringable|string $string, string|Stringable $from, string|Stringable $to): string - { - $string = (string) $string; - $from = normalize_string($from); - $to = normalize_string($to); +/** + * Removes the specified `$before` and `$after` from the beginning and the end of the string. + */ +function unwrap(Stringable|string $string, string|Stringable $before, string|Stringable|null $after = null, bool $strict = true): string +{ + $string = (string) $string; - if ($from === '' || $to === '') { - return $string; - } + if ($string === '') { + return $string; + } - return before_last(after_first($string, $from), $to); + if ($after === null) { + $after = $before; } - /** - * Wraps the string with the given string. If `$after` is specified, it will be appended instead of `$before`. - */ - function wrap(Stringable|string $string, string|Stringable $before, string|Stringable|null $after = null): string - { - return $before . $string . ($after ??= $before); + if (! $strict) { + return before_last(after_first($string, $before), $after); } - /** - * Removes the specified `$before` and `$after` from the beginning and the end of the string. - */ - function unwrap(Stringable|string $string, string|Stringable $before, string|Stringable|null $after = null, bool $strict = true): string - { - $string = (string) $string; + if (starts_with($string, $before) && ends_with($string, $after)) { + return before_last(after_first($string, $before), $after); + } - if ($string === '') { - return $string; - } + return $string; +} - if ($after === null) { - $after = $before; - } +/** + * Replaces all occurrences of the given `$search` with `$replace`. + */ +function replace(Stringable|string $string, Stringable|string|array $search, Stringable|string|array $replace): string +{ + $string = (string) $string; + $search = normalize_string($search); + $replace = normalize_string($replace); - if (! $strict) { - return before_last(after_first($string, $before), $after); - } + return str_replace($search, $replace, $string); +} - if (starts_with($string, $before) && ends_with($string, $after)) { - return before_last(after_first($string, $before), $after); - } +/** + * Extracts an excerpt from the string. + */ +function excerpt(Stringable|string $string, int $from, int $to, bool $asArray = false): string|array +{ + $string = (string) $string; + $lines = explode(PHP_EOL, $string); + + $from = max(0, $from - 1); + $to = min($to - 1, count($lines)); + $lines = array_slice($lines, offset: $from, length: $to - $from + 1, preserve_keys: true); + + if ($asArray) { + return arr($lines) + ->mapWithKeys(fn (string $line, int $number) => yield $number + 1 => $line) + ->toArray(); + } + + return implode(PHP_EOL, $lines); +} +/** + * Truncates the string to the specified amount of characters. + */ +function truncate_end(Stringable|string $string, int $characters, Stringable|string $end = ''): string +{ + $string = (string) $string; + $end = (string) $end; + + if (mb_strwidth($string, 'UTF-8') <= $characters) { return $string; } - /** - * Replaces all occurrences of the given `$search` with `$replace`. - */ - function replace(Stringable|string $string, Stringable|string|array $search, Stringable|string|array $replace): string - { - $string = (string) $string; - $search = normalize_string($search); - $replace = normalize_string($replace); - - return str_replace($search, $replace, $string); + if ($characters < 0) { + $characters = mb_strlen($string) + $characters; } - /** - * Extracts an excerpt from the string. - */ - function excerpt(Stringable|string $string, int $from, int $to, bool $asArray = false): string|array - { - $string = (string) $string; - $lines = explode(PHP_EOL, $string); + return rtrim(mb_strimwidth($string, 0, $characters, encoding: 'UTF-8')) . $end; +} + +/** + * Truncates the string to the specified amount of characters from the start. + */ +function truncate_start(Stringable|string $string, int $characters, Stringable|string $start = ''): string +{ + return reverse(truncate_end(reverse((string) $string), $characters, (string) $start)); +} - $from = max(0, $from - 1); - $to = min($to - 1, count($lines)); - $lines = array_slice($lines, offset: $from, length: $to - $from + 1, preserve_keys: true); +/** + * Reverses the string. + */ +function reverse(Stringable|string $string): string +{ + return implode('', array_reverse(mb_str_split((string) $string, length: 1))); +} - if ($asArray) { - return arr($lines) - ->mapWithKeys(fn (string $line, int $number) => yield $number + 1 => $line) - ->toArray(); - } +/** + * Gets parts of the string. + */ +function slice(Stringable|string $string, int $start, ?int $length = null): string +{ + $stringLength = namespace\length($string); - return implode(PHP_EOL, $lines); + if (0 === $start && (null === $length || $stringLength <= $length)) { + return $string; } - /** - * Truncates the string to the specified amount of characters. - */ - function truncate_end(Stringable|string $string, int $characters, Stringable|string $end = ''): string - { - $string = (string) $string; - $end = (string) $end; + return mb_substr((string) $string, $start, $length); +} - if (mb_strwidth($string, 'UTF-8') <= $characters) { - return $string; +/** + * Checks whether the given string contains the specified `$needle`. + */ +function contains(Stringable|string $string, Stringable|string|array $needle): bool +{ + foreach (Arr\wrap($needle) as $item) { + if (str_contains((string) $string, (string) $item)) { + return true; } + } - if ($characters < 0) { - $characters = mb_strlen($string) + $characters; - } + return false; +} - return rtrim(mb_strimwidth($string, 0, $characters, encoding: 'UTF-8')) . $end; - } +/** + * Takes the specified amount of characters. If `$length` is negative, starts from the end. + */ +function take(Stringable|string $string, int $length): string +{ + $string = (string) $string; - /** - * Truncates the string to the specified amount of characters from the start. - */ - function truncate_start(Stringable|string $string, int $characters, Stringable|string $start = ''): string - { - return reverse(truncate_end(reverse((string) $string), $characters, (string) $start)); + if ($length < 0) { + return slice($string, $length); } - /** - * Reverses the string. - */ - function reverse(Stringable|string $string): string - { - return implode('', array_reverse(mb_str_split((string) $string, length: 1))); - } + return slice($string, 0, $length); +} - /** - * Gets parts of the string. - */ - function slice(Stringable|string $string, int $start, ?int $length = null): string - { - $stringLength = namespace\length($string); +/** + * Chunks the string into parts of the specified `$length`. + */ +function chunk(Stringable|string $string, int $length): array +{ + $string = (string) $string; - if (0 === $start && (null === $length || $stringLength <= $length)) { - return $string; - } + if ($length <= 0) { + return []; + } - return mb_substr((string) $string, $start, $length); + if ($string === '') { + return ['']; } - /** - * Checks whether the given string contains the specified `$needle`. - */ - function contains(Stringable|string $string, Stringable|string|array $needle): bool - { - foreach (Arr\wrap($needle) as $item) { - if (str_contains((string) $string, (string) $item)) { - return true; - } - } + $chunks = []; - return false; + foreach (str_split($string, $length) as $chunk) { + $chunks[] = $chunk; } - /** - * Takes the specified amount of characters. If `$length` is negative, starts from the end. - */ - function take(Stringable|string $string, int $length): string - { - $string = (string) $string; + return $chunks; +} - if ($length < 0) { - return slice($string, $length); - } +/** + * Strips HTML and PHP tags from the string. + */ +function strip_tags(Stringable|string $string, null|string|array $allowed = null): string +{ + $string = (string) $string; - return slice($string, 0, $length); - } + $allowed = arr($allowed) + ->map(fn (string $tag) => wrap($tag, '<', '>')) + ->toArray(); - /** - * Chunks the string into parts of the specified `$length`. - */ - function chunk(Stringable|string $string, int $length): array - { - $string = (string) $string; + return php_strip_tags($string, $allowed); +} - if ($length <= 0) { - return []; - } +/** + * Pads the string to the given `$width` and centers the text in it. + */ +function align_center(Stringable|string $string, ?int $width, int $padding = 0): string +{ + $text = trim((string) $string); + $textLength = length($text); + $actualWidth = max($width ?? 0, $textLength + (2 * $padding)); + $leftPadding = (int) floor(($actualWidth - $textLength) / 2); + $rightPadding = $actualWidth - $leftPadding - $textLength; + + return str_repeat(' ', $leftPadding) . $text . str_repeat(' ', $rightPadding); +} - if ($string === '') { - return ['']; - } +/** + * Pads the string to the given `$width` and aligns the text to the right. + */ +function align_right(Stringable|string $string, ?int $width, int $padding = 0): string +{ + $text = trim((string) $string); + $textLength = length($text); + $actualWidth = max($width ?? 0, $textLength + (2 * $padding)); + $leftPadding = $actualWidth - $textLength - $padding; + + return str_repeat(' ', $leftPadding) . $text . str_repeat(' ', $padding); +} - $chunks = []; +/** + * Pads the string to the given `$width` and aligns the text to the left. + */ +function align_left(Stringable|string $string, ?int $width, int $padding = 0): string +{ + $text = trim((string) $string); + $textLength = length($text); + $actualWidth = max($width ?? 0, $textLength + (2 * $padding)); + $rightPadding = $actualWidth - $textLength - $padding; + + return str_repeat(' ', $padding) . $text . str_repeat(' ', $rightPadding); +} - foreach (str_split($string, $length) as $chunk) { - $chunks[] = $chunk; +/** + * Returns the string padded to the total length by appending the `$pad_string` to the left. + * + * If the length of the input string plus the pad string exceeds the total + * length, the pad string will be truncated. If the total length is less than or + * equal to the length of the input string, no padding will occur. + * + * Example: + * pad_left('Ay', 4) + * => ' Ay' + * + * pad_left('ay', 3, 'A') + * => 'Aay' + * + * pad_left('eet', 4, 'Yeeeee') + * => 'Yeet' + * + * pad_left('مرحبا', 8, 'م') + * => 'ممممرحبا' + * + * @param non-empty-string $padString + * @param int<0, max> $totalLength + */ +function pad_left(string $string, int $totalLength, string $padString = ' '): string +{ + do { + $length = namespace\length($string); + + if ($length >= $totalLength) { + return $string; } - return $chunks; - } + /** @var int<0, max> $remaining */ + $remaining = $totalLength - $length; - /** - * Strips HTML and PHP tags from the string. - */ - function strip_tags(Stringable|string $string, null|string|array $allowed = null): string - { - $string = (string) $string; + if ($remaining <= namespace\length($padString)) { + $padString = namespace\slice($padString, 0, $remaining); + } - $allowed = arr($allowed) - ->map(fn (string $tag) => wrap($tag, '<', '>')) - ->toArray(); + $string = $padString . $string; + } while (true); +} - return php_strip_tags($string, $allowed); - } - - /** - * Pads the string to the given `$width` and centers the text in it. - */ - function align_center(Stringable|string $string, ?int $width, int $padding = 0): string - { - $text = trim((string) $string); - $textLength = length($text); - $actualWidth = max($width ?? 0, $textLength + (2 * $padding)); - $leftPadding = (int) floor(($actualWidth - $textLength) / 2); - $rightPadding = $actualWidth - $leftPadding - $textLength; - - return str_repeat(' ', $leftPadding) . $text . str_repeat(' ', $rightPadding); - } - - /** - * Pads the string to the given `$width` and aligns the text to the right. - */ - function align_right(Stringable|string $string, ?int $width, int $padding = 0): string - { - $text = trim((string) $string); - $textLength = length($text); - $actualWidth = max($width ?? 0, $textLength + (2 * $padding)); - $leftPadding = $actualWidth - $textLength - $padding; - - return str_repeat(' ', $leftPadding) . $text . str_repeat(' ', $padding); - } - - /** - * Pads the string to the given `$width` and aligns the text to the left. - */ - function align_left(Stringable|string $string, ?int $width, int $padding = 0): string - { - $text = trim((string) $string); - $textLength = length($text); - $actualWidth = max($width ?? 0, $textLength + (2 * $padding)); - $rightPadding = $actualWidth - $textLength - $padding; - - return str_repeat(' ', $padding) . $text . str_repeat(' ', $rightPadding); - } - - /** - * Returns the string padded to the total length by appending the `$pad_string` to the left. - * - * If the length of the input string plus the pad string exceeds the total - * length, the pad string will be truncated. If the total length is less than or - * equal to the length of the input string, no padding will occur. - * - * Example: - * pad_left('Ay', 4) - * => ' Ay' - * - * pad_left('ay', 3, 'A') - * => 'Aay' - * - * pad_left('eet', 4, 'Yeeeee') - * => 'Yeet' - * - * pad_left('مرحبا', 8, 'م') - * => 'ممممرحبا' - * - * @param non-empty-string $padString - * @param int<0, max> $totalLength - */ - function pad_left(string $string, int $totalLength, string $padString = ' '): string - { - do { - $length = namespace\length($string); - - if ($length >= $totalLength) { - return $string; - } - - /** @var int<0, max> $remaining */ - $remaining = $totalLength - $length; - - if ($remaining <= namespace\length($padString)) { - $padString = namespace\slice($padString, 0, $remaining); - } - - $string = $padString . $string; - } while (true); - } - - /** - * Returns the string padded to the total length by appending the `$pad_string` to the right. - * - * If the length of the input string plus the pad string exceeds the total - * length, the pad string will be truncated. If the total length is less than or - * equal to the length of the input string, no padding will occur. - * - * Example: - * pad_right('Ay', 4) - * => 'Ay ' - * - * pad_right('Ay', 5, 'y') - * => 'Ayyyy' - * - * pad_right('Yee', 4, 't') - * => 'Yeet' - * - * pad_right('مرحبا', 8, 'ا') - * => 'مرحباااا' - * - * @param non-empty-string $padString - * @param int<0, max> $totalLength - */ - function pad_right(string $string, int $totalLength, string $padString = ' '): string - { - do { - $length = namespace\length($string); - - if ($length >= $totalLength) { - return $string; - } - - /** @var int<0, max> $remaining */ - $remaining = $totalLength - $length; - - if ($remaining <= namespace\length($padString)) { - $padString = namespace\slice($padString, 0, $remaining); - } - - $string .= $padString; - } while (true); - } - - /** - * Inserts the specified `$insertion` at the specified `$position`. - */ - function insert_at(Stringable|string $string, int $position, string $insertion): string - { - $string = (string) $string; - - return mb_substr($string, 0, $position) . $insertion . mb_substr($string, $position); - } - - /** - * Calculates the levenshtein difference between two strings. - */ - function levenshtein(Stringable|string $string, string|Stringable $other): int - { - return php_levenshtein((string) $string, (string) $other); - } - - /** - * Calculate the metaphone key of a string. - */ - function metaphone(Stringable|string $string, int $phonemes = 0): string - { - return php_metaphone((string) $string, $phonemes); - } - - /** - * Checks whether a string is empty - */ - function is_empty(Stringable|string $string): bool - { - return (string) $string === ''; - } - - /** - * Asserts whether the string is equal to the given string. - */ - function equals(Stringable|string $string, string|Stringable $other): bool - { - return (string) $string === (string) $other; - } - - /** - * Parses the given value to a string, returning the default value if it is not a string or `Stringable`. - */ - function parse(mixed $string, ?string $default = ''): ?string - { - if (is_string($string)) { +/** + * Returns the string padded to the total length by appending the `$pad_string` to the right. + * + * If the length of the input string plus the pad string exceeds the total + * length, the pad string will be truncated. If the total length is less than or + * equal to the length of the input string, no padding will occur. + * + * Example: + * pad_right('Ay', 4) + * => 'Ay ' + * + * pad_right('Ay', 5, 'y') + * => 'Ayyyy' + * + * pad_right('Yee', 4, 't') + * => 'Yeet' + * + * pad_right('مرحبا', 8, 'ا') + * => 'مرحباااا' + * + * @param non-empty-string $padString + * @param int<0, max> $totalLength + */ +function pad_right(string $string, int $totalLength, string $padString = ' '): string +{ + do { + $length = namespace\length($string); + + if ($length >= $totalLength) { return $string; } - if (is_int($string) || is_float($string)) { - return (string) $string; - } + /** @var int<0, max> $remaining */ + $remaining = $totalLength - $length; - if ($string instanceof Stringable) { - return (string) $string; + if ($remaining <= namespace\length($padString)) { + $padString = namespace\slice($padString, 0, $remaining); } - if ($string instanceof BackedEnum) { - return (string) $string->value; - } + $string .= $padString; + } while (true); +} - if ($string instanceof UnitEnum) { - return $string->name; - } +/** + * Inserts the specified `$insertion` at the specified `$position`. + */ +function insert_at(Stringable|string $string, int $position, string $insertion): string +{ + $string = (string) $string; - if (is_object($string) && method_exists($string, '__toString')) { - return (string) $string; - } + return mb_substr($string, 0, $position) . $insertion . mb_substr($string, $position); +} + +/** + * Calculates the levenshtein difference between two strings. + */ +function levenshtein(Stringable|string $string, string|Stringable $other): int +{ + return php_levenshtein((string) $string, (string) $other); +} + +/** + * Calculate the metaphone key of a string. + */ +function metaphone(Stringable|string $string, int $phonemes = 0): string +{ + return php_metaphone((string) $string, $phonemes); +} + +/** + * Checks whether a string is empty + */ +function is_empty(Stringable|string $string): bool +{ + return (string) $string === ''; +} + +/** + * Asserts whether the string is equal to the given string. + */ +function equals(Stringable|string $string, string|Stringable $other): bool +{ + return (string) $string === (string) $other; +} - return $default; +/** + * Parses the given value to a string, returning the default value if it is not a string or `Stringable`. + */ +function parse(mixed $string, ?string $default = ''): ?string +{ + if (is_string($string)) { + return $string; } - /** - * Normalizes `Stringable` to string, while keeping other values the same. - * - * @internal - */ - function normalize_string(mixed $value): mixed - { - if ($value instanceof Stringable) { - return (string) $value; - } + if (is_int($string) || is_float($string)) { + return (string) $string; + } - return $value; + if ($string instanceof Stringable) { + return (string) $string; } + + if ($string instanceof BackedEnum) { + return (string) $string->value; + } + + if ($string instanceof UnitEnum) { + return $string->name; + } + + if (is_object($string) && method_exists($string, '__toString')) { + return (string) $string; + } + + return $default; +} + +/** + * Normalizes `Stringable` to string, while keeping other values the same. + * + * @internal + */ +function normalize_string(mixed $value): mixed +{ + if ($value instanceof Stringable) { + return (string) $value; + } + + return $value; } diff --git a/packages/support/src/functions.php b/packages/support/src/functions.php index 46470069bf..111c7e03b3 100644 --- a/packages/support/src/functions.php +++ b/packages/support/src/functions.php @@ -2,81 +2,81 @@ declare(strict_types=1); -namespace Tempest\Support { - use Closure; - use Stringable; - use Tempest\Support\Arr\ImmutableArray; - use Tempest\Support\Path\Path; - use Tempest\Support\Str\ImmutableString; +namespace Tempest\Support; - /** - * Creates an instance of {@see \Tempest\Support\Str\ImmutableString} using the given `$string`. - */ - function str(Stringable|int|string|null $string = ''): ImmutableString - { - return new ImmutableString($string); - } +use Closure; +use Stringable; +use Tempest\Support\Arr\ImmutableArray; +use Tempest\Support\Path\Path; +use Tempest\Support\Str\ImmutableString; - /** - * Creates an instance of {@see \Tempest\Support\Arr\ImmutableArray} using the given `$input`. If `$input` is not an array, it will be wrapped in one. - */ - function arr(mixed $input = []): ImmutableArray - { - return new ImmutableArray($input); - } +/** + * Creates an instance of {@see \Tempest\Support\Str\ImmutableString} using the given `$string`. + */ +function str(Stringable|int|string|null $string = ''): ImmutableString +{ + return new ImmutableString($string); +} - /** - * Normalizes the given path without checking it against the filesystem. - */ - function path(Stringable|string ...$parts): Path - { - return new Path(...$parts); - } +/** + * Creates an instance of {@see \Tempest\Support\Arr\ImmutableArray} using the given `$input`. If `$input` is not an array, it will be wrapped in one. + */ +function arr(mixed $input = []): ImmutableArray +{ + return new ImmutableArray($input); +} - /** - * Executes the callback with the given `$value` and returns the same `$value`. - * - * @template T - * - * @param T $value - * @param (callable(T): void) $callback - * - * @return T - */ - function tap(mixed $value, callable $callback): mixed - { - $callback($value); +/** + * Normalizes the given path without checking it against the filesystem. + */ +function path(Stringable|string ...$parts): Path +{ + return new Path(...$parts); +} - return $value; - } +/** + * Executes the callback with the given `$value` and returns the same `$value`. + * + * @template T + * + * @param T $value + * @param (callable(T): void) $callback + * + * @return T + */ +function tap(mixed $value, callable $callback): mixed +{ + $callback($value); - /** - * Returns a tuple containing the result of the `$callback` as the first element and the error message as the second element if there was an error. - * - * @template T - * - * @param (Closure(): T) $callback - * - * @return array{0: T, 1: ?string} - */ - function box(Closure $callback): array - { - $lastMessage = null; + return $value; +} - set_error_handler(static function (int $_type, string $message) use (&$lastMessage): void { // @phpstan-ignore argument.type - $lastMessage = $message; - }); +/** + * Returns a tuple containing the result of the `$callback` as the first element and the error message as the second element if there was an error. + * + * @template T + * + * @param (Closure(): T) $callback + * + * @return array{0: T, 1: ?string} + */ +function box(Closure $callback): array +{ + $lastMessage = null; - try { - $value = $callback(); + set_error_handler(static function (int $_type, string $message) use (&$lastMessage): void { // @phpstan-ignore argument.type + $lastMessage = $message; + }); - if (null !== $lastMessage && Str\contains($lastMessage, '): ')) { - $lastMessage = Str\after_first(Str\to_lower_case($lastMessage), '): '); - } + try { + $value = $callback(); - return [$value, $lastMessage]; - } finally { - restore_error_handler(); + if (null !== $lastMessage && Str\contains($lastMessage, '): ')) { + $lastMessage = Str\after_first(Str\to_lower_case($lastMessage), '): '); } + + return [$value, $lastMessage]; + } finally { + restore_error_handler(); } } diff --git a/packages/upgrade/config/sets/tempest30.php b/packages/upgrade/config/sets/tempest30.php index 44393ae76a..de863211f2 100644 --- a/packages/upgrade/config/sets/tempest30.php +++ b/packages/upgrade/config/sets/tempest30.php @@ -1,16 +1,26 @@ importNames(); - $config->importShortClasses(); + SimpleParameterProvider::setParameter(Option::AUTO_IMPORT_NAMES, value: true); + SimpleParameterProvider::setParameter(Option::IMPORT_SHORT_CLASSES, value: true); + $config->rule(UpdateCommandFunctionImportsRector::class); + $config->rule(UpdateContainerFunctionImportsRector::class); + $config->rule(UpdateEventFunctionImportsRector::class); $config->rule(UpdateMapperFunctionImportsRector::class); + $config->rule(UpdateReflectionFunctionImportsRector::class); $config->rule(UpdateViewFunctionImportsRector::class); $config->rule(UpdateExceptionProcessorRector::class); $config->rule(UpdateHasContextRector::class); diff --git a/packages/upgrade/src/Tempest3/UpdateCommandFunctionImportsRector.php b/packages/upgrade/src/Tempest3/UpdateCommandFunctionImportsRector.php new file mode 100644 index 0000000000..9aad37e1a0 --- /dev/null +++ b/packages/upgrade/src/Tempest3/UpdateCommandFunctionImportsRector.php @@ -0,0 +1,40 @@ +name->toString() === 'Tempest\command') { + $node->name = new Node\Name('Tempest\CommandBus\command'); + } + + return null; + } + + if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) { + $functionName = $node->name->toString(); + + if ($functionName === 'Tempest\command') { + $node->name = new Node\Name\FullyQualified('Tempest\CommandBus\command'); + + return null; + } + } + + return null; + } +} diff --git a/packages/upgrade/src/Tempest3/UpdateContainerFunctionImportsRector.php b/packages/upgrade/src/Tempest3/UpdateContainerFunctionImportsRector.php new file mode 100644 index 0000000000..106193e620 --- /dev/null +++ b/packages/upgrade/src/Tempest3/UpdateContainerFunctionImportsRector.php @@ -0,0 +1,50 @@ +name->toString() === 'Tempest\get') { + $node->name = new Node\Name('Tempest\Container\get'); + } + + if ($node->name->toString() === 'Tempest\invoke') { + $node->name = new Node\Name('Tempest\Container\invoke'); + } + + return null; + } + + if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) { + $functionName = $node->name->toString(); + + if ($functionName === 'Tempest\get') { + $node->name = new Node\Name\FullyQualified('Tempest\Container\get'); + + return null; + } + + if ($functionName === 'Tempest\invoke') { + $node->name = new Node\Name\FullyQualified('Tempest\Container\invoke'); + + return null; + } + } + + return null; + } +} diff --git a/packages/upgrade/src/Tempest3/UpdateEventFunctionImportsRector.php b/packages/upgrade/src/Tempest3/UpdateEventFunctionImportsRector.php new file mode 100644 index 0000000000..30962f6b53 --- /dev/null +++ b/packages/upgrade/src/Tempest3/UpdateEventFunctionImportsRector.php @@ -0,0 +1,50 @@ +name->toString() === 'Tempest\event') { + $node->name = new Node\Name('Tempest\EventBus\event'); + } + + if ($node->name->toString() === 'Tempest\listen') { + $node->name = new Node\Name('Tempest\EventBus\listen'); + } + + return null; + } + + if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) { + $functionName = $node->name->toString(); + + if ($functionName === 'Tempest\event') { + $node->name = new Node\Name\FullyQualified('Tempest\EventBus\event'); + + return null; + } + + if ($functionName === 'Tempest\listen') { + $node->name = new Node\Name\FullyQualified('Tempest\EventBus\listen'); + + return null; + } + } + + return null; + } +} diff --git a/packages/upgrade/src/Tempest3/UpdateReflectionFunctionImportsRector.php b/packages/upgrade/src/Tempest3/UpdateReflectionFunctionImportsRector.php new file mode 100644 index 0000000000..1d12c45eac --- /dev/null +++ b/packages/upgrade/src/Tempest3/UpdateReflectionFunctionImportsRector.php @@ -0,0 +1,40 @@ +name->toString() === 'Tempest\reflect') { + $node->name = new Node\Name('Tempest\Reflection\reflect'); + } + + return null; + } + + if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) { + $functionName = $node->name->toString(); + + if ($functionName === 'Tempest\reflect') { + $node->name = new Node\Name\FullyQualified('Tempest\Reflection\reflect'); + + return null; + } + } + + return null; + } +} diff --git a/packages/view/src/Components/x-component.view.php b/packages/view/src/Components/x-component.view.php index c85419b037..fdf0baa252 100644 --- a/packages/view/src/Components/x-component.view.php +++ b/packages/view/src/Components/x-component.view.php @@ -7,7 +7,7 @@ use Tempest\View\Renderers\TempestViewRenderer; use Tempest\View\Slot; -use function Tempest\get; +use function Tempest\Container\get; use function Tempest\View\view; $attributeString = $attributes diff --git a/packages/view/src/Components/x-icon.view.php b/packages/view/src/Components/x-icon.view.php index d174973262..67905ea976 100644 --- a/packages/view/src/Components/x-icon.view.php +++ b/packages/view/src/Components/x-icon.view.php @@ -7,7 +7,7 @@ use Tempest\Core\Environment; use Tempest\Icon\Icon; -use function Tempest\get; +use function Tempest\Container\get; use function Tempest\Support\str; $class ??= null; diff --git a/packages/view/src/Components/x-input.view.php b/packages/view/src/Components/x-input.view.php index 2d867a307e..f1ddb3835b 100644 --- a/packages/view/src/Components/x-input.view.php +++ b/packages/view/src/Components/x-input.view.php @@ -10,7 +10,7 @@ use Tempest\Http\Session\FormSession; use Tempest\Validation\Validator; -use function Tempest\get; +use function Tempest\Container\get; use function Tempest\Support\str; /** @var FormSession $formSession */ diff --git a/packages/view/src/Components/x-markdown.view.php b/packages/view/src/Components/x-markdown.view.php index 083799365d..56c75b8bb7 100644 --- a/packages/view/src/Components/x-markdown.view.php +++ b/packages/view/src/Components/x-markdown.view.php @@ -7,7 +7,7 @@ use League\CommonMark\MarkdownConverter; use Tempest\View\Slot; -use function Tempest\get; +use function Tempest\Container\get; $content ??= $slots[Slot::DEFAULT]->content ?? ''; $markdown = get(MarkdownConverter::class); diff --git a/packages/vite/src/functions.php b/packages/vite/src/functions.php index 7340fda949..b217c05043 100644 --- a/packages/vite/src/functions.php +++ b/packages/vite/src/functions.php @@ -2,17 +2,16 @@ declare(strict_types=1); -namespace Tempest { - use Tempest\Support\Html\HtmlString; - use Tempest\Vite\Vite; +namespace Tempest\Vite; - /** - * Inject tags for the specified or configured `$entrypoints`. - */ - function vite_tags(null|string|array $entrypoints = null): HtmlString - { - return new HtmlString( - string: implode('', get(Vite::class)->getTags(is_array($entrypoints) ? $entrypoints : [$entrypoints])), - ); - } +use Tempest\Vite\Vite; + +use function Tempest\Container\get; + +/** + * Gets tags for the specified or configured `$entrypoints`. + */ +function get_tags(null|string|array $entrypoints = null): array +{ + return get(Vite::class)->getTags(is_array($entrypoints) ? $entrypoints : [$entrypoints]); } diff --git a/packages/vite/src/x-vite-tags.view.php b/packages/vite/src/x-vite-tags.view.php index d2d9674e14..3b745ddc20 100644 --- a/packages/vite/src/x-vite-tags.view.php +++ b/packages/vite/src/x-vite-tags.view.php @@ -4,14 +4,15 @@ * @var string|null $entrypoint */ +use Tempest\Support\Html\HtmlString; +use Tempest\Vite; use Tempest\Vite\ViteConfig; -use function Tempest\get; -use function Tempest\vite_tags; +use function Tempest\Container\get; $viteConfig = get(ViteConfig::class); - -$html = vite_tags($entrypoints ?? $entrypoint ?? $viteConfig->entrypoints); +$tags = Vite\get_tags($entrypoints ?? $entrypoint ?? $viteConfig->entrypoints); +$html = new HtmlString(implode('', $tags)); ?> {!! $html !!} diff --git a/tests/Fixtures/Console/DispatchAsyncCommand.php b/tests/Fixtures/Console/DispatchAsyncCommand.php index 96d1c5cee0..ae61b4aa51 100644 --- a/tests/Fixtures/Console/DispatchAsyncCommand.php +++ b/tests/Fixtures/Console/DispatchAsyncCommand.php @@ -9,7 +9,7 @@ use Tests\Tempest\Integration\CommandBus\Fixtures\MyAsyncCommand; use Tests\Tempest\Integration\CommandBus\Fixtures\MyFailingAsyncCommand; -use function Tempest\command; +use function Tempest\CommandBus\command; final readonly class DispatchAsyncCommand { diff --git a/tests/Integration/CommandBus/AsyncCommandTest.php b/tests/Integration/CommandBus/AsyncCommandTest.php index d43c014d3a..2899542c34 100644 --- a/tests/Integration/CommandBus/AsyncCommandTest.php +++ b/tests/Integration/CommandBus/AsyncCommandTest.php @@ -12,7 +12,7 @@ use Tests\Tempest\Integration\CommandBus\Fixtures\MyAsyncCommand; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; -use function Tempest\command; +use function Tempest\CommandBus\command; use function Tempest\Support\arr; /** diff --git a/tests/Integration/CommandBus/CommandBusTest.php b/tests/Integration/CommandBus/CommandBusTest.php index 2396cce630..b1bac0f315 100644 --- a/tests/Integration/CommandBus/CommandBusTest.php +++ b/tests/Integration/CommandBus/CommandBusTest.php @@ -12,7 +12,7 @@ use Tests\Tempest\Fixtures\Commands\MyCommandBusMiddleware; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; -use function Tempest\command; +use function Tempest\CommandBus\command; /** * @internal diff --git a/tests/Integration/Core/DiscoveryCacheTest.php b/tests/Integration/Core/DiscoveryCacheTest.php index 864c1619eb..270a657ddf 100644 --- a/tests/Integration/Core/DiscoveryCacheTest.php +++ b/tests/Integration/Core/DiscoveryCacheTest.php @@ -11,7 +11,7 @@ use Tests\Tempest\Integration\Core\Fixtures\TestDiscovery; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; -use function Tempest\reflect; +use function Tempest\Reflection\reflect; final class DiscoveryCacheTest extends FrameworkIntegrationTestCase { diff --git a/tests/Integration/EventBus/EventBusTest.php b/tests/Integration/EventBus/EventBusTest.php index 4a2640d1fb..9491287762 100644 --- a/tests/Integration/EventBus/EventBusTest.php +++ b/tests/Integration/EventBus/EventBusTest.php @@ -16,7 +16,7 @@ use Tests\Tempest\Fixtures\Handlers\EventInterfaceHandler; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; -use function Tempest\event; +use function Tempest\EventBus\event; /** * @internal diff --git a/tests/Integration/EventBus/EventIntegrationTestCase.php b/tests/Integration/EventBus/EventIntegrationTestCase.php index 78dcc6eaa2..376072f397 100644 --- a/tests/Integration/EventBus/EventIntegrationTestCase.php +++ b/tests/Integration/EventBus/EventIntegrationTestCase.php @@ -8,7 +8,7 @@ use Tests\Tempest\Fixtures\Events\ItHappened; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; -use function Tempest\event; +use function Tempest\EventBus\event; /** * @internal diff --git a/tests/Integration/Mapper/CasterFactoryTest.php b/tests/Integration/Mapper/CasterFactoryTest.php index 119b950a77..83cf61a51e 100644 --- a/tests/Integration/Mapper/CasterFactoryTest.php +++ b/tests/Integration/Mapper/CasterFactoryTest.php @@ -12,7 +12,7 @@ use Tests\Tempest\Integration\FrameworkIntegrationTestCase; use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithSerializerProperties; -use function Tempest\reflect; +use function Tempest\Reflection\reflect; final class CasterFactoryTest extends FrameworkIntegrationTestCase { diff --git a/tests/Integration/Mapper/SerializerFactoryTest.php b/tests/Integration/Mapper/SerializerFactoryTest.php index 7c9a6382c5..fbd2cac296 100644 --- a/tests/Integration/Mapper/SerializerFactoryTest.php +++ b/tests/Integration/Mapper/SerializerFactoryTest.php @@ -21,7 +21,7 @@ use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithSerializerProperties; use Tests\Tempest\Integration\Mapper\Fixtures\SerializableObject; -use function Tempest\reflect; +use function Tempest\Reflection\reflect; final class SerializerFactoryTest extends FrameworkIntegrationTestCase { diff --git a/tests/Integration/View/ViewComponentTest.php b/tests/Integration/View/ViewComponentTest.php index b871b1c275..2877dbdcbd 100644 --- a/tests/Integration/View/ViewComponentTest.php +++ b/tests/Integration/View/ViewComponentTest.php @@ -884,7 +884,7 @@ public function test_nested_slots_with_escaping(): void $this->registerViewComponent('x-a', ''); $this->registerViewComponent('x-b', <<<'HTML' {{ get(Environment::class)->value }} diff --git a/tests/Integration/Vite/FunctionsTest.php b/tests/Integration/Vite/FunctionsTest.php index 0424440f12..0849fefc0f 100644 --- a/tests/Integration/Vite/FunctionsTest.php +++ b/tests/Integration/Vite/FunctionsTest.php @@ -4,38 +4,34 @@ namespace Tests\Tempest\Integration\Vite; -use Tempest\Support\Html\HtmlString; +use PHPUnit\Framework\Attributes\PreCondition; +use PHPUnit\Framework\Attributes\Test; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; -use function Tempest\vite_tags; +use function Tempest\Vite\get_tags; /** * @internal */ final class FunctionsTest extends FrameworkIntegrationTestCase { - protected function setUp(): void + #[PreCondition] + protected function configure(): void { - parent::setUp(); - $this->vite->setRootDirectory(__DIR__ . '/Fixtures/tmp'); } - public function test_vite_tags(): void + #[Test] + public function vite_tags(): void { $this->vite->call( - callback: function (): void { - $tags = vite_tags('src/main.ts'); - - $this->assertInstanceOf(HtmlString::class, $tags); - $this->assertSame( - expected: implode('', [ - '', - '', - ]), - actual: (string) vite_tags('src/main.ts'), - ); - }, + callback: fn () => $this->assertSame( + expected: [ + '', + '', + ], + actual: get_tags('src/main.ts'), + ), files: [ 'public/vite-tempest' => ['url' => 'http://localhost:5173'], 'src/main.ts' => '',