Upgrade to Laravel 12

- laravel/framework ^11.45 -> ^12.0 (installed 12.63.0)
- phpunit/phpunit ^10.5 -> ^11.0; migrate phpunit.xml to the 11.5 schema
- graham-campbell/github ^12.5 -> ^13.0 (v12 caps illuminate/support at ^11)
- Carbon 3 pulled in by L12; no application code changes required
- gitignore /.phpunit.cache

Full suite green: 59 tests, 128 assertions (1 skipped).
This commit is contained in:
KodeStar
2026-07-08 19:49:05 +01:00
parent cb59689ead
commit 39191885e5
4259 changed files with 139504 additions and 57094 deletions
+1
View File
@@ -29,3 +29,4 @@ yarn-error.log
storage/app/public/avatars/* storage/app/public/avatars/*
.env .env
.phpunit.result.cache .phpunit.result.cache
/.phpunit.cache
+3 -3
View File
@@ -12,9 +12,9 @@
"ext-intl": "*", "ext-intl": "*",
"ext-json": "*", "ext-json": "*",
"enshrined/svg-sanitize": "^0.22.0", "enshrined/svg-sanitize": "^0.22.0",
"graham-campbell/github": "^12.5", "graham-campbell/github": "^13.0",
"guzzlehttp/guzzle": "^7.8", "guzzlehttp/guzzle": "^7.8",
"laravel/framework": "^11.45", "laravel/framework": "^12.0",
"laravel/tinker": "^2.9", "laravel/tinker": "^2.9",
"laravel/ui": "^4.4", "laravel/ui": "^4.4",
"league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-aws-s3-v3": "^3.0",
@@ -27,7 +27,7 @@
"barryvdh/laravel-ide-helper": "^3.0", "barryvdh/laravel-ide-helper": "^3.0",
"filp/whoops": "^2.8", "filp/whoops": "^2.8",
"mockery/mockery": "^1.6", "mockery/mockery": "^1.6",
"phpunit/phpunit": "^10.5", "phpunit/phpunit": "^11.0",
"squizlabs/php_codesniffer": "3.*", "squizlabs/php_codesniffer": "3.*",
"symfony/thanks": "^1.2", "symfony/thanks": "^1.2",
"fakerphp/faker": "^1.23" "fakerphp/faker": "^1.23"
Generated
+1479 -1075
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -1,10 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true"> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.5/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" cacheDirectory=".phpunit.cache">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./app</directory>
</include>
</coverage>
<testsuites> <testsuites>
<testsuite name="Unit"> <testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory> <directory suffix="Test.php">./tests/Unit</directory>
@@ -25,4 +20,9 @@
<env name="SESSION_DRIVER" value="array"/> <env name="SESSION_DRIVER" value="array"/>
<env name="TELESCOPE_ENABLED" value="false"/> <env name="TELESCOPE_ENABLED" value="false"/>
</php> </php>
<source>
<include>
<directory suffix=".php">./app</directory>
</include>
</source>
</phpunit> </phpunit>
+12 -2
View File
@@ -3,8 +3,18 @@
// autoload.php @generated by Composer // autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) { if (PHP_VERSION_ID < 50600) {
echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; if (!headers_sent()) {
exit(1); header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
throw new RuntimeException($err);
} }
require_once __DIR__ . '/composer/autoload_real.php'; require_once __DIR__ . '/composer/autoload_real.php';
@@ -1,62 +0,0 @@
<?php
// Share common rules between non-test and test files
return [
'@PSR12' => true,
'blank_line_after_opening_tag' => true,
'braces' => [
'allow_single_line_anonymous_class_with_empty_body' => true,
],
'compact_nullable_typehint' => true,
'declare_equal_normalize' => true,
'lowercase_cast' => true,
'lowercase_static_reference' => true,
'new_with_braces' => true,
'no_blank_lines_after_class_opening' => true,
'no_leading_import_slash' => true,
'no_whitespace_in_blank_line' => true,
'ordered_class_elements' => [
'order' => [
'use_trait',
],
],
'ordered_imports' => [
'imports_order' => [
'class',
'function',
'const',
],
'sort_algorithm' => 'alpha',
],
'return_type_declaration' => true,
'short_scalar_cast' => true,
'single_trait_insert_per_statement' => true,
'ternary_operator_spaces' => true,
'visibility_required' => [
'elements' => [
'const',
'method',
'property',
],
],
// Further quality-of-life improvements
'array_syntax' => [
'syntax' => 'short',
],
'concat_space' => [
'spacing' => 'one',
],
'fully_qualified_strict_types' => true,
'native_function_invocation' => [
'include' => [],
'strict' => true,
],
'no_unused_imports' => true,
'single_quote' => true,
'space_after_semicolon' => true,
'trailing_comma_in_multiline' => true,
'trim_array_spaces' => true,
'unary_operator_spaces' => true,
'whitespace_after_comma_in_array' => true,
];
@@ -1,14 +0,0 @@
<?php
require __DIR__ . '/vendor/autoload.php';
$finder = PhpCsFixer\Finder::create()
->in(__DIR__)
->exclude('tests');
$config = require __DIR__ . '/.php-cs-fixer.common.php';
return (new PhpCsFixer\Config())
->setFinder($finder)
->setRules($config)
->setRiskyAllowed(true)
->setCacheFile(__DIR__ . '/.php-cs-fixer.cache');
@@ -1,22 +0,0 @@
<?php
require __DIR__ . '/vendor/autoload.php';
$finder = PhpCsFixer\Finder::create()
->in(__DIR__ . '/tests')
->exclude('__snapshots__');
$config = require __DIR__ . '/.php-cs-fixer.common.php';
// Additional rules for tests
$config = array_merge(
$config,
[
'declare_strict_types' => true,
]
);
return (new PhpCsFixer\Config())
->setFinder($finder)
->setRules($config)
->setRiskyAllowed(true)
->setCacheFile(__DIR__ . '/.php-cs-fixer.tests.cache');
+72
View File
@@ -1,5 +1,77 @@
# Changelog # Changelog
## v3.6.1 - 2025-12-10
### What's Changed
* Fix `methodsto` typo in README by @peterchrjoergensen in https://github.com/barryvdh/laravel-ide-helper/pull/1723
* Bump actions/checkout from 4 to 5 in the deps group by @dependabot[bot] in https://github.com/barryvdh/laravel-ide-helper/pull/1731
* Fix typos in documentation and code comments by @kei1111 in https://github.com/barryvdh/laravel-ide-helper/pull/1733
* Add php 8.5 support by @sergiy-petrov in https://github.com/barryvdh/laravel-ide-helper/pull/1735
* Fix alias fake error by @WentTheFox in https://github.com/barryvdh/laravel-ide-helper/pull/1745
* Remove calls to PHP 8.5-deprecated `setAccessible` by @jnoordsij in https://github.com/barryvdh/laravel-ide-helper/pull/1744
* Bump the deps group across 1 directory with 2 updates by @dependabot[bot] in https://github.com/barryvdh/laravel-ide-helper/pull/1743
* Add support for `decimal` column type by @BrainStone in https://github.com/barryvdh/laravel-ide-helper/pull/1739
### New Contributors
* @peterchrjoergensen made their first contribution in https://github.com/barryvdh/laravel-ide-helper/pull/1723
* @kei1111 made their first contribution in https://github.com/barryvdh/laravel-ide-helper/pull/1733
* @WentTheFox made their first contribution in https://github.com/barryvdh/laravel-ide-helper/pull/1745
* @BrainStone made their first contribution in https://github.com/barryvdh/laravel-ide-helper/pull/1739
**Full Changelog**: https://github.com/barryvdh/laravel-ide-helper/compare/v3.6.0...v3.6.1
## v3.6.0 - 2025-07-18
### What's Changed
* fix: Change AsArrayObject cast to be Laravel's ArrayObject by @wsamoht in https://github.com/barryvdh/laravel-ide-helper/pull/1675
* Add extends declaration for Macroable classes to fix missing inherited methods by @KentarouTakeda in https://github.com/barryvdh/laravel-ide-helper/pull/1674
* fix(meta): ignore aliases in the autoloader (Fixes #1671) by @pataar in https://github.com/barryvdh/laravel-ide-helper/pull/1686
* feat(ModelsCommand): add support for the new Scope attribute by @pataar in https://github.com/barryvdh/laravel-ide-helper/pull/1694
* fix type change for scope default float parameter by @nivseb in https://github.com/barryvdh/laravel-ide-helper/pull/1697
* Revert #1629 - *Allow adding custom Macroable classes* by @erikn69 in https://github.com/barryvdh/laravel-ide-helper/pull/1707
* Configurable macro return type defaults by @erikn69 in https://github.com/barryvdh/laravel-ide-helper/pull/1711
* docs(readme): add Laravel 12 support information by @SantosVilanculos in https://github.com/barryvdh/laravel-ide-helper/pull/1717
* Add multi-level directory support for translation files by @RosiersRobin in https://github.com/barryvdh/laravel-ide-helper/pull/1718
* Support `AsCollection::of($map)`, `AsCollection::using($class, $map)` by @erikn69 in https://github.com/barryvdh/laravel-ide-helper/pull/1714
* fix: Fixed wrong doc for SoftDeletes `withTrashed` method by @eldair in https://github.com/barryvdh/laravel-ide-helper/pull/1688
* Support other OS on tests by @erikn69 in https://github.com/barryvdh/laravel-ide-helper/pull/1715
* Fix tests on windows by @barryvdh in https://github.com/barryvdh/laravel-ide-helper/pull/1720
* Bump stefanzweifel/git-auto-commit-action from 5 to 6 in the deps group by @dependabot[bot] in https://github.com/barryvdh/laravel-ide-helper/pull/1719
* Update .gitattributes - avoid all `.php-cs-fixer` files on vendor by @erikn69 in https://github.com/barryvdh/laravel-ide-helper/pull/1708
* fix(ModelsCommand): use 'string' as realType for 'encrypted' casts by @pataar in https://github.com/barryvdh/laravel-ide-helper/pull/1698
* Trim strings and bump reflection docblock by @barryvdh in https://github.com/barryvdh/laravel-ide-helper/pull/1721
* Add magic *_exists properties by @erikn69 in https://github.com/barryvdh/laravel-ide-helper/pull/1712
### New Contributors
* @wsamoht made their first contribution in https://github.com/barryvdh/laravel-ide-helper/pull/1675
* @nivseb made their first contribution in https://github.com/barryvdh/laravel-ide-helper/pull/1697
* @SantosVilanculos made their first contribution in https://github.com/barryvdh/laravel-ide-helper/pull/1717
* @RosiersRobin made their first contribution in https://github.com/barryvdh/laravel-ide-helper/pull/1718
**Full Changelog**: https://github.com/barryvdh/laravel-ide-helper/compare/v3.5.5...v3.6.0
## v3.5.5 - 2025-02-21
### What's Changed
* Fix for incorrect config item types in meta file by @eldair in https://github.com/barryvdh/laravel-ide-helper/pull/1662
* Prevent generation of incorrect property annotation by @skyler544 in https://github.com/barryvdh/laravel-ide-helper/pull/1665
* Fix MorphTo Model Doc Generation by @yparitcher in https://github.com/barryvdh/laravel-ide-helper/pull/1668
* Laravel 12 support by @jonnott in https://github.com/barryvdh/laravel-ide-helper/pull/1672
### New Contributors
* @eldair made their first contribution in https://github.com/barryvdh/laravel-ide-helper/pull/1662
* @skyler544 made their first contribution in https://github.com/barryvdh/laravel-ide-helper/pull/1665
* @yparitcher made their first contribution in https://github.com/barryvdh/laravel-ide-helper/pull/1668
* @jonnott made their first contribution in https://github.com/barryvdh/laravel-ide-helper/pull/1672
**Full Changelog**: https://github.com/barryvdh/laravel-ide-helper/compare/v3.5.4...v3.5.5
## v3.5.4 - 2025-01-14 ## v3.5.4 - 2025-01-14
### What's Changed ### What's Changed
+5 -7
View File
@@ -11,7 +11,7 @@
This package generates helper files that enable your IDE to provide accurate autocompletion. This package generates helper files that enable your IDE to provide accurate autocompletion.
Generation is done based on the files in your project, so they are always up-to-date. Generation is done based on the files in your project, so they are always up-to-date.
The 3.x branch supports Laravel 10 and 11. For older version, use the 2.x releases. The 3.x branch supports Laravel 10 and later. For older version, use the 2.x releases.
- [Installation](#installation) - [Installation](#installation)
- [Usage](#usage) - [Usage](#usage)
@@ -51,7 +51,7 @@ php artisan ide-helper:models -RW
If you don't want the full _ide_helper.php file, you can run add `--write-eloquent-helper` to the model command to generate small version, which is required for the `@mixin \Eloquent` to be able to add the QueryBuilder methods. If you don't want the full _ide_helper.php file, you can run add `--write-eloquent-helper` to the model command to generate small version, which is required for the `@mixin \Eloquent` to be able to add the QueryBuilder methods.
If you don't want to add all the phpdocs to your Models directly, you can use `--nowrite` to create a seperate file. The `--write-mixin` option can be used to only add a `@mixin` to your models, but add the generated phpdocs in a seperate file. This avoids having the results marked as duplicate. If you don't want to add all the phpdocs to your Models directly, you can use `--nowrite` to create a separate file. The `--write-mixin` option can be used to only add a `@mixin` to your models, but add the generated phpdocs in a separate file. This avoids having the results marked as duplicate.
_Check out [this Laracasts video](https://laracasts.com/series/how-to-be-awesome-in-phpstorm/episodes/15) for a quick introduction/explanation!_ _Check out [this Laracasts video](https://laracasts.com/series/how-to-be-awesome-in-phpstorm/episodes/15) for a quick introduction/explanation!_
@@ -115,8 +115,6 @@ Str::macro('concat', function(string $str1, string $str2) : string {
}); });
``` ```
You can add any custom Macroable traits to detect in the `macroable_traits` config option.
### Automatic PHPDocs for models ### Automatic PHPDocs for models
If you don't want to write your properties yourself, you can use the command `php artisan ide-helper:models` to generate If you don't want to write your properties yourself, you can use the command `php artisan ide-helper:models` to generate
@@ -212,11 +210,11 @@ Eloquent allows calling `where<Attribute>` on your models, e.g. `Post::whereTitl
If for some reason it's undesired to have them generated (one for each column), you can disable this via config `write_model_magic_where` and setting it to `false`. If for some reason it's undesired to have them generated (one for each column), you can disable this via config `write_model_magic_where` and setting it to `false`.
#### Magic `*_count` properties #### Magic `*_count` and `*_exists` properties
You may use the [`::withCount`](https://laravel.com/docs/master/eloquent-relationships#counting-related-models) method to count the number results from a relationship without actually loading them. Those results are then placed in attributes following the `<columname>_count` convention. You may use the [`::withCount`](https://laravel.com/docs/master/eloquent-relationships#counting-related-models) and [`::withExists`](https://laravel.com/docs/master/eloquent-relationships#other-aggregate-functions) methods to count the number results from a relationship without actually loading them. Those results are then placed in attributes following the `<columnname>_count` and `<columnname>_exists` convention.
By default, these attributes are generated in the phpdoc. You can turn them off by setting the config `write_model_relation_count_properties` to `false`. By default, these attributes are generated in the phpdoc. You can turn them off by setting the config `write_model_relation_count_properties` and `write_model_relation_exists_properties` to `false`.
#### Generics annotations #### Generics annotations
+15 -14
View File
@@ -23,23 +23,24 @@
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"ext-json": "*", "ext-json": "*",
"barryvdh/reflection-docblock": "^2.3", "barryvdh/reflection-docblock": "^2.4",
"composer/class-map-generator": "^1.0", "composer/class-map-generator": "^1.0",
"illuminate/console": "^11.15 || ^12", "illuminate/console": "^11.15 || ^12 || ^13.0",
"illuminate/database": "^11.15 || ^12", "illuminate/database": "^11.15 || ^12 || ^13.0",
"illuminate/filesystem": "^11.15 || ^12", "illuminate/filesystem": "^11.15 || ^12 || ^13.0",
"illuminate/support": "^11.15 || ^12" "illuminate/support": "^11.15 || ^12 || ^13.0"
}, },
"require-dev": { "require-dev": {
"ext-pdo_sqlite": "*", "ext-pdo_sqlite": "*",
"friendsofphp/php-cs-fixer": "^3", "friendsofphp/php-cs-fixer": "^3",
"illuminate/config": "^11.15 || ^12", "illuminate/config": "^11.15 || ^12 || ^13.0",
"illuminate/view": "^11.15 || ^12", "illuminate/view": "^11.15 || ^12 || ^13.0",
"larastan/larastan": "^3.1",
"mockery/mockery": "^1.4", "mockery/mockery": "^1.4",
"orchestra/testbench": "^9.2 || ^10", "orchestra/testbench": "^9.2 || ^10 || ^11.0",
"phpunit/phpunit": "^10.5 || ^11.5.3", "phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^10.5 || ^11.5.3 || ^12.5.12",
"spatie/phpunit-snapshot-assertions": "^4 || ^5", "spatie/phpunit-snapshot-assertions": "^4 || ^5",
"vimeo/psalm": "^5.4",
"vlucas/phpdotenv": "^5" "vlucas/phpdotenv": "^5"
}, },
"suggest": { "suggest": {
@@ -65,7 +66,7 @@
}, },
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "3.5-dev" "dev-master": "3.6-dev"
}, },
"laravel": { "laravel": {
"providers": [ "providers": [
@@ -74,7 +75,8 @@
} }
}, },
"scripts": { "scripts": {
"analyze": "psalm", "analyze": "phpstan",
"analyze-set-baseline": "phpstan --generate-baseline",
"check-style": [ "check-style": [
"php-cs-fixer fix --diff --diff-format=udiff --dry-run", "php-cs-fixer fix --diff --diff-format=udiff --dry-run",
"php-cs-fixer fix --diff --diff-format=udiff --dry-run --config=.php_cs.tests.php" "php-cs-fixer fix --diff --diff-format=udiff --dry-run --config=.php_cs.tests.php"
@@ -83,9 +85,8 @@
"php-cs-fixer fix", "php-cs-fixer fix",
"php-cs-fixer fix --config=.php-cs-fixer.tests.php" "php-cs-fixer fix --config=.php-cs-fixer.tests.php"
], ],
"psalm-set-baseline": "psalm --set-baseline=psalm-baseline.xml",
"test": "phpunit", "test": "phpunit",
"test-ci": "phpunit -d --without-creating-snapshots", "test-ci": "phpunit",
"test-regenerate": "phpunit -d --update-snapshots" "test-regenerate": "phpunit -d --update-snapshots"
} }
} }
+44 -22
View File
@@ -49,17 +49,14 @@ return [
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Factory builders | Write model query methods
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| Set to true to generate factory generators for better factory() | Set to false to disable generated docs for the 'query()', 'newQuery()' and 'newModelQuery()' methods.
| method auto-completion.
|
| Deprecated for Laravel 8 or latest.
| |
*/ */
'include_factory_builders' => false, 'write_query_methods' => true,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@@ -85,14 +82,16 @@ return [
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Write model relation count properties | Write model relation count and exists properties
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| Set to false to disable writing of relation count properties to model DocBlocks. | Set to false to disable writing of relation count and exists properties
| to model DocBlocks.
| |
*/ */
'write_model_relation_count_properties' => true, 'write_model_relation_count_properties' => true,
'write_model_relation_exists_properties' => false,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@@ -274,6 +273,20 @@ return [
*/ */
'use_generics_annotations' => true, 'use_generics_annotations' => true,
/*
|--------------------------------------------------------------------------
| Default return types for macros
|--------------------------------------------------------------------------
|
| Define default return types for macros without explicit return types.
| e.g. `\Illuminate\Database\Query\Builder::class => 'static'`,
| `\Illuminate\Support\Str::class => 'string'`
|
*/
'macro_default_return_types' => [
Illuminate\Http\Client\Factory::class => Illuminate\Http\Client\PendingRequest::class,
],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Additional relation types | Additional relation types
@@ -323,6 +336,29 @@ return [
'enforce_nullable_relationships' => true, 'enforce_nullable_relationships' => true,
/*
|--------------------------------------------------------------------------
| Make soft deletable relations nullable
|--------------------------------------------------------------------------
|
| When set to true (default), relationships to models using SoftDeletes trait
| will be marked as nullable. This is because soft-deleted records are excluded
| from queries by default, meaning even non-nullable foreign keys can return
| null when the related model is soft-deleted.
|
| Default: true
| A relationship to a soft-deletable model will include |null in the type:
| * @property-read Team|null $team
|
| Option: false
| A relationship to a soft-deletable model will NOT include |null (unless
| nullable for other reasons such as nullable foreign key column):
| * @property-read Team $team
|
*/
'soft_deletes_force_nullable' => true,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Run artisan commands after migrations to generate model helpers | Run artisan commands after migrations to generate model helpers
@@ -335,18 +371,4 @@ return [
// 'ide-helper:models --nowrite', // 'ide-helper:models --nowrite',
], ],
/*
|--------------------------------------------------------------------------
| Macroable Traits
|--------------------------------------------------------------------------
|
| Define which traits should be considered capable of adding Macro.
| You can add any custom trait that behaves like the original Laravel one.
|
*/
'macroable_traits' => [
Filament\Support\Concerns\Macroable::class,
Spatie\Macroable\Macroable::class,
],
]; ];
@@ -1,53 +1,68 @@
<?php <?php
function vsCodeGetTranslationsFromFile($file, $path, $namespace) function vsCodeGetTranslationsFromFile(Symfony\Component\Finder\SplFileInfo $file, $path, $namespace)
{ {
$key = pathinfo($file, PATHINFO_FILENAME); if ($file->getExtension() !== 'php') {
return null;
if ($namespace) {
$key = "{$namespace}::{$key}";
} }
$lang = collect(explode(DIRECTORY_SEPARATOR, str_replace($path, '', $file))) $filePath = $file->getRealPath();
->filter()
->first();
$fileLines = Illuminate\Support\Facades\File::lines($file); $relativePath = trim(str_replace($path, '', $file->getPath()), DIRECTORY_SEPARATOR);
$lang = explode(DIRECTORY_SEPARATOR, $relativePath)[0] ?? null;
if (!$lang) {
return null;
}
$keyPath = str_replace($path . DIRECTORY_SEPARATOR . $lang . DIRECTORY_SEPARATOR, '', $filePath);
$keyWithSlashes = str_replace('.php', '', $keyPath);
$baseKey = str_replace(DIRECTORY_SEPARATOR, '.', $keyWithSlashes);
if ($namespace) {
$baseKey = "{$namespace}::{$baseKey}";
}
try {
$translations = require $filePath;
} catch (Throwable $e) {
return null;
}
if (!is_array($translations)) {
return null;
}
$fileLines = Illuminate\Support\Facades\File::lines($filePath);
$lines = []; $lines = [];
$inComment = false; $inComment = false;
foreach ($fileLines as $index => $line) { foreach ($fileLines as $index => $line) {
$trimmed = trim($line); $trimmed = trim($line);
if (str_starts_with($trimmed, '/*')) {
if (substr($trimmed, 0, 2) === '/*') {
$inComment = true; $inComment = true;
continue;
} }
if ($inComment) { if ($inComment) {
if (substr($trimmed, -2) !== '*/') { if (str_ends_with($trimmed, '*/')) {
continue;
}
$inComment = false; $inComment = false;
} }
if (substr($trimmed, 0, 2) === '//') {
continue; continue;
} }
if (str_starts_with($trimmed, '//')) {
continue;
}
$lines[] = [$index + 1, $trimmed]; $lines[] = [$index + 1, $trimmed];
} }
return [ return [
'k' => $key, 'k' => $baseKey,
'la' => $lang, 'la' => $lang,
'vs' => collect(Illuminate\Support\Arr::dot((Illuminate\Support\Arr::wrap(__($key, [], $lang))))) 'vs' => collect(Illuminate\Support\Arr::dot($translations))
->map( ->map(
fn ($value, $key) => vsCodeTranslationValue( fn ($value, $dotKey) => vsCodeTranslationValue(
$key, $dotKey,
$value, $value,
str_replace(base_path(DIRECTORY_SEPARATOR), '', $file), str_replace(base_path(DIRECTORY_SEPARATOR), '', $filePath),
$lines $lines
) )
) )
@@ -63,13 +78,12 @@ function vsCodeTranslationValue($key, $value, $file, $lines): ?array
$lineNumber = 1; $lineNumber = 1;
$keys = explode('.', $key); $keys = explode('.', $key);
$index = 0;
$currentKey = array_shift($keys); $currentKey = array_shift($keys);
foreach ($lines as $index => $line) { foreach ($lines as $line) {
if ( if (
strpos($line[1], '"' . $currentKey . '"', 0) !== false || strpos($line[1], '"' . $currentKey . '"') !== false ||
strpos($line[1], "'" . $currentKey . "'", 0) !== false strpos($line[1], "'" . $currentKey . "'") !== false
) { ) {
$lineNumber = $line[0]; $lineNumber = $line[0];
$currentKey = array_shift($keys); $currentKey = array_shift($keys);
@@ -98,9 +112,9 @@ function vscodeCollectTranslations(string $path, ?string $namespace = null)
return collect(); return collect();
} }
return collect(Illuminate\Support\Facades\File::allFiles($realPath))->map( return collect(Illuminate\Support\Facades\File::allFiles($realPath))
fn ($file) => vsCodeGetTranslationsFromFile($file, $path, $namespace) ->map(fn ($file) => vsCodeGetTranslationsFromFile($file, $path, $namespace))
); ->filter();
} }
$loader = app('translator')->getLoader(); $loader = app('translator')->getLoader();
@@ -110,7 +124,6 @@ $reflection = new ReflectionClass($loader);
$property = $reflection->hasProperty('paths') $property = $reflection->hasProperty('paths')
? $reflection->getProperty('paths') ? $reflection->getProperty('paths')
: $reflection->getProperty('path'); : $reflection->getProperty('path');
$property->setAccessible(true);
$paths = Illuminate\Support\Arr::wrap($property->getValue($loader)); $paths = Illuminate\Support\Arr::wrap($property->getValue($loader));
@@ -125,6 +138,10 @@ $namespaced = collect($namespaces)->flatMap(
$final = []; $final = [];
foreach ($default->merge($namespaced) as $value) { foreach ($default->merge($namespaced) as $value) {
if (!isset($value['vs']) || !is_iterable($value['vs'])) {
continue;
}
foreach ($value['vs'] as $key => $v) { foreach ($value['vs'] as $key => $v) {
$dotKey = "{$value['k']}.{$key}"; $dotKey = "{$value['k']}.{$key}";
@@ -0,0 +1,67 @@
parameters:
ignoreErrors:
-
message: '#^File ends with a trailing whitespace\. This may cause problems when running the code in the web browser\. Remove the closing \?\> mark or remove the whitespace\.$#'
identifier: whitespace.fileEnd
count: 1
path: resources/views/helper.php
-
message: '#^Call to an undefined method Illuminate\\Contracts\\Filesystem\\Filesystem\:\:requireOnce\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Console/MetaCommand.php
-
message: '#^Call to an undefined method Illuminate\\Contracts\\Foundation\\Application\:\:getBindings\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Console/MetaCommand.php
-
message: '#^Call to an undefined method Barryvdh\\Reflection\\DocBlock\\Tag\:\:getMethodName\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Console/ModelsCommand.php
-
message: '#^Call to an undefined method Barryvdh\\Reflection\\DocBlock\\Tag\:\:getType\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Console/ModelsCommand.php
-
message: '#^Call to an undefined method Barryvdh\\Reflection\\DocBlock\\Tag\:\:getVariableName\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Console/ModelsCommand.php
-
message: '#^Call to an undefined method Illuminate\\Support\\Optional\:\:getNumberOfParameters\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Console/ModelsCommand.php
-
message: '#^Call to an undefined method Illuminate\\Support\\Optional\:\:getParameters\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Console/ModelsCommand.php
-
message: '#^Call to an undefined method Illuminate\\Support\\Optional\:\:getReturnType\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Console/ModelsCommand.php
-
message: '#^Call to an undefined static method Illuminate\\Database\\Eloquent\\Model\:\:newFactory\(\)\.$#'
identifier: staticMethod.notFound
count: 2
path: src/Console/ModelsCommand.php
-
message: '#^Call to an undefined method ReflectionFunctionAbstract\:\:getDeclaringClass\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Method.php
+11
View File
@@ -0,0 +1,11 @@
includes:
- phpstan-baseline.neon
- vendor/phpstan/phpstan-phpunit/extension.neon
parameters:
level: 2
paths:
- src
- resources/views
excludePaths:
@@ -3,6 +3,7 @@
/** /**
* @var Barryvdh\LaravelIdeHelper\Alias[][] $namespaces_by_alias_ns * @var Barryvdh\LaravelIdeHelper\Alias[][] $namespaces_by_alias_ns
* @var Barryvdh\LaravelIdeHelper\Alias[][] $namespaces_by_extends_ns * @var Barryvdh\LaravelIdeHelper\Alias[][] $namespaces_by_extends_ns
* @var string[] $real_time_facades
* @var bool $include_fluent * @var bool $include_fluent
* @var string $helpers * @var string $helpers
*/ */
@@ -29,7 +30,7 @@ $s3 = $s1 . $s2;
<?php foreach ($namespaces_by_extends_ns as $namespace => $aliases) : ?> <?php foreach ($namespaces_by_extends_ns as $namespace => $aliases) : ?>
namespace <?= $namespace === '__root' ? '' : trim($namespace, '\\') ?> { namespace <?= $namespace === '__root' ? '' : trim($namespace, '\\') ?> {
<?php foreach ($aliases as $alias) : ?> <?php foreach ($aliases as $alias) : ?>
<?php echo trim($alias->getDocComment($s1)) . "\n{$s1}" . $alias->getClassType() ?> <?= $alias->getExtendsClass() ?> { <?php echo trim($alias->getDocComment($s1)) . "\n{$s1}" . $alias->getClassType() ?> <?= $alias->getExtendsClass() ?><?php if ($alias->shouldExtendParentClass()): ?> extends <?= $alias->getParentClass() ?><?php endif; ?> {
<?php foreach ($alias->getMethods() as $method) : ?> <?php foreach ($alias->getMethods() as $method) : ?>
<?= trim($method->getDocComment($s2)) . "\n{$s2}" ?>public static function <?= $method->getName() ?>(<?= $method->getParamsWithDefault() ?>) <?= trim($method->getDocComment($s2)) . "\n{$s2}" ?>public static function <?= $method->getName() ?>(<?= $method->getParamsWithDefault() ?>)
{<?php if ($method->getDeclaringClass() !== $method->getRoot()) : ?> {<?php if ($method->getDeclaringClass() !== $method->getRoot()) : ?>
@@ -76,7 +77,7 @@ namespace <?= $namespace === '__root' ? '' : trim($namespace, '\\') ?> {
<?php endforeach; ?> <?php endforeach; ?>
<?php foreach($real_time_facades as $name): ?> <?php foreach ($real_time_facades as $name): ?>
<?php $nested = explode('\\', str_replace('\\' . class_basename($name), '', $name)); ?> <?php $nested = explode('\\', str_replace('\\' . class_basename($name), '', $name)); ?>
namespace <?php echo implode('\\', $nested); ?> { namespace <?php echo implode('\\', $nested); ?> {
/** /**
@@ -119,12 +120,3 @@ namespace Illuminate\Support {
} }
<?php endif ?> <?php endif ?>
<?php foreach ($factories as $factory) : ?>
namespace <?=$factory->getNamespaceName()?> {
/**
* @method \Illuminate\Database\Eloquent\Collection|<?=$factory->getShortName()?>[]|<?=$factory->getShortName()?> create($attributes = [])
* @method \Illuminate\Database\Eloquent\Collection|<?=$factory->getShortName()?>[]|<?=$factory->getShortName()?> make($attributes = [])
*/
class <?=$factory->getShortName()?>FactoryBuilder extends \Illuminate\Database\Eloquent\FactoryBuilder {}
}
<?php endforeach; ?>
+15 -12
View File
@@ -1,4 +1,16 @@
<?= '<?php' ?> <?= '<?php' ?>
<?php
/**
* @var array $bindings
* @var string[] $methods
* @var string[] $configMethods
* @var Illuminate\Support\Collection $configValues
* @var array<string, array> $expectedArgumentSets
* @var array $expectedArguments
* @var string[] $userMethods
* @var string $userModel/
* */
?>
/* @noinspection ALL */ /* @noinspection ALL */
// @formatter:off // @formatter:off
@@ -35,15 +47,6 @@ namespace PHPSTORM_META {
])); ]));
<?php endforeach; ?> <?php endforeach; ?>
<?php if (count($factories)) : ?>
override(\factory(0), map([
'' => '@FactoryBuilder',
<?php foreach ($factories as $factory) : ?>
'<?= $factory->getName() ?>' => \<?= $factory->getName() ?>FactoryBuilder::class,
<?php endforeach; ?>
]));
<?php endif; ?>
override(\Illuminate\Foundation\Testing\Concerns\InteractsWithContainer::mock(0), map(["" => "@&\Mockery\MockInterface"])); override(\Illuminate\Foundation\Testing\Concerns\InteractsWithContainer::mock(0), map(["" => "@&\Mockery\MockInterface"]));
override(\Illuminate\Foundation\Testing\Concerns\InteractsWithContainer::partialMock(0), map(["" => "@&\Mockery\MockInterface"])); override(\Illuminate\Foundation\Testing\Concerns\InteractsWithContainer::partialMock(0), map(["" => "@&\Mockery\MockInterface"]));
override(\Illuminate\Foundation\Testing\Concerns\InteractsWithContainer::instance(0), type(1)); override(\Illuminate\Foundation\Testing\Concerns\InteractsWithContainer::instance(0), type(1));
@@ -79,15 +82,15 @@ namespace PHPSTORM_META {
override(\tap(0), type(0)); override(\tap(0), type(0));
override(\optional(0), type(0)); override(\optional(0), type(0));
<?php if (isset($expectedArgumentSets)): ?> <?php if ($expectedArgumentSets): ?>
<?php foreach ($expectedArgumentSets as $name => $argumentsList) : ?> <?php foreach ($expectedArgumentSets as $name => $argumentsList) : ?>
registerArgumentsSet('<?= $name ?>', <?php foreach ($argumentsList as $i => $arg) : ?><?php if($i % 5 == 0) { registerArgumentsSet('<?= $name ?>', <?php foreach ($argumentsList as $i => $arg) : ?><?php if ($i % 5 == 0) {
echo "\n"; echo "\n";
} ?><?= var_export($arg, true); ?>,<?php endforeach; ?>); } ?><?= var_export($arg, true); ?>,<?php endforeach; ?>);
<?php endforeach; ?> <?php endforeach; ?>
<?php endif ?> <?php endif ?>
<?php if (isset($expectedArguments)) : ?> <?php if ($expectedArguments) : ?>
<?php foreach ($expectedArguments as $arguments) : ?> <?php foreach ($expectedArguments as $arguments) : ?>
<?php <?php
$classes = isset($arguments['class']) ? (array) $arguments['class'] : [null]; $classes = isset($arguments['class']) ? (array) $arguments['class'] : [null];
+55 -22
View File
@@ -22,6 +22,7 @@ use Illuminate\Config\Repository as ConfigRepository;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\Facade;
use Illuminate\Support\Traits\Macroable;
use ReflectionClass; use ReflectionClass;
use Throwable; use Throwable;
@@ -36,6 +37,7 @@ class Alias
protected $classType = 'class'; protected $classType = 'class';
protected $short; protected $short;
protected $namespace = '__root'; protected $namespace = '__root';
protected $parentClass;
protected $root = null; protected $root = null;
protected $classes = []; protected $classes = [];
protected $methods = []; protected $methods = [];
@@ -46,8 +48,6 @@ class Alias
protected $phpdoc = null; protected $phpdoc = null;
protected $classAliases = []; protected $classAliases = [];
protected $isMacroable = false;
/** @var ConfigRepository */ /** @var ConfigRepository */
protected $config; protected $config;
@@ -62,13 +62,12 @@ class Alias
* @param array $magicMethods * @param array $magicMethods
* @param array $interfaces * @param array $interfaces
*/ */
public function __construct($config, $alias, $facade, $magicMethods = [], $interfaces = [], $isMacroable = false) public function __construct($config, $alias, $facade, $magicMethods = [], $interfaces = [])
{ {
$this->alias = $alias; $this->alias = $alias;
$this->magicMethods = $magicMethods; $this->magicMethods = $magicMethods;
$this->interfaces = $interfaces; $this->interfaces = $interfaces;
$this->config = $config; $this->config = $config;
$this->isMacroable = $isMacroable;
// Make the class absolute // Make the class absolute
$facade = '\\' . ltrim($facade, '\\'); $facade = '\\' . ltrim($facade, '\\');
@@ -87,6 +86,7 @@ class Alias
$this->detectNamespace(); $this->detectNamespace();
$this->detectClassType(); $this->detectClassType();
$this->detectExtendsNamespace(); $this->detectExtendsNamespace();
$this->detectParentClass();
if (!empty($this->namespace)) { if (!empty($this->namespace)) {
try { try {
@@ -101,7 +101,7 @@ class Alias
} }
if ($facade === '\Illuminate\Database\Eloquent\Model') { if ($facade === '\Illuminate\Database\Eloquent\Model') {
$this->usedMethods = ['decrement', 'increment']; $this->usedMethods = ['decrement' => true, 'increment' => true];
} }
} }
@@ -171,6 +171,25 @@ class Alias
return $this->extendsNamespace; return $this->extendsNamespace;
} }
/**
* Get the parent class of the class which this alias extends
*
* @return null|string
*/
public function getParentClass()
{
return $this->parentClass;
}
/**
* Check if this class should extend the parent class
*/
public function shouldExtendParentClass()
{
return $this->parentClass
&& !is_subclass_of($this->extends, Facade::class);
}
/** /**
* Get the Alias by which this class is called * Get the Alias by which this class is called
* *
@@ -229,11 +248,17 @@ class Alias
return; return;
} }
$reflection = new \ReflectionMethod($facade, 'fake');
if ($reflection->getNumberOfRequiredParameters() > 0) {
return;
}
$real = $facade::getFacadeRoot(); $real = $facade::getFacadeRoot();
try { try {
$facade::fake(); $facade::fake();
$fake = $facade::getFacadeRoot(); $fake = $facade::getFacadeRoot();
if ($fake !== $real) { if ($fake !== $real) {
$this->addClass(get_class($fake)); $this->addClass(get_class($fake));
} }
@@ -268,6 +293,18 @@ class Alias
} }
} }
/**
* Detect the parent class
*/
protected function detectParentClass()
{
$reflection = new ReflectionClass($this->root);
$parentClass = $reflection->getParentClass();
$this->parentClass = $parentClass ? '\\' . $parentClass->getName() : null;
}
/** /**
* Detect the class type * Detect the class type
*/ */
@@ -288,9 +325,8 @@ class Alias
/** /**
* Get the real root of a facade * Get the real root of a facade
* *
* @return bool|string
*/ */
protected function detectRoot() protected function detectRoot(): void
{ {
$facade = $this->facade; $facade = $this->facade;
@@ -345,11 +381,10 @@ class Alias
$method = new \ReflectionMethod($className, $name); $method = new \ReflectionMethod($className, $name);
$class = new ReflectionClass($className); $class = new ReflectionClass($className);
if (!in_array($magic, $this->usedMethods)) { if (!isset($this->usedMethods[$magic])) {
if ($class !== $this->root) { if ($class !== $this->root) {
$this->methods[] = new Method( $this->methods[] = new Method(
$method, $method,
$this->alias,
$class, $class,
$magic, $magic,
$this->interfaces, $this->interfaces,
@@ -358,7 +393,7 @@ class Alias
$this->getTemplateNames() $this->getTemplateNames()
); );
} }
$this->usedMethods[] = $magic; $this->usedMethods[$magic] = true;
} }
} }
} }
@@ -366,7 +401,6 @@ class Alias
/** /**
* Get the methods for one or multiple classes. * Get the methods for one or multiple classes.
* *
* @return string
*/ */
protected function detectMethods() protected function detectMethods()
{ {
@@ -376,13 +410,12 @@ class Alias
$methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC); $methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
if ($methods) { if ($methods) {
foreach ($methods as $method) { foreach ($methods as $method) {
if (!in_array($method->name, $this->usedMethods)) { if (!isset($this->usedMethods[$method->name])) {
// Only add the methods to the output when the root is not the same as the class. // Only add the methods to the output when the root is not the same as the class.
// And don't add the __*() methods // And don't add the __*() methods
if ($this->extends !== $class && substr($method->name, 0, 2) !== '__') { if ($this->extends !== $class && substr($method->name, 0, 2) !== '__') {
$this->methods[] = new Method( $this->methods[] = new Method(
$method, $method,
$this->alias,
$reflection, $reflection,
$method->name, $method->name,
$this->interfaces, $this->interfaces,
@@ -391,18 +424,18 @@ class Alias
$this->getTemplateNames(), $this->getTemplateNames(),
); );
} }
$this->usedMethods[] = $method->name; $this->usedMethods[$method->name] = true;
} }
} }
} }
// Check if the class is macroable // Check if the class is macroable
// (Eloquent\Builder is also macroable but doesn't use Macroable trait) // (Eloquent\Builder is also macroable but doesn't use Macroable trait)
if ($this->isMacroable || $class === EloquentBuilder::class) { if ($class === EloquentBuilder::class || in_array(Macroable::class, $reflection->getTraitNames())) {
$properties = $reflection->getStaticProperties(); $properties = $reflection->getStaticProperties();
$macros = isset($properties['macros']) ? $properties['macros'] : []; $macros = isset($properties['macros']) ? $properties['macros'] : [];
foreach ($macros as $macro_name => $macro_func) { foreach ($macros as $macro_name => $macro_func) {
if (!in_array($macro_name, $this->usedMethods)) { if (!isset($this->usedMethods[$macro_name])) {
try { try {
$method = $this->getMacroFunction($macro_func); $method = $this->getMacroFunction($macro_func);
} catch (Throwable $e) { } catch (Throwable $e) {
@@ -412,14 +445,13 @@ class Alias
// Add macros // Add macros
$this->methods[] = new Macro( $this->methods[] = new Macro(
$method, $method,
$this->alias,
$reflection, $reflection,
$macro_name, $macro_name,
$this->interfaces, $this->interfaces,
$this->classAliases, $this->classAliases,
$this->getReturnTypeNormalizers($reflection) $this->getReturnTypeNormalizers($reflection)
); );
$this->usedMethods[] = $macro_name; $this->usedMethods[$macro_name] = true;
} }
} }
} }
@@ -557,12 +589,13 @@ class Alias
*/ */
protected function removeDuplicateMethodsFromPhpDoc() protected function removeDuplicateMethodsFromPhpDoc()
{ {
$methodNames = array_map(function (Method $method) { $methodNames = [];
return $method->getName(); foreach ($this->getMethods() as $method) {
}, $this->getMethods()); $methodNames[$method->getName()] = true;
}
foreach ($this->phpdoc->getTags() as $tag) { foreach ($this->phpdoc->getTags() as $tag) {
if ($tag instanceof MethodTag && in_array($tag->getMethodName(), $methodNames)) { if ($tag instanceof MethodTag && isset($methodNames[$tag->getMethodName()])) {
$this->phpdoc->deleteTag($tag); $this->phpdoc->deleteTag($tag);
} }
} }
@@ -11,7 +11,6 @@
namespace Barryvdh\LaravelIdeHelper\Console; namespace Barryvdh\LaravelIdeHelper\Console;
use Barryvdh\LaravelIdeHelper\Factories;
use Dotenv\Parser\Entry; use Dotenv\Parser\Entry;
use Dotenv\Parser\Parser; use Dotenv\Parser\Parser;
use Illuminate\Console\Command; use Illuminate\Console\Command;
@@ -108,9 +107,6 @@ class MetaCommand extends Command
*/ */
public function handle() public function handle()
{ {
// Needs to run before exception handler is registered
$factories = $this->config->get('ide-helper.include_factory_builders') ? Factories::all() : [];
$ourAutoloader = $this->registerClassAutoloadExceptions(); $ourAutoloader = $this->registerClassAutoloadExceptions();
$bindings = []; $bindings = [];
@@ -149,7 +145,6 @@ class MetaCommand extends Command
$content = $this->view->make('ide-helper::meta', [ $content = $this->view->make('ide-helper::meta', [
'bindings' => $bindings, 'bindings' => $bindings,
'methods' => $this->methods, 'methods' => $this->methods,
'factories' => $factories,
'configMethods' => $this->configMethods, 'configMethods' => $this->configMethods,
'configValues' => $configValues, 'configValues' => $configValues,
'expectedArgumentSets' => $this->getExpectedArgumentSets(), 'expectedArgumentSets' => $this->getExpectedArgumentSets(),
@@ -192,10 +187,31 @@ class MetaCommand extends Command
*/ */
protected function registerClassAutoloadExceptions(): callable protected function registerClassAutoloadExceptions(): callable
{ {
$autoloader = function ($class) { $aliases = array_filter([...$this->getAbstracts(), 'config'], fn ($abstract) => !str_contains($abstract, '\\'));
$autoloader = function ($class) use ($aliases) {
// ignore aliases as they're meant to be resolved elsewhere
if (in_array($class, $aliases, true)) {
return;
}
// Don't throw when class existence is being checked via class_exists(),
// interface_exists(), trait_exists(), or enum_exists(). These functions
// expect the autoloader to return gracefully when the class doesn't exist.
// Throwing here would break libraries that use class_exists() to check for
// optional dependencies (e.g. Doctrine ORM checking for removed classes).
$existsFunctions = ['class_exists', 'interface_exists', 'trait_exists', 'enum_exists'];
foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3) as $frame) {
if (isset($frame['function']) && in_array($frame['function'], $existsFunctions, true)) {
return;
}
}
throw new \ReflectionException("Class '$class' not found."); throw new \ReflectionException("Class '$class' not found.");
}; };
spl_autoload_register($autoloader); spl_autoload_register($autoloader);
return $autoloader; return $autoloader;
} }
@@ -114,6 +114,7 @@ class ModelsCommand extends Command
protected $write_model_magic_where; protected $write_model_magic_where;
protected $write_model_relation_count_properties; protected $write_model_relation_count_properties;
protected $write_model_relation_exists_properties;
protected $properties = []; protected $properties = [];
protected $methods = []; protected $methods = [];
protected $write = false; protected $write = false;
@@ -122,6 +123,14 @@ class ModelsCommand extends Command
protected $reset; protected $reset;
protected $phpstorm_noinspections; protected $phpstorm_noinspections;
protected $write_model_external_builder_methods; protected $write_model_external_builder_methods;
/**
* @var array<string, \SplFileObject>
*/
protected $fileCache = [];
/**
* @var array<string, Context>
*/
protected $contextCache = [];
/** /**
* @var array<string, true> * @var array<string, true>
*/ */
@@ -173,6 +182,8 @@ class ModelsCommand extends Command
$this->write_model_external_builder_methods = $this->laravel['config']->get('ide-helper.write_model_external_builder_methods', true); $this->write_model_external_builder_methods = $this->laravel['config']->get('ide-helper.write_model_external_builder_methods', true);
$this->write_model_relation_count_properties = $this->write_model_relation_count_properties =
$this->laravel['config']->get('ide-helper.write_model_relation_count_properties', true); $this->laravel['config']->get('ide-helper.write_model_relation_count_properties', true);
$this->write_model_relation_exists_properties =
$this->laravel['config']->get('ide-helper.write_model_relation_exists_properties', false);
$this->write = $this->write_mixin ? true : $this->write; $this->write = $this->write_mixin ? true : $this->write;
//If filename is default and Write is not specified, ask what to do //If filename is default and Write is not specified, ask what to do
@@ -410,9 +421,6 @@ class ModelsCommand extends Command
$params = []; $params = [];
switch ($type) { switch ($type) {
case 'encrypted':
$realType = 'mixed';
break;
case 'boolean': case 'boolean':
case 'bool': case 'bool':
$realType = 'bool'; $realType = 'bool';
@@ -420,6 +428,7 @@ class ModelsCommand extends Command
case 'decimal': case 'decimal':
$realType = 'numeric'; $realType = 'numeric';
break; break;
case 'encrypted':
case 'string': case 'string':
case 'hashed': case 'hashed':
$realType = 'string'; $realType = 'string';
@@ -453,7 +462,7 @@ class ModelsCommand extends Command
$realType = '\Illuminate\Support\Collection<array-key, mixed>'; $realType = '\Illuminate\Support\Collection<array-key, mixed>';
break; break;
case AsArrayObject::class: case AsArrayObject::class:
$realType = '\ArrayObject<array-key, mixed>'; $realType = '\Illuminate\Database\Eloquent\Casts\ArrayObject<array-key, mixed>';
break; break;
default: default:
// In case of an optional custom cast parameter , only evaluate // In case of an optional custom cast parameter , only evaluate
@@ -475,7 +484,11 @@ class ModelsCommand extends Command
} }
if (Str::startsWith($type, AsCollection::class)) { if (Str::startsWith($type, AsCollection::class)) {
$realType = $this->getTypeInModel($model, $params[0] ?? null) ?? '\Illuminate\Support\Collection'; $realType = $this->getTypeInModel($model, $params[0] ?? null) ?: '\Illuminate\Support\Collection';
$relatedModel = $this->getTypeInModel($model, $params[1] ?? null);
if ($relatedModel) {
$realType = $this->getCollectionTypeHint($realType, $relatedModel);
}
} }
if (Str::startsWith($type, AsEnumCollection::class)) { if (Str::startsWith($type, AsEnumCollection::class)) {
@@ -521,7 +534,7 @@ class ModelsCommand extends Command
} }
if ($isNullable) { if ($isNullable) {
$type .= '|null'; $type = $this->wrapIntersectionType($type) . '|null';
} else { } else {
$type = str_replace($nullString, '', $type); $type = str_replace($nullString, '', $type);
} }
@@ -529,6 +542,22 @@ class ModelsCommand extends Command
return $type; return $type;
} }
/**
* Wraps a bare intersection type in parentheses for correct DNF syntax.
*
* For example, `A&B` becomes `(A&B)` so that adding `|null` produces
* `(A&B)|null` instead of the ambiguous `A&B|null`.
* Types that are already parenthesized or contain union types are returned as-is.
*/
protected function wrapIntersectionType(string $type): string
{
if (str_contains($type, '&') && !str_contains($type, '|') && $type[0] !== '(') {
return '(' . $type . ')';
}
return $type;
}
/** /**
* Returns the override type for the give type. * Returns the override type for the give type.
* *
@@ -579,6 +608,8 @@ class ModelsCommand extends Command
'float', 'real', 'float4', 'float', 'real', 'float4',
'double', 'float8' => 'float', 'double', 'float8' => 'float',
'decimal', 'numeric' => 'numeric',
default => 'string', default => 'string',
}; };
} }
@@ -669,9 +700,11 @@ class ModelsCommand extends Command
$comment = $this->getCommentFromDocBlock($reflection); $comment = $this->getCommentFromDocBlock($reflection);
$this->setProperty($name, null, null, true, $comment); $this->setProperty($name, null, null, true, $comment);
} }
} elseif (Str::startsWith($method, 'scope') && $method !== 'scopeQuery' && $method !== 'scope' && $method !== 'scopes') { } elseif (!empty($reflection->getAttributes('Illuminate\Database\Eloquent\Attributes\Scope')) || (Str::startsWith($method, 'scope') && $method !== 'scopeQuery' && $method !== 'scope' && $method !== 'scopes')) {
$scopeUsingAttribute = !empty($reflection->getAttributes('Illuminate\Database\Eloquent\Attributes\Scope'));
//Magic scope<name>Attribute //Magic scope<name>Attribute
$name = Str::camel(substr($method, 5)); $name = $scopeUsingAttribute ? $method : Str::camel(substr($method, 5));
if (!empty($name)) { if (!empty($name)) {
$comment = $this->getCommentFromDocBlock($reflection); $comment = $this->getCommentFromDocBlock($reflection);
$args = $this->getParameters($reflection); $args = $this->getParameters($reflection);
@@ -687,13 +720,16 @@ class ModelsCommand extends Command
); );
$this->setMethod($name, $builder . '<static>|' . $modelName, $args, $comment); $this->setMethod($name, $builder . '<static>|' . $modelName, $args, $comment);
} }
} elseif (in_array($method, ['query', 'newQuery', 'newModelQuery'])) { } elseif (in_array($method, ['query', 'newQuery', 'newModelQuery'])
) {
if ($this->laravel['config']->get('ide-helper.write_query_methods', true)) {
$builder = $this->getClassNameInDestinationFile($model, get_class($model->newModelQuery())); $builder = $this->getClassNameInDestinationFile($model, get_class($model->newModelQuery()));
$this->setMethod( $this->setMethod(
$method, $method,
$builder . '<static>|' . $this->getClassNameInDestinationFile($model, get_class($model)) $builder . '<static>|' . $this->getClassNameInDestinationFile($model, get_class($model))
); );
}
if ($this->write_model_external_builder_methods) { if ($this->write_model_external_builder_methods) {
$this->writeModelExternalBuilderMethods($model); $this->writeModelExternalBuilderMethods($model);
@@ -712,14 +748,19 @@ class ModelsCommand extends Command
$type = (string)$this->getReturnTypeFromDocBlock($reflection); $type = (string)$this->getReturnTypeFromDocBlock($reflection);
} }
$file = new \SplFileObject($reflection->getFileName()); $fileName = $reflection->getFileName();
if (!isset($this->fileCache[$fileName])) {
$this->fileCache[$fileName] = new \SplFileObject($fileName);
}
$file = $this->fileCache[$fileName];
$file->seek($reflection->getStartLine() - 1); $file->seek($reflection->getStartLine() - 1);
$code = ''; $lines = [];
while ($file->key() < $reflection->getEndLine()) { while ($file->key() < $reflection->getEndLine()) {
$code .= $file->current(); $lines[] = $file->current();
$file->next(); $file->next();
} }
$code = implode('', $lines);
$code = trim(preg_replace('/\s\s+/', '', $code)); $code = trim(preg_replace('/\s\s+/', '', $code));
$begin = strpos($code, 'function('); $begin = strpos($code, 'function(');
$code = substr($code, $begin, strrpos($code, '}') - $begin + 1); $code = substr($code, $begin, strrpos($code, '}') - $begin + 1);
@@ -816,6 +857,15 @@ class ModelsCommand extends Command
// What kind of comments should be added to the relation count here? // What kind of comments should be added to the relation count here?
); );
} }
if ($this->write_model_relation_exists_properties) {
$this->setProperty(
Str::snake($method) . '_exists',
'bool|null',
true,
false
// What kind of comments should be added to the relation count here?
);
}
} elseif ( } elseif (
$relationReturnType === 'morphTo' || $relationReturnType === 'morphTo' ||
( (
@@ -871,7 +921,6 @@ class ModelsCommand extends Command
if (in_array($relation, ['hasOne', 'hasOneThrough', 'morphOne'], true)) { if (in_array($relation, ['hasOne', 'hasOneThrough', 'morphOne'], true)) {
$defaultProp = $reflectionObj->getProperty('withDefault'); $defaultProp = $reflectionObj->getProperty('withDefault');
$defaultProp->setAccessible(true);
return !$defaultProp->getValue($relationObj); return !$defaultProp->getValue($relationObj);
} }
@@ -881,7 +930,6 @@ class ModelsCommand extends Command
} }
$fkProp = $reflectionObj->getProperty('foreignKey'); $fkProp = $reflectionObj->getProperty('foreignKey');
$fkProp->setAccessible(true);
$enforceNullableRelation = $this->laravel['config']->get('ide-helper.enforce_nullable_relationships', true); $enforceNullableRelation = $this->laravel['config']->get('ide-helper.enforce_nullable_relationships', true);
@@ -895,9 +943,31 @@ class ModelsCommand extends Command
} }
} }
if ($this->relatedModelUsesSoftDeletes($relationObj)) {
return true;
}
return false; return false;
} }
/**
* Check if the related model uses the SoftDeletes trait
*
* @param Relation $relationObj
*
* @return bool
*/
protected function relatedModelUsesSoftDeletes(Relation $relationObj): bool
{
if (!$this->laravel['config']->get('ide-helper.soft_deletes_force_nullable', true)) {
return false;
}
$relatedModel = $relationObj->getRelated();
return in_array('Illuminate\\Database\\Eloquent\\SoftDeletes', class_uses_recursive($relatedModel));
}
/** /**
* Check if the morphTo relation is nullable * Check if the morphTo relation is nullable
* *
@@ -914,7 +984,6 @@ class ModelsCommand extends Command
} }
$fkProp = $reflectionObj->getProperty('foreignKey'); $fkProp = $reflectionObj->getProperty('foreignKey');
$fkProp->setAccessible(true);
foreach (Arr::wrap($fkProp->getValue($relationObj)) as $foreignKey) { foreach (Arr::wrap($fkProp->getValue($relationObj)) as $foreignKey) {
if (isset($this->nullableColumns[$foreignKey])) { if (isset($this->nullableColumns[$foreignKey])) {
@@ -945,7 +1014,7 @@ class ModelsCommand extends Command
if ($type !== null) { if ($type !== null) {
$newType = $this->getTypeOverride($type); $newType = $this->getTypeOverride($type);
if ($nullable) { if ($nullable) {
$newType .= '|null'; $newType = $this->wrapIntersectionType($newType) . '|null';
} }
$this->properties[$name]['type'] = $newType; $this->properties[$name]['type'] = $newType;
} }
@@ -1096,6 +1165,7 @@ class ModelsCommand extends Command
$serializer = new DocBlockSerializer(); $serializer = new DocBlockSerializer();
$docComment = $serializer->getDocComment($phpdoc); $docComment = $serializer->getDocComment($phpdoc);
$mixinClassName = null;
if ($this->write_mixin) { if ($this->write_mixin) {
$phpdocMixin = new DocBlock($reflection, new Context($namespace)); $phpdocMixin = new DocBlock($reflection, new Context($namespace));
@@ -1132,6 +1202,14 @@ class ModelsCommand extends Command
$replace = "{$modelDocComment}\n"; $replace = "{$modelDocComment}\n";
$pos = strpos($contents, "final class {$classname}") ?: strpos($contents, "class {$classname}"); $pos = strpos($contents, "final class {$classname}") ?: strpos($contents, "class {$classname}");
if ($pos !== false) { if ($pos !== false) {
// If PHP 8 attributes (e.g. #[ObservedBy(...)]) precede the class
// declaration, insert the docblock before the first attribute so that
// the resulting order is: docblock → attributes → class.
$before = substr($contents, 0, $pos);
if (preg_match('/((?:#\[.+?\]\s*)+)$/s', $before, $matches)) {
$pos -= strlen($matches[1]);
$replace = "{$modelDocComment}\n";
}
$contents = substr_replace($contents, $replace, $pos, 0); $contents = substr_replace($contents, $replace, $pos, 0);
} }
} }
@@ -1184,7 +1262,7 @@ class ModelsCommand extends Command
$default = '[]'; $default = '[]';
} elseif (is_null($default)) { } elseif (is_null($default)) {
$default = 'null'; $default = 'null';
} elseif (is_int($default)) { } elseif (is_int($default) || is_float($default)) {
//$default = $default; //$default = $default;
} elseif ($default instanceof \UnitEnum) { } elseif ($default instanceof \UnitEnum) {
$default = '\\' . get_class($default) . '::' . $default->name; $default = '\\' . get_class($default) . '::' . $default->name;
@@ -1237,13 +1315,18 @@ class ModelsCommand extends Command
} }
} }
protected ?array $cachedRelationTypes = null;
protected ?array $cachedRelationReturnTypes = null;
/** /**
* Returns the available relation types * Returns the available relation types
*/ */
protected function getRelationTypes(): array protected function getRelationTypes(): array
{ {
$configuredRelations = $this->laravel['config']->get('ide-helper.additional_relation_types', []); return $this->cachedRelationTypes ??= array_merge(
return array_merge(self::RELATION_TYPES, $configuredRelations); self::RELATION_TYPES,
$this->laravel['config']->get('ide-helper.additional_relation_types', [])
);
} }
/** /**
@@ -1251,7 +1334,7 @@ class ModelsCommand extends Command
*/ */
protected function getRelationReturnTypes(): array protected function getRelationReturnTypes(): array
{ {
return $this->laravel['config']->get('ide-helper.additional_relation_return_types', []); return $this->cachedRelationReturnTypes ??= $this->laravel['config']->get('ide-helper.additional_relation_return_types', []);
} }
/** /**
@@ -1267,9 +1350,6 @@ class ModelsCommand extends Command
*/ */
protected function getAttributeTypes(Model $model, \ReflectionMethod $reflectionMethod): Collection protected function getAttributeTypes(Model $model, \ReflectionMethod $reflectionMethod): Collection
{ {
// Private/protected ReflectionMethods require setAccessible prior to PHP 8.1
$reflectionMethod->setAccessible(true);
/** @var Attribute $attribute */ /** @var Attribute $attribute */
$attribute = $reflectionMethod->invoke($model); $attribute = $reflectionMethod->invoke($model);
@@ -1329,11 +1409,7 @@ class ModelsCommand extends Command
*/ */
protected function getCommentFromDocBlock(\ReflectionMethod $reflection) protected function getCommentFromDocBlock(\ReflectionMethod $reflection)
{ {
$phpDocContext = (new ContextFactory())->createFromReflector($reflection); $context = $this->getDocBlockContext($reflection);
$context = new Context(
$phpDocContext->getNamespace(),
$phpDocContext->getNamespaceAliases()
);
$comment = ''; $comment = '';
$phpdoc = new DocBlock($reflection, $context); $phpdoc = new DocBlock($reflection, $context);
@@ -1354,11 +1430,7 @@ class ModelsCommand extends Command
*/ */
protected function getReturnTypeFromDocBlock(\ReflectionMethod $reflection, ?\Reflector $reflectorForContext = null) protected function getReturnTypeFromDocBlock(\ReflectionMethod $reflection, ?\Reflector $reflectorForContext = null)
{ {
$phpDocContext = (new ContextFactory())->createFromReflector($reflectorForContext ?? $reflection); $context = $this->getDocBlockContext($reflectorForContext ?? $reflection);
$context = new Context(
$phpDocContext->getNamespace(),
$phpDocContext->getNamespaceAliases()
);
$type = null; $type = null;
$phpdoc = new DocBlock($reflection, $context); $phpdoc = new DocBlock($reflection, $context);
@@ -1376,6 +1448,31 @@ class ModelsCommand extends Command
return $type; return $type;
} }
protected function getDocBlockContext(\Reflector $reflector): Context
{
if ($reflector instanceof \ReflectionMethod) {
$key = $reflector->getDeclaringClass()->getName();
} elseif ($reflector instanceof ReflectionClass) {
$key = $reflector->getName();
} else {
$phpDocContext = (new ContextFactory())->createFromReflector($reflector);
return new Context(
$phpDocContext->getNamespace(),
$phpDocContext->getNamespaceAliases()
);
}
if (!isset($this->contextCache[$key])) {
$phpDocContext = (new ContextFactory())->createFromReflector($reflector);
$this->contextCache[$key] = new Context(
$phpDocContext->getNamespace(),
$phpDocContext->getNamespaceAliases()
);
}
return $this->contextCache[$key];
}
protected function getReturnTypeFromReflection(\ReflectionMethod $reflection): ?string protected function getReturnTypeFromReflection(\ReflectionMethod $reflection): ?string
{ {
$returnType = $reflection->getReturnType(); $returnType = $reflection->getReturnType();
@@ -1405,7 +1502,7 @@ class ModelsCommand extends Command
if (in_array('Illuminate\\Database\\Eloquent\\SoftDeletes', $traits)) { if (in_array('Illuminate\\Database\\Eloquent\\SoftDeletes', $traits)) {
$modelName = $this->getClassNameInDestinationFile($model, get_class($model)); $modelName = $this->getClassNameInDestinationFile($model, get_class($model));
$builder = $this->getClassNameInDestinationFile($model, \Illuminate\Database\Eloquent\Builder::class); $builder = $this->getClassNameInDestinationFile($model, \Illuminate\Database\Eloquent\Builder::class);
$this->setMethod('withTrashed', $builder . '<static>|' . $modelName, []); $this->setMethod('withTrashed', $builder . '<static>|' . $modelName, ['bool $withTrashed = true']);
$this->setMethod('withoutTrashed', $builder . '<static>|' . $modelName, []); $this->setMethod('withoutTrashed', $builder . '<static>|' . $modelName, []);
$this->setMethod('onlyTrashed', $builder . '<static>|' . $modelName, []); $this->setMethod('onlyTrashed', $builder . '<static>|' . $modelName, []);
} }
@@ -1581,9 +1678,10 @@ class ModelsCommand extends Command
*/ */
protected function getUsedClassNames(ReflectionClass $reflection): array protected function getUsedClassNames(ReflectionClass $reflection): array
{ {
$context = $this->getDocBlockContext($reflection);
$namespaceAliases = array_flip(array_map(function ($alias) { $namespaceAliases = array_flip(array_map(function ($alias) {
return ltrim($alias, '\\'); return ltrim($alias, '\\');
}, (new ContextFactory())->createFromReflector($reflection)->getNamespaceAliases())); }, $context->getNamespaceAliases()));
$namespaceAliases[$reflection->getName()] = $reflection->getShortName(); $namespaceAliases[$reflection->getName()] = $reflection->getShortName();
return $namespaceAliases; return $namespaceAliases;
@@ -1627,7 +1725,8 @@ class ModelsCommand extends Command
$type = implode('|', $types); $type = implode('|', $types);
if ($paramType->allowsNull()) { if ($paramType->allowsNull()) {
if (count($types) == 1) { // Use ?Type syntax only for single named types, not for intersection types
if (count($types) == 1 && !str_starts_with($type, '(')) {
$type = '?' . $type; $type = '?' . $type;
} else { } else {
$type .= '|null'; $type .= '|null';
@@ -1645,7 +1744,7 @@ class ModelsCommand extends Command
preg_match( preg_match(
'/@param ((?:(?:[\w?|\\\\<>])+(?:\[])?)+)/', '/@param ((?:(?:[\w?|\\\\<>])+(?:\[])?)+)/',
$docComment ?? '', $docComment,
$matches $matches
); );
$type = $matches[1] ?? ''; $type = $matches[1] ?? '';
@@ -1700,24 +1799,53 @@ class ModelsCommand extends Command
return $type; return $type;
} }
protected function extractReflectionTypes(ReflectionType $reflection_type) protected function extractReflectionTypes(ReflectionType $reflection_type): array
{ {
if ($reflection_type instanceof ReflectionNamedType) { if ($reflection_type instanceof ReflectionNamedType) {
$types[] = $this->getReflectionNamedType($reflection_type); return [$this->getReflectionNamedType($reflection_type)];
} else {
$types = [];
foreach ($reflection_type->getTypes() as $named_type) {
if ($named_type->getName() === 'null') {
continue;
} }
$types[] = $this->getReflectionNamedType($named_type); if ($reflection_type instanceof \ReflectionIntersectionType) {
return [$this->formatIntersectionType($reflection_type)];
} }
if ($reflection_type instanceof \ReflectionUnionType) {
return $this->extractUnionTypes($reflection_type);
}
// Unknown type - return empty array as fallback
return [];
}
protected function extractUnionTypes(\ReflectionUnionType $union_type): array
{
$types = [];
foreach ($union_type->getTypes() as $inner_type) {
if ($inner_type instanceof ReflectionNamedType) {
if ($inner_type->getName() === 'null') {
continue;
}
$types[] = $this->getReflectionNamedType($inner_type);
} elseif ($inner_type instanceof \ReflectionIntersectionType) {
$types[] = $this->formatIntersectionType($inner_type);
}
// ReflectionUnionType cannot be nested per PHP's DNF rules
} }
return $types; return $types;
} }
protected function formatIntersectionType(\ReflectionIntersectionType $intersection_type): string
{
$parts = [];
foreach ($intersection_type->getTypes() as $type) {
$parts[] = $this->getReflectionNamedType($type);
}
return '(' . implode('&', $parts) . ')';
}
protected function getReflectionNamedType(ReflectionNamedType $paramType): string protected function getReflectionNamedType(ReflectionNamedType $paramType): string
{ {
$parameterName = $paramType->getName(); $parameterName = $paramType->getName();
-36
View File
@@ -1,36 +0,0 @@
<?php
namespace Barryvdh\LaravelIdeHelper;
use Exception;
use Illuminate\Database\Eloquent\Factory;
use ReflectionClass;
class Factories
{
public static function all()
{
$factories = [];
if (static::isLaravelSevenOrLower()) {
$factory = app(Factory::class);
$definitions = (new ReflectionClass(Factory::class))->getProperty('definitions');
$definitions->setAccessible(true);
foreach ($definitions->getValue($factory) as $factory_target => $config) {
try {
$factories[] = new ReflectionClass($factory_target);
} catch (Exception $exception) {
}
}
}
return $factories;
}
protected static function isLaravelSevenOrLower()
{
return class_exists('Illuminate\Database\Eloquent\Factory');
}
}
+11 -19
View File
@@ -35,7 +35,6 @@ class Generator
protected $magic = []; protected $magic = [];
protected $interfaces = []; protected $interfaces = [];
protected $helpers; protected $helpers;
protected array $macroableTraits = [];
/** /**
* @param \Illuminate\Config\Repository $config * @param \Illuminate\Config\Repository $config
@@ -58,9 +57,13 @@ class Generator
// Find the drivers to add to the extra/interfaces // Find the drivers to add to the extra/interfaces
$this->detectDrivers(); $this->detectDrivers();
$this->extra = array_merge($this->extra, $this->config->get('ide-helper.extra'), []); $this->extra = array_merge($this->extra, $this->config->get('ide-helper.extra', []));
$this->magic = array_merge($this->magic, $this->config->get('ide-helper.magic'), []); $this->magic = array_merge($this->magic, $this->config->get('ide-helper.magic', []));
$this->interfaces = array_merge($this->interfaces, $this->config->get('ide-helper.interfaces'), []); $this->interfaces = array_merge($this->interfaces, $this->config->get('ide-helper.interfaces', []));
Macro::setDefaultReturnTypes($this->config->get('ide-helper.macro_default_return_types', [
\Illuminate\Http\Client\Factory::class => \Illuminate\Http\Client\PendingRequest::class,
]));
// Make all interface classes absolute // Make all interface classes absolute
foreach ($this->interfaces as &$interface) { foreach ($this->interfaces as &$interface) {
$interface = '\\' . ltrim($interface, '\\'); $interface = '\\' . ltrim($interface, '\\');
@@ -71,7 +74,7 @@ class Generator
/** /**
* Generate the helper file contents; * Generate the helper file contents;
* *
* @return string; * @return string
*/ */
public function generate() public function generate()
{ {
@@ -82,7 +85,6 @@ class Generator
->with('real_time_facades', $this->getRealTimeFacades()) ->with('real_time_facades', $this->getRealTimeFacades())
->with('helpers', $this->detectHelpers()) ->with('helpers', $this->detectHelpers())
->with('include_fluent', $this->config->get('ide-helper.include_fluent', true)) ->with('include_fluent', $this->config->get('ide-helper.include_fluent', true))
->with('factories', $this->config->get('ide-helper.include_factory_builders') ? Factories::all() : [])
->render(); ->render();
} }
@@ -360,7 +362,7 @@ class Generator
continue; continue;
} }
$aliases[] = new Alias($this->config, $class, $class, [], $this->interfaces, true); $aliases[] = new Alias($this->config, $class, $class, [], $this->interfaces);
} }
} }
@@ -382,18 +384,8 @@ class Generator
->filter(function ($class) { ->filter(function ($class) {
$traits = class_uses_recursive($class); $traits = class_uses_recursive($class);
if (isset($traits[Macroable::class])) { // Filter only classes with the macroable trait
return true; return isset($traits[Macroable::class]);
}
// Filter only classes with a macroable trait
foreach ($this->config->get('ide-helper.macroable_traits', []) as $trait) {
if (isset($traits[$trait])) {
return true;
}
}
return false;
}) })
->filter(function ($class) use ($aliases) { ->filter(function ($class) use ($aliases) {
$class = Str::start($class, '\\'); $class = Str::start($class, '\\');
+46 -16
View File
@@ -5,20 +5,16 @@ namespace Barryvdh\LaravelIdeHelper;
use Barryvdh\Reflection\DocBlock; use Barryvdh\Reflection\DocBlock;
use Barryvdh\Reflection\DocBlock\Tag; use Barryvdh\Reflection\DocBlock\Tag;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
class Macro extends Method class Macro extends Method
{ {
protected $macroDefaults = [ protected static $macroDefaults = [];
\Illuminate\Http\Client\Factory::class => PendingRequest::class,
];
/** /**
* Macro constructor. * Macro constructor.
* *
* @param \ReflectionFunctionAbstract $method * @param \ReflectionFunctionAbstract $method
* @param string $alias
* @param \ReflectionClass $class * @param \ReflectionClass $class
* @param null $methodName * @param null $methodName
* @param array $interfaces * @param array $interfaces
@@ -27,16 +23,19 @@ class Macro extends Method
*/ */
public function __construct( public function __construct(
$method, $method,
$alias,
$class, $class,
$methodName = null, $methodName = null,
$interfaces = [], $interfaces = [],
$classAliases = [], $classAliases = [],
$returnTypeNormalizers = [] $returnTypeNormalizers = []
) { ) {
parent::__construct($method, $alias, $class, $methodName, $interfaces, $classAliases, $returnTypeNormalizers); parent::__construct($method, $class, $methodName, $interfaces, $classAliases, $returnTypeNormalizers);
} }
public static function setDefaultReturnTypes(array $map = [])
{
static::$macroDefaults = array_merge(static::$macroDefaults, $map);
}
/** /**
* @param \ReflectionFunctionAbstract $method * @param \ReflectionFunctionAbstract $method
*/ */
@@ -74,8 +73,8 @@ class Macro extends Method
$type = $this->concatReflectionTypes($return); $type = $this->concatReflectionTypes($return);
/** @psalm-suppress UndefinedClass */
if (!$return instanceof \ReflectionUnionType) { if (!$return instanceof \ReflectionUnionType) {
/** @phpstan-ignore method.notFound */
$type .= $this->root === "\\{$builder}" && $return->getName() === $builder ? '|static' : ''; $type .= $this->root === "\\{$builder}" && $return->getName() === $builder ? '|static' : '';
$type .= $return->allowsNull() ? '|null' : ''; $type .= $return->allowsNull() ? '|null' : '';
} }
@@ -84,25 +83,56 @@ class Macro extends Method
} }
$class = ltrim($this->declaringClassName, '\\'); $class = ltrim($this->declaringClassName, '\\');
if (!$this->phpdoc->hasTag('return') && isset($this->macroDefaults[$class])) { if (!$this->phpdoc->hasTag('return') && isset(static::$macroDefaults[$class])) {
$type = $this->macroDefaults[$class]; $type = static::$macroDefaults[$class];
$this->phpdoc->appendTag(Tag::createInstance("@return {$type}")); $this->phpdoc->appendTag(Tag::createInstance("@return {$type}"));
} }
} }
protected function concatReflectionTypes(?\ReflectionType $type): string protected function concatReflectionTypes(?\ReflectionType $type): string
{ {
/** @psalm-suppress UndefinedClass */ if ($type instanceof \ReflectionNamedType) {
$returnTypes = $type instanceof \ReflectionUnionType return $type->getName();
? $type->getTypes() }
: [$type];
return Collection::make($returnTypes) if ($type instanceof \ReflectionIntersectionType) {
return $this->formatIntersectionType($type);
}
if ($type instanceof \ReflectionUnionType) {
return $this->formatUnionType($type);
}
// Unknown or null type
return '';
}
protected function formatUnionType(\ReflectionUnionType $type): string
{
return Collection::make($type->getTypes())
->map(function (\ReflectionType $inner) {
if ($inner instanceof \ReflectionNamedType) {
return $inner->getName();
}
if ($inner instanceof \ReflectionIntersectionType) {
return $this->formatIntersectionType($inner);
}
// ReflectionUnionType cannot be nested per PHP's DNF rules
return null;
})
->filter() ->filter()
->map->getName()
->implode('|'); ->implode('|');
} }
protected function formatIntersectionType(\ReflectionIntersectionType $type): string
{
$parts = Collection::make($type->getTypes())
->map(fn (\ReflectionNamedType $t) => $t->getName())
->toArray();
return '(' . implode('&', $parts) . ')';
}
protected function addLocationToPhpDoc() protected function addLocationToPhpDoc()
{ {
if ($this->method->name === '__invoke') { if ($this->method->name === '__invoke') {
+3 -4
View File
@@ -44,7 +44,6 @@ class Method
/** /**
* @param \ReflectionMethod|\ReflectionFunctionAbstract $method * @param \ReflectionMethod|\ReflectionFunctionAbstract $method
* @param string $alias
* @param \ReflectionClass $class * @param \ReflectionClass $class
* @param string|null $methodName * @param string|null $methodName
* @param array $interfaces * @param array $interfaces
@@ -52,7 +51,7 @@ class Method
* @param array $returnTypeNormalizers * @param array $returnTypeNormalizers
* @param string[] $templateNames * @param string[] $templateNames
*/ */
public function __construct($method, $alias, $class, $methodName = null, $interfaces = [], array $classAliases = [], array $returnTypeNormalizers = [], array $templateNames = []) public function __construct($method, $class, $methodName = null, $interfaces = [], array $classAliases = [], array $returnTypeNormalizers = [], array $templateNames = [])
{ {
$this->method = $method; $this->method = $method;
$this->interfaces = $interfaces; $this->interfaces = $interfaces;
@@ -178,7 +177,7 @@ class Method
/** /**
* Get the parameters for this method * Get the parameters for this method
* *
* @param bool $implode Wether to implode the array or not * @param bool $implode Whether to implode the array or not
* @return string * @return string
*/ */
public function getParams($implode = true) public function getParams($implode = true)
@@ -208,7 +207,7 @@ class Method
/** /**
* Get the parameters for this method including default values * Get the parameters for this method including default values
* *
* @param bool $implode Wether to implode the array or not * @param bool $implode Whether to implode the array or not
* @return string * @return string
*/ */
public function getParamsWithDefault($implode = true) public function getParamsWithDefault($implode = true)
@@ -7,8 +7,6 @@ on:
pull_request: pull_request:
branches: branches:
- "*" - "*"
schedule:
- cron: '0 0 * * *'
jobs: jobs:
php-tests: php-tests:
@@ -206,8 +206,9 @@ class Serializer
$text = wordwrap($text, $wrapLength); $text = wordwrap($text, $wrapLength);
} }
$text = str_replace("\n", "\n{$indent} * ", $text); $text = str_replace("\n", "\n{$indent} * ", $text);
$text = preg_replace('/^(\s*\*)[ \t]+$/m', '$1', $text);
$comment = "{$firstIndent}/**\n{$indent} * {$text}\n{$indent} *\n"; $comment = !empty($text)? "{$firstIndent}/**\n{$indent} * {$text}\n{$indent} *\n" : "{$firstIndent}/**\n";
$tags = array_values($docblock->getTags()); $tags = array_values($docblock->getTags());
@@ -220,6 +221,7 @@ class Serializer
$tagText = wordwrap($tagText, $wrapLength); $tagText = wordwrap($tagText, $wrapLength);
} }
$tagText = str_replace("\n", "\n{$indent} * ", $tagText); $tagText = str_replace("\n", "\n{$indent} * ", $tagText);
$tagText = preg_replace('/^(\s*\*)[ \t]+$/m', '$1', $tagText);
$comment .= "{$indent} * {$tagText}\n"; $comment .= "{$indent} * {$tagText}\n";
@@ -409,6 +409,6 @@ class Tag implements \Reflector
*/ */
public function __toString() public function __toString()
{ {
return "@{$this->getName()} {$this->getContent()}"; return trim("@{$this->getName()} {$this->getContent()}");
} }
} }
+2 -3
View File
@@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) {
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) { ) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/nesbot/carbon/bin/carbon'); return include("phpvfscomposer://" . __DIR__ . '/..'.'/nesbot/carbon/bin/carbon');
exit(0);
} }
} }
include __DIR__ . '/..'.'/nesbot/carbon/bin/carbon'; return include __DIR__ . '/..'.'/nesbot/carbon/bin/carbon';
+2 -3
View File
@@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) {
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) { ) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations'); return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations');
exit(0);
} }
} }
include __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations'; return include __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations';
+2 -3
View File
@@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) {
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) { ) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcbf'); return include("phpvfscomposer://" . __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcbf');
exit(0);
} }
} }
include __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcbf'; return include __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcbf';
+2 -3
View File
@@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) {
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) { ) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcs'); return include("phpvfscomposer://" . __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcs');
exit(0);
} }
} }
include __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcs'; return include __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcs';
+2 -3
View File
@@ -115,9 +115,8 @@ if (PHP_VERSION_ID < 80000) {
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) { ) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/phpunit/phpunit/phpunit'); return include("phpvfscomposer://" . __DIR__ . '/..'.'/phpunit/phpunit/phpunit');
exit(0);
} }
} }
include __DIR__ . '/..'.'/phpunit/phpunit/phpunit'; return include __DIR__ . '/..'.'/phpunit/phpunit/phpunit';
+2 -3
View File
@@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) {
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) { ) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/psy/psysh/bin/psysh'); return include("phpvfscomposer://" . __DIR__ . '/..'.'/psy/psysh/bin/psysh');
exit(0);
} }
} }
include __DIR__ . '/..'.'/psy/psysh/bin/psysh'; return include __DIR__ . '/..'.'/psy/psysh/bin/psysh';
+2 -3
View File
@@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) {
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) { ) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server'); return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server');
exit(0);
} }
} }
include __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server'; return include __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server';
+2 -3
View File
@@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) {
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) { ) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint'); return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint');
exit(0);
} }
} }
include __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint'; return include __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint';
+156 -1
View File
@@ -2,6 +2,162 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [0.14.8](https://github.com/brick/math/releases/tag/0.14.8) - 2026-02-10
🗑️ **Deprecations**
- Method `BigInteger::testBit()` is deprecated, use `isBitSet()` instead
**New features**
- New method: `BigInteger::isBitSet()` (replaces `testBit()`)
- New method: `BigNumber::toString()` (alias of magic method `__toString()`)
👌 **Improvements**
- Performance optimization of `BigRational` comparison methods
- More exceptions have been documented with `@throws` annotations
## [0.14.7](https://github.com/brick/math/releases/tag/0.14.7) - 2026-02-07
**New features**
- `clamp()` is now available on the base `BigNumber` class
👌 **Improvements**
- Improved `@throws` exception documentation
## [0.14.6](https://github.com/brick/math/releases/tag/0.14.6) - 2026-02-05
🗑️ **Deprecations**
- Not passing a `$scale` to `BigDecimal::dividedBy()` is deprecated; **`$scale` will be required in 0.15**
👌 **Improvements**
- `BigRational::toFloat()` never returns `NAN` anymore
## [0.14.5](https://github.com/brick/math/releases/tag/0.14.5) - 2026-02-03
🗑️ **Deprecations**
- Not passing a rounding mode to `BigInteger::sqrt()` and `BigDecimal::sqrt()` triggers a deprecation notice: **the default rounding mode will change from `Down` to `Unnecessary` in 0.15**
**New features**
- `BigInteger::sqrt()` and `BigDecimal::sqrt()` now support rounding
- `abs()` and `negated()` methods are now available on the base `BigNumber` class
👌 **Improvements**
- Alphabet is now checked for duplicate characters in `BigInteger::(from|to)ArbitraryBase()`
- `BigNumber::ofNullable()` is now marked as `@pure`
## [0.14.4](https://github.com/brick/math/releases/tag/0.14.4) - 2026-02-02
🗑️ **Deprecations**
- Passing a negative modulus to `BigInteger::mod()` is deprecated to align with Euclidean modulo semantics; it will throw `NegativeNumberException` in 0.15
- Method `BigDecimal::stripTrailingZeros()` is deprecated, use `strippedOfTrailingZeros()` instead
**New features**
- `BigInteger::modPow()` now accepts negative bases
- New method: `BigDecimal::strippedOfTrailingZeros()` (replaces `stripTrailingZeros()`)
👌 **Improvements**
- `clamp()` methods are now marked as `@pure`
## [0.14.3](https://github.com/brick/math/releases/tag/0.14.3) - 2026-02-01
**New features**
- New method: `BigInteger::lcm()`
- New method: `BigInteger::lcmAll()`
- New method: `BigRational::toRepeatingDecimalString()`
🐛 **Bug fixes**
- `BigInteger::gcdAll()` / `gcdMultiple()` could return a negative result when used with a single negative number
## [0.14.2](https://github.com/brick/math/releases/tag/0.14.2) - 2026-01-30
🗑️ **Deprecations**
- **Passing `float` values to `of()` or arithmetic methods is deprecated** and will be removed in 0.15; cast to string explicitly to preserve the previous behaviour (#105)
- **Accessing `RoundingMode` enum cases through upper snake case (e.g. `HALF_UP`) is deprecated**, use the pascal case version (e.g. `HalfUp`) instead
- Method `BigInteger::gcdMultiple()` is deprecated, use `gcdAll()` instead
- Method `BigDecimal::exactlyDividedBy()` is deprecated, use `dividedByExact()` instead
- Method `BigDecimal::getIntegralPart()` is deprecated (will be removed in 0.15, and re-introduced as returning `BigInteger` in 0.16)
- Method `BigDecimal::getFractionalPart()` is deprecated (will be removed in 0.15, and re-introduced as returning `BigDecimal` with a different meaning in 0.16)
- Method `BigRational::nd()` is deprecated, use `ofFraction()` instead
- Method `BigRational::quotient()` is deprecated, use `getIntegralPart()` instead
- Method `BigRational::remainder()` is deprecated, use `$number->getNumerator()->remainder($number->getDenominator())` instead
- Method `BigRational::quotientAndRemainder()` is deprecated, use `$number->getNumerator()->quotientAndRemainder($number->getDenominator())` instead
**New features**
- New method: `BigInteger::gcdAll()` (replaces `gcdMultiple()`)
- New method: `BigRational::clamp()`
- New method: `BigRational::ofFraction()` (replaces `nd()`)
- New method: `BigRational::getIntegralPart()` (replaces `quotient()`)
- New method: `BigRational::getFractionalPart()`
👌 **Improvements**
- `BigInteger::modInverse()` now accepts `BigNumber|int|float|string` instead of just `BigInteger`
- `BigInteger::gcdMultiple()` now accepts `BigNumber|int|float|string` instead of just `BigInteger`
🐛 **Bug fixes**
- `BigInteger::clamp()` and `BigDecimal::clamp()` now throw an exception on inverted bounds, instead of returning an incorrect result
## [0.14.1](https://github.com/brick/math/releases/tag/0.14.1) - 2025-11-24
**New features**
- New method: `BigNumber::ofNullable()` (#94 by @mrkh995)
**Compatibility fixes**
- Fixed warnings on PHP 8.5 (#101 and #102 by @julien-boudry)
## [0.14.0](https://github.com/brick/math/releases/tag/0.14.0) - 2025-08-29
**New features**
- New methods: `BigInteger::clamp()` and `BigDecimal::clamp()` (#96 by @JesterIruka)
**Improvements**
- All pure methods in `BigNumber` classes are now marked as `@pure` for better static analysis
💥 **Breaking changes**
- Minimum PHP version is now 8.2
- `BigNumber` classes are now `readonly`
- `BigNumber` is now marked as sealed: it must not be extended outside of this package
- Exception classes are now `final`
## [0.13.1](https://github.com/brick/math/releases/tag/0.13.1) - 2025-03-29
**Improvements**
- `__toString()` methods of `BigInteger` and `BigDecimal` are now type-hinted as returning `numeric-string` instead of `string` (#90 by @vudaltsov)
## [0.13.0](https://github.com/brick/math/releases/tag/0.13.0) - 2025-03-03
💥 **Breaking changes**
- `BigDecimal::ofUnscaledValue()` no longer throws an exception if the scale is negative
- `MathException` now extends `RuntimeException` instead of `Exception`; this reverts the change introduced in version `0.11.0` (#82)
**New features**
- `BigDecimal::ofUnscaledValue()` allows a negative scale (and converts the values to create a zero scale number)
## [0.12.3](https://github.com/brick/math/releases/tag/0.12.3) - 2025-02-28 ## [0.12.3](https://github.com/brick/math/releases/tag/0.12.3) - 2025-02-28
**New features** **New features**
@@ -476,4 +632,3 @@ Added `BigDecimal::divideAndRemainder()`
## [0.1.0](https://github.com/brick/math/releases/tag/0.1.0) - 2014-08-31 ## [0.1.0](https://github.com/brick/math/releases/tag/0.1.0) - 2014-08-31
First beta release. First beta release.
+3 -3
View File
@@ -19,12 +19,12 @@
], ],
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.1" "php": "^8.2"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^10.1", "phpunit/phpunit": "^11.5",
"php-coveralls/php-coveralls": "^2.2", "php-coveralls/php-coveralls": "^2.2",
"vimeo/psalm": "6.8.8" "phpstan/phpstan": "2.1.22"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
-70
View File
@@ -1,70 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="6.8.8@1361cd33008feb3ae2b4a93f1860e14e538ec8c2">
<file src="src/BigInteger.php">
<FalsableReturnStatement>
<code><![CDATA[\hex2bin($hex)]]></code>
</FalsableReturnStatement>
<InvalidFalsableReturnType>
<code><![CDATA[string]]></code>
</InvalidFalsableReturnType>
</file>
<file src="src/Exception/DivisionByZeroException.php">
<ClassMustBeFinal>
<code><![CDATA[DivisionByZeroException]]></code>
</ClassMustBeFinal>
</file>
<file src="src/Exception/IntegerOverflowException.php">
<ClassMustBeFinal>
<code><![CDATA[IntegerOverflowException]]></code>
</ClassMustBeFinal>
</file>
<file src="src/Exception/NegativeNumberException.php">
<ClassMustBeFinal>
<code><![CDATA[NegativeNumberException]]></code>
</ClassMustBeFinal>
</file>
<file src="src/Exception/NumberFormatException.php">
<ClassMustBeFinal>
<code><![CDATA[NumberFormatException]]></code>
</ClassMustBeFinal>
</file>
<file src="src/Exception/RoundingNecessaryException.php">
<ClassMustBeFinal>
<code><![CDATA[RoundingNecessaryException]]></code>
</ClassMustBeFinal>
</file>
<file src="src/Internal/Calculator/BcMathCalculator.php">
<ClassMustBeFinal>
<code><![CDATA[BcMathCalculator]]></code>
</ClassMustBeFinal>
</file>
<file src="src/Internal/Calculator/GmpCalculator.php">
<ClassMustBeFinal>
<code><![CDATA[GmpCalculator]]></code>
</ClassMustBeFinal>
</file>
<file src="src/Internal/Calculator/NativeCalculator.php">
<ClassMustBeFinal>
<code><![CDATA[NativeCalculator]]></code>
</ClassMustBeFinal>
<InvalidOperand>
<code><![CDATA[$a * $b]]></code>
<code><![CDATA[$a * 1]]></code>
<code><![CDATA[$a + $b]]></code>
<code><![CDATA[$b * 1]]></code>
<code><![CDATA[$b * 1]]></code>
<code><![CDATA[$blockA * $blockB + $carry]]></code>
<code><![CDATA[$blockA + $blockB]]></code>
<code><![CDATA[$blockA + $blockB + $carry]]></code>
<code><![CDATA[$blockA - $blockB]]></code>
<code><![CDATA[$blockA - $blockB - $carry]]></code>
<code><![CDATA[$carry]]></code>
<code><![CDATA[$mul % $complement]]></code>
<code><![CDATA[$mul - $value]]></code>
<code><![CDATA[$nb - 1]]></code>
<code><![CDATA[$sum += $complement]]></code>
<code><![CDATA[($mul - $value) / $complement]]></code>
<code><![CDATA[($nb - 1) * 10]]></code>
</InvalidOperand>
</file>
</files>
+335 -152
View File
@@ -7,15 +7,36 @@ namespace Brick\Math;
use Brick\Math\Exception\DivisionByZeroException; use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException; use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NegativeNumberException; use Brick\Math\Exception\NegativeNumberException;
use Brick\Math\Exception\RoundingNecessaryException;
use Brick\Math\Internal\Calculator; use Brick\Math\Internal\Calculator;
use Brick\Math\Internal\CalculatorRegistry;
use InvalidArgumentException;
use LogicException;
use Override; use Override;
use function func_num_args;
use function in_array;
use function intdiv;
use function max;
use function rtrim;
use function sprintf;
use function str_pad;
use function str_repeat;
use function strlen;
use function substr;
use function trigger_error;
use const E_USER_DEPRECATED;
use const STR_PAD_LEFT;
/** /**
* Immutable, arbitrary-precision signed decimal numbers. * An arbitrarily large decimal number.
* *
* @psalm-immutable * This class is immutable.
*
* The scale of the number is the number of digits after the decimal point. It is always positive or zero.
*/ */
final class BigDecimal extends BigNumber final readonly class BigDecimal extends BigNumber
{ {
/** /**
* The unscaled value of this decimal number. * The unscaled value of this decimal number.
@@ -24,20 +45,22 @@ final class BigDecimal extends BigNumber
* No leading zero must be present. * No leading zero must be present.
* No leading minus sign must be present if the value is 0. * No leading minus sign must be present if the value is 0.
*/ */
private readonly string $value; private string $value;
/** /**
* The scale (number of digits after the decimal point) of this decimal number. * The scale (number of digits after the decimal point) of this decimal number.
* *
* This must be zero or more. * This must be zero or more.
*/ */
private readonly int $scale; private int $scale;
/** /**
* Protected constructor. Use a factory method to obtain an instance. * Protected constructor. Use a factory method to obtain an instance.
* *
* @param string $value The unscaled value, validated. * @param string $value The unscaled value, validated.
* @param int $scale The scale, validated. * @param int $scale The scale, validated.
*
* @pure
*/ */
protected function __construct(string $value, int $scale = 0) protected function __construct(string $value, int $scale = 0)
{ {
@@ -45,47 +68,45 @@ final class BigDecimal extends BigNumber
$this->scale = $scale; $this->scale = $scale;
} }
/**
* @psalm-pure
*/
#[Override]
protected static function from(BigNumber $number): static
{
return $number->toBigDecimal();
}
/** /**
* Creates a BigDecimal from an unscaled value and a scale. * Creates a BigDecimal from an unscaled value and a scale.
* *
* Example: `(12345, 3)` will result in the BigDecimal `12.345`. * Example: `(12345, 3)` will result in the BigDecimal `12.345`.
* *
* A negative scale is normalized to zero by appending zeros to the unscaled value.
*
* Example: `(12345, -3)` will result in the BigDecimal `12345000`.
*
* @param BigNumber|int|float|string $value The unscaled value. Must be convertible to a BigInteger. * @param BigNumber|int|float|string $value The unscaled value. Must be convertible to a BigInteger.
* @param int $scale The scale of the number, positive or zero. * @param int $scale The scale of the number. If negative, the scale will be set to zero
* and the unscaled value will be adjusted accordingly.
* *
* @throws \InvalidArgumentException If the scale is negative. * @throws MathException If the value is not valid, or is not convertible to a BigInteger.
* *
* @psalm-pure * @pure
*/ */
public static function ofUnscaledValue(BigNumber|int|float|string $value, int $scale = 0) : BigDecimal public static function ofUnscaledValue(BigNumber|int|float|string $value, int $scale = 0): BigDecimal
{ {
$value = BigInteger::of($value)->toString();
if ($scale < 0) { if ($scale < 0) {
throw new \InvalidArgumentException('The scale cannot be negative.'); if ($value !== '0') {
$value .= str_repeat('0', -$scale);
}
$scale = 0;
} }
return new BigDecimal((string) BigInteger::of($value), $scale); return new BigDecimal($value, $scale);
} }
/** /**
* Returns a BigDecimal representing zero, with a scale of zero. * Returns a BigDecimal representing zero, with a scale of zero.
* *
* @psalm-pure * @pure
*/ */
public static function zero() : BigDecimal public static function zero(): BigDecimal
{ {
/** /** @var BigDecimal|null $zero */
* @psalm-suppress ImpureStaticVariable
* @var BigDecimal|null $zero
*/
static $zero; static $zero;
if ($zero === null) { if ($zero === null) {
@@ -98,14 +119,11 @@ final class BigDecimal extends BigNumber
/** /**
* Returns a BigDecimal representing one, with a scale of zero. * Returns a BigDecimal representing one, with a scale of zero.
* *
* @psalm-pure * @pure
*/ */
public static function one() : BigDecimal public static function one(): BigDecimal
{ {
/** /** @var BigDecimal|null $one */
* @psalm-suppress ImpureStaticVariable
* @var BigDecimal|null $one
*/
static $one; static $one;
if ($one === null) { if ($one === null) {
@@ -118,14 +136,11 @@ final class BigDecimal extends BigNumber
/** /**
* Returns a BigDecimal representing ten, with a scale of zero. * Returns a BigDecimal representing ten, with a scale of zero.
* *
* @psalm-pure * @pure
*/ */
public static function ten() : BigDecimal public static function ten(): BigDecimal
{ {
/** /** @var BigDecimal|null $ten */
* @psalm-suppress ImpureStaticVariable
* @var BigDecimal|null $ten
*/
static $ten; static $ten;
if ($ten === null) { if ($ten === null) {
@@ -143,8 +158,10 @@ final class BigDecimal extends BigNumber
* @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigDecimal. * @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigDecimal.
* *
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal. * @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
*
* @pure
*/ */
public function plus(BigNumber|int|float|string $that) : BigDecimal public function plus(BigNumber|int|float|string $that): BigDecimal
{ {
$that = BigDecimal::of($that); $that = BigDecimal::of($that);
@@ -158,8 +175,8 @@ final class BigDecimal extends BigNumber
[$a, $b] = $this->scaleValues($this, $that); [$a, $b] = $this->scaleValues($this, $that);
$value = Calculator::get()->add($a, $b); $value = CalculatorRegistry::get()->add($a, $b);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale; $scale = max($this->scale, $that->scale);
return new BigDecimal($value, $scale); return new BigDecimal($value, $scale);
} }
@@ -172,8 +189,10 @@ final class BigDecimal extends BigNumber
* @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigDecimal. * @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigDecimal.
* *
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal. * @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
*
* @pure
*/ */
public function minus(BigNumber|int|float|string $that) : BigDecimal public function minus(BigNumber|int|float|string $that): BigDecimal
{ {
$that = BigDecimal::of($that); $that = BigDecimal::of($that);
@@ -183,8 +202,8 @@ final class BigDecimal extends BigNumber
[$a, $b] = $this->scaleValues($this, $that); [$a, $b] = $this->scaleValues($this, $that);
$value = Calculator::get()->sub($a, $b); $value = CalculatorRegistry::get()->sub($a, $b);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale; $scale = max($this->scale, $that->scale);
return new BigDecimal($value, $scale); return new BigDecimal($value, $scale);
} }
@@ -196,9 +215,11 @@ final class BigDecimal extends BigNumber
* *
* @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigDecimal. * @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigDecimal.
* *
* @throws MathException If the multiplier is not a valid number, or is not convertible to a BigDecimal. * @throws MathException If the multiplier is not valid, or is not convertible to a BigDecimal.
*
* @pure
*/ */
public function multipliedBy(BigNumber|int|float|string $that) : BigDecimal public function multipliedBy(BigNumber|int|float|string $that): BigDecimal
{ {
$that = BigDecimal::of($that); $that = BigDecimal::of($that);
@@ -210,7 +231,7 @@ final class BigDecimal extends BigNumber
return $that; return $that;
} }
$value = Calculator::get()->mul($this->value, $that->value); $value = CalculatorRegistry::get()->mul($this->value, $that->value);
$scale = $this->scale + $that->scale; $scale = $this->scale + $that->scale;
return new BigDecimal($value, $scale); return new BigDecimal($value, $scale);
@@ -219,14 +240,19 @@ final class BigDecimal extends BigNumber
/** /**
* Returns the result of the division of this number by the given one, at the given scale. * Returns the result of the division of this number by the given one, at the given scale.
* *
* @param BigNumber|int|float|string $that The divisor. * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
* @param int|null $scale The desired scale, or null to use the scale of this number. * @param int|null $scale The desired scale. Omitting this parameter is deprecated; it will be required in 0.15.
* @param RoundingMode $roundingMode An optional rounding mode, defaults to UNNECESSARY. * @param RoundingMode $roundingMode An optional rounding mode, defaults to Unnecessary.
* *
* @throws \InvalidArgumentException If the scale or rounding mode is invalid. * @throws InvalidArgumentException If the scale is negative.
* @throws MathException If the number is invalid, is zero, or rounding was necessary. * @throws MathException If the divisor is not valid, or is not convertible to a BigDecimal.
* @throws DivisionByZeroException If the divisor is zero.
* @throws RoundingNecessaryException If RoundingMode::Unnecessary is used and the result cannot be represented
* exactly at the given scale.
*
* @pure
*/ */
public function dividedBy(BigNumber|int|float|string $that, ?int $scale = null, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal public function dividedBy(BigNumber|int|float|string $that, ?int $scale = null, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal
{ {
$that = BigDecimal::of($that); $that = BigDecimal::of($that);
@@ -235,9 +261,15 @@ final class BigDecimal extends BigNumber
} }
if ($scale === null) { if ($scale === null) {
// @phpstan-ignore-next-line
trigger_error(
'Not passing a $scale to BigDecimal::dividedBy() is deprecated. ' .
'Use $a->dividedBy($b, $a->getScale(), $roundingMode) to retain current behavior.',
E_USER_DEPRECATED,
);
$scale = $this->scale; $scale = $this->scale;
} elseif ($scale < 0) { } elseif ($scale < 0) {
throw new \InvalidArgumentException('Scale cannot be negative.'); throw new InvalidArgumentException('Scale must not be negative.');
} }
if ($that->value === '1' && $that->scale === 0 && $scale === $this->scale) { if ($that->value === '1' && $that->scale === 0 && $scale === $this->scale) {
@@ -247,7 +279,7 @@ final class BigDecimal extends BigNumber
$p = $this->valueWithMinScale($that->scale + $scale); $p = $this->valueWithMinScale($that->scale + $scale);
$q = $that->valueWithMinScale($this->scale - $scale); $q = $that->valueWithMinScale($this->scale - $scale);
$result = Calculator::get()->divRound($p, $q, $roundingMode); $result = CalculatorRegistry::get()->divRound($p, $q, $roundingMode);
return new BigDecimal($result, $scale); return new BigDecimal($result, $scale);
} }
@@ -257,12 +289,37 @@ final class BigDecimal extends BigNumber
* *
* The scale of the result is automatically calculated to fit all the fraction digits. * The scale of the result is automatically calculated to fit all the fraction digits.
* *
* @deprecated Will be removed in 0.15. Use dividedByExact() instead.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
* *
* @throws MathException If the divisor is not a valid number, is not convertible to a BigDecimal, is zero, * @throws MathException If the divisor is not a valid number, is not convertible to a BigDecimal, is zero,
* or the result yields an infinite number of digits. * or the result yields an infinite number of digits.
*/ */
public function exactlyDividedBy(BigNumber|int|float|string $that) : BigDecimal public function exactlyDividedBy(BigNumber|int|float|string $that): BigDecimal
{
trigger_error(
'BigDecimal::exactlyDividedBy() is deprecated and will be removed in 0.15. Use dividedByExact() instead.',
E_USER_DEPRECATED,
);
return $this->dividedByExact($that);
}
/**
* Returns the exact result of the division of this number by the given one.
*
* The scale of the result is automatically calculated to fit all the fraction digits.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @throws MathException If the divisor is not valid, or is not convertible to a BigDecimal.
* @throws DivisionByZeroException If the divisor is zero.
* @throws RoundingNecessaryException If the result yields an infinite number of digits.
*
* @pure
*/
public function dividedByExact(BigNumber|int|float|string $that): BigDecimal
{ {
$that = BigDecimal::of($that); $that = BigDecimal::of($that);
@@ -272,13 +329,13 @@ final class BigDecimal extends BigNumber
[, $b] = $this->scaleValues($this, $that); [, $b] = $this->scaleValues($this, $that);
$d = \rtrim($b, '0'); $d = rtrim($b, '0');
$scale = \strlen($b) - \strlen($d); $scale = strlen($b) - strlen($d);
$calculator = Calculator::get(); $calculator = CalculatorRegistry::get();
foreach ([5, 2] as $prime) { foreach ([5, 2] as $prime) {
for (;;) { for (; ;) {
$lastDigit = (int) $d[-1]; $lastDigit = (int) $d[-1];
if ($lastDigit % $prime !== 0) { if ($lastDigit % $prime !== 0) {
@@ -290,7 +347,7 @@ final class BigDecimal extends BigNumber
} }
} }
return $this->dividedBy($that, $scale)->stripTrailingZeros(); return $this->dividedBy($that, $scale)->strippedOfTrailingZeros();
} }
/** /**
@@ -298,9 +355,11 @@ final class BigDecimal extends BigNumber
* *
* The result has a scale of `$this->scale * $exponent`. * The result has a scale of `$this->scale * $exponent`.
* *
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000. * @throws InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
*
* @pure
*/ */
public function power(int $exponent) : BigDecimal public function power(int $exponent): BigDecimal
{ {
if ($exponent === 0) { if ($exponent === 0) {
return BigDecimal::one(); return BigDecimal::one();
@@ -311,14 +370,14 @@ final class BigDecimal extends BigNumber
} }
if ($exponent < 0 || $exponent > Calculator::MAX_POWER) { if ($exponent < 0 || $exponent > Calculator::MAX_POWER) {
throw new \InvalidArgumentException(\sprintf( throw new InvalidArgumentException(sprintf(
'The exponent %d is not in the range 0 to %d.', 'The exponent %d is not in the range 0 to %d.',
$exponent, $exponent,
Calculator::MAX_POWER Calculator::MAX_POWER,
)); ));
} }
return new BigDecimal(Calculator::get()->pow($this->value, $exponent), $this->scale * $exponent); return new BigDecimal(CalculatorRegistry::get()->pow($this->value, $exponent), $this->scale * $exponent);
} }
/** /**
@@ -326,11 +385,21 @@ final class BigDecimal extends BigNumber
* *
* The quotient has a scale of `0`. * The quotient has a scale of `0`.
* *
* Examples:
*
* - `7.5` quotient `3` returns `2`
* - `7.5` quotient `-3` returns `-2`
* - `-7.5` quotient `3` returns `-2`
* - `-7.5` quotient `-3` returns `2`
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
* *
* @throws MathException If the divisor is not a valid decimal number, or is zero. * @throws MathException If the divisor is not valid, or is not convertible to a BigDecimal.
* @throws DivisionByZeroException If the divisor is zero.
*
* @pure
*/ */
public function quotient(BigNumber|int|float|string $that) : BigDecimal public function quotient(BigNumber|int|float|string $that): BigDecimal
{ {
$that = BigDecimal::of($that); $that = BigDecimal::of($that);
@@ -341,7 +410,7 @@ final class BigDecimal extends BigNumber
$p = $this->valueWithMinScale($that->scale); $p = $this->valueWithMinScale($that->scale);
$q = $that->valueWithMinScale($this->scale); $q = $that->valueWithMinScale($this->scale);
$quotient = Calculator::get()->divQ($p, $q); $quotient = CalculatorRegistry::get()->divQ($p, $q);
return new BigDecimal($quotient, 0); return new BigDecimal($quotient, 0);
} }
@@ -350,12 +419,23 @@ final class BigDecimal extends BigNumber
* Returns the remainder of the division of this number by the given one. * Returns the remainder of the division of this number by the given one.
* *
* The remainder has a scale of `max($this->scale, $that->scale)`. * The remainder has a scale of `max($this->scale, $that->scale)`.
* The remainder, when non-zero, has the same sign as the dividend.
*
* Examples:
*
* - `7.5` remainder `3` returns `1.5`
* - `7.5` remainder `-3` returns `1.5`
* - `-7.5` remainder `3` returns `-1.5`
* - `-7.5` remainder `-3` returns `-1.5`
* *
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
* *
* @throws MathException If the divisor is not a valid decimal number, or is zero. * @throws MathException If the divisor is not valid, or is not convertible to a BigDecimal.
* @throws DivisionByZeroException If the divisor is zero.
*
* @pure
*/ */
public function remainder(BigNumber|int|float|string $that) : BigDecimal public function remainder(BigNumber|int|float|string $that): BigDecimal
{ {
$that = BigDecimal::of($that); $that = BigDecimal::of($that);
@@ -366,9 +446,9 @@ final class BigDecimal extends BigNumber
$p = $this->valueWithMinScale($that->scale); $p = $this->valueWithMinScale($that->scale);
$q = $that->valueWithMinScale($this->scale); $q = $that->valueWithMinScale($this->scale);
$remainder = Calculator::get()->divR($p, $q); $remainder = CalculatorRegistry::get()->divR($p, $q);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale; $scale = max($this->scale, $that->scale);
return new BigDecimal($remainder, $scale); return new BigDecimal($remainder, $scale);
} }
@@ -378,15 +458,23 @@ final class BigDecimal extends BigNumber
* *
* The quotient has a scale of `0`, and the remainder has a scale of `max($this->scale, $that->scale)`. * The quotient has a scale of `0`, and the remainder has a scale of `max($this->scale, $that->scale)`.
* *
* Examples:
*
* - `7.5` quotientAndRemainder `3` returns [`2`, `1.5`]
* - `7.5` quotientAndRemainder `-3` returns [`-2`, `1.5`]
* - `-7.5` quotientAndRemainder `3` returns [`-2`, `-1.5`]
* - `-7.5` quotientAndRemainder `-3` returns [`2`, `-1.5`]
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
* *
* @return BigDecimal[] An array containing the quotient and the remainder. * @return array{BigDecimal, BigDecimal} An array containing the quotient and the remainder.
* *
* @psalm-return array{BigDecimal, BigDecimal} * @throws MathException If the divisor is not valid, or is not convertible to a BigDecimal.
* @throws DivisionByZeroException If the divisor is zero.
* *
* @throws MathException If the divisor is not a valid decimal number, or is zero. * @pure
*/ */
public function quotientAndRemainder(BigNumber|int|float|string $that) : array public function quotientAndRemainder(BigNumber|int|float|string $that): array
{ {
$that = BigDecimal::of($that); $that = BigDecimal::of($that);
@@ -397,9 +485,9 @@ final class BigDecimal extends BigNumber
$p = $this->valueWithMinScale($that->scale); $p = $this->valueWithMinScale($that->scale);
$q = $that->valueWithMinScale($this->scale); $q = $that->valueWithMinScale($this->scale);
[$quotient, $remainder] = Calculator::get()->divQR($p, $q); [$quotient, $remainder] = CalculatorRegistry::get()->divQR($p, $q);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale; $scale = max($this->scale, $that->scale);
$quotient = new BigDecimal($quotient, 0); $quotient = new BigDecimal($quotient, 0);
$remainder = new BigDecimal($remainder, $scale); $remainder = new BigDecimal($remainder, $scale);
@@ -408,15 +496,34 @@ final class BigDecimal extends BigNumber
} }
/** /**
* Returns the square root of this number, rounded down to the given number of decimals. * Returns the square root of this number, rounded to the given scale according to the given rounding mode.
* *
* @throws \InvalidArgumentException If the scale is negative. * @param int $scale The target scale. Must be non-negative.
* @param RoundingMode $roundingMode The rounding mode to use, defaults to Down.
* ⚠️ WARNING: the default rounding mode was kept as Down for backward
* compatibility, but will change to Unnecessary in version 0.15. Pass a rounding
* mode explicitly to avoid this upcoming breaking change.
*
* @throws InvalidArgumentException If the scale is negative.
* @throws NegativeNumberException If this number is negative. * @throws NegativeNumberException If this number is negative.
* @throws RoundingNecessaryException If RoundingMode::Unnecessary is used and the result cannot be represented
* exactly at the given scale.
*
* @pure
*/ */
public function sqrt(int $scale) : BigDecimal public function sqrt(int $scale, RoundingMode $roundingMode = RoundingMode::Down): BigDecimal
{ {
if (func_num_args() === 1) {
// @phpstan-ignore-next-line
trigger_error(
'The default rounding mode of BigDecimal::sqrt() will change from Down to Unnecessary in version 0.15. ' .
'Pass a rounding mode explicitly to avoid this breaking change.',
E_USER_DEPRECATED,
);
}
if ($scale < 0) { if ($scale < 0) {
throw new \InvalidArgumentException('Scale cannot be negative.'); throw new InvalidArgumentException('Scale must not be negative.');
} }
if ($this->value === '0') { if ($this->value === '0') {
@@ -428,30 +535,48 @@ final class BigDecimal extends BigNumber
} }
$value = $this->value; $value = $this->value;
$addDigits = 2 * $scale - $this->scale; $inputScale = $this->scale;
if ($addDigits > 0) { if ($inputScale % 2 !== 0) {
// add zeros $value .= '0';
$value .= \str_repeat('0', $addDigits); $inputScale++;
} elseif ($addDigits < 0) {
// trim digits
if (-$addDigits >= \strlen($this->value)) {
// requesting a scale too low, will always yield a zero result
return new BigDecimal('0', $scale);
} }
$value = \substr($value, 0, $addDigits); $calculator = CalculatorRegistry::get();
// Keep one extra digit for rounding.
$intermediateScale = max($scale, intdiv($inputScale, 2)) + 1;
$value .= str_repeat('0', 2 * $intermediateScale - $inputScale);
$sqrt = $calculator->sqrt($value);
$isExact = $calculator->mul($sqrt, $sqrt) === $value;
if (! $isExact) {
if ($roundingMode === RoundingMode::Unnecessary) {
throw RoundingNecessaryException::roundingNecessary();
} }
$value = Calculator::get()->sqrt($value); // Non-perfect-square sqrt is irrational, so the true value is strictly above this sqrt floor.
// Add one at the intermediate scale to guarantee Up/Ceiling round up at the target scale.
if (in_array($roundingMode, [RoundingMode::Up, RoundingMode::Ceiling], true)) {
$sqrt = $calculator->add($sqrt, '1');
}
return new BigDecimal($value, $scale); // Irrational sqrt cannot land exactly on a midpoint; treat tie-to-down modes as HalfUp.
elseif (in_array($roundingMode, [RoundingMode::HalfDown, RoundingMode::HalfEven, RoundingMode::HalfFloor], true)) {
$roundingMode = RoundingMode::HalfUp;
}
}
return (new BigDecimal($sqrt, $intermediateScale))->toScale($scale, $roundingMode);
} }
/** /**
* Returns a copy of this BigDecimal with the decimal point moved $n places to the left. * Returns a copy of this BigDecimal with the decimal point moved to the left by the given number of places.
*
* @pure
*/ */
public function withPointMovedLeft(int $n) : BigDecimal public function withPointMovedLeft(int $n): BigDecimal
{ {
if ($n === 0) { if ($n === 0) {
return $this; return $this;
@@ -465,9 +590,11 @@ final class BigDecimal extends BigNumber
} }
/** /**
* Returns a copy of this BigDecimal with the decimal point moved $n places to the right. * Returns a copy of this BigDecimal with the decimal point moved to the right by the given number of places.
*
* @pure
*/ */
public function withPointMovedRight(int $n) : BigDecimal public function withPointMovedRight(int $n): BigDecimal
{ {
if ($n === 0) { if ($n === 0) {
return $this; return $this;
@@ -482,7 +609,7 @@ final class BigDecimal extends BigNumber
if ($scale < 0) { if ($scale < 0) {
if ($value !== '0') { if ($value !== '0') {
$value .= \str_repeat('0', -$scale); $value .= str_repeat('0', -$scale);
} }
$scale = 0; $scale = 0;
} }
@@ -492,20 +619,37 @@ final class BigDecimal extends BigNumber
/** /**
* Returns a copy of this BigDecimal with any trailing zeros removed from the fractional part. * Returns a copy of this BigDecimal with any trailing zeros removed from the fractional part.
*
* @deprecated Use strippedOfTrailingZeros() instead.
*/ */
public function stripTrailingZeros() : BigDecimal public function stripTrailingZeros(): BigDecimal
{
trigger_error(
'BigDecimal::stripTrailingZeros() is deprecated, use strippedOfTrailingZeros() instead.',
E_USER_DEPRECATED,
);
return $this->strippedOfTrailingZeros();
}
/**
* Returns a copy of this BigDecimal with any trailing zeros removed from the fractional part.
*
* @pure
*/
public function strippedOfTrailingZeros(): BigDecimal
{ {
if ($this->scale === 0) { if ($this->scale === 0) {
return $this; return $this;
} }
$trimmedValue = \rtrim($this->value, '0'); $trimmedValue = rtrim($this->value, '0');
if ($trimmedValue === '') { if ($trimmedValue === '') {
return BigDecimal::zero(); return BigDecimal::zero();
} }
$trimmableZeros = \strlen($this->value) - \strlen($trimmedValue); $trimmableZeros = strlen($this->value) - strlen($trimmedValue);
if ($trimmableZeros === 0) { if ($trimmableZeros === 0) {
return $this; return $this;
@@ -515,30 +659,20 @@ final class BigDecimal extends BigNumber
$trimmableZeros = $this->scale; $trimmableZeros = $this->scale;
} }
$value = \substr($this->value, 0, -$trimmableZeros); $value = substr($this->value, 0, -$trimmableZeros);
$scale = $this->scale - $trimmableZeros; $scale = $this->scale - $trimmableZeros;
return new BigDecimal($value, $scale); return new BigDecimal($value, $scale);
} }
/** #[Override]
* Returns the absolute value of this number. public function negated(): static
*/
public function abs() : BigDecimal
{ {
return $this->isNegative() ? $this->negated() : $this; return new BigDecimal(CalculatorRegistry::get()->neg($this->value), $this->scale);
}
/**
* Returns the negated value of this number.
*/
public function negated() : BigDecimal
{
return new BigDecimal(Calculator::get()->neg($this->value), $this->scale);
} }
#[Override] #[Override]
public function compareTo(BigNumber|int|float|string $that) : int public function compareTo(BigNumber|int|float|string $that): int
{ {
$that = BigNumber::of($that); $that = BigNumber::of($that);
@@ -549,24 +683,30 @@ final class BigDecimal extends BigNumber
if ($that instanceof BigDecimal) { if ($that instanceof BigDecimal) {
[$a, $b] = $this->scaleValues($this, $that); [$a, $b] = $this->scaleValues($this, $that);
return Calculator::get()->cmp($a, $b); return CalculatorRegistry::get()->cmp($a, $b);
} }
return - $that->compareTo($this); return -$that->compareTo($this);
} }
#[Override] #[Override]
public function getSign() : int public function getSign(): int
{ {
return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1); return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1);
} }
public function getUnscaledValue() : BigInteger /**
* @pure
*/
public function getUnscaledValue(): BigInteger
{ {
return self::newBigInteger($this->value); return self::newBigInteger($this->value);
} }
public function getScale() : int /**
* @pure
*/
public function getScale(): int
{ {
return $this->scale; return $this->scale;
} }
@@ -584,6 +724,8 @@ final class BigDecimal extends BigNumber
* 123.456 => 6 * 123.456 => 6
* 0.00123 => 3 * 0.00123 => 3
* 0.0012300 => 5 * 0.0012300 => 5
*
* @pure
*/ */
public function getPrecision(): int public function getPrecision(): int
{ {
@@ -593,7 +735,7 @@ final class BigDecimal extends BigNumber
return 0; return 0;
} }
$length = \strlen($value); $length = strlen($value);
return ($value[0] === '-') ? $length - 1 : $length; return ($value[0] === '-') ? $length - 1 : $length;
} }
@@ -602,16 +744,23 @@ final class BigDecimal extends BigNumber
* Returns a string representing the integral part of this decimal number. * Returns a string representing the integral part of this decimal number.
* *
* Example: `-123.456` => `-123`. * Example: `-123.456` => `-123`.
*
* @deprecated Will be removed in 0.15 and re-introduced as returning BigInteger in 0.16.
*/ */
public function getIntegralPart() : string public function getIntegralPart(): string
{ {
trigger_error(
'BigDecimal::getIntegralPart() is deprecated and will be removed in 0.15. It will be re-introduced as returning BigInteger in 0.16.',
E_USER_DEPRECATED,
);
if ($this->scale === 0) { if ($this->scale === 0) {
return $this->value; return $this->value;
} }
$value = $this->getUnscaledValueWithLeadingZeros(); $value = $this->getUnscaledValueWithLeadingZeros();
return \substr($value, 0, -$this->scale); return substr($value, 0, -$this->scale);
} }
/** /**
@@ -620,28 +769,43 @@ final class BigDecimal extends BigNumber
* If the scale is zero, an empty string is returned. * If the scale is zero, an empty string is returned.
* *
* Examples: `-123.456` => '456', `123` => ''. * Examples: `-123.456` => '456', `123` => ''.
*
* @deprecated Will be removed in 0.15 and re-introduced as returning BigDecimal with a different meaning in 0.16.
*/ */
public function getFractionalPart() : string public function getFractionalPart(): string
{ {
trigger_error(
'BigDecimal::getFractionalPart() is deprecated and will be removed in 0.15. It will be re-introduced as returning BigDecimal with a different meaning in 0.16.',
E_USER_DEPRECATED,
);
if ($this->scale === 0) { if ($this->scale === 0) {
return ''; return '';
} }
$value = $this->getUnscaledValueWithLeadingZeros(); $value = $this->getUnscaledValueWithLeadingZeros();
return \substr($value, -$this->scale); return substr($value, -$this->scale);
} }
/** /**
* Returns whether this decimal number has a non-zero fractional part. * Returns whether this decimal number has a non-zero fractional part.
*
* @pure
*/ */
public function hasNonZeroFractionalPart() : bool public function hasNonZeroFractionalPart(): bool
{ {
return $this->getFractionalPart() !== \str_repeat('0', $this->scale); if ($this->scale === 0) {
return false;
}
$value = $this->getUnscaledValueWithLeadingZeros();
return substr($value, -$this->scale) !== str_repeat('0', $this->scale);
} }
#[Override] #[Override]
public function toBigInteger() : BigInteger public function toBigInteger(): BigInteger
{ {
$zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0); $zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0);
@@ -649,22 +813,22 @@ final class BigDecimal extends BigNumber
} }
#[Override] #[Override]
public function toBigDecimal() : BigDecimal public function toBigDecimal(): BigDecimal
{ {
return $this; return $this;
} }
#[Override] #[Override]
public function toBigRational() : BigRational public function toBigRational(): BigRational
{ {
$numerator = self::newBigInteger($this->value); $numerator = self::newBigInteger($this->value);
$denominator = self::newBigInteger('1' . \str_repeat('0', $this->scale)); $denominator = self::newBigInteger('1' . str_repeat('0', $this->scale));
return self::newBigRational($numerator, $denominator, false); return self::newBigRational($numerator, $denominator, false);
} }
#[Override] #[Override]
public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal
{ {
if ($scale === $this->scale) { if ($scale === $this->scale) {
return $this; return $this;
@@ -674,27 +838,32 @@ final class BigDecimal extends BigNumber
} }
#[Override] #[Override]
public function toInt() : int public function toInt(): int
{ {
return $this->toBigInteger()->toInt(); return $this->toBigInteger()->toInt();
} }
#[Override] #[Override]
public function toFloat() : float public function toFloat(): float
{ {
return (float) (string) $this; return (float) $this->toString();
} }
/**
* @return numeric-string
*/
#[Override] #[Override]
public function __toString() : string public function toString(): string
{ {
if ($this->scale === 0) { if ($this->scale === 0) {
/** @var numeric-string */
return $this->value; return $this->value;
} }
$value = $this->getUnscaledValueWithLeadingZeros(); $value = $this->getUnscaledValueWithLeadingZeros();
return \substr($value, 0, -$this->scale) . '.' . \substr($value, -$this->scale); /** @phpstan-ignore return.type */
return substr($value, 0, -$this->scale) . '.' . substr($value, -$this->scale);
} }
/** /**
@@ -713,47 +882,59 @@ final class BigDecimal extends BigNumber
* This method is only here to allow unserializing the object and cannot be accessed directly. * This method is only here to allow unserializing the object and cannot be accessed directly.
* *
* @internal * @internal
* @psalm-suppress RedundantPropertyInitializationCheck
* *
* @param array{value: string, scale: int} $data * @param array{value: string, scale: int} $data
* *
* @throws \LogicException * @throws LogicException
*/ */
public function __unserialize(array $data): void public function __unserialize(array $data): void
{ {
/** @phpstan-ignore isset.initializedProperty */
if (isset($this->value)) { if (isset($this->value)) {
throw new \LogicException('__unserialize() is an internal function, it must not be called directly.'); throw new LogicException('__unserialize() is an internal function, it must not be called directly.');
} }
/** @phpstan-ignore deadCode.unreachable */
$this->value = $data['value']; $this->value = $data['value'];
$this->scale = $data['scale']; $this->scale = $data['scale'];
} }
#[Override]
protected static function from(BigNumber $number): static
{
return $number->toBigDecimal();
}
/** /**
* Puts the internal values of the given decimal numbers on the same scale. * Puts the internal values of the given decimal numbers on the same scale.
* *
* @return array{string, string} The scaled integer values of $x and $y. * @return array{string, string} The scaled integer values of $x and $y.
*
* @pure
*/ */
private function scaleValues(BigDecimal $x, BigDecimal $y) : array private function scaleValues(BigDecimal $x, BigDecimal $y): array
{ {
$a = $x->value; $a = $x->value;
$b = $y->value; $b = $y->value;
if ($b !== '0' && $x->scale > $y->scale) { if ($b !== '0' && $x->scale > $y->scale) {
$b .= \str_repeat('0', $x->scale - $y->scale); $b .= str_repeat('0', $x->scale - $y->scale);
} elseif ($a !== '0' && $x->scale < $y->scale) { } elseif ($a !== '0' && $x->scale < $y->scale) {
$a .= \str_repeat('0', $y->scale - $x->scale); $a .= str_repeat('0', $y->scale - $x->scale);
} }
return [$a, $b]; return [$a, $b];
} }
private function valueWithMinScale(int $scale) : string /**
* @pure
*/
private function valueWithMinScale(int $scale): string
{ {
$value = $this->value; $value = $this->value;
if ($this->value !== '0' && $scale > $this->scale) { if ($this->value !== '0' && $scale > $this->scale) {
$value .= \str_repeat('0', $scale - $this->scale); $value .= str_repeat('0', $scale - $this->scale);
} }
return $value; return $value;
@@ -761,13 +942,15 @@ final class BigDecimal extends BigNumber
/** /**
* Adds leading zeros if necessary to the unscaled value to represent the full decimal number. * Adds leading zeros if necessary to the unscaled value to represent the full decimal number.
*
* @pure
*/ */
private function getUnscaledValueWithLeadingZeros() : string private function getUnscaledValueWithLeadingZeros(): string
{ {
$value = $this->value; $value = $this->value;
$targetLength = $this->scale + 1; $targetLength = $this->scale + 1;
$negative = ($value[0] === '-'); $negative = ($value[0] === '-');
$length = \strlen($value); $length = strlen($value);
if ($negative) { if ($negative) {
$length--; $length--;
@@ -778,10 +961,10 @@ final class BigDecimal extends BigNumber
} }
if ($negative) { if ($negative) {
$value = \substr($value, 1); $value = substr($value, 1);
} }
$value = \str_pad($value, $targetLength, '0', STR_PAD_LEFT); $value = str_pad($value, $targetLength, '0', STR_PAD_LEFT);
if ($negative) { if ($negative) {
$value = '-' . $value; $value = '-' . $value;
+493 -204
View File
File diff suppressed because it is too large Load Diff
+535 -338
View File
@@ -8,14 +8,39 @@ use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException; use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NumberFormatException; use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Exception\RoundingNecessaryException; use Brick\Math\Exception\RoundingNecessaryException;
use InvalidArgumentException;
use JsonSerializable;
use Override; use Override;
use Stringable;
use function array_shift;
use function assert;
use function filter_var;
use function is_float;
use function is_int;
use function is_nan;
use function is_null;
use function ltrim;
use function preg_match;
use function str_contains;
use function str_repeat;
use function strlen;
use function substr;
use function trigger_error;
use const E_USER_DEPRECATED;
use const FILTER_VALIDATE_INT;
use const PREG_UNMATCHED_AS_NULL;
/** /**
* Common interface for arbitrary-precision rational numbers. * Base class for arbitrary-precision numbers.
* *
* @psalm-immutable * This class is sealed: it is part of the public API but should not be subclassed in userland.
* Protected methods may change in any version.
*
* @phpstan-sealed BigInteger|BigDecimal|BigRational
*/ */
abstract class BigNumber implements \JsonSerializable abstract readonly class BigNumber implements JsonSerializable, Stringable
{ {
/** /**
* The regular expression used to parse integer or decimal numbers. * The regular expression used to parse integer or decimal numbers.
@@ -36,34 +61,37 @@ abstract class BigNumber implements \JsonSerializable
'/^' . '/^' .
'(?<sign>[\-\+])?' . '(?<sign>[\-\+])?' .
'(?<numerator>[0-9]+)' . '(?<numerator>[0-9]+)' .
'\/?' . '\/' .
'(?<denominator>[0-9]+)' . '(?<denominator>[0-9]+)' .
'$/'; '$/';
/** /**
* Creates a BigNumber of the given value. * Creates a BigNumber of the given value.
* *
* The concrete return type is dependent on the given value, with the following rules: * When of() is called on BigNumber, the concrete return type is dependent on the given value, with the following
* rules:
* *
* - BigNumber instances are returned as is * - BigNumber instances are returned as is
* - integer numbers are returned as BigInteger * - integer numbers are returned as BigInteger
* - floating point numbers are converted to a string then parsed as such * - floating point numbers are converted to a string then parsed as such (deprecated, will be removed in 0.15)
* - strings containing a `/` character are returned as BigRational * - strings containing a `/` character are returned as BigRational
* - strings containing a `.` character or using an exponential notation are returned as BigDecimal * - strings containing a `.` character or using an exponential notation are returned as BigDecimal
* - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger * - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger
* *
* When of() is called on BigInteger, BigDecimal, or BigRational, the resulting number is converted to an instance
* of the subclass when possible; otherwise a RoundingNecessaryException exception is thrown.
*
* @throws NumberFormatException If the format of the number is not valid. * @throws NumberFormatException If the format of the number is not valid.
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero. * @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
* @throws RoundingNecessaryException If the value cannot be converted to an instance of the subclass without rounding. * @throws RoundingNecessaryException If the value cannot be converted to an instance of the subclass without rounding.
* *
* @psalm-pure * @pure
*/ */
final public static function of(BigNumber|int|float|string $value) : static final public static function of(BigNumber|int|float|string $value): static
{ {
$value = self::_of($value); $value = self::_of($value);
if (static::class === BigNumber::class) { if (static::class === BigNumber::class) {
// https://github.com/vimeo/psalm/issues/10309
assert($value instanceof static); assert($value instanceof static);
return $value; return $value;
@@ -72,29 +100,487 @@ abstract class BigNumber implements \JsonSerializable
return static::from($value); return static::from($value);
} }
/**
* Creates a BigNumber of the given value, or returns null if the input is null.
*
* Behaves like of() for non-null values.
*
* @see BigNumber::of()
*
* @throws NumberFormatException If the format of the number is not valid.
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
* @throws RoundingNecessaryException If the value cannot be converted to an instance of the subclass without rounding.
*
* @pure
*/
final public static function ofNullable(BigNumber|int|float|string|null $value): ?static
{
if (is_null($value)) {
return null;
}
return static::of($value);
}
/**
* Returns the minimum of the given values.
*
* If several values are equal and minimal, the first one is returned.
* This can affect the concrete return type when calling this method on BigNumber.
*
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers must be convertible to an
* instance of the class this method is called on.
*
* @throws InvalidArgumentException If no values are given.
* @throws MathException If a number is not valid, or is not convertible to an instance of the class
* this method is called on.
*
* @pure
*/
final public static function min(BigNumber|int|float|string ...$values): static
{
$min = null;
foreach ($values as $value) {
$value = static::of($value);
if ($min === null || $value->isLessThan($min)) {
$min = $value;
}
}
if ($min === null) {
throw new InvalidArgumentException(__METHOD__ . '() expects at least one value.');
}
return $min;
}
/**
* Returns the maximum of the given values.
*
* If several values are equal and maximal, the first one is returned.
* This can affect the concrete return type when calling this method on BigNumber.
*
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers must be convertible to an
* instance of the class this method is called on.
*
* @throws InvalidArgumentException If no values are given.
* @throws MathException If a number is not valid, or is not convertible to an instance of the class
* this method is called on.
*
* @pure
*/
final public static function max(BigNumber|int|float|string ...$values): static
{
$max = null;
foreach ($values as $value) {
$value = static::of($value);
if ($max === null || $value->isGreaterThan($max)) {
$max = $value;
}
}
if ($max === null) {
throw new InvalidArgumentException(__METHOD__ . '() expects at least one value.');
}
return $max;
}
/**
* Returns the sum of the given values.
*
* When called on BigNumber, sum() accepts any supported type and returns a result whose type is the widest among
* the given values (BigInteger < BigDecimal < BigRational).
*
* When called on BigInteger, BigDecimal, or BigRational, sum() requires that all values can be converted to that
* specific subclass, and returns a result of the same type.
*
* @param BigNumber|int|float|string ...$values The numbers to add. All the numbers must be convertible to an
* instance of the class this method is called on.
*
* @throws InvalidArgumentException If no values are given.
* @throws MathException If a number is not valid, or is not convertible to an instance of the class
* this method is called on.
*
* @pure
*/
final public static function sum(BigNumber|int|float|string ...$values): static
{
$first = array_shift($values);
if ($first === null) {
throw new InvalidArgumentException(__METHOD__ . '() expects at least one value.');
}
$sum = static::of($first);
foreach ($values as $value) {
$sum = self::add($sum, static::of($value));
}
assert($sum instanceof static);
return $sum;
}
/**
* Checks if this number is equal to the given one.
*
* @throws MathException If the given number is not valid.
*
* @pure
*/
final public function isEqualTo(BigNumber|int|float|string $that): bool
{
return $this->compareTo($that) === 0;
}
/**
* Checks if this number is strictly less than the given one.
*
* @throws MathException If the given number is not valid.
*
* @pure
*/
final public function isLessThan(BigNumber|int|float|string $that): bool
{
return $this->compareTo($that) < 0;
}
/**
* Checks if this number is less than or equal to the given one.
*
* @throws MathException If the given number is not valid.
*
* @pure
*/
final public function isLessThanOrEqualTo(BigNumber|int|float|string $that): bool
{
return $this->compareTo($that) <= 0;
}
/**
* Checks if this number is strictly greater than the given one.
*
* @throws MathException If the given number is not valid.
*
* @pure
*/
final public function isGreaterThan(BigNumber|int|float|string $that): bool
{
return $this->compareTo($that) > 0;
}
/**
* Checks if this number is greater than or equal to the given one.
*
* @throws MathException If the given number is not valid.
*
* @pure
*/
final public function isGreaterThanOrEqualTo(BigNumber|int|float|string $that): bool
{
return $this->compareTo($that) >= 0;
}
/**
* Checks if this number equals zero.
*
* @pure
*/
final public function isZero(): bool
{
return $this->getSign() === 0;
}
/**
* Checks if this number is strictly negative.
*
* @pure
*/
final public function isNegative(): bool
{
return $this->getSign() < 0;
}
/**
* Checks if this number is negative or zero.
*
* @pure
*/
final public function isNegativeOrZero(): bool
{
return $this->getSign() <= 0;
}
/**
* Checks if this number is strictly positive.
*
* @pure
*/
final public function isPositive(): bool
{
return $this->getSign() > 0;
}
/**
* Checks if this number is positive or zero.
*
* @pure
*/
final public function isPositiveOrZero(): bool
{
return $this->getSign() >= 0;
}
/**
* Returns the absolute value of this number.
*
* @pure
*/
final public function abs(): static
{
return $this->isNegative() ? $this->negated() : $this;
}
/**
* Returns the negated value of this number.
*
* @pure
*/
abstract public function negated(): static;
/**
* Returns the sign of this number.
*
* Returns -1 if the number is negative, 0 if zero, 1 if positive.
*
* @return -1|0|1
*
* @pure
*/
abstract public function getSign(): int;
/**
* Compares this number to the given one.
*
* Returns -1 if `$this` is lower than, 0 if equal to, 1 if greater than `$that`.
*
* @return -1|0|1
*
* @throws MathException If the number is not valid.
*
* @pure
*/
abstract public function compareTo(BigNumber|int|float|string $that): int;
/**
* Limits (clamps) this number between the given minimum and maximum values.
*
* If the number is lower than $min, returns $min.
* If the number is greater than $max, returns $max.
* Otherwise, returns this number unchanged.
*
* @param BigNumber|int|float|string $min The minimum. Must be convertible to an instance of the class this method is called on.
* @param BigNumber|int|float|string $max The maximum. Must be convertible to an instance of the class this method is called on.
*
* @throws MathException If min/max are not convertible to an instance of the class this method is called on.
* @throws InvalidArgumentException If min is greater than max.
*
* @pure
*/
final public function clamp(BigNumber|int|float|string $min, BigNumber|int|float|string $max): static
{
$min = static::of($min);
$max = static::of($max);
if ($min->isGreaterThan($max)) {
throw new InvalidArgumentException('Minimum value must be less than or equal to maximum value.');
}
if ($this->isLessThan($min)) {
return $min;
}
if ($this->isGreaterThan($max)) {
return $max;
}
return $this;
}
/**
* Converts this number to a BigInteger.
*
* @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding.
*
* @pure
*/
abstract public function toBigInteger(): BigInteger;
/**
* Converts this number to a BigDecimal.
*
* @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding.
*
* @pure
*/
abstract public function toBigDecimal(): BigDecimal;
/**
* Converts this number to a BigRational.
*
* @pure
*/
abstract public function toBigRational(): BigRational;
/**
* Converts this number to a BigDecimal with the given scale, using rounding if necessary.
*
* @param int $scale The scale of the resulting `BigDecimal`. Must be non-negative.
* @param RoundingMode $roundingMode An optional rounding mode, defaults to Unnecessary.
*
* @throws InvalidArgumentException If the scale is negative.
* @throws RoundingNecessaryException If RoundingMode::Unnecessary is used, and this number cannot be converted to
* the given scale without rounding.
*
* @pure
*/
abstract public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal;
/**
* Returns the exact value of this number as a native integer.
*
* If this number cannot be converted to a native integer without losing precision, an exception is thrown.
* Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit.
*
* @throws MathException If this number cannot be exactly converted to a native integer.
*
* @pure
*/
abstract public function toInt(): int;
/**
* Returns an approximation of this number as a floating-point value.
*
* Note that this method can discard information as the precision of a floating-point value
* is inherently limited.
*
* If the number is greater than the largest representable floating point number, positive infinity is returned.
* If the number is less than the smallest representable floating point number, negative infinity is returned.
* This method never returns NaN.
*
* @pure
*/
abstract public function toFloat(): float;
/**
* Returns a string representation of this number.
*
* The output of this method can be parsed by the `of()` factory method; this will yield an object equal to this
* one, but possibly of a different type if instantiated through `BigNumber::of()`.
*
* @pure
*/
abstract public function toString(): string;
#[Override]
final public function jsonSerialize(): string
{
return $this->toString();
}
/**
* @pure
*/
final public function __toString(): string
{
return $this->toString();
}
/**
* Overridden by subclasses to convert a BigNumber to an instance of the subclass.
*
* @throws RoundingNecessaryException If the value cannot be converted.
*
* @pure
*/
abstract protected static function from(BigNumber $number): static;
/**
* Proxy method to access BigInteger's protected constructor from sibling classes.
*
* @internal
*
* @pure
*/
final protected function newBigInteger(string $value): BigInteger
{
return new BigInteger($value);
}
/**
* Proxy method to access BigDecimal's protected constructor from sibling classes.
*
* @internal
*
* @pure
*/
final protected function newBigDecimal(string $value, int $scale = 0): BigDecimal
{
return new BigDecimal($value, $scale);
}
/**
* Proxy method to access BigRational's protected constructor from sibling classes.
*
* @internal
*
* @pure
*/
final protected function newBigRational(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator): BigRational
{
return new BigRational($numerator, $denominator, $checkDenominator);
}
/** /**
* @throws NumberFormatException If the format of the number is not valid. * @throws NumberFormatException If the format of the number is not valid.
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero. * @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
* *
* @psalm-pure * @pure
*/ */
private static function _of(BigNumber|int|float|string $value) : BigNumber private static function _of(BigNumber|int|float|string $value): BigNumber
{ {
if ($value instanceof BigNumber) { if ($value instanceof BigNumber) {
return $value; return $value;
} }
if (\is_int($value)) { if (is_int($value)) {
return new BigInteger((string) $value); return new BigInteger((string) $value);
} }
if (is_float($value)) { if (is_float($value)) {
// @phpstan-ignore-next-line
trigger_error(
'Passing floats to BigNumber::of() and arithmetic methods is deprecated and will be removed in 0.15. ' .
'Cast the float to string explicitly to preserve the previous behaviour.',
E_USER_DEPRECATED,
);
if (is_nan($value)) {
$value = 'NAN';
} else {
$value = (string) $value; $value = (string) $value;
} }
}
if (str_contains($value, '/')) { if (str_contains($value, '/')) {
// Rational number // Rational number
if (\preg_match(self::PARSE_REGEXP_RATIONAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { if (preg_match(self::PARSE_REGEXP_RATIONAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
throw NumberFormatException::invalidFormat($value); throw NumberFormatException::invalidFormat($value);
} }
@@ -102,9 +588,6 @@ abstract class BigNumber implements \JsonSerializable
$numerator = $matches['numerator']; $numerator = $matches['numerator'];
$denominator = $matches['denominator']; $denominator = $matches['denominator'];
assert($numerator !== null);
assert($denominator !== null);
$numerator = self::cleanUp($sign, $numerator); $numerator = self::cleanUp($sign, $numerator);
$denominator = self::cleanUp(null, $denominator); $denominator = self::cleanUp(null, $denominator);
@@ -115,11 +598,11 @@ abstract class BigNumber implements \JsonSerializable
return new BigRational( return new BigRational(
new BigInteger($numerator), new BigInteger($numerator),
new BigInteger($denominator), new BigInteger($denominator),
false false,
); );
} else { } else {
// Integer or decimal number // Integer or decimal number
if (\preg_match(self::PARSE_REGEXP_NUMERICAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { if (preg_match(self::PARSE_REGEXP_NUMERICAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
throw NumberFormatException::invalidFormat($value); throw NumberFormatException::invalidFormat($value);
} }
@@ -138,20 +621,37 @@ abstract class BigNumber implements \JsonSerializable
} }
if ($point !== null || $exponent !== null) { if ($point !== null || $exponent !== null) {
$fractional = ($fractional ?? ''); $fractional ??= '';
$exponent = ($exponent !== null) ? (int)$exponent : 0;
if ($exponent === PHP_INT_MIN || $exponent === PHP_INT_MAX) { if ($exponent !== null) {
if ($exponent[0] === '-') {
$exponent = ltrim(substr($exponent, 1), '0') ?: '0';
$exponent = filter_var($exponent, FILTER_VALIDATE_INT);
if ($exponent !== false) {
$exponent = -$exponent;
}
} else {
if ($exponent[0] === '+') {
$exponent = substr($exponent, 1);
}
$exponent = ltrim($exponent, '0') ?: '0';
$exponent = filter_var($exponent, FILTER_VALIDATE_INT);
}
} else {
$exponent = 0;
}
if ($exponent === false) {
throw new NumberFormatException('Exponent too large.'); throw new NumberFormatException('Exponent too large.');
} }
$unscaledValue = self::cleanUp($sign, $integral . $fractional); $unscaledValue = self::cleanUp($sign, $integral . $fractional);
$scale = \strlen($fractional) - $exponent; $scale = strlen($fractional) - $exponent;
if ($scale < 0) { if ($scale < 0) {
if ($unscaledValue !== '0') { if ($unscaledValue !== '0') {
$unscaledValue .= \str_repeat('0', -$scale); $unscaledValue .= str_repeat('0', -$scale);
} }
$scale = 0; $scale = 0;
} }
@@ -166,147 +666,30 @@ abstract class BigNumber implements \JsonSerializable
} }
/** /**
* Overridden by subclasses to convert a BigNumber to an instance of the subclass. * Removes optional leading zeros and applies sign.
* *
* @throws RoundingNecessaryException If the value cannot be converted. * @param string|null $sign The sign, '+' or '-', optional. Null is allowed for convenience and treated as '+'.
* @param string $number The number, validated as a string of digits.
* *
* @psalm-pure * @pure
*/ */
abstract protected static function from(BigNumber $number): static; private static function cleanUp(string|null $sign, string $number): string
/**
* Proxy method to access BigInteger's protected constructor from sibling classes.
*
* @internal
* @psalm-pure
*/
final protected function newBigInteger(string $value) : BigInteger
{ {
return new BigInteger($value); $number = ltrim($number, '0');
if ($number === '') {
return '0';
} }
/** return $sign === '-' ? '-' . $number : $number;
* Proxy method to access BigDecimal's protected constructor from sibling classes.
*
* @internal
* @psalm-pure
*/
final protected function newBigDecimal(string $value, int $scale = 0) : BigDecimal
{
return new BigDecimal($value, $scale);
}
/**
* Proxy method to access BigRational's protected constructor from sibling classes.
*
* @internal
* @psalm-pure
*/
final protected function newBigRational(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator) : BigRational
{
return new BigRational($numerator, $denominator, $checkDenominator);
}
/**
* Returns the minimum of the given values.
*
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
* to an instance of the class this method is called on.
*
* @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid.
*
* @psalm-pure
*/
final public static function min(BigNumber|int|float|string ...$values) : static
{
$min = null;
foreach ($values as $value) {
$value = static::of($value);
if ($min === null || $value->isLessThan($min)) {
$min = $value;
}
}
if ($min === null) {
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
}
return $min;
}
/**
* Returns the maximum of the given values.
*
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
* to an instance of the class this method is called on.
*
* @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid.
*
* @psalm-pure
*/
final public static function max(BigNumber|int|float|string ...$values) : static
{
$max = null;
foreach ($values as $value) {
$value = static::of($value);
if ($max === null || $value->isGreaterThan($max)) {
$max = $value;
}
}
if ($max === null) {
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
}
return $max;
}
/**
* Returns the sum of the given values.
*
* @param BigNumber|int|float|string ...$values The numbers to add. All the numbers need to be convertible
* to an instance of the class this method is called on.
*
* @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid.
*
* @psalm-pure
*/
final public static function sum(BigNumber|int|float|string ...$values) : static
{
/** @var static|null $sum */
$sum = null;
foreach ($values as $value) {
$value = static::of($value);
$sum = $sum === null ? $value : self::add($sum, $value);
}
if ($sum === null) {
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
}
return $sum;
} }
/** /**
* Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException. * Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException.
* *
* @todo This could be better resolved by creating an abstract protected method in BigNumber, and leaving to * @pure
* concrete classes the responsibility to perform the addition themselves or delegate it to the given number,
* depending on their ability to perform the operation. This will also require a version bump because we're
* potentially breaking custom BigNumber implementations (if any...)
*
* @psalm-pure
*/ */
private static function add(BigNumber $a, BigNumber $b) : BigNumber private static function add(BigNumber $a, BigNumber $b): BigNumber
{ {
if ($a instanceof BigRational) { if ($a instanceof BigRational) {
return $a->plus($b); return $a->plus($b);
@@ -324,192 +707,6 @@ abstract class BigNumber implements \JsonSerializable
return $b->plus($a); return $b->plus($a);
} }
/** @var BigInteger $a */
return $a->plus($b); return $a->plus($b);
} }
/**
* Removes optional leading zeros and applies sign.
*
* @param string|null $sign The sign, '+' or '-', optional. Null is allowed for convenience and treated as '+'.
* @param string $number The number, validated as a non-empty string of digits.
*
* @psalm-pure
*/
private static function cleanUp(string|null $sign, string $number) : string
{
$number = \ltrim($number, '0');
if ($number === '') {
return '0';
}
return $sign === '-' ? '-' . $number : $number;
}
/**
* Checks if this number is equal to the given one.
*/
final public function isEqualTo(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) === 0;
}
/**
* Checks if this number is strictly lower than the given one.
*/
final public function isLessThan(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) < 0;
}
/**
* Checks if this number is lower than or equal to the given one.
*/
final public function isLessThanOrEqualTo(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) <= 0;
}
/**
* Checks if this number is strictly greater than the given one.
*/
final public function isGreaterThan(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) > 0;
}
/**
* Checks if this number is greater than or equal to the given one.
*/
final public function isGreaterThanOrEqualTo(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) >= 0;
}
/**
* Checks if this number equals zero.
*/
final public function isZero() : bool
{
return $this->getSign() === 0;
}
/**
* Checks if this number is strictly negative.
*/
final public function isNegative() : bool
{
return $this->getSign() < 0;
}
/**
* Checks if this number is negative or zero.
*/
final public function isNegativeOrZero() : bool
{
return $this->getSign() <= 0;
}
/**
* Checks if this number is strictly positive.
*/
final public function isPositive() : bool
{
return $this->getSign() > 0;
}
/**
* Checks if this number is positive or zero.
*/
final public function isPositiveOrZero() : bool
{
return $this->getSign() >= 0;
}
/**
* Returns the sign of this number.
*
* @psalm-return -1|0|1
*
* @return int -1 if the number is negative, 0 if zero, 1 if positive.
*/
abstract public function getSign() : int;
/**
* Compares this number to the given one.
*
* @psalm-return -1|0|1
*
* @return int -1 if `$this` is lower than, 0 if equal to, 1 if greater than `$that`.
*
* @throws MathException If the number is not valid.
*/
abstract public function compareTo(BigNumber|int|float|string $that) : int;
/**
* Converts this number to a BigInteger.
*
* @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding.
*/
abstract public function toBigInteger() : BigInteger;
/**
* Converts this number to a BigDecimal.
*
* @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding.
*/
abstract public function toBigDecimal() : BigDecimal;
/**
* Converts this number to a BigRational.
*/
abstract public function toBigRational() : BigRational;
/**
* Converts this number to a BigDecimal with the given scale, using rounding if necessary.
*
* @param int $scale The scale of the resulting `BigDecimal`.
* @param RoundingMode $roundingMode An optional rounding mode, defaults to UNNECESSARY.
*
* @throws RoundingNecessaryException If this number cannot be converted to the given scale without rounding.
* This only applies when RoundingMode::UNNECESSARY is used.
*/
abstract public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal;
/**
* Returns the exact value of this number as a native integer.
*
* If this number cannot be converted to a native integer without losing precision, an exception is thrown.
* Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit.
*
* @throws MathException If this number cannot be exactly converted to a native integer.
*/
abstract public function toInt() : int;
/**
* Returns an approximation of this number as a floating-point value.
*
* Note that this method can discard information as the precision of a floating-point value
* is inherently limited.
*
* If the number is greater than the largest representable floating point number, positive infinity is returned.
* If the number is less than the smallest representable floating point number, negative infinity is returned.
*/
abstract public function toFloat() : float;
/**
* Returns a string representation of this number.
*
* The output of this method can be parsed by the `of()` factory method;
* this will yield an object equal to this one, without any information loss.
*/
abstract public function __toString() : string;
#[Override]
final public function jsonSerialize() : string
{
return $this->__toString();
}
} }
+264 -82
View File
@@ -8,26 +8,38 @@ use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException; use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NumberFormatException; use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Exception\RoundingNecessaryException; use Brick\Math\Exception\RoundingNecessaryException;
use InvalidArgumentException;
use LogicException;
use Override; use Override;
use function is_finite;
use function max;
use function min;
use function strlen;
use function substr;
use function trigger_error;
use const E_USER_DEPRECATED;
/** /**
* An arbitrarily large rational number. * An arbitrarily large rational number.
* *
* This class is immutable. * This class is immutable.
* *
* @psalm-immutable * Fractions are automatically simplified to lowest terms. For example, `2/4` becomes `1/2`.
* The denominator is always strictly positive; the sign is carried by the numerator.
*/ */
final class BigRational extends BigNumber final readonly class BigRational extends BigNumber
{ {
/** /**
* The numerator. * The numerator.
*/ */
private readonly BigInteger $numerator; private BigInteger $numerator;
/** /**
* The denominator. Always strictly positive. * The denominator. Always strictly positive.
*/ */
private readonly BigInteger $denominator; private BigInteger $denominator;
/** /**
* Protected constructor. Use a factory method to obtain an instance. * Protected constructor. Use a factory method to obtain an instance.
@@ -37,6 +49,8 @@ final class BigRational extends BigNumber
* @param bool $checkDenominator Whether to check the denominator for negative and zero. * @param bool $checkDenominator Whether to check the denominator for negative and zero.
* *
* @throws DivisionByZeroException If the denominator is zero. * @throws DivisionByZeroException If the denominator is zero.
*
* @pure
*/ */
protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator) protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator)
{ {
@@ -56,12 +70,30 @@ final class BigRational extends BigNumber
} }
/** /**
* @psalm-pure * Creates a BigRational out of a numerator and a denominator.
*
* If the denominator is negative, the signs of both the numerator and the denominator
* will be inverted to ensure that the denominator is always positive.
*
* @deprecated Use ofFraction() instead.
*
* @param BigNumber|int|float|string $numerator The numerator. Must be convertible to a BigInteger.
* @param BigNumber|int|float|string $denominator The denominator. Must be convertible to a BigInteger.
*
* @throws NumberFormatException If an argument does not represent a valid number.
* @throws RoundingNecessaryException If an argument represents a non-integer number.
* @throws DivisionByZeroException If the denominator is zero.
*/ */
#[Override] public static function nd(
protected static function from(BigNumber $number): static BigNumber|int|float|string $numerator,
{ BigNumber|int|float|string $denominator,
return $number->toBigRational(); ): BigRational {
trigger_error(
'The BigRational::nd() method is deprecated, use BigRational::ofFraction() instead.',
E_USER_DEPRECATED,
);
return self::ofFraction($numerator, $denominator);
} }
/** /**
@@ -73,16 +105,15 @@ final class BigRational extends BigNumber
* @param BigNumber|int|float|string $numerator The numerator. Must be convertible to a BigInteger. * @param BigNumber|int|float|string $numerator The numerator. Must be convertible to a BigInteger.
* @param BigNumber|int|float|string $denominator The denominator. Must be convertible to a BigInteger. * @param BigNumber|int|float|string $denominator The denominator. Must be convertible to a BigInteger.
* *
* @throws NumberFormatException If an argument does not represent a valid number. * @throws MathException If an argument is not valid, or is not convertible to a BigInteger.
* @throws RoundingNecessaryException If an argument represents a non-integer number.
* @throws DivisionByZeroException If the denominator is zero. * @throws DivisionByZeroException If the denominator is zero.
* *
* @psalm-pure * @pure
*/ */
public static function nd( public static function ofFraction(
BigNumber|int|float|string $numerator, BigNumber|int|float|string $numerator,
BigNumber|int|float|string $denominator, BigNumber|int|float|string $denominator,
) : BigRational { ): BigRational {
$numerator = BigInteger::of($numerator); $numerator = BigInteger::of($numerator);
$denominator = BigInteger::of($denominator); $denominator = BigInteger::of($denominator);
@@ -92,14 +123,11 @@ final class BigRational extends BigNumber
/** /**
* Returns a BigRational representing zero. * Returns a BigRational representing zero.
* *
* @psalm-pure * @pure
*/ */
public static function zero() : BigRational public static function zero(): BigRational
{ {
/** /** @var BigRational|null $zero */
* @psalm-suppress ImpureStaticVariable
* @var BigRational|null $zero
*/
static $zero; static $zero;
if ($zero === null) { if ($zero === null) {
@@ -112,14 +140,11 @@ final class BigRational extends BigNumber
/** /**
* Returns a BigRational representing one. * Returns a BigRational representing one.
* *
* @psalm-pure * @pure
*/ */
public static function one() : BigRational public static function one(): BigRational
{ {
/** /** @var BigRational|null $one */
* @psalm-suppress ImpureStaticVariable
* @var BigRational|null $one
*/
static $one; static $one;
if ($one === null) { if ($one === null) {
@@ -132,14 +157,11 @@ final class BigRational extends BigNumber
/** /**
* Returns a BigRational representing ten. * Returns a BigRational representing ten.
* *
* @psalm-pure * @pure
*/ */
public static function ten() : BigRational public static function ten(): BigRational
{ {
/** /** @var BigRational|null $ten */
* @psalm-suppress ImpureStaticVariable
* @var BigRational|null $ten
*/
static $ten; static $ten;
if ($ten === null) { if ($ten === null) {
@@ -149,52 +171,113 @@ final class BigRational extends BigNumber
return $ten; return $ten;
} }
public function getNumerator() : BigInteger /**
* @pure
*/
public function getNumerator(): BigInteger
{ {
return $this->numerator; return $this->numerator;
} }
public function getDenominator() : BigInteger /**
* @pure
*/
public function getDenominator(): BigInteger
{ {
return $this->denominator; return $this->denominator;
} }
/** /**
* Returns the quotient of the division of the numerator by the denominator. * Returns the quotient of the division of the numerator by the denominator.
*
* @deprecated Will be removed in 0.15. Use getIntegralPart() instead.
*/ */
public function quotient() : BigInteger public function quotient(): BigInteger
{ {
trigger_error(
'BigRational::quotient() is deprecated and will be removed in 0.15. Use getIntegralPart() instead.',
E_USER_DEPRECATED,
);
return $this->numerator->quotient($this->denominator); return $this->numerator->quotient($this->denominator);
} }
/** /**
* Returns the remainder of the division of the numerator by the denominator. * Returns the remainder of the division of the numerator by the denominator.
*
* @deprecated Will be removed in 0.15. Use `$number->getNumerator()->remainder($number->getDenominator())` instead.
*/ */
public function remainder() : BigInteger public function remainder(): BigInteger
{ {
trigger_error(
'BigRational::remainder() is deprecated and will be removed in 0.15. Use `$number->getNumerator()->remainder($number->getDenominator())` instead.',
E_USER_DEPRECATED,
);
return $this->numerator->remainder($this->denominator); return $this->numerator->remainder($this->denominator);
} }
/** /**
* Returns the quotient and remainder of the division of the numerator by the denominator. * Returns the quotient and remainder of the division of the numerator by the denominator.
* *
* @return BigInteger[] * @deprecated Will be removed in 0.15. Use `$number->getNumerator()->quotientAndRemainder($number->getDenominator())` instead.
* *
* @psalm-return array{BigInteger, BigInteger} * @return array{BigInteger, BigInteger}
*/ */
public function quotientAndRemainder() : array public function quotientAndRemainder(): array
{ {
trigger_error(
'BigRational::quotientAndRemainder() is deprecated and will be removed in 0.15. Use `$number->getNumerator()->quotientAndRemainder($number->getDenominator())` instead.',
E_USER_DEPRECATED,
);
return $this->numerator->quotientAndRemainder($this->denominator); return $this->numerator->quotientAndRemainder($this->denominator);
} }
/**
* Returns the integral part of this rational number.
*
* Examples:
*
* - `7/3` returns `2` (since 7/3 = 2 + 1/3)
* - `-7/3` returns `-2` (since -7/3 = -2 + (-1/3))
*
* The following identity holds: `$r->isEqualTo($r->getFractionalPart()->plus($r->getIntegralPart()))`.
*
* @pure
*/
public function getIntegralPart(): BigInteger
{
return $this->numerator->quotient($this->denominator);
}
/**
* Returns the fractional part of this rational number.
*
* Examples:
*
* - `7/3` returns `1/3` (since 7/3 = 2 + 1/3)
* - `-7/3` returns `-1/3` (since -7/3 = -2 + (-1/3))
*
* The following identity holds: `$r->isEqualTo($r->getFractionalPart()->plus($r->getIntegralPart()))`.
*
* @pure
*/
public function getFractionalPart(): BigRational
{
return new BigRational($this->numerator->remainder($this->denominator), $this->denominator, false);
}
/** /**
* Returns the sum of this number and the given one. * Returns the sum of this number and the given one.
* *
* @param BigNumber|int|float|string $that The number to add. * @param BigNumber|int|float|string $that The number to add.
* *
* @throws MathException If the number is not valid. * @throws MathException If the number is not valid.
*
* @pure
*/ */
public function plus(BigNumber|int|float|string $that) : BigRational public function plus(BigNumber|int|float|string $that): BigRational
{ {
$that = BigRational::of($that); $that = BigRational::of($that);
@@ -211,8 +294,10 @@ final class BigRational extends BigNumber
* @param BigNumber|int|float|string $that The number to subtract. * @param BigNumber|int|float|string $that The number to subtract.
* *
* @throws MathException If the number is not valid. * @throws MathException If the number is not valid.
*
* @pure
*/ */
public function minus(BigNumber|int|float|string $that) : BigRational public function minus(BigNumber|int|float|string $that): BigRational
{ {
$that = BigRational::of($that); $that = BigRational::of($that);
@@ -228,9 +313,11 @@ final class BigRational extends BigNumber
* *
* @param BigNumber|int|float|string $that The multiplier. * @param BigNumber|int|float|string $that The multiplier.
* *
* @throws MathException If the multiplier is not a valid number. * @throws MathException If the multiplier is not valid.
*
* @pure
*/ */
public function multipliedBy(BigNumber|int|float|string $that) : BigRational public function multipliedBy(BigNumber|int|float|string $that): BigRational
{ {
$that = BigRational::of($that); $that = BigRational::of($that);
@@ -245,12 +332,19 @@ final class BigRational extends BigNumber
* *
* @param BigNumber|int|float|string $that The divisor. * @param BigNumber|int|float|string $that The divisor.
* *
* @throws MathException If the divisor is not a valid number, or is zero. * @throws MathException If the divisor is not valid.
* @throws DivisionByZeroException If the divisor is zero.
*
* @pure
*/ */
public function dividedBy(BigNumber|int|float|string $that) : BigRational public function dividedBy(BigNumber|int|float|string $that): BigRational
{ {
$that = BigRational::of($that); $that = BigRational::of($that);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
$numerator = $this->numerator->multipliedBy($that->denominator); $numerator = $this->numerator->multipliedBy($that->denominator);
$denominator = $this->denominator->multipliedBy($that->numerator); $denominator = $this->denominator->multipliedBy($that->numerator);
@@ -260,14 +354,14 @@ final class BigRational extends BigNumber
/** /**
* Returns this number exponentiated to the given value. * Returns this number exponentiated to the given value.
* *
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000. * @throws InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
*
* @pure
*/ */
public function power(int $exponent) : BigRational public function power(int $exponent): BigRational
{ {
if ($exponent === 0) { if ($exponent === 0) {
$one = BigInteger::one(); return BigRational::one();
return new BigRational($one, $one, false);
} }
if ($exponent === 1) { if ($exponent === 1) {
@@ -277,7 +371,7 @@ final class BigRational extends BigNumber
return new BigRational( return new BigRational(
$this->numerator->power($exponent), $this->numerator->power($exponent),
$this->denominator->power($exponent), $this->denominator->power($exponent),
false false,
); );
} }
@@ -287,32 +381,26 @@ final class BigRational extends BigNumber
* The reciprocal has the numerator and denominator swapped. * The reciprocal has the numerator and denominator swapped.
* *
* @throws DivisionByZeroException If the numerator is zero. * @throws DivisionByZeroException If the numerator is zero.
*
* @pure
*/ */
public function reciprocal() : BigRational public function reciprocal(): BigRational
{ {
return new BigRational($this->denominator, $this->numerator, true); return new BigRational($this->denominator, $this->numerator, true);
} }
/** #[Override]
* Returns the absolute value of this BigRational. public function negated(): static
*/
public function abs() : BigRational
{
return new BigRational($this->numerator->abs(), $this->denominator, false);
}
/**
* Returns the negated value of this BigRational.
*/
public function negated() : BigRational
{ {
return new BigRational($this->numerator->negated(), $this->denominator, false); return new BigRational($this->numerator->negated(), $this->denominator, false);
} }
/** /**
* Returns the simplified value of this BigRational. * Returns the simplified value of this BigRational.
*
* @pure
*/ */
public function simplified() : BigRational public function simplified(): BigRational
{ {
$gcd = $this->numerator->gcd($this->denominator); $gcd = $this->numerator->gcd($this->denominator);
@@ -323,19 +411,27 @@ final class BigRational extends BigNumber
} }
#[Override] #[Override]
public function compareTo(BigNumber|int|float|string $that) : int public function compareTo(BigNumber|int|float|string $that): int
{ {
return $this->minus($that)->getSign(); $that = BigRational::of($that);
if ($this->denominator->isEqualTo($that->denominator)) {
return $this->numerator->compareTo($that->numerator);
}
return $this->numerator
->multipliedBy($that->denominator)
->compareTo($that->numerator->multipliedBy($this->denominator));
} }
#[Override] #[Override]
public function getSign() : int public function getSign(): int
{ {
return $this->numerator->getSign(); return $this->numerator->getSign();
} }
#[Override] #[Override]
public function toBigInteger() : BigInteger public function toBigInteger(): BigInteger
{ {
$simplified = $this->simplified(); $simplified = $this->simplified();
@@ -347,41 +443,59 @@ final class BigRational extends BigNumber
} }
#[Override] #[Override]
public function toBigDecimal() : BigDecimal public function toBigDecimal(): BigDecimal
{ {
return $this->numerator->toBigDecimal()->exactlyDividedBy($this->denominator); return $this->numerator->toBigDecimal()->dividedByExact($this->denominator);
} }
#[Override] #[Override]
public function toBigRational() : BigRational public function toBigRational(): BigRational
{ {
return $this; return $this;
} }
#[Override] #[Override]
public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal
{ {
return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode); return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode);
} }
#[Override] #[Override]
public function toInt() : int public function toInt(): int
{ {
return $this->toBigInteger()->toInt(); return $this->toBigInteger()->toInt();
} }
#[Override] #[Override]
public function toFloat() : float public function toFloat(): float
{ {
$simplified = $this->simplified(); $simplified = $this->simplified();
return $simplified->numerator->toFloat() / $simplified->denominator->toFloat(); $numeratorFloat = $simplified->numerator->toFloat();
$denominatorFloat = $simplified->denominator->toFloat();
if (is_finite($numeratorFloat) && is_finite($denominatorFloat)) {
return $numeratorFloat / $denominatorFloat;
}
// At least one side overflows to INF; use a decimal approximation instead.
// We need ~17 significant digits for double precision (we use 20 for some margin). Since $scale controls
// decimal places (not significant digits), we subtract the estimated order of magnitude so that large results
// use fewer decimal places and small results use more (to look past leading zeros). Clamped to [0, 350] as
// doubles range from e-324 to e308 (350 ≈ 324 + 20 significant digits + margin).
$magnitude = strlen($simplified->numerator->abs()->toString()) - strlen($simplified->denominator->toString());
$scale = min(350, max(0, 20 - $magnitude));
return $simplified->numerator
->toBigDecimal()
->dividedBy($simplified->denominator, $scale, RoundingMode::HalfEven)
->toFloat();
} }
#[Override] #[Override]
public function __toString() : string public function toString(): string
{ {
$numerator = (string) $this->numerator; $numerator = $this->numerator->toString();
$denominator = (string) $this->denominator; $denominator = $this->denominator->toString();
if ($denominator === '1') { if ($denominator === '1') {
return $numerator; return $numerator;
@@ -390,6 +504,67 @@ final class BigRational extends BigNumber
return $numerator . '/' . $denominator; return $numerator . '/' . $denominator;
} }
/**
* Returns the decimal representation of this rational number, with repeating decimals in parentheses.
*
* WARNING: This method is unbounded.
* The length of the repeating decimal period can be as large as `denominator - 1`.
* For fractions with large denominators, this method can use excessive memory and CPU time.
* For example, `1/100019` has a repeating period of 100,018 digits.
*
* Examples:
*
* - `10/3` returns `3.(3)`
* - `171/70` returns `2.4(428571)`
* - `1/2` returns `0.5`
*
* @pure
*/
public function toRepeatingDecimalString(): string
{
if ($this->numerator->isZero()) {
return '0';
}
$sign = $this->numerator->isNegative() ? '-' : '';
$numerator = $this->numerator->abs();
$denominator = $this->denominator;
$integral = $numerator->quotient($denominator);
$remainder = $numerator->remainder($denominator);
$integralString = $integral->toString();
if ($remainder->isZero()) {
return $sign . $integralString;
}
$digits = '';
$remainderPositions = [];
$index = 0;
while (! $remainder->isZero()) {
$remainderString = $remainder->toString();
if (isset($remainderPositions[$remainderString])) {
$repeatIndex = $remainderPositions[$remainderString];
$nonRepeating = substr($digits, 0, $repeatIndex);
$repeating = substr($digits, $repeatIndex);
return $sign . $integralString . '.' . $nonRepeating . '(' . $repeating . ')';
}
$remainderPositions[$remainderString] = $index;
$remainder = $remainder->multipliedBy(10);
$digits .= $remainder->quotient($denominator)->toString();
$remainder = $remainder->remainder($denominator);
$index++;
}
return $sign . $integralString . '.' . $digits;
}
/** /**
* This method is required for serializing the object and SHOULD NOT be accessed directly. * This method is required for serializing the object and SHOULD NOT be accessed directly.
* *
@@ -406,19 +581,26 @@ final class BigRational extends BigNumber
* This method is only here to allow unserializing the object and cannot be accessed directly. * This method is only here to allow unserializing the object and cannot be accessed directly.
* *
* @internal * @internal
* @psalm-suppress RedundantPropertyInitializationCheck
* *
* @param array{numerator: BigInteger, denominator: BigInteger} $data * @param array{numerator: BigInteger, denominator: BigInteger} $data
* *
* @throws \LogicException * @throws LogicException
*/ */
public function __unserialize(array $data): void public function __unserialize(array $data): void
{ {
/** @phpstan-ignore isset.initializedProperty */
if (isset($this->numerator)) { if (isset($this->numerator)) {
throw new \LogicException('__unserialize() is an internal function, it must not be called directly.'); throw new LogicException('__unserialize() is an internal function, it must not be called directly.');
} }
/** @phpstan-ignore deadCode.unreachable */
$this->numerator = $data['numerator']; $this->numerator = $data['numerator'];
$this->denominator = $data['denominator']; $this->denominator = $data['denominator'];
} }
#[Override]
protected static function from(BigNumber $number): static
{
return $number->toBigRational();
}
} }
@@ -7,28 +7,28 @@ namespace Brick\Math\Exception;
/** /**
* Exception thrown when a division by zero occurs. * Exception thrown when a division by zero occurs.
*/ */
class DivisionByZeroException extends MathException final class DivisionByZeroException extends MathException
{ {
/** /**
* @psalm-pure * @pure
*/ */
public static function divisionByZero() : DivisionByZeroException public static function divisionByZero(): DivisionByZeroException
{ {
return new self('Division by zero.'); return new self('Division by zero.');
} }
/** /**
* @psalm-pure * @pure
*/ */
public static function modulusMustNotBeZero() : DivisionByZeroException public static function modulusMustNotBeZero(): DivisionByZeroException
{ {
return new self('The modulus must not be zero.'); return new self('The modulus must not be zero.');
} }
/** /**
* @psalm-pure * @pure
*/ */
public static function denominatorMustNotBeZero() : DivisionByZeroException public static function denominatorMustNotBeZero(): DivisionByZeroException
{ {
return new self('The denominator of a rational number cannot be zero.'); return new self('The denominator of a rational number cannot be zero.');
} }
@@ -6,18 +6,23 @@ namespace Brick\Math\Exception;
use Brick\Math\BigInteger; use Brick\Math\BigInteger;
use function sprintf;
use const PHP_INT_MAX;
use const PHP_INT_MIN;
/** /**
* Exception thrown when an integer overflow occurs. * Exception thrown when an integer overflow occurs.
*/ */
class IntegerOverflowException extends MathException final class IntegerOverflowException extends MathException
{ {
/** /**
* @psalm-pure * @pure
*/ */
public static function toIntOverflow(BigInteger $value) : IntegerOverflowException public static function toIntOverflow(BigInteger $value): IntegerOverflowException
{ {
$message = '%s is out of range %d to %d and cannot be represented as an integer.'; $message = '%s is out of range %d to %d and cannot be represented as an integer.';
return new self(\sprintf($message, (string) $value, PHP_INT_MIN, PHP_INT_MAX)); return new self(sprintf($message, $value->toString(), PHP_INT_MIN, PHP_INT_MAX));
} }
} }
+3 -1
View File
@@ -4,9 +4,11 @@ declare(strict_types=1);
namespace Brick\Math\Exception; namespace Brick\Math\Exception;
use RuntimeException;
/** /**
* Base class for all math exceptions. * Base class for all math exceptions.
*/ */
class MathException extends \Exception class MathException extends RuntimeException
{ {
} }
@@ -7,6 +7,6 @@ namespace Brick\Math\Exception;
/** /**
* Exception thrown when attempting to perform an unsupported operation, such as a square root, on a negative number. * Exception thrown when attempting to perform an unsupported operation, such as a square root, on a negative number.
*/ */
class NegativeNumberException extends MathException final class NegativeNumberException extends MathException
{ {
} }
+30 -11
View File
@@ -4,14 +4,22 @@ declare(strict_types=1);
namespace Brick\Math\Exception; namespace Brick\Math\Exception;
use function dechex;
use function ord;
use function sprintf;
use function strtoupper;
/** /**
* Exception thrown when attempting to create a number from a string with an invalid format. * Exception thrown when attempting to create a number from a string with an invalid format.
*/ */
class NumberFormatException extends MathException final class NumberFormatException extends MathException
{ {
public static function invalidFormat(string $value) : self /**
* @pure
*/
public static function invalidFormat(string $value): self
{ {
return new self(\sprintf( return new self(sprintf(
'The given value "%s" does not represent a valid number.', 'The given value "%s" does not represent a valid number.',
$value, $value,
)); ));
@@ -20,22 +28,33 @@ class NumberFormatException extends MathException
/** /**
* @param string $char The failing character. * @param string $char The failing character.
* *
* @psalm-pure * @pure
*/ */
public static function charNotInAlphabet(string $char) : self public static function charNotInAlphabet(string $char): self
{ {
$ord = \ord($char); return new self(sprintf(
'Character %s is not valid in the given alphabet.',
self::charToString($char),
));
}
/**
* @pure
*/
private static function charToString(string $char): string
{
$ord = ord($char);
if ($ord < 32 || $ord > 126) { if ($ord < 32 || $ord > 126) {
$char = \strtoupper(\dechex($ord)); $char = strtoupper(dechex($ord));
if ($ord < 10) { if ($ord < 16) {
$char = '0' . $char; $char = '0' . $char;
} }
} else {
$char = '"' . $char . '"'; return '0x' . $char;
} }
return new self(\sprintf('Char %s is not a valid character in the given alphabet.', $char)); return '"' . $char . '"';
} }
} }
@@ -7,12 +7,12 @@ namespace Brick\Math\Exception;
/** /**
* Exception thrown when a number cannot be represented at the requested scale without rounding. * Exception thrown when a number cannot be represented at the requested scale without rounding.
*/ */
class RoundingNecessaryException extends MathException final class RoundingNecessaryException extends MathException
{ {
/** /**
* @psalm-pure * @pure
*/ */
public static function roundingNecessary() : RoundingNecessaryException public static function roundingNecessary(): RoundingNecessaryException
{ {
return new self('Rounding is necessary to represent the result of the operation at this scale.'); return new self('Rounding is necessary to represent the result of the operation at this scale.');
} }
+193 -157
View File
@@ -7,6 +7,16 @@ namespace Brick\Math\Internal;
use Brick\Math\Exception\RoundingNecessaryException; use Brick\Math\Exception\RoundingNecessaryException;
use Brick\Math\RoundingMode; use Brick\Math\RoundingMode;
use function chr;
use function ltrim;
use function ord;
use function str_repeat;
use function strlen;
use function strpos;
use function strrev;
use function strtolower;
use function substr;
/** /**
* Performs basic operations on arbitrary size integers. * Performs basic operations on arbitrary size integers.
* *
@@ -17,10 +27,8 @@ use Brick\Math\RoundingMode;
* All methods must return strings respecting this format, unless specified otherwise. * All methods must return strings respecting this format, unless specified otherwise.
* *
* @internal * @internal
*
* @psalm-immutable
*/ */
abstract class Calculator abstract readonly class Calculator
{ {
/** /**
* The maximum exponent value allowed for the pow() method. * The maximum exponent value allowed for the pow() method.
@@ -32,94 +40,29 @@ abstract class Calculator
*/ */
public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'; public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
/**
* The Calculator instance in use.
*/
private static ?Calculator $instance = null;
/**
* Sets the Calculator instance to use.
*
* An instance is typically set only in unit tests: the autodetect is usually the best option.
*
* @param Calculator|null $calculator The calculator instance, or NULL to revert to autodetect.
*/
final public static function set(?Calculator $calculator) : void
{
self::$instance = $calculator;
}
/**
* Returns the Calculator instance to use.
*
* If none has been explicitly set, the fastest available implementation will be returned.
*
* @psalm-pure
* @psalm-suppress ImpureStaticProperty
*/
final public static function get() : Calculator
{
if (self::$instance === null) {
/** @psalm-suppress ImpureMethodCall */
self::$instance = self::detect();
}
return self::$instance;
}
/**
* Returns the fastest available Calculator implementation.
*
* @codeCoverageIgnore
*/
private static function detect() : Calculator
{
if (\extension_loaded('gmp')) {
return new Calculator\GmpCalculator();
}
if (\extension_loaded('bcmath')) {
return new Calculator\BcMathCalculator();
}
return new Calculator\NativeCalculator();
}
/**
* Extracts the sign & digits of the operands.
*
* @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits.
*/
final protected function init(string $a, string $b) : array
{
return [
$aNeg = ($a[0] === '-'),
$bNeg = ($b[0] === '-'),
$aNeg ? \substr($a, 1) : $a,
$bNeg ? \substr($b, 1) : $b,
];
}
/** /**
* Returns the absolute value of a number. * Returns the absolute value of a number.
*
* @pure
*/ */
final public function abs(string $n) : string final public function abs(string $n): string
{ {
return ($n[0] === '-') ? \substr($n, 1) : $n; return ($n[0] === '-') ? substr($n, 1) : $n;
} }
/** /**
* Negates a number. * Negates a number.
*
* @pure
*/ */
final public function neg(string $n) : string final public function neg(string $n): string
{ {
if ($n === '0') { if ($n === '0') {
return '0'; return '0';
} }
if ($n[0] === '-') { if ($n[0] === '-') {
return \substr($n, 1); return substr($n, 1);
} }
return '-' . $n; return '-' . $n;
@@ -128,11 +71,13 @@ abstract class Calculator
/** /**
* Compares two numbers. * Compares two numbers.
* *
* @psalm-return -1|0|1 * Returns -1 if the first number is less than, 0 if equal to, 1 if greater than the second number.
* *
* @return int -1 if the first number is less than, 0 if equal to, 1 if greater than the second number. * @return -1|0|1
*
* @pure
*/ */
final public function cmp(string $a, string $b) : int final public function cmp(string $a, string $b): int
{ {
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b); [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
@@ -144,8 +89,8 @@ abstract class Calculator
return 1; return 1;
} }
$aLen = \strlen($aDig); $aLen = strlen($aDig);
$bLen = \strlen($bDig); $bLen = strlen($bDig);
if ($aLen < $bLen) { if ($aLen < $bLen) {
$result = -1; $result = -1;
@@ -160,18 +105,24 @@ abstract class Calculator
/** /**
* Adds two numbers. * Adds two numbers.
*
* @pure
*/ */
abstract public function add(string $a, string $b) : string; abstract public function add(string $a, string $b): string;
/** /**
* Subtracts two numbers. * Subtracts two numbers.
*
* @pure
*/ */
abstract public function sub(string $a, string $b) : string; abstract public function sub(string $a, string $b): string;
/** /**
* Multiplies two numbers. * Multiplies two numbers.
*
* @pure
*/ */
abstract public function mul(string $a, string $b) : string; abstract public function mul(string $a, string $b): string;
/** /**
* Returns the quotient of the division of two numbers. * Returns the quotient of the division of two numbers.
@@ -180,8 +131,10 @@ abstract class Calculator
* @param string $b The divisor, must not be zero. * @param string $b The divisor, must not be zero.
* *
* @return string The quotient. * @return string The quotient.
*
* @pure
*/ */
abstract public function divQ(string $a, string $b) : string; abstract public function divQ(string $a, string $b): string;
/** /**
* Returns the remainder of the division of two numbers. * Returns the remainder of the division of two numbers.
@@ -190,8 +143,10 @@ abstract class Calculator
* @param string $b The divisor, must not be zero. * @param string $b The divisor, must not be zero.
* *
* @return string The remainder. * @return string The remainder.
*
* @pure
*/ */
abstract public function divR(string $a, string $b) : string; abstract public function divR(string $a, string $b): string;
/** /**
* Returns the quotient and remainder of the division of two numbers. * Returns the quotient and remainder of the division of two numbers.
@@ -200,8 +155,10 @@ abstract class Calculator
* @param string $b The divisor, must not be zero. * @param string $b The divisor, must not be zero.
* *
* @return array{string, string} An array containing the quotient and remainder. * @return array{string, string} An array containing the quotient and remainder.
*
* @pure
*/ */
abstract public function divQR(string $a, string $b) : array; abstract public function divQR(string $a, string $b): array;
/** /**
* Exponentiates a number. * Exponentiates a number.
@@ -210,13 +167,17 @@ abstract class Calculator
* @param int $e The exponent, validated as an integer between 0 and MAX_POWER. * @param int $e The exponent, validated as an integer between 0 and MAX_POWER.
* *
* @return string The power. * @return string The power.
*
* @pure
*/ */
abstract public function pow(string $a, int $e) : string; abstract public function pow(string $a, int $e): string;
/** /**
* @param string $b The modulus; must not be zero. * @param string $b The modulus; must not be zero.
*
* @pure
*/ */
public function mod(string $a, string $b) : string public function mod(string $a, string $b): string
{ {
return $this->divR($this->add($this->divR($a, $b), $b), $b); return $this->divR($this->add($this->divR($a, $b), $b), $b);
} }
@@ -229,8 +190,10 @@ abstract class Calculator
* This method can be overridden by the concrete implementation if the underlying library has built-in support. * This method can be overridden by the concrete implementation if the underlying library has built-in support.
* *
* @param string $m The modulus; must not be negative or zero. * @param string $m The modulus; must not be negative or zero.
*
* @pure
*/ */
public function modInverse(string $x, string $m) : ?string public function modInverse(string $x, string $m): ?string
{ {
if ($m === '1') { if ($m === '1') {
return '0'; return '0';
@@ -254,11 +217,13 @@ abstract class Calculator
/** /**
* Raises a number into power with modulo. * Raises a number into power with modulo.
* *
* @param string $base The base number; must be positive or zero. * @param string $base The base number.
* @param string $exp The exponent; must be positive or zero. * @param string $exp The exponent; must be positive or zero.
* @param string $mod The modulus; must be strictly positive. * @param string $mod The modulus; must be strictly positive.
*
* @pure
*/ */
abstract public function modPow(string $base, string $exp, string $mod) : string; abstract public function modPow(string $base, string $exp, string $mod): string;
/** /**
* Returns the greatest common divisor of the two numbers. * Returns the greatest common divisor of the two numbers.
@@ -267,8 +232,10 @@ abstract class Calculator
* has built-in support for GCD calculations. * has built-in support for GCD calculations.
* *
* @return string The GCD, always positive, or zero if both arguments are zero. * @return string The GCD, always positive, or zero if both arguments are zero.
*
* @pure
*/ */
public function gcd(string $a, string $b) : string public function gcd(string $a, string $b): string
{ {
if ($a === '0') { if ($a === '0') {
return $this->abs($b); return $this->abs($b);
@@ -282,20 +249,22 @@ abstract class Calculator
} }
/** /**
* @return array{string, string, string} GCD, X, Y * Returns the least common multiple of the two numbers.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for LCM calculations.
*
* @return string The LCM, always positive, or zero if at least one argument is zero.
*
* @pure
*/ */
private function gcdExtended(string $a, string $b) : array public function lcm(string $a, string $b): string
{ {
if ($a === '0') { if ($a === '0' || $b === '0') {
return [$b, '0', '1']; return '0';
} }
[$gcd, $x1, $y1] = $this->gcdExtended($this->mod($b, $a), $a); return $this->divQ($this->abs($this->mul($a, $b)), $this->gcd($a, $b));
$x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1));
$y = $x1;
return [$gcd, $x, $y];
} }
/** /**
@@ -303,8 +272,10 @@ abstract class Calculator
* *
* The result is the largest x such that x² ≤ n. * The result is the largest x such that x² ≤ n.
* The input MUST NOT be negative. * The input MUST NOT be negative.
*
* @pure
*/ */
abstract public function sqrt(string $n) : string; abstract public function sqrt(string $n): string;
/** /**
* Converts a number from an arbitrary base. * Converts a number from an arbitrary base.
@@ -316,10 +287,12 @@ abstract class Calculator
* @param int $base The base of the number, validated from 2 to 36. * @param int $base The base of the number, validated from 2 to 36.
* *
* @return string The converted number, following the Calculator conventions. * @return string The converted number, following the Calculator conventions.
*
* @pure
*/ */
public function fromBase(string $number, int $base) : string public function fromBase(string $number, int $base): string
{ {
return $this->fromArbitraryBase(\strtolower($number), self::ALPHABET, $base); return $this->fromArbitraryBase(strtolower($number), self::ALPHABET, $base);
} }
/** /**
@@ -332,13 +305,15 @@ abstract class Calculator
* @param int $base The base to convert to, validated from 2 to 36. * @param int $base The base to convert to, validated from 2 to 36.
* *
* @return string The converted number, lowercase. * @return string The converted number, lowercase.
*
* @pure
*/ */
public function toBase(string $number, int $base) : string public function toBase(string $number, int $base): string
{ {
$negative = ($number[0] === '-'); $negative = ($number[0] === '-');
if ($negative) { if ($negative) {
$number = \substr($number, 1); $number = substr($number, 1);
} }
$number = $this->toArbitraryBase($number, self::ALPHABET, $base); $number = $this->toArbitraryBase($number, self::ALPHABET, $base);
@@ -359,11 +334,13 @@ abstract class Calculator
* @param int $base The base of the number, validated from 2 to alphabet length. * @param int $base The base of the number, validated from 2 to alphabet length.
* *
* @return string The number in base 10, following the Calculator conventions. * @return string The number in base 10, following the Calculator conventions.
*
* @pure
*/ */
final public function fromArbitraryBase(string $number, string $alphabet, int $base) : string final public function fromArbitraryBase(string $number, string $alphabet, int $base): string
{ {
// remove leading "zeros" // remove leading "zeros"
$number = \ltrim($number, $alphabet[0]); $number = ltrim($number, $alphabet[0]);
if ($number === '') { if ($number === '') {
return '0'; return '0';
@@ -379,13 +356,13 @@ abstract class Calculator
$base = (string) $base; $base = (string) $base;
for ($i = \strlen($number) - 1; $i >= 0; $i--) { for ($i = strlen($number) - 1; $i >= 0; $i--) {
$index = \strpos($alphabet, $number[$i]); $index = strpos($alphabet, $number[$i]);
if ($index !== 0) { if ($index !== 0) {
$result = $this->add($result, ($index === 1) $result = $this->add(
? $power $result,
: $this->mul($power, (string) $index) ($index === 1) ? $power : $this->mul($power, (string) $index),
); );
} }
@@ -405,8 +382,10 @@ abstract class Calculator
* @param int $base The base to convert to, validated from 2 to alphabet length. * @param int $base The base to convert to, validated from 2 to alphabet length.
* *
* @return string The converted number in the given alphabet. * @return string The converted number in the given alphabet.
*
* @pure
*/ */
final public function toArbitraryBase(string $number, string $alphabet, int $base) : string final public function toArbitraryBase(string $number, string $alphabet, int $base): string
{ {
if ($number === '0') { if ($number === '0') {
return $alphabet[0]; return $alphabet[0];
@@ -422,7 +401,7 @@ abstract class Calculator
$result .= $alphabet[$remainder]; $result .= $alphabet[$remainder];
} }
return \strrev($result); return strrev($result);
} }
/** /**
@@ -434,19 +413,18 @@ abstract class Calculator
* @param string $b The divisor, must not be zero. * @param string $b The divisor, must not be zero.
* @param RoundingMode $roundingMode The rounding mode. * @param RoundingMode $roundingMode The rounding mode.
* *
* @throws \InvalidArgumentException If the rounding mode is invalid. * @throws RoundingNecessaryException If RoundingMode::Unnecessary is provided but rounding is necessary.
* @throws RoundingNecessaryException If RoundingMode::UNNECESSARY is provided but rounding is necessary.
* *
* @psalm-suppress ImpureFunctionCall * @pure
*/ */
final public function divRound(string $a, string $b, RoundingMode $roundingMode) : string final public function divRound(string $a, string $b, RoundingMode $roundingMode): string
{ {
[$quotient, $remainder] = $this->divQR($a, $b); [$quotient, $remainder] = $this->divQR($a, $b);
$hasDiscardedFraction = ($remainder !== '0'); $hasDiscardedFraction = ($remainder !== '0');
$isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-'); $isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-');
$discardedFractionSign = function() use ($remainder, $b) : int { $discardedFractionSign = function () use ($remainder, $b): int {
$r = $this->abs($this->mul($remainder, '2')); $r = $this->abs($this->mul($remainder, '2'));
$b = $this->abs($b); $b = $this->abs($b);
@@ -456,51 +434,57 @@ abstract class Calculator
$increment = false; $increment = false;
switch ($roundingMode) { switch ($roundingMode) {
case RoundingMode::UNNECESSARY: case RoundingMode::Unnecessary:
if ($hasDiscardedFraction) { if ($hasDiscardedFraction) {
throw RoundingNecessaryException::roundingNecessary(); throw RoundingNecessaryException::roundingNecessary();
} }
break; break;
case RoundingMode::UP: case RoundingMode::Up:
$increment = $hasDiscardedFraction; $increment = $hasDiscardedFraction;
break; break;
case RoundingMode::DOWN: case RoundingMode::Down:
break; break;
case RoundingMode::CEILING: case RoundingMode::Ceiling:
$increment = $hasDiscardedFraction && $isPositiveOrZero; $increment = $hasDiscardedFraction && $isPositiveOrZero;
break; break;
case RoundingMode::FLOOR: case RoundingMode::Floor:
$increment = $hasDiscardedFraction && ! $isPositiveOrZero; $increment = $hasDiscardedFraction && ! $isPositiveOrZero;
break; break;
case RoundingMode::HALF_UP: case RoundingMode::HalfUp:
$increment = $discardedFractionSign() >= 0; $increment = $discardedFractionSign() >= 0;
break; break;
case RoundingMode::HALF_DOWN: case RoundingMode::HalfDown:
$increment = $discardedFractionSign() > 0; $increment = $discardedFractionSign() > 0;
break; break;
case RoundingMode::HALF_CEILING: case RoundingMode::HalfCeiling:
$increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0; $increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0;
break; break;
case RoundingMode::HALF_FLOOR: case RoundingMode::HalfFloor:
$increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0; $increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
break; break;
case RoundingMode::HALF_EVEN: case RoundingMode::HalfEven:
$lastDigit = (int) $quotient[-1]; $lastDigit = (int) $quotient[-1];
$lastDigitIsEven = ($lastDigit % 2 === 0); $lastDigitIsEven = ($lastDigit % 2 === 0);
$increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0; $increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
break;
default: break;
throw new \InvalidArgumentException('Invalid rounding mode.');
} }
if ($increment) { if ($increment) {
@@ -515,8 +499,10 @@ abstract class Calculator
* *
* This method can be overridden by the concrete implementation if the underlying library * This method can be overridden by the concrete implementation if the underlying library
* has built-in support for bitwise operations. * has built-in support for bitwise operations.
*
* @pure
*/ */
public function and(string $a, string $b) : string public function and(string $a, string $b): string
{ {
return $this->bitwise('and', $a, $b); return $this->bitwise('and', $a, $b);
} }
@@ -526,8 +512,10 @@ abstract class Calculator
* *
* This method can be overridden by the concrete implementation if the underlying library * This method can be overridden by the concrete implementation if the underlying library
* has built-in support for bitwise operations. * has built-in support for bitwise operations.
*
* @pure
*/ */
public function or(string $a, string $b) : string public function or(string $a, string $b): string
{ {
return $this->bitwise('or', $a, $b); return $this->bitwise('or', $a, $b);
} }
@@ -537,33 +525,74 @@ abstract class Calculator
* *
* This method can be overridden by the concrete implementation if the underlying library * This method can be overridden by the concrete implementation if the underlying library
* has built-in support for bitwise operations. * has built-in support for bitwise operations.
*
* @pure
*/ */
public function xor(string $a, string $b) : string public function xor(string $a, string $b): string
{ {
return $this->bitwise('xor', $a, $b); return $this->bitwise('xor', $a, $b);
} }
/**
* Extracts the sign & digits of the operands.
*
* @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits.
*
* @pure
*/
final protected function init(string $a, string $b): array
{
return [
$aNeg = ($a[0] === '-'),
$bNeg = ($b[0] === '-'),
$aNeg ? substr($a, 1) : $a,
$bNeg ? substr($b, 1) : $b,
];
}
/**
* @return array{string, string, string} GCD, X, Y
*
* @pure
*/
private function gcdExtended(string $a, string $b): array
{
if ($a === '0') {
return [$b, '0', '1'];
}
[$gcd, $x1, $y1] = $this->gcdExtended($this->mod($b, $a), $a);
$x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1));
$y = $x1;
return [$gcd, $x, $y];
}
/** /**
* Performs a bitwise operation on a decimal number. * Performs a bitwise operation on a decimal number.
* *
* @param 'and'|'or'|'xor' $operator The operator to use. * @param 'and'|'or'|'xor' $operator The operator to use.
* @param string $a The left operand. * @param string $a The left operand.
* @param string $b The right operand. * @param string $b The right operand.
*
* @pure
*/ */
private function bitwise(string $operator, string $a, string $b) : string private function bitwise(string $operator, string $a, string $b): string
{ {
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b); [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
$aBin = $this->toBinary($aDig); $aBin = $this->toBinary($aDig);
$bBin = $this->toBinary($bDig); $bBin = $this->toBinary($bDig);
$aLen = \strlen($aBin); $aLen = strlen($aBin);
$bLen = \strlen($bBin); $bLen = strlen($bBin);
if ($aLen > $bLen) { if ($aLen > $bLen) {
$bBin = \str_repeat("\x00", $aLen - $bLen) . $bBin; $bBin = str_repeat("\x00", $aLen - $bLen) . $bBin;
} elseif ($bLen > $aLen) { } elseif ($bLen > $aLen) {
$aBin = \str_repeat("\x00", $bLen - $aLen) . $aBin; $aBin = str_repeat("\x00", $bLen - $aLen) . $aBin;
} }
if ($aNeg) { if ($aNeg) {
@@ -596,18 +625,21 @@ abstract class Calculator
/** /**
* @param string $number A positive, binary number. * @param string $number A positive, binary number.
*
* @pure
*/ */
private function twosComplement(string $number) : string private function twosComplement(string $number): string
{ {
$xor = \str_repeat("\xff", \strlen($number)); $xor = str_repeat("\xff", strlen($number));
$number ^= $xor; $number ^= $xor;
for ($i = \strlen($number) - 1; $i >= 0; $i--) { for ($i = strlen($number) - 1; $i >= 0; $i--) {
$byte = \ord($number[$i]); $byte = ord($number[$i]);
if (++$byte !== 256) { if (++$byte !== 256) {
$number[$i] = \chr($byte); $number[$i] = chr($byte);
break; break;
} }
@@ -625,36 +657,40 @@ abstract class Calculator
* Converts a decimal number to a binary string. * Converts a decimal number to a binary string.
* *
* @param string $number The number to convert, positive or zero, only digits. * @param string $number The number to convert, positive or zero, only digits.
*
* @pure
*/ */
private function toBinary(string $number) : string private function toBinary(string $number): string
{ {
$result = ''; $result = '';
while ($number !== '0') { while ($number !== '0') {
[$number, $remainder] = $this->divQR($number, '256'); [$number, $remainder] = $this->divQR($number, '256');
$result .= \chr((int) $remainder); $result .= chr((int) $remainder);
} }
return \strrev($result); return strrev($result);
} }
/** /**
* Returns the positive decimal representation of a binary number. * Returns the positive decimal representation of a binary number.
* *
* @param string $bytes The bytes representing the number. * @param string $bytes The bytes representing the number.
*
* @pure
*/ */
private function toDecimal(string $bytes) : string private function toDecimal(string $bytes): string
{ {
$result = '0'; $result = '0';
$power = '1'; $power = '1';
for ($i = \strlen($bytes) - 1; $i >= 0; $i--) { for ($i = strlen($bytes) - 1; $i >= 0; $i--) {
$index = \ord($bytes[$i]); $index = ord($bytes[$i]);
if ($index !== 0) { if ($index !== 0) {
$result = $this->add($result, ($index === 1) $result = $this->add(
? $power $result,
: $this->mul($power, (string) $index) ($index === 1) ? $power : $this->mul($power, (string) $index),
); );
} }
@@ -7,69 +7,79 @@ namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator; use Brick\Math\Internal\Calculator;
use Override; use Override;
use function bcadd;
use function bcdiv;
use function bcmod;
use function bcmul;
use function bcpow;
use function bcpowmod;
use function bcsqrt;
use function bcsub;
/** /**
* Calculator implementation built around the bcmath library. * Calculator implementation built around the bcmath library.
* *
* @internal * @internal
*
* @psalm-immutable
*/ */
class BcMathCalculator extends Calculator final readonly class BcMathCalculator extends Calculator
{ {
#[Override] #[Override]
public function add(string $a, string $b) : string public function add(string $a, string $b): string
{ {
return \bcadd($a, $b, 0); return bcadd($a, $b, 0);
} }
#[Override] #[Override]
public function sub(string $a, string $b) : string public function sub(string $a, string $b): string
{ {
return \bcsub($a, $b, 0); return bcsub($a, $b, 0);
} }
#[Override] #[Override]
public function mul(string $a, string $b) : string public function mul(string $a, string $b): string
{ {
return \bcmul($a, $b, 0); return bcmul($a, $b, 0);
} }
#[Override] #[Override]
public function divQ(string $a, string $b) : string public function divQ(string $a, string $b): string
{ {
return \bcdiv($a, $b, 0); return bcdiv($a, $b, 0);
} }
#[Override] #[Override]
public function divR(string $a, string $b) : string public function divR(string $a, string $b): string
{ {
return \bcmod($a, $b, 0); return bcmod($a, $b, 0);
} }
#[Override] #[Override]
public function divQR(string $a, string $b) : array public function divQR(string $a, string $b): array
{ {
$q = \bcdiv($a, $b, 0); $q = bcdiv($a, $b, 0);
$r = \bcmod($a, $b, 0); $r = bcmod($a, $b, 0);
return [$q, $r]; return [$q, $r];
} }
#[Override] #[Override]
public function pow(string $a, int $e) : string public function pow(string $a, int $e): string
{ {
return \bcpow($a, (string) $e, 0); return bcpow($a, (string) $e, 0);
} }
#[Override] #[Override]
public function modPow(string $base, string $exp, string $mod) : string public function modPow(string $base, string $exp, string $mod): string
{ {
return \bcpowmod($base, $exp, $mod, 0); // normalize to Euclidean representative so modPow() stays consistent with mod()
$base = $this->mod($base, $mod);
return bcpowmod($base, $exp, $mod, 0);
} }
#[Override] #[Override]
public function sqrt(string $n) : string public function sqrt(string $n): string
{ {
return \bcsqrt($n, 0); return bcsqrt($n, 0);
} }
} }
+65 -38
View File
@@ -5,121 +5,148 @@ declare(strict_types=1);
namespace Brick\Math\Internal\Calculator; namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator; use Brick\Math\Internal\Calculator;
use GMP;
use Override; use Override;
use function gmp_add;
use function gmp_and;
use function gmp_div_q;
use function gmp_div_qr;
use function gmp_div_r;
use function gmp_gcd;
use function gmp_init;
use function gmp_invert;
use function gmp_lcm;
use function gmp_mul;
use function gmp_or;
use function gmp_pow;
use function gmp_powm;
use function gmp_sqrt;
use function gmp_strval;
use function gmp_sub;
use function gmp_xor;
/** /**
* Calculator implementation built around the GMP library. * Calculator implementation built around the GMP library.
* *
* @internal * @internal
*
* @psalm-immutable
*/ */
class GmpCalculator extends Calculator final readonly class GmpCalculator extends Calculator
{ {
#[Override] #[Override]
public function add(string $a, string $b) : string public function add(string $a, string $b): string
{ {
return \gmp_strval(\gmp_add($a, $b)); return gmp_strval(gmp_add($a, $b));
} }
#[Override] #[Override]
public function sub(string $a, string $b) : string public function sub(string $a, string $b): string
{ {
return \gmp_strval(\gmp_sub($a, $b)); return gmp_strval(gmp_sub($a, $b));
} }
#[Override] #[Override]
public function mul(string $a, string $b) : string public function mul(string $a, string $b): string
{ {
return \gmp_strval(\gmp_mul($a, $b)); return gmp_strval(gmp_mul($a, $b));
} }
#[Override] #[Override]
public function divQ(string $a, string $b) : string public function divQ(string $a, string $b): string
{ {
return \gmp_strval(\gmp_div_q($a, $b)); return gmp_strval(gmp_div_q($a, $b));
} }
#[Override] #[Override]
public function divR(string $a, string $b) : string public function divR(string $a, string $b): string
{ {
return \gmp_strval(\gmp_div_r($a, $b)); return gmp_strval(gmp_div_r($a, $b));
} }
#[Override] #[Override]
public function divQR(string $a, string $b) : array public function divQR(string $a, string $b): array
{ {
[$q, $r] = \gmp_div_qr($a, $b); [$q, $r] = gmp_div_qr($a, $b);
/**
* @var GMP $q
* @var GMP $r
*/
return [ return [
\gmp_strval($q), gmp_strval($q),
\gmp_strval($r) gmp_strval($r),
]; ];
} }
#[Override] #[Override]
public function pow(string $a, int $e) : string public function pow(string $a, int $e): string
{ {
return \gmp_strval(\gmp_pow($a, $e)); return gmp_strval(gmp_pow($a, $e));
} }
#[Override] #[Override]
public function modInverse(string $x, string $m) : ?string public function modInverse(string $x, string $m): ?string
{ {
$result = \gmp_invert($x, $m); $result = gmp_invert($x, $m);
if ($result === false) { if ($result === false) {
return null; return null;
} }
return \gmp_strval($result); return gmp_strval($result);
} }
#[Override] #[Override]
public function modPow(string $base, string $exp, string $mod) : string public function modPow(string $base, string $exp, string $mod): string
{ {
return \gmp_strval(\gmp_powm($base, $exp, $mod)); return gmp_strval(gmp_powm($base, $exp, $mod));
} }
#[Override] #[Override]
public function gcd(string $a, string $b) : string public function gcd(string $a, string $b): string
{ {
return \gmp_strval(\gmp_gcd($a, $b)); return gmp_strval(gmp_gcd($a, $b));
} }
#[Override] #[Override]
public function fromBase(string $number, int $base) : string public function lcm(string $a, string $b): string
{ {
return \gmp_strval(\gmp_init($number, $base)); return gmp_strval(gmp_lcm($a, $b));
} }
#[Override] #[Override]
public function toBase(string $number, int $base) : string public function fromBase(string $number, int $base): string
{ {
return \gmp_strval($number, $base); return gmp_strval(gmp_init($number, $base));
} }
#[Override] #[Override]
public function and(string $a, string $b) : string public function toBase(string $number, int $base): string
{ {
return \gmp_strval(\gmp_and($a, $b)); return gmp_strval($number, $base);
} }
#[Override] #[Override]
public function or(string $a, string $b) : string public function and(string $a, string $b): string
{ {
return \gmp_strval(\gmp_or($a, $b)); return gmp_strval(gmp_and($a, $b));
} }
#[Override] #[Override]
public function xor(string $a, string $b) : string public function or(string $a, string $b): string
{ {
return \gmp_strval(\gmp_xor($a, $b)); return gmp_strval(gmp_or($a, $b));
} }
#[Override] #[Override]
public function sqrt(string $n) : string public function xor(string $a, string $b): string
{ {
return \gmp_strval(\gmp_sqrt($n)); return gmp_strval(gmp_xor($a, $b));
}
#[Override]
public function sqrt(string $n): string
{
return gmp_strval(gmp_sqrt($n));
} }
} }
+102 -84
View File
@@ -7,14 +7,26 @@ namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator; use Brick\Math\Internal\Calculator;
use Override; use Override;
use function assert;
use function in_array;
use function intdiv;
use function is_int;
use function ltrim;
use function str_pad;
use function str_repeat;
use function strcmp;
use function strlen;
use function substr;
use const PHP_INT_SIZE;
use const STR_PAD_LEFT;
/** /**
* Calculator implementation using only native PHP code. * Calculator implementation using only native PHP code.
* *
* @internal * @internal
*
* @psalm-immutable
*/ */
class NativeCalculator extends Calculator final readonly class NativeCalculator extends Calculator
{ {
/** /**
* The max number of digits the platform can natively add, subtract, multiply or divide without overflow. * The max number of digits the platform can natively add, subtract, multiply or divide without overflow.
@@ -24,9 +36,11 @@ class NativeCalculator extends Calculator
* Example: 32-bit: max number 1,999,999,999 (9 digits + carry) * Example: 32-bit: max number 1,999,999,999 (9 digits + carry)
* 64-bit: max number 1,999,999,999,999,999,999 (18 digits + carry) * 64-bit: max number 1,999,999,999,999,999,999 (18 digits + carry)
*/ */
private readonly int $maxDigits; private int $maxDigits;
/** /**
* @pure
*
* @codeCoverageIgnore * @codeCoverageIgnore
*/ */
public function __construct() public function __construct()
@@ -34,16 +48,15 @@ class NativeCalculator extends Calculator
$this->maxDigits = match (PHP_INT_SIZE) { $this->maxDigits = match (PHP_INT_SIZE) {
4 => 9, 4 => 9,
8 => 18, 8 => 18,
default => throw new \RuntimeException('The platform is not 32-bit or 64-bit as expected.')
}; };
} }
#[Override] #[Override]
public function add(string $a, string $b) : string public function add(string $a, string $b): string
{ {
/** /**
* @psalm-var numeric-string $a * @var numeric-string $a
* @psalm-var numeric-string $b * @var numeric-string $b
*/ */
$result = $a + $b; $result = $a + $b;
@@ -71,17 +84,17 @@ class NativeCalculator extends Calculator
} }
#[Override] #[Override]
public function sub(string $a, string $b) : string public function sub(string $a, string $b): string
{ {
return $this->add($a, $this->neg($b)); return $this->add($a, $this->neg($b));
} }
#[Override] #[Override]
public function mul(string $a, string $b) : string public function mul(string $a, string $b): string
{ {
/** /**
* @psalm-var numeric-string $a * @var numeric-string $a
* @psalm-var numeric-string $b * @var numeric-string $b
*/ */
$result = $a * $b; $result = $a * $b;
@@ -121,7 +134,7 @@ class NativeCalculator extends Calculator
} }
#[Override] #[Override]
public function divQ(string $a, string $b) : string public function divQ(string $a, string $b): string
{ {
return $this->divQR($a, $b)[0]; return $this->divQR($a, $b)[0];
} }
@@ -133,7 +146,7 @@ class NativeCalculator extends Calculator
} }
#[Override] #[Override]
public function divQR(string $a, string $b) : array public function divQR(string $a, string $b): array
{ {
if ($a === '0') { if ($a === '0') {
return ['0', '0']; return ['0', '0'];
@@ -151,11 +164,11 @@ class NativeCalculator extends Calculator
return [$this->neg($a), '0']; return [$this->neg($a), '0'];
} }
/** @psalm-var numeric-string $a */ /** @var numeric-string $a */
$na = $a * 1; // cast to number $na = $a * 1; // cast to number
if (is_int($na)) { if (is_int($na)) {
/** @psalm-var numeric-string $b */ /** @var numeric-string $b */
$nb = $b * 1; $nb = $b * 1;
if (is_int($nb)) { if (is_int($nb)) {
@@ -166,7 +179,7 @@ class NativeCalculator extends Calculator
return [ return [
(string) $q, (string) $q,
(string) $r (string) $r,
]; ];
} }
} }
@@ -187,7 +200,7 @@ class NativeCalculator extends Calculator
} }
#[Override] #[Override]
public function pow(string $a, int $e) : string public function pow(string $a, int $e): string
{ {
if ($e === 0) { if ($e === 0) {
return '1'; return '1';
@@ -202,7 +215,6 @@ class NativeCalculator extends Calculator
$aa = $this->mul($a, $a); $aa = $this->mul($a, $a);
/** @psalm-suppress PossiblyInvalidArgument We're sure that $e / 2 is an int now */
$result = $this->pow($aa, $e / 2); $result = $this->pow($aa, $e / 2);
if ($odd === 1) { if ($odd === 1) {
@@ -213,15 +225,13 @@ class NativeCalculator extends Calculator
} }
/** /**
* Algorithm from: https://www.geeksforgeeks.org/modular-exponentiation-power-in-modular-arithmetic/ * Algorithm from: https://www.geeksforgeeks.org/modular-exponentiation-power-in-modular-arithmetic/.
*/ */
#[Override] #[Override]
public function modPow(string $base, string $exp, string $mod) : string public function modPow(string $base, string $exp, string $mod): string
{ {
// special case: the algorithm below fails with 0 power 0 mod 1 (returns 1 instead of 0) // normalize to Euclidean representative so modPow() stays consistent with mod()
if ($base === '0' && $exp === '0' && $mod === '1') { $base = $this->mod($base, $mod);
return '0';
}
// special case: the algorithm below fails with power 0 mod 1 (returns 1 instead of 0) // special case: the algorithm below fails with power 0 mod 1 (returns 1 instead of 0)
if ($exp === '0' && $mod === '1') { if ($exp === '0' && $mod === '1') {
@@ -248,21 +258,21 @@ class NativeCalculator extends Calculator
} }
/** /**
* Adapted from https://cp-algorithms.com/num_methods/roots_newton.html * Adapted from https://cp-algorithms.com/num_methods/roots_newton.html.
*/ */
#[Override] #[Override]
public function sqrt(string $n) : string public function sqrt(string $n): string
{ {
if ($n === '0') { if ($n === '0') {
return '0'; return '0';
} }
// initial approximation // initial approximation
$x = \str_repeat('9', \intdiv(\strlen($n), 2) ?: 1); $x = str_repeat('9', intdiv(strlen($n), 2) ?: 1);
$decreased = false; $decreased = false;
for (;;) { for (; ;) {
$nx = $this->divQ($this->add($x, $this->divQ($n, $x)), '2'); $nx = $this->divQ($this->add($x, $this->divQ($n, $x)), '2');
if ($x === $nx || $this->cmp($nx, $x) > 0 && $decreased) { if ($x === $nx || $this->cmp($nx, $x) > 0 && $decreased) {
@@ -278,38 +288,39 @@ class NativeCalculator extends Calculator
/** /**
* Performs the addition of two non-signed large integers. * Performs the addition of two non-signed large integers.
*
* @pure
*/ */
private function doAdd(string $a, string $b) : string private function doAdd(string $a, string $b): string
{ {
[$a, $b, $length] = $this->pad($a, $b); [$a, $b, $length] = $this->pad($a, $b);
$carry = 0; $carry = 0;
$result = ''; $result = '';
for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) { for ($i = $length - $this->maxDigits; ; $i -= $this->maxDigits) {
$blockLength = $this->maxDigits; $blockLength = $this->maxDigits;
if ($i < 0) { if ($i < 0) {
$blockLength += $i; $blockLength += $i;
/** @psalm-suppress LoopInvalidation */
$i = 0; $i = 0;
} }
/** @psalm-var numeric-string $blockA */ /** @var numeric-string $blockA */
$blockA = \substr($a, $i, $blockLength); $blockA = substr($a, $i, $blockLength);
/** @psalm-var numeric-string $blockB */ /** @var numeric-string $blockB */
$blockB = \substr($b, $i, $blockLength); $blockB = substr($b, $i, $blockLength);
$sum = (string) ($blockA + $blockB + $carry); $sum = (string) ($blockA + $blockB + $carry);
$sumLength = \strlen($sum); $sumLength = strlen($sum);
if ($sumLength > $blockLength) { if ($sumLength > $blockLength) {
$sum = \substr($sum, 1); $sum = substr($sum, 1);
$carry = 1; $carry = 1;
} else { } else {
if ($sumLength < $blockLength) { if ($sumLength < $blockLength) {
$sum = \str_repeat('0', $blockLength - $sumLength) . $sum; $sum = str_repeat('0', $blockLength - $sumLength) . $sum;
} }
$carry = 0; $carry = 0;
} }
@@ -330,8 +341,10 @@ class NativeCalculator extends Calculator
/** /**
* Performs the subtraction of two non-signed large integers. * Performs the subtraction of two non-signed large integers.
*
* @pure
*/ */
private function doSub(string $a, string $b) : string private function doSub(string $a, string $b): string
{ {
if ($a === $b) { if ($a === $b) {
return '0'; return '0';
@@ -355,20 +368,19 @@ class NativeCalculator extends Calculator
$complement = 10 ** $this->maxDigits; $complement = 10 ** $this->maxDigits;
for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) { for ($i = $length - $this->maxDigits; ; $i -= $this->maxDigits) {
$blockLength = $this->maxDigits; $blockLength = $this->maxDigits;
if ($i < 0) { if ($i < 0) {
$blockLength += $i; $blockLength += $i;
/** @psalm-suppress LoopInvalidation */
$i = 0; $i = 0;
} }
/** @psalm-var numeric-string $blockA */ /** @var numeric-string $blockA */
$blockA = \substr($a, $i, $blockLength); $blockA = substr($a, $i, $blockLength);
/** @psalm-var numeric-string $blockB */ /** @var numeric-string $blockB */
$blockB = \substr($b, $i, $blockLength); $blockB = substr($b, $i, $blockLength);
$sum = $blockA - $blockB - $carry; $sum = $blockA - $blockB - $carry;
@@ -380,10 +392,10 @@ class NativeCalculator extends Calculator
} }
$sum = (string) $sum; $sum = (string) $sum;
$sumLength = \strlen($sum); $sumLength = strlen($sum);
if ($sumLength < $blockLength) { if ($sumLength < $blockLength) {
$sum = \str_repeat('0', $blockLength - $sumLength) . $sum; $sum = str_repeat('0', $blockLength - $sumLength) . $sum;
} }
$result = $sum . $result; $result = $sum . $result;
@@ -396,7 +408,7 @@ class NativeCalculator extends Calculator
// Carry cannot be 1 when the loop ends, as a > b // Carry cannot be 1 when the loop ends, as a > b
assert($carry === 0); assert($carry === 0);
$result = \ltrim($result, '0'); $result = ltrim($result, '0');
if ($invert) { if ($invert) {
$result = $this->neg($result); $result = $this->neg($result);
@@ -407,48 +419,48 @@ class NativeCalculator extends Calculator
/** /**
* Performs the multiplication of two non-signed large integers. * Performs the multiplication of two non-signed large integers.
*
* @pure
*/ */
private function doMul(string $a, string $b) : string private function doMul(string $a, string $b): string
{ {
$x = \strlen($a); $x = strlen($a);
$y = \strlen($b); $y = strlen($b);
$maxDigits = \intdiv($this->maxDigits, 2); $maxDigits = intdiv($this->maxDigits, 2);
$complement = 10 ** $maxDigits; $complement = 10 ** $maxDigits;
$result = '0'; $result = '0';
for ($i = $x - $maxDigits;; $i -= $maxDigits) { for ($i = $x - $maxDigits; ; $i -= $maxDigits) {
$blockALength = $maxDigits; $blockALength = $maxDigits;
if ($i < 0) { if ($i < 0) {
$blockALength += $i; $blockALength += $i;
/** @psalm-suppress LoopInvalidation */
$i = 0; $i = 0;
} }
$blockA = (int) \substr($a, $i, $blockALength); $blockA = (int) substr($a, $i, $blockALength);
$line = ''; $line = '';
$carry = 0; $carry = 0;
for ($j = $y - $maxDigits;; $j -= $maxDigits) { for ($j = $y - $maxDigits; ; $j -= $maxDigits) {
$blockBLength = $maxDigits; $blockBLength = $maxDigits;
if ($j < 0) { if ($j < 0) {
$blockBLength += $j; $blockBLength += $j;
/** @psalm-suppress LoopInvalidation */
$j = 0; $j = 0;
} }
$blockB = (int) \substr($b, $j, $blockBLength); $blockB = (int) substr($b, $j, $blockBLength);
$mul = $blockA * $blockB + $carry; $mul = $blockA * $blockB + $carry;
$value = $mul % $complement; $value = $mul % $complement;
$carry = ($mul - $value) / $complement; $carry = ($mul - $value) / $complement;
$value = (string) $value; $value = (string) $value;
$value = \str_pad($value, $maxDigits, '0', STR_PAD_LEFT); $value = str_pad($value, $maxDigits, '0', STR_PAD_LEFT);
$line = $value . $line; $line = $value . $line;
@@ -461,10 +473,10 @@ class NativeCalculator extends Calculator
$line = $carry . $line; $line = $carry . $line;
} }
$line = \ltrim($line, '0'); $line = ltrim($line, '0');
if ($line !== '') { if ($line !== '') {
$line .= \str_repeat('0', $x - $blockALength - $i); $line .= str_repeat('0', $x - $blockALength - $i);
$result = $this->add($result, $line); $result = $this->add($result, $line);
} }
@@ -480,8 +492,10 @@ class NativeCalculator extends Calculator
* Performs the division of two non-signed large integers. * Performs the division of two non-signed large integers.
* *
* @return string[] The quotient and remainder. * @return string[] The quotient and remainder.
*
* @pure
*/ */
private function doDiv(string $a, string $b) : array private function doDiv(string $a, string $b): array
{ {
$cmp = $this->doCmp($a, $b); $cmp = $this->doCmp($a, $b);
@@ -489,8 +503,8 @@ class NativeCalculator extends Calculator
return ['0', $a]; return ['0', $a];
} }
$x = \strlen($a); $x = strlen($a);
$y = \strlen($b); $y = strlen($b);
// we now know that a >= b && x >= y // we now know that a >= b && x >= y
@@ -498,24 +512,24 @@ class NativeCalculator extends Calculator
$r = $a; // remainder $r = $a; // remainder
$z = $y; // focus length, always $y or $y+1 $z = $y; // focus length, always $y or $y+1
/** @psalm-var numeric-string $b */ /** @var numeric-string $b */
$nb = $b * 1; // cast to number $nb = $b * 1; // cast to number
// performance optimization in cases where the remainder will never cause int overflow // performance optimization in cases where the remainder will never cause int overflow
if (is_int(($nb - 1) * 10 + 9)) { if (is_int(($nb - 1) * 10 + 9)) {
$r = (int) \substr($a, 0, $z - 1); $r = (int) substr($a, 0, $z - 1);
for ($i = $z - 1; $i < $x; $i++) { for ($i = $z - 1; $i < $x; $i++) {
$n = $r * 10 + (int) $a[$i]; $n = $r * 10 + (int) $a[$i];
/** @psalm-var int $nb */ /** @var int $nb */
$q .= \intdiv($n, $nb); $q .= intdiv($n, $nb);
$r = $n % $nb; $r = $n % $nb;
} }
return [\ltrim($q, '0') ?: '0', (string) $r]; return [ltrim($q, '0') ?: '0', (string) $r];
} }
for (;;) { for (; ;) {
$focus = \substr($a, 0, $z); $focus = substr($a, 0, $z);
$cmp = $this->doCmp($focus, $b); $cmp = $this->doCmp($focus, $b);
@@ -527,7 +541,7 @@ class NativeCalculator extends Calculator
$z++; $z++;
} }
$zeros = \str_repeat('0', $x - $z); $zeros = str_repeat('0', $x - $z);
$q = $this->add($q, '1' . $zeros); $q = $this->add($q, '1' . $zeros);
$a = $this->sub($a, $b . $zeros); $a = $this->sub($a, $b . $zeros);
@@ -538,7 +552,7 @@ class NativeCalculator extends Calculator
break; break;
} }
$x = \strlen($a); $x = strlen($a);
if ($x < $y) { // remainder < dividend if ($x < $y) { // remainder < dividend
break; break;
@@ -553,12 +567,14 @@ class NativeCalculator extends Calculator
/** /**
* Compares two non-signed large numbers. * Compares two non-signed large numbers.
* *
* @psalm-return -1|0|1 * @return -1|0|1
*
* @pure
*/ */
private function doCmp(string $a, string $b) : int private function doCmp(string $a, string $b): int
{ {
$x = \strlen($a); $x = strlen($a);
$y = \strlen($b); $y = strlen($b);
$cmp = $x <=> $y; $cmp = $x <=> $y;
@@ -566,7 +582,7 @@ class NativeCalculator extends Calculator
return $cmp; return $cmp;
} }
return \strcmp($a, $b) <=> 0; // enforce -1|0|1 return strcmp($a, $b) <=> 0; // enforce -1|0|1
} }
/** /**
@@ -575,20 +591,22 @@ class NativeCalculator extends Calculator
* The numbers must only consist of digits, without leading minus sign. * The numbers must only consist of digits, without leading minus sign.
* *
* @return array{string, string, int} * @return array{string, string, int}
*
* @pure
*/ */
private function pad(string $a, string $b) : array private function pad(string $a, string $b): array
{ {
$x = \strlen($a); $x = strlen($a);
$y = \strlen($b); $y = strlen($b);
if ($x > $y) { if ($x > $y) {
$b = \str_repeat('0', $x - $y) . $b; $b = str_repeat('0', $x - $y) . $b;
return [$a, $b, $x]; return [$a, $b, $x];
} }
if ($x < $y) { if ($x < $y) {
$a = \str_repeat('0', $y - $x) . $a; $a = str_repeat('0', $y - $x) . $a;
return [$a, $b, $y]; return [$a, $b, $y];
} }
+74
View File
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal;
use function extension_loaded;
/**
* Stores the current Calculator instance used by BigNumber classes.
*
* @internal
*/
final class CalculatorRegistry
{
/**
* The Calculator instance in use.
*/
private static ?Calculator $instance = null;
/**
* Sets the Calculator instance to use.
*
* An instance is typically set only in unit tests: autodetect is usually the best option.
*
* @param Calculator|null $calculator The calculator instance, or null to revert to autodetect.
*/
final public static function set(?Calculator $calculator): void
{
self::$instance = $calculator;
}
/**
* Returns the Calculator instance to use.
*
* If none has been explicitly set, the fastest available implementation will be returned.
*
* Note: even though this method is not technically pure, it is considered pure when used in a normal context, when
* only relying on autodetect.
*
* @pure
*/
final public static function get(): Calculator
{
/** @phpstan-ignore impure.staticPropertyAccess */
if (self::$instance === null) {
/** @phpstan-ignore impure.propertyAssign */
self::$instance = self::detect();
}
/** @phpstan-ignore impure.staticPropertyAccess */
return self::$instance;
}
/**
* Returns the fastest available Calculator implementation.
*
* @pure
*
* @codeCoverageIgnore
*/
private static function detect(): Calculator
{
if (extension_loaded('gmp')) {
return new Calculator\GmpCalculator();
}
if (extension_loaded('bcmath')) {
return new Calculator\BcMathCalculator();
}
return new Calculator\NativeCalculator();
}
}
+70 -25
View File
@@ -5,13 +5,8 @@ declare(strict_types=1);
namespace Brick\Math; namespace Brick\Math;
/** /**
* Specifies a rounding behavior for numerical operations capable of discarding precision. * Specifies rounding behavior by defining how discarded digits affect the returned result when an exact value cannot
* * be represented at the requested scale.
* Each rounding mode indicates how the least significant returned digit of a rounded result
* is to be calculated. If fewer digits are returned than the digits needed to represent the
* exact numerical result, the discarded digits will be referred to as the discarded fraction
* regardless the digits' contribution to the value of the number. In other words, considered
* as a numerical value, the discarded fraction could have an absolute value greater than one.
*/ */
enum RoundingMode enum RoundingMode
{ {
@@ -21,7 +16,7 @@ enum RoundingMode
* If this rounding mode is specified on an operation that yields a result that * If this rounding mode is specified on an operation that yields a result that
* cannot be represented at the requested scale, a RoundingNecessaryException is thrown. * cannot be represented at the requested scale, a RoundingNecessaryException is thrown.
*/ */
case UNNECESSARY; case Unnecessary;
/** /**
* Rounds away from zero. * Rounds away from zero.
@@ -29,7 +24,7 @@ enum RoundingMode
* Always increments the digit prior to a nonzero discarded fraction. * Always increments the digit prior to a nonzero discarded fraction.
* Note that this rounding mode never decreases the magnitude of the calculated value. * Note that this rounding mode never decreases the magnitude of the calculated value.
*/ */
case UP; case Up;
/** /**
* Rounds towards zero. * Rounds towards zero.
@@ -37,62 +32,112 @@ enum RoundingMode
* Never increments the digit prior to a discarded fraction (i.e., truncates). * Never increments the digit prior to a discarded fraction (i.e., truncates).
* Note that this rounding mode never increases the magnitude of the calculated value. * Note that this rounding mode never increases the magnitude of the calculated value.
*/ */
case DOWN; case Down;
/** /**
* Rounds towards positive infinity. * Rounds towards positive infinity.
* *
* If the result is positive, behaves as for UP; if negative, behaves as for DOWN. * If the result is positive, behaves as for Up; if negative, behaves as for Down.
* Note that this rounding mode never decreases the calculated value. * Note that this rounding mode never decreases the calculated value.
*/ */
case CEILING; case Ceiling;
/** /**
* Rounds towards negative infinity. * Rounds towards negative infinity.
* *
* If the result is positive, behave as for DOWN; if negative, behave as for UP. * If the result is positive, behaves as for Down; if negative, behaves as for Up.
* Note that this rounding mode never increases the calculated value. * Note that this rounding mode never increases the calculated value.
*/ */
case FLOOR; case Floor;
/** /**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round up. * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round up.
* *
* Behaves as for UP if the discarded fraction is >= 0.5; otherwise, behaves as for DOWN. * Behaves as for Up if the discarded fraction is >= 0.5; otherwise, behaves as for Down.
* Note that this is the rounding mode commonly taught at school. * Note that this is the rounding mode commonly taught at school.
*/ */
case HALF_UP; case HalfUp;
/** /**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round down. * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round down.
* *
* Behaves as for UP if the discarded fraction is > 0.5; otherwise, behaves as for DOWN. * Behaves as for Up if the discarded fraction is > 0.5; otherwise, behaves as for Down.
*/ */
case HALF_DOWN; case HalfDown;
/** /**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards positive infinity. * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards positive infinity.
* *
* If the result is positive, behaves as for HALF_UP; if negative, behaves as for HALF_DOWN. * If the result is positive, behaves as for HalfUp; if negative, behaves as for HalfDown.
*/ */
case HALF_CEILING; case HalfCeiling;
/** /**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards negative infinity. * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards negative infinity.
* *
* If the result is positive, behaves as for HALF_DOWN; if negative, behaves as for HALF_UP. * If the result is positive, behaves as for HalfDown; if negative, behaves as for HalfUp.
*/ */
case HALF_FLOOR; case HalfFloor;
/** /**
* Rounds towards the "nearest neighbor" unless both neighbors are equidistant, in which case rounds towards the even neighbor. * Rounds towards the "nearest neighbor" unless both neighbors are equidistant, in which case rounds towards the even neighbor.
* *
* Behaves as for HALF_UP if the digit to the left of the discarded fraction is odd; * Behaves as for HalfUp if the digit to the left of the discarded fraction is odd;
* behaves as for HALF_DOWN if it's even. * behaves as for HalfDown if it's even.
* *
* Note that this is the rounding mode that statistically minimizes * Note that this is the rounding mode that statistically minimizes
* cumulative error when applied repeatedly over a sequence of calculations. * cumulative error when applied repeatedly over a sequence of calculations.
* It is sometimes known as "Banker's rounding", and is chiefly used in the USA. * It is sometimes known as "Banker's rounding", and is chiefly used in the USA.
*/ */
case HALF_EVEN; case HalfEven;
/**
* @deprecated Use RoundingMode::Unnecessary instead.
*/
public const UNNECESSARY = self::Unnecessary;
/**
* @deprecated Use RoundingMode::Up instead.
*/
public const UP = self::Up;
/**
* @deprecated Use RoundingMode::Down instead.
*/
public const DOWN = self::Down;
/**
* @deprecated Use RoundingMode::Ceiling instead.
*/
public const CEILING = self::Ceiling;
/**
* @deprecated Use RoundingMode::Floor instead.
*/
public const FLOOR = self::Floor;
/**
* @deprecated Use RoundingMode::HalfUp instead.
*/
public const HALF_UP = self::HalfUp;
/**
* @deprecated Use RoundingMode::HalfDown instead.
*/
public const HALF_DOWN = self::HalfDown;
/**
* @deprecated Use RoundingMode::HalfCeiling instead.
*/
public const HALF_CEILING = self::HalfCeiling;
/**
* @deprecated Use RoundingMode::HalfFloor instead.
*/
public const HALF_FLOOR = self::HalfFloor;
/**
* @deprecated Use RoundingMode::HalfEven instead.
*/
public const HALF_EVEN = self::HalfEven;
} }
+58 -51
View File
@@ -42,35 +42,37 @@ namespace Composer\Autoload;
*/ */
class ClassLoader class ClassLoader
{ {
/** @var ?string */ /** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir; private $vendorDir;
// PSR-4 // PSR-4
/** /**
* @var array[] * @var array<string, array<string, int>>
* @psalm-var array<string, array<string, int>>
*/ */
private $prefixLengthsPsr4 = array(); private $prefixLengthsPsr4 = array();
/** /**
* @var array[] * @var array<string, list<string>>
* @psalm-var array<string, array<int, string>>
*/ */
private $prefixDirsPsr4 = array(); private $prefixDirsPsr4 = array();
/** /**
* @var array[] * @var list<string>
* @psalm-var array<string, string>
*/ */
private $fallbackDirsPsr4 = array(); private $fallbackDirsPsr4 = array();
// PSR-0 // PSR-0
/** /**
* @var array[] * List of PSR-0 prefixes
* @psalm-var array<string, array<string, string[]>> *
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/ */
private $prefixesPsr0 = array(); private $prefixesPsr0 = array();
/** /**
* @var array[] * @var list<string>
* @psalm-var array<string, string>
*/ */
private $fallbackDirsPsr0 = array(); private $fallbackDirsPsr0 = array();
@@ -78,8 +80,7 @@ class ClassLoader
private $useIncludePath = false; private $useIncludePath = false;
/** /**
* @var string[] * @var array<string, string>
* @psalm-var array<string, string>
*/ */
private $classMap = array(); private $classMap = array();
@@ -87,29 +88,29 @@ class ClassLoader
private $classMapAuthoritative = false; private $classMapAuthoritative = false;
/** /**
* @var bool[] * @var array<string, bool>
* @psalm-var array<string, bool>
*/ */
private $missingClasses = array(); private $missingClasses = array();
/** @var ?string */ /** @var string|null */
private $apcuPrefix; private $apcuPrefix;
/** /**
* @var self[] * @var array<string, self>
*/ */
private static $registeredLoaders = array(); private static $registeredLoaders = array();
/** /**
* @param ?string $vendorDir * @param string|null $vendorDir
*/ */
public function __construct($vendorDir = null) public function __construct($vendorDir = null)
{ {
$this->vendorDir = $vendorDir; $this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
} }
/** /**
* @return string[] * @return array<string, list<string>>
*/ */
public function getPrefixes() public function getPrefixes()
{ {
@@ -121,8 +122,7 @@ class ClassLoader
} }
/** /**
* @return array[] * @return array<string, list<string>>
* @psalm-return array<string, array<int, string>>
*/ */
public function getPrefixesPsr4() public function getPrefixesPsr4()
{ {
@@ -130,8 +130,7 @@ class ClassLoader
} }
/** /**
* @return array[] * @return list<string>
* @psalm-return array<string, string>
*/ */
public function getFallbackDirs() public function getFallbackDirs()
{ {
@@ -139,8 +138,7 @@ class ClassLoader
} }
/** /**
* @return array[] * @return list<string>
* @psalm-return array<string, string>
*/ */
public function getFallbackDirsPsr4() public function getFallbackDirsPsr4()
{ {
@@ -148,8 +146,7 @@ class ClassLoader
} }
/** /**
* @return string[] Array of classname => path * @return array<string, string> Array of classname => path
* @psalm-return array<string, string>
*/ */
public function getClassMap() public function getClassMap()
{ {
@@ -157,8 +154,7 @@ class ClassLoader
} }
/** /**
* @param string[] $classMap Class to filename map * @param array<string, string> $classMap Class to filename map
* @psalm-param array<string, string> $classMap
* *
* @return void * @return void
*/ */
@@ -176,23 +172,24 @@ class ClassLoader
* appending or prepending to the ones previously set for this prefix. * appending or prepending to the ones previously set for this prefix.
* *
* @param string $prefix The prefix * @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 root directories * @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories * @param bool $prepend Whether to prepend the directories
* *
* @return void * @return void
*/ */
public function add($prefix, $paths, $prepend = false) public function add($prefix, $paths, $prepend = false)
{ {
$paths = (array) $paths;
if (!$prefix) { if (!$prefix) {
if ($prepend) { if ($prepend) {
$this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0 = array_merge(
(array) $paths, $paths,
$this->fallbackDirsPsr0 $this->fallbackDirsPsr0
); );
} else { } else {
$this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0, $this->fallbackDirsPsr0,
(array) $paths $paths
); );
} }
@@ -201,19 +198,19 @@ class ClassLoader
$first = $prefix[0]; $first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) { if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths; $this->prefixesPsr0[$first][$prefix] = $paths;
return; return;
} }
if ($prepend) { if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths, $paths,
$this->prefixesPsr0[$first][$prefix] $this->prefixesPsr0[$first][$prefix]
); );
} else { } else {
$this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix], $this->prefixesPsr0[$first][$prefix],
(array) $paths $paths
); );
} }
} }
@@ -223,7 +220,7 @@ class ClassLoader
* appending or prepending to the ones previously set for this namespace. * appending or prepending to the ones previously set for this namespace.
* *
* @param string $prefix The prefix/namespace, with trailing '\\' * @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories * @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories * @param bool $prepend Whether to prepend the directories
* *
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
@@ -232,17 +229,18 @@ class ClassLoader
*/ */
public function addPsr4($prefix, $paths, $prepend = false) public function addPsr4($prefix, $paths, $prepend = false)
{ {
$paths = (array) $paths;
if (!$prefix) { if (!$prefix) {
// Register directories for the root namespace. // Register directories for the root namespace.
if ($prepend) { if ($prepend) {
$this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4 = array_merge(
(array) $paths, $paths,
$this->fallbackDirsPsr4 $this->fallbackDirsPsr4
); );
} else { } else {
$this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4, $this->fallbackDirsPsr4,
(array) $paths $paths
); );
} }
} elseif (!isset($this->prefixDirsPsr4[$prefix])) { } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
@@ -252,18 +250,18 @@ class ClassLoader
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
} }
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths; $this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) { } elseif ($prepend) {
// Prepend directories for an already registered namespace. // Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths, $paths,
$this->prefixDirsPsr4[$prefix] $this->prefixDirsPsr4[$prefix]
); );
} else { } else {
// Append directories for an already registered namespace. // Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix], $this->prefixDirsPsr4[$prefix],
(array) $paths $paths
); );
} }
} }
@@ -273,7 +271,7 @@ class ClassLoader
* replacing any others previously set for this prefix. * replacing any others previously set for this prefix.
* *
* @param string $prefix The prefix * @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 base directories * @param list<string>|string $paths The PSR-0 base directories
* *
* @return void * @return void
*/ */
@@ -291,7 +289,7 @@ class ClassLoader
* replacing any others previously set for this namespace. * replacing any others previously set for this namespace.
* *
* @param string $prefix The prefix/namespace, with trailing '\\' * @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories * @param list<string>|string $paths The PSR-4 base directories
* *
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
* *
@@ -425,7 +423,8 @@ class ClassLoader
public function loadClass($class) public function loadClass($class)
{ {
if ($file = $this->findFile($class)) { if ($file = $this->findFile($class)) {
includeFile($file); $includeFile = self::$includeFile;
$includeFile($file);
return true; return true;
} }
@@ -476,9 +475,9 @@ class ClassLoader
} }
/** /**
* Returns the currently registered loaders indexed by their corresponding vendor directories. * Returns the currently registered loaders keyed by their corresponding vendor directories.
* *
* @return self[] * @return array<string, self>
*/ */
public static function getRegisteredLoaders() public static function getRegisteredLoaders()
{ {
@@ -555,18 +554,26 @@ class ClassLoader
return false; return false;
} }
}
/** /**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include. * Scope isolated include.
* *
* Prevents access to $this/self from included files. * Prevents access to $this/self from included files.
* *
* @param string $file * @param string $file
* @return void * @return void
* @private
*/ */
function includeFile($file) self::$includeFile = \Closure::bind(static function($file) {
{
include $file; include $file;
}, null, null);
}
} }
+50 -6
View File
@@ -26,12 +26,23 @@ use Composer\Semver\VersionParser;
*/ */
class InstalledVersions class InstalledVersions
{ {
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/** /**
* @var mixed[]|null * @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/ */
private static $installed; private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/** /**
* @var bool|null * @var bool|null
*/ */
@@ -98,7 +109,7 @@ class InstalledVersions
{ {
foreach (self::getInstalled() as $installed) { foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) { if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']); return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
} }
} }
@@ -119,7 +130,7 @@ class InstalledVersions
*/ */
public static function satisfies(VersionParser $parser, $packageName, $constraint) public static function satisfies(VersionParser $parser, $packageName, $constraint)
{ {
$constraint = $parser->parseConstraints($constraint); $constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName)); $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint); return $provided->matches($constraint);
@@ -309,6 +320,24 @@ class InstalledVersions
{ {
self::$installed = $data; self::$installed = $data;
self::$installedByVendor = array(); self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
} }
/** /**
@@ -322,17 +351,27 @@ class InstalledVersions
} }
$installed = array(); $installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) { if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) { if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir]; $installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) { } elseif (is_file($vendorDir.'/composer/installed.php')) {
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { $required = require $vendorDir.'/composer/installed.php';
self::$installed = $installed[count($installed) - 1]; self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
} }
} }
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
} }
} }
@@ -340,12 +379,17 @@ class InstalledVersions
// only require the installed.php file if this file is loaded from its dumped location, // only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') { if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = require __DIR__ . '/installed.php'; /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else { } else {
self::$installed = array(); self::$installed = array();
} }
} }
if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed; $installed[] = self::$installed;
}
return $installed; return $installed;
} }
+525 -91
View File
File diff suppressed because it is too large Load Diff
+8 -4
View File
@@ -8,8 +8,8 @@ $baseDir = dirname($vendorDir);
return array( return array(
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php', '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php', 'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php', '8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php', 'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
@@ -18,25 +18,29 @@ return array(
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php', '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'9c67151ae59aff4788964ce8eb2a0f43' => $vendorDir . '/clue/stream-filter/src/functions_include.php', '9c67151ae59aff4788964ce8eb2a0f43' => $vendorDir . '/clue/stream-filter/src/functions_include.php',
'8cff32064859f4559445b89279f3199c' => $vendorDir . '/php-http/message/src/filters.php', '8cff32064859f4559445b89279f3199c' => $vendorDir . '/php-http/message/src/filters.php',
'606a39d89246991a373564698c2d8383' => $vendorDir . '/symfony/polyfill-php85/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php', '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php', '2203a247e6fda86070a5e4e07aed533a' => $vendorDir . '/symfony/clock/Resources/now.php',
'deecf9d6b2672fb73429f9b28fdee35f' => $vendorDir . '/symfony/polyfill-deepclone/bootstrap.php',
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
'b067bc7112e384b61c701452d53a14a8' => $vendorDir . '/mtdowling/jmespath.php/src/JmesPath.php', 'b067bc7112e384b61c701452d53a14a8' => $vendorDir . '/mtdowling/jmespath.php/src/JmesPath.php',
'35a6ad97d21e794e7e22a17d806652e4' => $vendorDir . '/nunomaduro/termwind/src/Functions.php', '35a6ad97d21e794e7e22a17d806652e4' => $vendorDir . '/nunomaduro/termwind/src/Functions.php',
'3bd81c9b8fcc150b69d8b63b4d2ccf23' => $vendorDir . '/spatie/flare-client-php/src/helpers.php', '3bd81c9b8fcc150b69d8b63b4d2ccf23' => $vendorDir . '/spatie/flare-client-php/src/helpers.php',
'2203a247e6fda86070a5e4e07aed533a' => $vendorDir . '/symfony/clock/Resources/now.php',
'09f6b20656683369174dd6fa83b7e5fb' => $vendorDir . '/symfony/polyfill-uuid/bootstrap.php', '09f6b20656683369174dd6fa83b7e5fb' => $vendorDir . '/symfony/polyfill-uuid/bootstrap.php',
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
'8a9dc1de0ca7e01f3e08231539562f61' => $vendorDir . '/aws/aws-sdk-php/src/functions.php', '8a9dc1de0ca7e01f3e08231539562f61' => $vendorDir . '/aws/aws-sdk-php/src/functions.php',
'47e1160838b5e5a10346ac4084b58c23' => $vendorDir . '/laravel/prompts/src/helpers.php', '47e1160838b5e5a10346ac4084b58c23' => $vendorDir . '/laravel/prompts/src/helpers.php',
'6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php', '6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
'801c31d8ed748cfa537fa45402288c95' => $vendorDir . '/psy/psysh/src/functions.php', '801c31d8ed748cfa537fa45402288c95' => $vendorDir . '/psy/psysh/src/functions.php',
'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php', 'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php',
'662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php',
'9d2b9fc6db0f153a0a149fefb182415e' => $vendorDir . '/symfony/polyfill-php84/bootstrap.php',
'476ca15b8d69b04665cd879be9cb4c68' => $vendorDir . '/laravel/framework/src/Illuminate/Collections/functions.php', '476ca15b8d69b04665cd879be9cb4c68' => $vendorDir . '/laravel/framework/src/Illuminate/Collections/functions.php',
'265b4faa2b3a9766332744949e83bf97' => $vendorDir . '/laravel/framework/src/Illuminate/Collections/helpers.php', '265b4faa2b3a9766332744949e83bf97' => $vendorDir . '/laravel/framework/src/Illuminate/Collections/helpers.php',
'c7a3c339e7e14b60e06a2d7fcce9476b' => $vendorDir . '/laravel/framework/src/Illuminate/Events/functions.php', 'c7a3c339e7e14b60e06a2d7fcce9476b' => $vendorDir . '/laravel/framework/src/Illuminate/Events/functions.php',
'f57d353b41eb2e234b26064d63d8c5dd' => $vendorDir . '/laravel/framework/src/Illuminate/Filesystem/functions.php', 'f57d353b41eb2e234b26064d63d8c5dd' => $vendorDir . '/laravel/framework/src/Illuminate/Filesystem/functions.php',
'f0906e6318348a765ffb6eb24e0d0938' => $vendorDir . '/laravel/framework/src/Illuminate/Foundation/helpers.php', 'f0906e6318348a765ffb6eb24e0d0938' => $vendorDir . '/laravel/framework/src/Illuminate/Foundation/helpers.php',
'7f7ac2ddea9cc3fb4b2cc201d63dbc10' => $vendorDir . '/laravel/framework/src/Illuminate/Log/functions.php', '7f7ac2ddea9cc3fb4b2cc201d63dbc10' => $vendorDir . '/laravel/framework/src/Illuminate/Log/functions.php',
'91892b814db86b8442ad76273bb7aec5' => $vendorDir . '/laravel/framework/src/Illuminate/Reflection/helpers.php',
'493c6aea52f6009bab023b26c21a386a' => $vendorDir . '/laravel/framework/src/Illuminate/Support/functions.php', '493c6aea52f6009bab023b26c21a386a' => $vendorDir . '/laravel/framework/src/Illuminate/Support/functions.php',
'58571171fd5812e6e447dce228f52f4d' => $vendorDir . '/laravel/framework/src/Illuminate/Support/helpers.php', '58571171fd5812e6e447dce228f52f4d' => $vendorDir . '/laravel/framework/src/Illuminate/Support/helpers.php',
'c72349b1fe8d0deeedd3a52e8aa814d8' => $vendorDir . '/mockery/mockery/library/helpers.php', 'c72349b1fe8d0deeedd3a52e8aa814d8' => $vendorDir . '/mockery/mockery/library/helpers.php',
+8 -5
View File
@@ -9,18 +9,20 @@ return array(
'voku\\' => array($vendorDir . '/voku/portable-ascii/src/voku'), 'voku\\' => array($vendorDir . '/voku/portable-ascii/src/voku'),
'enshrined\\svgSanitize\\' => array($vendorDir . '/enshrined/svg-sanitize/src'), 'enshrined\\svgSanitize\\' => array($vendorDir . '/enshrined/svg-sanitize/src'),
'Whoops\\' => array($vendorDir . '/filp/whoops/src/Whoops'), 'Whoops\\' => array($vendorDir . '/filp/whoops/src/Whoops'),
'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'),
'TijsVerkoyen\\CssToInlineStyles\\' => array($vendorDir . '/tijsverkoyen/css-to-inline-styles/src'), 'TijsVerkoyen\\CssToInlineStyles\\' => array($vendorDir . '/tijsverkoyen/css-to-inline-styles/src'),
'Tests\\' => array($baseDir . '/tests'), 'Tests\\' => array($baseDir . '/tests'),
'Termwind\\' => array($vendorDir . '/nunomaduro/termwind/src'), 'Termwind\\' => array($vendorDir . '/nunomaduro/termwind/src'),
'Symfony\\Thanks\\' => array($vendorDir . '/symfony/thanks/src'), 'Symfony\\Thanks\\' => array($vendorDir . '/symfony/thanks/src'),
'Symfony\\Polyfill\\Uuid\\' => array($vendorDir . '/symfony/polyfill-uuid'), 'Symfony\\Polyfill\\Uuid\\' => array($vendorDir . '/symfony/polyfill-uuid'),
'Symfony\\Polyfill\\Php85\\' => array($vendorDir . '/symfony/polyfill-php85'),
'Symfony\\Polyfill\\Php84\\' => array($vendorDir . '/symfony/polyfill-php84'),
'Symfony\\Polyfill\\Php83\\' => array($vendorDir . '/symfony/polyfill-php83'), 'Symfony\\Polyfill\\Php83\\' => array($vendorDir . '/symfony/polyfill-php83'),
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'), 'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'), 'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'),
'Symfony\\Polyfill\\Intl\\Idn\\' => array($vendorDir . '/symfony/polyfill-intl-idn'), 'Symfony\\Polyfill\\Intl\\Idn\\' => array($vendorDir . '/symfony/polyfill-intl-idn'),
'Symfony\\Polyfill\\Intl\\Grapheme\\' => array($vendorDir . '/symfony/polyfill-intl-grapheme'), 'Symfony\\Polyfill\\Intl\\Grapheme\\' => array($vendorDir . '/symfony/polyfill-intl-grapheme'),
'Symfony\\Polyfill\\DeepClone\\' => array($vendorDir . '/symfony/polyfill-deepclone'),
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'), 'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
'Symfony\\Contracts\\Translation\\' => array($vendorDir . '/symfony/translation-contracts'), 'Symfony\\Contracts\\Translation\\' => array($vendorDir . '/symfony/translation-contracts'),
'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'), 'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'),
@@ -47,8 +49,8 @@ return array(
'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'), 'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
'Symfony\\Component\\Clock\\' => array($vendorDir . '/symfony/clock'), 'Symfony\\Component\\Clock\\' => array($vendorDir . '/symfony/clock'),
'Symfony\\Component\\Cache\\' => array($vendorDir . '/symfony/cache'), 'Symfony\\Component\\Cache\\' => array($vendorDir . '/symfony/cache'),
'Spatie\\LaravelIgnition\\' => array($vendorDir . '/spatie/error-solutions/legacy/laravel-ignition', $vendorDir . '/spatie/laravel-ignition/src'), 'Spatie\\LaravelIgnition\\' => array($vendorDir . '/spatie/laravel-ignition/src', $vendorDir . '/spatie/error-solutions/legacy/laravel-ignition'),
'Spatie\\Ignition\\' => array($vendorDir . '/spatie/error-solutions/legacy/ignition', $vendorDir . '/spatie/ignition/src'), 'Spatie\\Ignition\\' => array($vendorDir . '/spatie/ignition/src', $vendorDir . '/spatie/error-solutions/legacy/ignition'),
'Spatie\\Html\\' => array($vendorDir . '/spatie/laravel-html/src'), 'Spatie\\Html\\' => array($vendorDir . '/spatie/laravel-html/src'),
'Spatie\\FlareClient\\' => array($vendorDir . '/spatie/flare-client-php/src'), 'Spatie\\FlareClient\\' => array($vendorDir . '/spatie/flare-client-php/src'),
'Spatie\\ErrorSolutions\\' => array($vendorDir . '/spatie/error-solutions/src'), 'Spatie\\ErrorSolutions\\' => array($vendorDir . '/spatie/error-solutions/src'),
@@ -67,6 +69,7 @@ return array(
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'), 'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'), 'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'),
'NunoMaduro\\Collision\\' => array($vendorDir . '/nunomaduro/collision/src'), 'NunoMaduro\\Collision\\' => array($vendorDir . '/nunomaduro/collision/src'),
'Nette\\' => array($vendorDir . '/nette/schema/src', $vendorDir . '/nette/utils/src'),
'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'), 'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
'Mockery\\' => array($vendorDir . '/mockery/mockery/library/Mockery'), 'Mockery\\' => array($vendorDir . '/mockery/mockery/library/Mockery'),
'League\\Uri\\' => array($vendorDir . '/league/uri', $vendorDir . '/league/uri-interfaces'), 'League\\Uri\\' => array($vendorDir . '/league/uri', $vendorDir . '/league/uri-interfaces'),
@@ -82,7 +85,7 @@ return array(
'Laravel\\SerializableClosure\\' => array($vendorDir . '/laravel/serializable-closure/src'), 'Laravel\\SerializableClosure\\' => array($vendorDir . '/laravel/serializable-closure/src'),
'Laravel\\Prompts\\' => array($vendorDir . '/laravel/prompts/src'), 'Laravel\\Prompts\\' => array($vendorDir . '/laravel/prompts/src'),
'JmesPath\\' => array($vendorDir . '/mtdowling/jmespath.php/src'), 'JmesPath\\' => array($vendorDir . '/mtdowling/jmespath.php/src'),
'Illuminate\\Support\\' => array($vendorDir . '/laravel/framework/src/Illuminate/Macroable', $vendorDir . '/laravel/framework/src/Illuminate/Collections', $vendorDir . '/laravel/framework/src/Illuminate/Conditionable'), 'Illuminate\\Support\\' => array($vendorDir . '/laravel/framework/src/Illuminate/Macroable', $vendorDir . '/laravel/framework/src/Illuminate/Collections', $vendorDir . '/laravel/framework/src/Illuminate/Conditionable', $vendorDir . '/laravel/framework/src/Illuminate/Reflection'),
'Illuminate\\Foundation\\Auth\\' => array($vendorDir . '/laravel/ui/auth-backend'), 'Illuminate\\Foundation\\Auth\\' => array($vendorDir . '/laravel/ui/auth-backend'),
'Illuminate\\' => array($vendorDir . '/laravel/framework/src/Illuminate'), 'Illuminate\\' => array($vendorDir . '/laravel/framework/src/Illuminate'),
'Http\\Promise\\' => array($vendorDir . '/php-http/promise/src'), 'Http\\Promise\\' => array($vendorDir . '/php-http/promise/src'),
@@ -105,7 +108,7 @@ return array(
'Faker\\' => array($vendorDir . '/fakerphp/faker/src/Faker'), 'Faker\\' => array($vendorDir . '/fakerphp/faker/src/Faker'),
'Egulias\\EmailValidator\\' => array($vendorDir . '/egulias/email-validator/src'), 'Egulias\\EmailValidator\\' => array($vendorDir . '/egulias/email-validator/src'),
'Dotenv\\' => array($vendorDir . '/vlucas/phpdotenv/src'), 'Dotenv\\' => array($vendorDir . '/vlucas/phpdotenv/src'),
'Doctrine\\Inflector\\' => array($vendorDir . '/doctrine/inflector/lib/Doctrine/Inflector'), 'Doctrine\\Inflector\\' => array($vendorDir . '/doctrine/inflector/src'),
'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/src'), 'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/src'),
'Dflydev\\DotAccessData\\' => array($vendorDir . '/dflydev/dot-access-data/src'), 'Dflydev\\DotAccessData\\' => array($vendorDir . '/dflydev/dot-access-data/src'),
'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'), 'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'),
+9 -16
View File
@@ -33,25 +33,18 @@ class ComposerAutoloaderInitb2555e5ff7197b9e020da74bbd3b7cfa
$loader->register(true); $loader->register(true);
$includeFiles = \Composer\Autoload\ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa::$files; $filesToLoad = \Composer\Autoload\ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa::$files;
foreach ($includeFiles as $fileIdentifier => $file) { $requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
composerRequireb2555e5ff7197b9e020da74bbd3b7cfa($fileIdentifier, $file);
}
return $loader;
}
}
/**
* @param string $fileIdentifier
* @param string $file
* @return void
*/
function composerRequireb2555e5ff7197b9e020da74bbd3b7cfa($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file; require $file;
} }
}, null, null);
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
}
} }
+560 -105
View File
File diff suppressed because it is too large Load Diff
+97
View File
@@ -0,0 +1,97 @@
<?php
$header = <<<EOF
This file is part of Composer.
(c) Nils Adermann <naderman@naderman.de>
Jordi Boggiano <j.boggiano@seld.be>
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
EOF;
$finder = PhpCsFixer\Finder::create()
->files()
->in(__DIR__.'/src')
->in(__DIR__.'/tests')
->name('*.php')
->notPath('Fixtures')
;
$config = new PhpCsFixer\Config();
return $config
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect())
->setRules([
'@PSR2' => true,
'binary_operator_spaces' => true,
'blank_line_before_statement' => ['statements' => ['declare', 'return']],
'cast_spaces' => ['space' => 'single'],
'header_comment' => ['header' => $header],
'statement_indentation' => ['stick_comment_to_next_continuous_control_statement' => true],
'include' => true,
'class_attributes_separation' => ['elements' => ['method' => 'one', 'trait_import' => 'none']],
'no_blank_lines_after_class_opening' => true,
'no_blank_lines_after_phpdoc' => true,
'no_empty_statement' => true,
'no_extra_blank_lines' => true,
'no_leading_namespace_whitespace' => true,
'no_trailing_comma_in_singleline' => true,
'no_whitespace_in_blank_line' => true,
'object_operator_without_whitespace' => true,
//'phpdoc_align' => true,
'phpdoc_indent' => true,
'no_empty_comment' => true,
'no_empty_phpdoc' => true,
'phpdoc_no_access' => true,
'phpdoc_no_package' => true,
//'phpdoc_order' => true,
'phpdoc_scalar' => true,
'phpdoc_trim' => true,
'phpdoc_types' => true,
'psr_autoloading' => true,
'blank_lines_before_namespace' => true,
'standardize_not_equals' => true,
'ternary_operator_spaces' => true,
'trailing_comma_in_multiline' => ['elements' => ['arrays']],
'unary_operator_spaces' => true,
'native_function_invocation' => [
'include' => ['@compiler_optimized'], // Targets functions with special Zend opcodes (e.g., strlen, count)
'scope' => 'namespaced', // Only fixes functions inside a namespace
'strict' => true, // Removes leading \ if not native
],
// imports
'no_unused_imports' => true,
'fully_qualified_strict_types' => true,
'single_line_after_imports' => true,
//'global_namespace_import' => ['import_classes' => true],
'no_leading_import_slash' => true,
'single_import_per_statement' => true,
// PHP 7.2 migration
'array_syntax' => true,
'list_syntax' => true,
'regular_callable_call' => true,
'static_lambda' => true,
'nullable_type_declaration_for_default_null_value' => true,
'explicit_indirect_variable' => true,
'visibility_required' => ['elements' => ['property', 'method', 'const']],
'non_printable_character' => true,
'combine_nested_dirname' => true,
'random_api_migration' => true,
'ternary_to_null_coalescing' => true,
'phpdoc_to_param_type' => false,
'declare_strict_types' => true,
'no_superfluous_phpdoc_tags' => [
'allow_mixed' => true,
],
// TODO php 7.4 migration (one day..)
// 'phpdoc_to_property_type' => true,
])
->setUsingCache(true)
->setRiskyAllowed(true)
->setFinder($finder)
;
+1 -2
View File
@@ -3,8 +3,7 @@ composer/class-map-generator
Utilities to generate class maps and scan PHP code. Utilities to generate class maps and scan PHP code.
[![Continuous Integration](https://github.com/composer/class-map-generator/workflows/Continuous%20Integration/badge.svg?branch=main)](https://github.com/composer/class-map-generator/actions) [![Continuous Integration](https://github.com/composer/class-map-generator/actions/workflows/continuous-integration.yml/badge.svg?branch=main)](https://github.com/composer/class-map-generator/actions)
Installation Installation
------------ ------------
+2 -2
View File
@@ -15,7 +15,7 @@
], ],
"require": { "require": {
"php": "^7.2 || ^8.0", "php": "^7.2 || ^8.0",
"symfony/finder": "^4.4 || ^5.3 || ^6 || ^7", "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7 || ^8",
"composer/pcre": "^2.1 || ^3.1" "composer/pcre": "^2.1 || ^3.1"
}, },
"require-dev": { "require-dev": {
@@ -24,7 +24,7 @@
"phpstan/phpstan-deprecation-rules": "^1 || ^2", "phpstan/phpstan-deprecation-rules": "^1 || ^2",
"phpstan/phpstan-strict-rules": "^1.1 || ^2", "phpstan/phpstan-strict-rules": "^1.1 || ^2",
"phpstan/phpstan-phpunit": "^1 || ^2", "phpstan/phpstan-phpunit": "^1 || ^2",
"symfony/filesystem": "^5.4 || ^6" "symfony/filesystem": "^5.4 || ^6 || ^7 || ^8"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
+2 -2
View File
@@ -94,7 +94,7 @@ class ClassMap implements \Countable
$ambiguousClasses = []; $ambiguousClasses = [];
foreach ($this->ambiguousClasses as $class => $paths) { foreach ($this->ambiguousClasses as $class => $paths) {
$paths = array_filter($paths, function ($path) use ($duplicatesFilter) { $paths = array_filter($paths, static function ($path) use ($duplicatesFilter): bool {
return !Preg::isMatch($duplicatesFilter, strtr($path, '\\', '/')); return !Preg::isMatch($duplicatesFilter, strtr($path, '\\', '/'));
}); });
if (\count($paths) > 0) { if (\count($paths) > 0) {
@@ -157,7 +157,7 @@ class ClassMap implements \Countable
$pathPrefix = rtrim(strtr($pathPrefix, '\\', '/'), '/'); $pathPrefix = rtrim(strtr($pathPrefix, '\\', '/'), '/');
foreach ($this->psrViolations as $path => $violations) { foreach ($this->psrViolations as $path => $violations) {
if ($path === $pathPrefix || 0 === \strpos($path, $pathPrefix.'/')) { if ($path === $pathPrefix || 0 === strpos($path, $pathPrefix.'/')) {
unset($this->psrViolations[$path]); unset($this->psrViolations[$path]);
} }
} }
+16 -16
View File
@@ -20,7 +20,6 @@ namespace Composer\ClassMapGenerator;
use Composer\Pcre\Preg; use Composer\Pcre\Preg;
use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Finder;
use Composer\IO\IOInterface;
/** /**
* ClassMapGenerator * ClassMapGenerator
@@ -57,7 +56,7 @@ class ClassMapGenerator
{ {
$this->extensions = $extensions; $this->extensions = $extensions;
$this->classMap = new ClassMap; $this->classMap = new ClassMap;
$this->streamWrappersRegex = sprintf('{^(?:%s)://}', implode('|', array_map('preg_quote', stream_get_wrappers()))); $this->streamWrappersRegex = \sprintf('{^(?:%s)://}', implode('|', array_map('preg_quote', stream_get_wrappers())));
} }
/** /**
@@ -109,21 +108,21 @@ class ClassMapGenerator
*/ */
public function scanPaths($path, ?string $excluded = null, string $autoloadType = 'classmap', ?string $namespace = null, array $excludedDirs = []): void public function scanPaths($path, ?string $excluded = null, string $autoloadType = 'classmap', ?string $namespace = null, array $excludedDirs = []): void
{ {
if (!in_array($autoloadType, ['psr-0', 'psr-4', 'classmap'], true)) { if (!\in_array($autoloadType, ['psr-0', 'psr-4', 'classmap'], true)) {
throw new \InvalidArgumentException('$autoloadType must be one of: "psr-0", "psr-4" or "classmap"'); throw new \InvalidArgumentException('$autoloadType must be one of: "psr-0", "psr-4" or "classmap"');
} }
if ('classmap' !== $autoloadType) { if ('classmap' !== $autoloadType) {
if (!is_string($path)) { if (!\is_string($path)) {
throw new \InvalidArgumentException('$path must be a string when specifying a psr-0 or psr-4 autoload type'); throw new \InvalidArgumentException('$path must be a string when specifying a psr-0 or psr-4 autoload type');
} }
if (!is_string($namespace)) { if (!\is_string($namespace)) {
throw new \InvalidArgumentException('$namespace must be given (even if it is an empty string if you do not want to filter) when specifying a psr-0 or psr-4 autoload type'); throw new \InvalidArgumentException('$namespace must be given (even if it is an empty string if you do not want to filter) when specifying a psr-0 or psr-4 autoload type');
} }
$basePath = $path; $basePath = $path;
} }
if (is_string($path)) { if (\is_string($path)) {
if (is_file($path)) { if (is_file($path)) {
$path = [new \SplFileInfo($path)]; $path = [new \SplFileInfo($path)];
} elseif (is_dir($path) || strpos($path, '*') !== false) { } elseif (is_dir($path) || strpos($path, '*') !== false) {
@@ -144,7 +143,7 @@ class ClassMapGenerator
foreach ($path as $file) { foreach ($path as $file) {
$filePath = $file->getPathname(); $filePath = $file->getPathname();
if (!in_array(pathinfo($filePath, PATHINFO_EXTENSION), $this->extensions, true)) { if (!\in_array(pathinfo($filePath, PATHINFO_EXTENSION), $this->extensions, true)) {
continue; continue;
} }
@@ -224,13 +223,18 @@ class ClassMapGenerator
$validClasses = []; $validClasses = [];
$rejectedClasses = []; $rejectedClasses = [];
$realSubPath = substr($filePath, strlen($basePath) + 1); $realSubPath = substr($filePath, \strlen($basePath) + 1);
$dotPosition = strrpos($realSubPath, '.'); $dotPosition = strrpos($realSubPath, '.');
$realSubPath = substr($realSubPath, 0, $dotPosition === false ? PHP_INT_MAX : $dotPosition); $realSubPath = substr($realSubPath, 0, $dotPosition === false ? PHP_INT_MAX : $dotPosition);
foreach ($classes as $class) { foreach ($classes as $class) {
// transform class name to file path and validate // transform class name to file path and validate
if ('psr-0' === $namespaceType) { if ('psr-0' === $namespaceType) {
if ('' !== $baseNamespace && !str_starts_with($class, $baseNamespace)) {
$rejectedClasses[] = $class;
continue;
}
$namespaceLength = strrpos($class, '\\'); $namespaceLength = strrpos($class, '\\');
if (false !== $namespaceLength) { if (false !== $namespaceLength) {
$namespace = substr($class, 0, $namespaceLength + 1); $namespace = substr($class, 0, $namespaceLength + 1);
@@ -241,7 +245,7 @@ class ClassMapGenerator
$subPath = str_replace('_', DIRECTORY_SEPARATOR, $class); $subPath = str_replace('_', DIRECTORY_SEPARATOR, $class);
} }
} elseif ('psr-4' === $namespaceType) { } elseif ('psr-4' === $namespaceType) {
$subNamespace = ('' !== $baseNamespace) ? substr($class, strlen($baseNamespace)) : $class; $subNamespace = ('' !== $baseNamespace) ? substr($class, \strlen($baseNamespace)) : $class;
$subPath = str_replace('\\', DIRECTORY_SEPARATOR, $subNamespace); $subPath = str_replace('\\', DIRECTORY_SEPARATOR, $subNamespace);
} else { } else {
throw new \InvalidArgumentException('$namespaceType must be "psr-0" or "psr-4"'); throw new \InvalidArgumentException('$namespaceType must be "psr-0" or "psr-4"');
@@ -276,11 +280,8 @@ class ClassMapGenerator
* Checks if the given path is absolute * Checks if the given path is absolute
* *
* @see Composer\Util\Filesystem::isAbsolutePath * @see Composer\Util\Filesystem::isAbsolutePath
*
* @param string $path
* @return bool
*/ */
private static function isAbsolutePath(string $path) private static function isAbsolutePath(string $path): bool
{ {
return strpos($path, '/') === 0 || substr($path, 1, 1) === ':' || strpos($path, '\\\\') === 0; return strpos($path, '/') === 0 || substr($path, 1, 1) === ':' || strpos($path, '\\\\') === 0;
} }
@@ -292,9 +293,8 @@ class ClassMapGenerator
* @see Composer\Util\Filesystem::normalizePath * @see Composer\Util\Filesystem::normalizePath
* *
* @param string $path Path to the file or directory * @param string $path Path to the file or directory
* @return string
*/ */
private static function normalizePath(string $path) private static function normalizePath(string $path): string
{ {
$parts = []; $parts = [];
$path = strtr($path, '\\', '/'); $path = strtr($path, '\\', '/');
@@ -330,7 +330,7 @@ class ClassMapGenerator
} }
// ensure c: is normalized to C: // ensure c: is normalized to C:
$prefix = Preg::replaceCallback('{(?:^|://)[a-z]:$}i', function (array $m) { return strtoupper((string) $m[0]); }, $prefix); $prefix = Preg::replaceCallback('{(?:^|://)[a-z]:$}i', static function (array $m) { return strtoupper((string) $m[0]); }, $prefix);
return $prefix.$absolute.implode('/', $parts); return $prefix.$absolute.implode('/', $parts);
} }
+26 -26
View File
@@ -24,7 +24,7 @@ class PhpFileCleaner
private static $typeConfig; private static $typeConfig;
/** @var non-empty-string */ /** @var non-empty-string */
private static $restPattern; private static $rejectChars;
/** /**
* @readonly * @readonly
@@ -53,14 +53,14 @@ class PhpFileCleaner
public static function setTypeConfig(array $types): void public static function setTypeConfig(array $types): void
{ {
foreach ($types as $type) { foreach ($types as $type) {
self::$typeConfig[$type[0]] = array( self::$typeConfig[$type[0]] = [
'name' => $type, 'name' => $type,
'length' => \strlen($type), 'length' => \strlen($type),
'pattern' => '{.\b(?<![\$:>])'.$type.'\s++[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+}Ais', 'pattern' => '{.\b(?<![\$:>])'.$type.'\s++[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+}Ais',
); ];
} }
self::$restPattern = '{[^?"\'</'.implode('', array_keys(self::$typeConfig)).']+}A'; self::$rejectChars = '?"\'</'.implode('', array_keys(self::$typeConfig));
} }
public function __construct(string $contents, int $maxMatches) public function __construct(string $contents, int $maxMatches)
@@ -110,6 +110,7 @@ class PhpFileCleaner
$this->skipToNewline(); $this->skipToNewline();
continue; continue;
} }
if ($this->peek('*')) { if ($this->peek('*')) {
$this->skipComment(); $this->skipComment();
continue; continue;
@@ -119,19 +120,18 @@ class PhpFileCleaner
if ($this->maxMatches === 1 && isset(self::$typeConfig[$char])) { if ($this->maxMatches === 1 && isset(self::$typeConfig[$char])) {
$type = self::$typeConfig[$char]; $type = self::$typeConfig[$char];
if ( if (
\substr($this->contents, $this->index, $type['length']) === $type['name'] substr($this->contents, $this->index, $type['length']) === $type['name']
&& Preg::isMatch($type['pattern'], $this->contents, $match, 0, $this->index - 1) && Preg::isMatch($type['pattern'], $this->contents, $match, 0, $this->index - 1)
) { ) {
$clean .= $match[0]; return $clean . $match[0];
return $clean;
} }
} }
$this->index += 1; $this->index += 1;
if ($this->match(self::$restPattern, $match)) { $skip = strcspn($this->contents, self::$rejectChars, $this->index);
$clean .= $char . $match[0]; if ($skip > 0) {
$this->index += \strlen($match[0]); $clean .= $char . substr($this->contents, $this->index, $skip);
$this->index += $skip;
} else { } else {
$clean .= $char; $clean .= $char;
} }
@@ -155,16 +155,24 @@ class PhpFileCleaner
private function skipString(string $delimiter): void private function skipString(string $delimiter): void
{ {
$rejectChars = '\\' . $delimiter;
$this->index += 1; $this->index += 1;
while ($this->index < $this->len) { while ($this->index < $this->len) {
$this->index += strcspn($this->contents, $rejectChars, $this->index);
if ($this->index >= $this->len) {
break;
}
if ($this->contents[$this->index] === '\\' && ($this->peek('\\') || $this->peek($delimiter))) { if ($this->contents[$this->index] === '\\' && ($this->peek('\\') || $this->peek($delimiter))) {
$this->index += 2; $this->index += 2;
continue; continue;
} }
if ($this->contents[$this->index] === $delimiter) { if ($this->contents[$this->index] === $delimiter) {
$this->index += 1; $this->index += 1;
break; break;
} }
$this->index += 1; $this->index += 1;
} }
} }
@@ -173,7 +181,9 @@ class PhpFileCleaner
{ {
$this->index += 2; $this->index += 2;
while ($this->index < $this->len) { while ($this->index < $this->len) {
if ($this->contents[$this->index] === '*' && $this->peek('/')) { $this->index += strcspn($this->contents, '*', $this->index);
if ($this->peek('/')) {
$this->index += 2; $this->index += 2;
break; break;
} }
@@ -184,12 +194,7 @@ class PhpFileCleaner
private function skipToNewline(): void private function skipToNewline(): void
{ {
while ($this->index < $this->len) { $this->index += strcspn($this->contents, "\r\n", $this->index);
if ($this->contents[$this->index] === "\r" || $this->contents[$this->index] === "\n") {
return;
}
$this->index += 1;
}
} }
private function skipHeredoc(string $delimiter): void private function skipHeredoc(string $delimiter): void
@@ -207,27 +212,22 @@ class PhpFileCleaner
continue 2; continue 2;
case $firstDelimiterChar: case $firstDelimiterChar:
if ( if (
\substr($this->contents, $this->index, $delimiterLength) === $delimiter substr($this->contents, $this->index, $delimiterLength) === $delimiter
&& $this->match($delimiterPattern) && $this->match($delimiterPattern)
) { ) {
$this->index += $delimiterLength; $this->index += $delimiterLength;
return; return;
} }
break; break;
} }
// skip the rest of the line // skip the rest of the line
while ($this->index < $this->len) {
$this->skipToNewline(); $this->skipToNewline();
// skip newlines // skip newlines
while ($this->index < $this->len && ($this->contents[$this->index] === "\r" || $this->contents[$this->index] === "\n")) { $this->index += strspn($this->contents, "\r\n", $this->index);
$this->index += 1;
}
break;
}
} }
} }
+26 -22
View File
@@ -12,6 +12,7 @@
namespace Composer\ClassMapGenerator; namespace Composer\ClassMapGenerator;
use RuntimeException;
use Composer\Pcre\Preg; use Composer\Pcre\Preg;
/** /**
@@ -23,16 +24,17 @@ class PhpFileParser
* Extract the classes in the given file * Extract the classes in the given file
* *
* @param string $path The file to check * @param string $path The file to check
* @throws \RuntimeException * @throws RuntimeException
* @return list<class-string> The found classes * @return list<class-string> The found classes
*/ */
public static function findClasses(string $path): array public static function findClasses(string $path): array
{ {
$extraTypes = self::getExtraTypes(); $extraTypes = self::getExtraTypes();
if (!function_exists('php_strip_whitespace')) { if (!\function_exists('php_strip_whitespace')) {
throw new \RuntimeException('Classmap generation relies on the php_strip_whitespace function, but it has been disabled by the disable_functions directive.'); throw new RuntimeException('Classmap generation relies on the php_strip_whitespace function, but it has been disabled by the disable_functions directive.');
} }
// Use @ here instead of Silencer to actively suppress 'unhelpful' output // Use @ here instead of Silencer to actively suppress 'unhelpful' output
// @link https://github.com/composer/composer/pull/4886 // @link https://github.com/composer/composer/pull/4886
$contents = @php_strip_whitespace($path); $contents = @php_strip_whitespace($path);
@@ -43,24 +45,26 @@ class PhpFileParser
$message = 'File at "%s" is not readable, check its permissions'; $message = 'File at "%s" is not readable, check its permissions';
} elseif ('' === trim((string) file_get_contents($path))) { } elseif ('' === trim((string) file_get_contents($path))) {
// The input file was really empty and thus contains no classes // The input file was really empty and thus contains no classes
return array(); return [];
} else { } else {
$message = 'File at "%s" could not be parsed as PHP, it may be binary or corrupted'; $message = 'File at "%s" could not be parsed as PHP, it may be binary or corrupted';
} }
$error = error_get_last(); $error = error_get_last();
if (isset($error['message'])) { if (isset($error['message'])) {
$message .= PHP_EOL . 'The following message may be helpful:' . PHP_EOL . $error['message']; $message .= PHP_EOL . 'The following message may be helpful:' . PHP_EOL . $error['message'];
} }
throw new \RuntimeException(sprintf($message, $path));
throw new RuntimeException(\sprintf($message, $path));
} }
// return early if there is no chance of matching anything in this file // return early if there is no chance of matching anything in this file
Preg::matchAllStrictGroups('{\b(?:class|interface|trait'.$extraTypes.')\s}i', $contents, $matches); Preg::matchAllStrictGroups('{\b(?:class|interface|trait'.$extraTypes.')\s}i', $contents, $matches);
if (0 === \count($matches)) { if ([] === $matches[0]) {
return array(); return [];
} }
$p = new PhpFileCleaner($contents, count($matches[0])); $p = new PhpFileCleaner($contents, \count($matches[0]));
$contents = $p->clean(); $contents = $p->clean();
unset($p); unset($p);
@@ -71,22 +75,26 @@ class PhpFileParser
) )
}ix', $contents, $matches); }ix', $contents, $matches);
$classes = array(); $classes = [];
$namespace = ''; $namespace = '';
for ($i = 0, $len = count($matches['type']); $i < $len; $i++) { for ($i = 0, $len = \count($matches['type']); $i < $len; ++$i) {
if (isset($matches['ns'][$i]) && $matches['ns'][$i] !== '') { if (isset($matches['ns'][$i]) && $matches['ns'][$i] !== '') {
$namespace = str_replace(array(' ', "\t", "\r", "\n"), '', (string) $matches['nsname'][$i]) . '\\'; $namespace = str_replace([' ', "\t", "\r", "\n"], '', (string) $matches['nsname'][$i]) . '\\';
} else { } else {
$name = $matches['name'][$i]; $name = $matches['name'][$i];
assert(is_string($name)); \assert(\is_string($name));
// skip anon classes extending/implementing // skip anon classes extending/implementing
if ($name === 'extends' || $name === 'implements') { if ($name === 'extends') {
continue; continue;
} }
if ($name === 'implements') {
continue;
}
if ($name[0] === ':') { if ($name[0] === ':') {
// This is an XHP class, https://github.com/facebook/xhp // This is an XHP class, https://github.com/facebook/xhp
$name = 'xhp'.substr(str_replace(array('-', ':'), array('_', '__'), $name), 1); $name = 'xhp'.substr(str_replace(['-', ':'], ['_', '__'], $name), 1);
} elseif (strtolower((string) $matches['type'][$i]) === 'enum') { } elseif (strtolower((string) $matches['type'][$i]) === 'enum') {
// something like: // something like:
// enum Foo: int { HERP = '123'; } // enum Foo: int { HERP = '123'; }
@@ -101,6 +109,7 @@ class PhpFileParser
$name = substr($name, 0, $colonPos); $name = substr($name, 0, $colonPos);
} }
} }
/** @var class-string */ /** @var class-string */
$className = ltrim($namespace . $name, '\\'); $className = ltrim($namespace . $name, '\\');
$classes[] = $className; $classes[] = $className;
@@ -110,22 +119,18 @@ class PhpFileParser
return $classes; return $classes;
} }
/**
* @return string
*/
private static function getExtraTypes(): string private static function getExtraTypes(): string
{ {
static $extraTypes = null; static $extraTypes = null;
if (null === $extraTypes) { if (null === $extraTypes) {
$extraTypes = ''; $extraTypes = '';
if (PHP_VERSION_ID >= 80100 || (defined('HHVM_VERSION') && version_compare(HHVM_VERSION, '3.3', '>='))) { $extraTypesArray = [];
if (PHP_VERSION_ID >= 80100 || (\defined('HHVM_VERSION') && version_compare(HHVM_VERSION, '3.3', '>='))) {
$extraTypes .= '|enum'; $extraTypes .= '|enum';
$extraTypesArray = ['enum'];
} }
$extraTypesArray = array_filter(explode('|', $extraTypes), function (string $type) {
return $type !== '';
});
PhpFileCleaner::setTypeConfig(array_merge(['class', 'interface', 'trait'], $extraTypesArray)); PhpFileCleaner::setTypeConfig(array_merge(['class', 'interface', 'trait'], $extraTypesArray));
} }
@@ -140,7 +145,6 @@ class PhpFileParser
* *
* @see Composer\Util\Filesystem::isReadable * @see Composer\Util\Filesystem::isReadable
* *
* @param string $path
* @return bool * @return bool
*/ */
private static function isReadable(string $path) private static function isReadable(string $path)
+1582 -1168
View File
File diff suppressed because it is too large Load Diff
+367 -328
View File
File diff suppressed because it is too large Load Diff
+9 -5
View File
@@ -20,12 +20,13 @@
"php": "^7.4 || ^8.0" "php": "^7.4 || ^8.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^8 || ^9", "phpunit/phpunit": "^9",
"phpstan/phpstan": "^1.12 || ^2", "phpstan/phpstan": "^2",
"phpstan/phpstan-strict-rules": "^1 || ^2" "phpstan/phpstan-strict-rules": "^2",
"phpstan/phpstan-deprecation-rules": "^2"
}, },
"conflict": { "conflict": {
"phpstan/phpstan": "<1.11.10" "phpstan/phpstan": "<2.2.2"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@@ -48,7 +49,10 @@
} }
}, },
"scripts": { "scripts": {
"test": "@php vendor/bin/phpunit", "test": [
"@php vendor/bin/phpunit",
"@php vendor/bin/phpunit --testsuite phpstan"
],
"phpstan": "@php phpstan analyse" "phpstan": "@php phpstan analyse"
} }
} }
@@ -57,18 +57,38 @@ final class PregMatchTypeSpecifyingExtension implements StaticMethodTypeSpecifyi
{ {
$args = $node->getArgs(); $args = $node->getArgs();
$patternArg = $args[0] ?? null; $patternArg = $args[0] ?? null;
$subjectArg = $args[1] ?? null;
$matchesArg = $args[2] ?? null; $matchesArg = $args[2] ?? null;
$flagsArg = $args[3] ?? null; $flagsArg = $args[3] ?? null;
$subjectTypes = new SpecifiedTypes();
if ($patternArg === null) {
return $subjectTypes;
}
if ( if (
$patternArg === null || $matchesArg === null $subjectArg !== null
&& $context->true()
&& $scope->getType($subjectArg->value)->isString()->yes()
) { ) {
return new SpecifiedTypes(); $subjectType = $this->regexShapeMatcher->matchSubjectExpr($patternArg->value, $scope);
if ($subjectType !== null) {
$subjectTypes = $this->typeSpecifier->create(
$subjectArg->value,
$subjectType,
$context,
$scope,
)->setRootExpr($node);
}
}
if ($matchesArg === null) {
return $subjectTypes;
} }
$flagsType = PregMatchFlags::getType($flagsArg, $scope); $flagsType = PregMatchFlags::getType($flagsArg, $scope);
if ($flagsType === null) { if ($flagsType === null) {
return new SpecifiedTypes(); return $subjectTypes;
} }
if (stripos($methodReflection->getName(), 'matchAll') !== false) { if (stripos($methodReflection->getName(), 'matchAll') !== false) {
@@ -78,7 +98,7 @@ final class PregMatchTypeSpecifyingExtension implements StaticMethodTypeSpecifyi
} }
if ($matchedType === null) { if ($matchedType === null) {
return new SpecifiedTypes(); return $subjectTypes;
} }
if ( if (
@@ -93,27 +113,13 @@ final class PregMatchTypeSpecifyingExtension implements StaticMethodTypeSpecifyi
$context = $context->negate(); $context = $context->negate();
} }
// @phpstan-ignore function.alreadyNarrowedType $specifiedTypes = $this->typeSpecifier->create(
if (method_exists('PHPStan\Analyser\SpecifiedTypes', 'setRootExpr')) {
$typeSpecifier = $this->typeSpecifier->create(
$matchesArg->value, $matchesArg->value,
$matchedType, $matchedType,
$context, $context,
$scope $scope
)->setRootExpr($node); )->setRootExpr($node);
return $overwrite ? $typeSpecifier->setAlwaysOverwriteTypes() : $typeSpecifier; return $subjectTypes->unionWith($overwrite ? $specifiedTypes->setAlwaysOverwriteTypes() : $specifiedTypes);
}
// @phpstan-ignore arguments.count
return $this->typeSpecifier->create(
$matchesArg->value,
$matchedType,
$context,
// @phpstan-ignore argument.type
$overwrite,
$scope,
$node
);
} }
} }
+2 -2
View File
@@ -395,7 +395,7 @@ class Preg
* @return array<int|string, string> * @return array<int|string, string>
* @throws UnexpectedNullMatchException * @throws UnexpectedNullMatchException
*/ */
private static function enforceNonNullMatches(string $pattern, array $matches, string $variantMethod) private static function enforceNonNullMatches(string $pattern, array $matches, string $variantMethod): array
{ {
foreach ($matches as $group => $match) { foreach ($matches as $group => $match) {
if (is_string($match) || (is_array($match) && is_string($match[0]))) { if (is_string($match) || (is_array($match) && is_string($match[0]))) {
@@ -414,7 +414,7 @@ class Preg
* @return array<int|string, list<string>> * @return array<int|string, list<string>>
* @throws UnexpectedNullMatchException * @throws UnexpectedNullMatchException
*/ */
private static function enforceNonNullMatchAll(string $pattern, array $matches, string $variantMethod) private static function enforceNonNullMatchAll(string $pattern, array $matches, string $variantMethod): array
{ {
foreach ($matches as $group => $groupMatches) { foreach ($matches as $group => $groupMatches) {
foreach ($groupMatches as $match) { foreach ($groupMatches as $match) {
+2 -3
View File
@@ -19,8 +19,7 @@ if ($issues) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
} }
} }
trigger_error( throw new \RuntimeException(
'Composer detected issues in your platform: ' . implode(' ', $issues), 'Composer detected issues in your platform: ' . implode(' ', $issues)
E_USER_ERROR
); );
} }
+44 -18
View File
@@ -1,41 +1,67 @@
{ {
"name": "doctrine/inflector", "name": "doctrine/inflector",
"type": "library",
"description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.",
"keywords": ["php", "strings", "words", "manipulation", "inflector", "inflection", "uppercase", "lowercase", "singular", "plural"],
"homepage": "https://www.doctrine-project.org/projects/inflector.html",
"license": "MIT", "license": "MIT",
"authors": [ "type": "library",
{"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"}, "keywords": [
{"name": "Roman Borschel", "email": "roman@code-factory.org"}, "php",
{"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"}, "strings",
{"name": "Jonathan Wage", "email": "jonwage@gmail.com"}, "words",
{"name": "Johannes Schmitt", "email": "schmittjoh@gmail.com"} "manipulation",
"inflector",
"inflection",
"uppercase",
"lowercase",
"singular",
"plural"
], ],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"homepage": "https://www.doctrine-project.org/projects/inflector.html",
"require": { "require": {
"php": "^7.2 || ^8.0" "php": "^7.2 || ^8.0"
}, },
"require-dev": { "require-dev": {
"doctrine/coding-standard": "^11.0", "doctrine/coding-standard": "^12.0 || ^13.0",
"phpstan/phpstan": "^1.8", "phpstan/phpstan": "^1.12 || ^2.0",
"phpstan/phpstan-phpunit": "^1.1", "phpstan/phpstan-phpunit": "^1.4 || ^2.0",
"phpstan/phpstan-strict-rules": "^1.3", "phpstan/phpstan-strict-rules": "^1.6 || ^2.0",
"phpunit/phpunit": "^8.5 || ^9.5", "phpunit/phpunit": "^8.5 || ^12.2"
"vimeo/psalm": "^4.25 || ^5.4"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Doctrine\\Inflector\\": "lib/Doctrine/Inflector" "Doctrine\\Inflector\\": "src"
} }
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"Doctrine\\Tests\\Inflector\\": "tests/Doctrine/Tests/Inflector" "Doctrine\\Tests\\Inflector\\": "tests"
} }
}, },
"config": { "config": {
"allow-plugins": { "allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true "dealerdirect/phpcodesniffer-composer-installer": true
} },
"sort-packages": true
} }
} }
+1
View File
@@ -39,6 +39,7 @@ you want to create an inflector for to the ``createForLanguage()`` method:
The supported languages are as follows: The supported languages are as follows:
- ``Language::ENGLISH`` - ``Language::ENGLISH``
- ``Language::ESPERANTO``
- ``Language::FRENCH`` - ``Language::FRENCH``
- ``Language::NORWEGIAN_BOKMAL`` - ``Language::NORWEGIAN_BOKMAL``
- ``Language::PORTUGUESE`` - ``Language::PORTUGUESE``
@@ -5,7 +5,9 @@ declare(strict_types=1);
namespace Doctrine\Inflector; namespace Doctrine\Inflector;
use Doctrine\Inflector\Rules\English; use Doctrine\Inflector\Rules\English;
use Doctrine\Inflector\Rules\Esperanto;
use Doctrine\Inflector\Rules\French; use Doctrine\Inflector\Rules\French;
use Doctrine\Inflector\Rules\Italian;
use Doctrine\Inflector\Rules\NorwegianBokmal; use Doctrine\Inflector\Rules\NorwegianBokmal;
use Doctrine\Inflector\Rules\Portuguese; use Doctrine\Inflector\Rules\Portuguese;
use Doctrine\Inflector\Rules\Spanish; use Doctrine\Inflector\Rules\Spanish;
@@ -27,9 +29,15 @@ final class InflectorFactory
case Language::ENGLISH: case Language::ENGLISH:
return new English\InflectorFactory(); return new English\InflectorFactory();
case Language::ESPERANTO:
return new Esperanto\InflectorFactory();
case Language::FRENCH: case Language::FRENCH:
return new French\InflectorFactory(); return new French\InflectorFactory();
case Language::ITALIAN:
return new Italian\InflectorFactory();
case Language::NORWEGIAN_BOKMAL: case Language::NORWEGIAN_BOKMAL:
return new NorwegianBokmal\InflectorFactory(); return new NorwegianBokmal\InflectorFactory();
@@ -7,7 +7,9 @@ namespace Doctrine\Inflector;
final class Language final class Language
{ {
public const ENGLISH = 'english'; public const ENGLISH = 'english';
public const ESPERANTO = 'esperanto';
public const FRENCH = 'french'; public const FRENCH = 'french';
public const ITALIAN = 'italian';
public const NORWEGIAN_BOKMAL = 'norwegian-bokmal'; public const NORWEGIAN_BOKMAL = 'norwegian-bokmal';
public const PORTUGUESE = 'portuguese'; public const PORTUGUESE = 'portuguese';
public const SPANISH = 'spanish'; public const SPANISH = 'spanish';
@@ -92,28 +92,35 @@ class Inflectible
/** @return Substitution[] */ /** @return Substitution[] */
public static function getIrregular(): iterable public static function getIrregular(): iterable
{ {
yield new Substitution(new Word('abuse'), new Word('abuses'));
yield new Substitution(new Word('alga'), new Word('algae'));
yield new Substitution(new Word('atlas'), new Word('atlases')); yield new Substitution(new Word('atlas'), new Word('atlases'));
yield new Substitution(new Word('avalanche'), new Word('avalanches'));
yield new Substitution(new Word('axis'), new Word('axes')); yield new Substitution(new Word('axis'), new Word('axes'));
yield new Substitution(new Word('axe'), new Word('axes')); yield new Substitution(new Word('axe'), new Word('axes'));
yield new Substitution(new Word('beef'), new Word('beefs')); yield new Substitution(new Word('beef'), new Word('beefs'));
yield new Substitution(new Word('blouse'), new Word('blouses')); yield new Substitution(new Word('blouse'), new Word('blouses'));
yield new Substitution(new Word('brother'), new Word('brothers')); yield new Substitution(new Word('brother'), new Word('brothers'));
yield new Substitution(new Word('brownie'), new Word('brownies'));
yield new Substitution(new Word('cache'), new Word('caches'));
yield new Substitution(new Word('cafe'), new Word('cafes')); yield new Substitution(new Word('cafe'), new Word('cafes'));
yield new Substitution(new Word('canvas'), new Word('canvases'));
yield new Substitution(new Word('cave'), new Word('caves')); yield new Substitution(new Word('cave'), new Word('caves'));
yield new Substitution(new Word('chateau'), new Word('chateaux')); yield new Substitution(new Word('chateau'), new Word('chateaux'));
yield new Substitution(new Word('niveau'), new Word('niveaux'));
yield new Substitution(new Word('child'), new Word('children')); yield new Substitution(new Word('child'), new Word('children'));
yield new Substitution(new Word('canvas'), new Word('canvases'));
yield new Substitution(new Word('cookie'), new Word('cookies')); yield new Substitution(new Word('cookie'), new Word('cookies'));
yield new Substitution(new Word('brownie'), new Word('brownies'));
yield new Substitution(new Word('corpus'), new Word('corpuses')); yield new Substitution(new Word('corpus'), new Word('corpuses'));
yield new Substitution(new Word('cow'), new Word('cows')); yield new Substitution(new Word('cow'), new Word('cows'));
yield new Substitution(new Word('criterion'), new Word('criteria')); yield new Substitution(new Word('criterion'), new Word('criteria'));
yield new Substitution(new Word('curriculum'), new Word('curricula')); yield new Substitution(new Word('curriculum'), new Word('curricula'));
yield new Substitution(new Word('curve'), new Word('curves'));
yield new Substitution(new Word('demo'), new Word('demos')); yield new Substitution(new Word('demo'), new Word('demos'));
yield new Substitution(new Word('die'), new Word('dice'));
yield new Substitution(new Word('domino'), new Word('dominoes')); yield new Substitution(new Word('domino'), new Word('dominoes'));
yield new Substitution(new Word('echo'), new Word('echoes')); yield new Substitution(new Word('echo'), new Word('echoes'));
yield new Substitution(new Word('emphasis'), new Word('emphases'));
yield new Substitution(new Word('epoch'), new Word('epochs')); yield new Substitution(new Word('epoch'), new Word('epochs'));
yield new Substitution(new Word('foe'), new Word('foes'));
yield new Substitution(new Word('foot'), new Word('feet')); yield new Substitution(new Word('foot'), new Word('feet'));
yield new Substitution(new Word('fungus'), new Word('fungi')); yield new Substitution(new Word('fungus'), new Word('fungi'));
yield new Substitution(new Word('ganglion'), new Word('ganglions')); yield new Substitution(new Word('ganglion'), new Word('ganglions'));
@@ -122,7 +129,9 @@ class Inflectible
yield new Substitution(new Word('genus'), new Word('genera')); yield new Substitution(new Word('genus'), new Word('genera'));
yield new Substitution(new Word('goose'), new Word('geese')); yield new Substitution(new Word('goose'), new Word('geese'));
yield new Substitution(new Word('graffito'), new Word('graffiti')); yield new Substitution(new Word('graffito'), new Word('graffiti'));
yield new Substitution(new Word('grave'), new Word('graves'));
yield new Substitution(new Word('hippopotamus'), new Word('hippopotami')); yield new Substitution(new Word('hippopotamus'), new Word('hippopotami'));
yield new Substitution(new Word('hoax'), new Word('hoaxes'));
yield new Substitution(new Word('hoof'), new Word('hoofs')); yield new Substitution(new Word('hoof'), new Word('hoofs'));
yield new Substitution(new Word('human'), new Word('humans')); yield new Substitution(new Word('human'), new Word('humans'));
yield new Substitution(new Word('iris'), new Word('irises')); yield new Substitution(new Word('iris'), new Word('irises'));
@@ -138,9 +147,13 @@ class Inflectible
yield new Substitution(new Word('motto'), new Word('mottoes')); yield new Substitution(new Word('motto'), new Word('mottoes'));
yield new Substitution(new Word('move'), new Word('moves')); yield new Substitution(new Word('move'), new Word('moves'));
yield new Substitution(new Word('mythos'), new Word('mythoi')); yield new Substitution(new Word('mythos'), new Word('mythoi'));
yield new Substitution(new Word('neurosis'), new Word('neuroses'));
yield new Substitution(new Word('niche'), new Word('niches')); yield new Substitution(new Word('niche'), new Word('niches'));
yield new Substitution(new Word('niveau'), new Word('niveaux'));
yield new Substitution(new Word('nucleus'), new Word('nuclei')); yield new Substitution(new Word('nucleus'), new Word('nuclei'));
yield new Substitution(new Word('numen'), new Word('numina')); yield new Substitution(new Word('numen'), new Word('numina'));
yield new Substitution(new Word('nursery'), new Word('nurseries'));
yield new Substitution(new Word('oasis'), new Word('oases'));
yield new Substitution(new Word('occiput'), new Word('occiputs')); yield new Substitution(new Word('occiput'), new Word('occiputs'));
yield new Substitution(new Word('octopus'), new Word('octopuses')); yield new Substitution(new Word('octopus'), new Word('octopuses'));
yield new Substitution(new Word('opus'), new Word('opuses')); yield new Substitution(new Word('opus'), new Word('opuses'));
@@ -151,10 +164,12 @@ class Inflectible
yield new Substitution(new Word('plateau'), new Word('plateaux')); yield new Substitution(new Word('plateau'), new Word('plateaux'));
yield new Substitution(new Word('runner-up'), new Word('runners-up')); yield new Substitution(new Word('runner-up'), new Word('runners-up'));
yield new Substitution(new Word('safe'), new Word('safes')); yield new Substitution(new Word('safe'), new Word('safes'));
yield new Substitution(new Word('save'), new Word('saves'));
yield new Substitution(new Word('sex'), new Word('sexes')); yield new Substitution(new Word('sex'), new Word('sexes'));
yield new Substitution(new Word('sieve'), new Word('sieves')); yield new Substitution(new Word('sieve'), new Word('sieves'));
yield new Substitution(new Word('soliloquy'), new Word('soliloquies')); yield new Substitution(new Word('soliloquy'), new Word('soliloquies'));
yield new Substitution(new Word('son-in-law'), new Word('sons-in-law')); yield new Substitution(new Word('son-in-law'), new Word('sons-in-law'));
yield new Substitution(new Word('stadium'), new Word('stadiums'));
yield new Substitution(new Word('syllabus'), new Word('syllabi')); yield new Substitution(new Word('syllabus'), new Word('syllabi'));
yield new Substitution(new Word('testis'), new Word('testes')); yield new Substitution(new Word('testis'), new Word('testes'));
yield new Substitution(new Word('thief'), new Word('thieves')); yield new Substitution(new Word('thief'), new Word('thieves'));
@@ -164,21 +179,7 @@ class Inflectible
yield new Substitution(new Word('turf'), new Word('turfs')); yield new Substitution(new Word('turf'), new Word('turfs'));
yield new Substitution(new Word('valve'), new Word('valves')); yield new Substitution(new Word('valve'), new Word('valves'));
yield new Substitution(new Word('volcano'), new Word('volcanoes')); yield new Substitution(new Word('volcano'), new Word('volcanoes'));
yield new Substitution(new Word('abuse'), new Word('abuses'));
yield new Substitution(new Word('avalanche'), new Word('avalanches'));
yield new Substitution(new Word('cache'), new Word('caches'));
yield new Substitution(new Word('criterion'), new Word('criteria'));
yield new Substitution(new Word('curve'), new Word('curves'));
yield new Substitution(new Word('emphasis'), new Word('emphases'));
yield new Substitution(new Word('foe'), new Word('foes'));
yield new Substitution(new Word('grave'), new Word('graves'));
yield new Substitution(new Word('hoax'), new Word('hoaxes'));
yield new Substitution(new Word('medium'), new Word('media'));
yield new Substitution(new Word('neurosis'), new Word('neuroses'));
yield new Substitution(new Word('save'), new Word('saves'));
yield new Substitution(new Word('wave'), new Word('waves')); yield new Substitution(new Word('wave'), new Word('waves'));
yield new Substitution(new Word('oasis'), new Word('oases'));
yield new Substitution(new Word('valve'), new Word('valves'));
yield new Substitution(new Word('zombie'), new Word('zombies')); yield new Substitution(new Word('zombie'), new Word('zombies'));
} }
} }
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\Inflector\Rules\Esperanto;
use Doctrine\Inflector\Rules\Pattern;
use Doctrine\Inflector\Rules\Substitution;
use Doctrine\Inflector\Rules\Transformation;
use Doctrine\Inflector\Rules\Word;
class Inflectible
{
/** @return Transformation[] */
public static function getSingular(): iterable
{
yield new Transformation(new Pattern('oj$'), 'o');
}
/** @return Transformation[] */
public static function getPlural(): iterable
{
yield new Transformation(new Pattern('o$'), 'oj');
}
/** @return Substitution[] */
public static function getIrregular(): iterable
{
yield new Substitution(new Word(''), new Word(''));
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\Inflector\Rules\Esperanto;
use Doctrine\Inflector\GenericLanguageInflectorFactory;
use Doctrine\Inflector\Rules\Ruleset;
final class InflectorFactory extends GenericLanguageInflectorFactory
{
protected function getSingularRuleset(): Ruleset
{
return Rules::getSingularRuleset();
}
protected function getPluralRuleset(): Ruleset
{
return Rules::getPluralRuleset();
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\Inflector\Rules\Esperanto;
use Doctrine\Inflector\Rules\Patterns;
use Doctrine\Inflector\Rules\Ruleset;
use Doctrine\Inflector\Rules\Substitutions;
use Doctrine\Inflector\Rules\Transformations;
final class Rules
{
public static function getSingularRuleset(): Ruleset
{
return new Ruleset(
new Transformations(...Inflectible::getSingular()),
new Patterns(...Uninflected::getSingular()),
(new Substitutions(...Inflectible::getIrregular()))->getFlippedSubstitutions()
);
}
public static function getPluralRuleset(): Ruleset
{
return new Ruleset(
new Transformations(...Inflectible::getPlural()),
new Patterns(...Uninflected::getPlural()),
new Substitutions(...Inflectible::getIrregular())
);
}
}
@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace Doctrine\Inflector\Rules\French; namespace Doctrine\Inflector\Rules\Esperanto;
use Doctrine\Inflector\Rules\Pattern; use Doctrine\Inflector\Rules\Pattern;
@@ -16,7 +16,7 @@ class Inflectible
{ {
yield new Transformation(new Pattern('/(b|cor|ém|gemm|soupir|trav|vant|vitr)aux$/'), '\1ail'); yield new Transformation(new Pattern('/(b|cor|ém|gemm|soupir|trav|vant|vitr)aux$/'), '\1ail');
yield new Transformation(new Pattern('/ails$/'), 'ail'); yield new Transformation(new Pattern('/ails$/'), 'ail');
yield new Transformation(new Pattern('/(journ|chev)aux$/'), '\1al'); yield new Transformation(new Pattern('/(journ|chev|loc)aux$/'), '\1al');
yield new Transformation(new Pattern('/(bijou|caillou|chou|genou|hibou|joujou|pou|au|eu|eau)x$/'), '\1'); yield new Transformation(new Pattern('/(bijou|caillou|chou|genou|hibou|joujou|pou|au|eu|eau)x$/'), '\1');
yield new Transformation(new Pattern('/s$/'), ''); yield new Transformation(new Pattern('/s$/'), '');
} }
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\Inflector\Rules\French;
use Doctrine\Inflector\Rules\Pattern;
final class Uninflected
{
/** @return Pattern[] */
public static function getSingular(): iterable
{
yield from self::getDefault();
yield new Pattern('bois');
yield new Pattern('mas');
}
/** @return Pattern[] */
public static function getPlural(): iterable
{
yield from self::getDefault();
}
/** @return Pattern[] */
private static function getDefault(): iterable
{
yield new Pattern('');
}
}
@@ -0,0 +1,218 @@
<?php
declare(strict_types=1);
namespace Doctrine\Inflector\Rules\Italian;
use Doctrine\Inflector\Rules\Pattern;
use Doctrine\Inflector\Rules\Substitution;
use Doctrine\Inflector\Rules\Transformation;
use Doctrine\Inflector\Rules\Word;
class Inflectible
{
/** @return iterable<Transformation> */
public static function getSingular(): iterable
{
// Reverse of -sce → -scia (fasce → fascia)
yield new Transformation(new Pattern('([aeiou])sce$'), '\\1scia');
// Reverse of -cie → -cia (farmacia → farmacie)
yield new Transformation(new Pattern('cie$'), 'cia');
// Reverse of -gie → -gia (bugia → bugie)
yield new Transformation(new Pattern('gie$'), 'gia');
// Reverse of -ce → -cia (arance → arancia)
yield new Transformation(new Pattern('([^aeiou])ce$'), '\1cia');
// Reverse of -ge → -gia (valige → valigia)
yield new Transformation(new Pattern('([^aeiou])ge$'), '\1gia');
// Reverse of -chi → -co (bachi → baco)
yield new Transformation(new Pattern('([bcdfghjklmnpqrstvwxyz][aeiou])chi$'), '\1co');
// Reverse of -ghi → -go (laghi → lago)
yield new Transformation(new Pattern('([bcdfghjklmnpqrstvwxyz][aeiou])ghi$'), '\1go');
// Reverse of -ci → -co (medici → medico)
yield new Transformation(new Pattern('([aeiou][bcdfghjklmnpqrstvwxyz])ci$'), '\1co');
// Reverse of -gi → -go (psicologi → psicologo)
yield new Transformation(new Pattern('([aeiou][bcdfghjklmnpqrstvwxyz])gi$'), '\1go');
// Reverse of -i → -io (zii → zio, negozi → negozio)
// This is more complex due to Italian's stress patterns, but we'll handle the basic case
yield new Transformation(new Pattern('([^aeiou])i$'), '\1io');
// Handle words that end with -i but should go to -co/-go (amici → amico, not amice)
yield new Transformation(new Pattern('([^aeiou])ci$'), '\1co');
yield new Transformation(new Pattern('([^aeiou])gi$'), '\1go');
// Reverse of -a → -e
yield new Transformation(new Pattern('e$'), 'a');
// Reverse of -e → -i
yield new Transformation(new Pattern('i$'), 'e');
// Reverse of -o → -i
yield new Transformation(new Pattern('i$'), 'o');
}
/** @return iterable<Transformation> */
public static function getPlural(): iterable
{
// Words ending in -scia without stress on 'i' become -sce (e.g. fascia → fasce)
yield new Transformation(new Pattern('([aeiou])scia$'), '\\1sce');
// Words ending in -cia/gia with stress on 'i' keep the 'i' in plural
yield new Transformation(new Pattern('cia$'), 'cie'); // e.g. farmacia → farmacie
yield new Transformation(new Pattern('gia$'), 'gie'); // e.g. bugia → bugie
// Words ending in -cia/gia without stress on 'i' lose the 'i' in plural
yield new Transformation(new Pattern('([^aeiou])cia$'), '\\1ce'); // e.g. arancia → arance
yield new Transformation(new Pattern('([^aeiou])gia$'), '\\1ge'); // e.g. valigia → valige
// Words ending in -co/-go with stress on 'o' become -chi/-ghi
yield new Transformation(new Pattern('([bcdfghjklmnpqrstvwxyz][aeiou])co$'), '\\1chi'); // e.g. baco → bachi
yield new Transformation(new Pattern('([bcdfghjklmnpqrstvwxyz][aeiou])go$'), '\\1ghi'); // e.g. lago → laghi
// Words ending in -co/-go with stress on the penultimate syllable become -ci/-gi
yield new Transformation(new Pattern('([aeiou][bcdfghjklmnpqrstvwxyz])co$'), '\\1ci'); // e.g. medico → medici
yield new Transformation(new Pattern('([aeiou][bcdfghjklmnpqrstvwxyz])go$'), '\\1gi'); // e.g. psicologo → psicologi
// Words ending in -io with stress on 'i' keep the 'i' in plural
yield new Transformation(new Pattern('([^aeiou])io$'), '\\1i'); // e.g. zio → zii
// Words ending in -io with stress on 'o' lose the 'i' in plural
yield new Transformation(new Pattern('([aeiou])io$'), '\\1i'); // e.g. negozio → negozi
// Standard ending rules
yield new Transformation(new Pattern('a$'), 'e'); // -a → -e
yield new Transformation(new Pattern('e$'), 'i'); // -e → -i
yield new Transformation(new Pattern('o$'), 'i'); // -o → -i
}
/** @return iterable<Substitution> */
public static function getIrregular(): iterable
{
// Irregular substitutions (singular => plural)
$irregulars = [
'ala' => 'ali',
'albergo' => 'alberghi',
'amica' => 'amiche',
'amico' => 'amici',
'ampio' => 'ampi',
'arancia' => 'arance',
'arma' => 'armi',
'asparago' => 'asparagi',
'banca' => 'banche',
'belga' => 'belgi',
'braccio' => 'braccia',
'budello' => 'budella',
'bue' => 'buoi',
'caccia' => 'cacce',
'calcagno' => 'calcagna',
'camicia' => 'camicie',
'cane' => 'cani',
'capitale' => 'capitali',
'carcere' => 'carceri',
'casa' => 'case',
'cavaliere' => 'cavalieri',
'centinaio' => 'centinaia',
'cerchio' => 'cerchia',
'cervello' => 'cervella',
'chiave' => 'chiavi',
'chirurgo' => 'chirurgi',
'ciglio' => 'ciglia',
'città' => 'città',
'corno' => 'corna',
'corpo' => 'corpi',
'crisi' => 'crisi',
'dente' => 'denti',
'dio' => 'dei',
'dito' => 'dita',
'dottore' => 'dottori',
'fiore' => 'fiori',
'fratello' => 'fratelli',
'fuoco' => 'fuochi',
'gamba' => 'gambe',
'ginocchio' => 'ginocchia',
'gioco' => 'giochi',
'giornale' => 'giornali',
'giraffa' => 'giraffe',
'labbro' => 'labbra',
'lenzuolo' => 'lenzuola',
'libro' => 'libri',
'madre' => 'madri',
'maestro' => 'maestri',
'magico' => 'magici',
'mago' => 'maghi',
'maniaco' => 'maniaci',
'manico' => 'manici',
'mano' => 'mani',
'medico' => 'medici',
'membro' => 'membri',
'metropoli' => 'metropoli',
'migliaio' => 'migliaia',
'miglio' => 'miglia',
'mille' => 'mila',
'mio' => 'miei',
'moglie' => 'mogli',
'mosaico' => 'mosaici',
'muro' => 'muri',
'nemico' => 'nemici',
'nome' => 'nomi',
'occhio' => 'occhi',
'orecchio' => 'orecchi',
'osso' => 'ossa',
'paio' => 'paia',
'pane' => 'pani',
'papa' => 'papi',
'pasta' => 'paste',
'penna' => 'penne',
'pesce' => 'pesci',
'piede' => 'piedi',
'pittore' => 'pittori',
'poeta' => 'poeti',
'porco' => 'porci',
'porto' => 'porti',
'problema' => 'problemi',
'ragazzo' => 'ragazzi',
're' => 're',
'rene' => 'reni',
'riso' => 'risa',
'rosa' => 'rosa',
'sale' => 'sali',
'sarto' => 'sarti',
'scuola' => 'scuole',
'serie' => 'serie',
'serramento' => 'serramenta',
'sorella' => 'sorelle',
'specie' => 'specie',
'staio' => 'staia',
'stazione' => 'stazioni',
'strido' => 'strida',
'strillo' => 'strilla',
'studio' => 'studi',
'suo' => 'suoi',
'superficie' => 'superfici',
'tavolo' => 'tavoli',
'tempio' => 'templi',
'treno' => 'treni',
'tuo' => 'tuoi',
'uomo' => 'uomini',
'uovo' => 'uova',
'urlo' => 'urla',
'valigia' => 'valigie',
'vestigio' => 'vestigia',
'vino' => 'vini',
'viola' => 'viola',
'zio' => 'zii',
];
foreach ($irregulars as $singular => $plural) {
yield new Substitution(new Word($singular), new Word($plural));
}
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\Inflector\Rules\Italian;
use Doctrine\Inflector\GenericLanguageInflectorFactory;
use Doctrine\Inflector\Rules\Ruleset;
final class InflectorFactory extends GenericLanguageInflectorFactory
{
protected function getSingularRuleset(): Ruleset
{
return Rules::getSingularRuleset();
}
protected function getPluralRuleset(): Ruleset
{
return Rules::getPluralRuleset();
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\Inflector\Rules\Italian;
use Doctrine\Inflector\Rules\Patterns;
use Doctrine\Inflector\Rules\Ruleset;
use Doctrine\Inflector\Rules\Substitutions;
use Doctrine\Inflector\Rules\Transformations;
final class Rules
{
public static function getSingularRuleset(): Ruleset
{
return new Ruleset(
new Transformations(...Inflectible::getSingular()),
new Patterns(...Uninflected::getSingular()),
(new Substitutions(...Inflectible::getIrregular()))->getFlippedSubstitutions()
);
}
public static function getPluralRuleset(): Ruleset
{
return new Ruleset(
new Transformations(...Inflectible::getPlural()),
new Patterns(...Uninflected::getPlural()),
new Substitutions(...Inflectible::getIrregular())
);
}
}
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Doctrine\Inflector\Rules\Italian;
use Doctrine\Inflector\Rules\Pattern;
final class Uninflected
{
/** @return iterable<Pattern> */
public static function getSingular(): iterable
{
yield from self::getDefault();
}
/** @return iterable<Pattern> */
public static function getPlural(): iterable
{
yield from self::getDefault();
}
/** @return iterable<Pattern> */
private static function getDefault(): iterable
{
// Invariable words (same form in singular and plural)
$invariables = [
'alpaca',
'auto',
'bar',
'blu',
'boia',
'boomerang',
'brindisi',
'campus',
'computer',
'crisi',
'crocevia',
'dopocena',
'film',
'foto',
'fuchsia',
'gnu',
'gorilla',
'gru',
'iguana',
'kamikaze',
'karaoke',
'koala',
'lama',
'menu',
'metropoli',
'moto',
'opossum',
'panda',
'quiz',
'radio',
're',
'scacciapensieri',
'serie',
'smartphone',
'sosia',
'sottoscala',
'specie',
'sport',
'tablet',
'taxi',
'vaglia',
'virtù',
'virus',
'yogurt',
'foto',
'fuchsia',
];
foreach ($invariables as $word) {
yield new Pattern($word);
}
}
}

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