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/*
.env
.phpunit.result.cache
/.phpunit.cache
+3 -3
View File
@@ -12,9 +12,9 @@
"ext-intl": "*",
"ext-json": "*",
"enshrined/svg-sanitize": "^0.22.0",
"graham-campbell/github": "^12.5",
"graham-campbell/github": "^13.0",
"guzzlehttp/guzzle": "^7.8",
"laravel/framework": "^11.45",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.9",
"laravel/ui": "^4.4",
"league/flysystem-aws-s3-v3": "^3.0",
@@ -27,7 +27,7 @@
"barryvdh/laravel-ide-helper": "^3.0",
"filp/whoops": "^2.8",
"mockery/mockery": "^1.6",
"phpunit/phpunit": "^10.5",
"phpunit/phpunit": "^11.0",
"squizlabs/php_codesniffer": "3.*",
"symfony/thanks": "^1.2",
"fakerphp/faker": "^1.23"
Generated
+1479 -1075
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -1,10 +1,5 @@
<?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">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./app</directory>
</include>
</coverage>
<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">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
@@ -21,8 +16,13 @@
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
<env name="MAIL_MAILER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="TELESCOPE_ENABLED" value="false"/>
</php>
<source>
<include>
<directory suffix=".php">./app</directory>
</include>
</source>
</phpunit>
+12 -2
View File
@@ -3,8 +3,18 @@
// autoload.php @generated by Composer
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;
exit(1);
if (!headers_sent()) {
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';
@@ -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
## 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
### What's Changed
+5 -7
View File
@@ -11,7 +11,7 @@
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.
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)
- [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 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!_
@@ -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
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`.
#### 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
+15 -14
View File
@@ -23,23 +23,24 @@
"require": {
"php": "^8.2",
"ext-json": "*",
"barryvdh/reflection-docblock": "^2.3",
"barryvdh/reflection-docblock": "^2.4",
"composer/class-map-generator": "^1.0",
"illuminate/console": "^11.15 || ^12",
"illuminate/database": "^11.15 || ^12",
"illuminate/filesystem": "^11.15 || ^12",
"illuminate/support": "^11.15 || ^12"
"illuminate/console": "^11.15 || ^12 || ^13.0",
"illuminate/database": "^11.15 || ^12 || ^13.0",
"illuminate/filesystem": "^11.15 || ^12 || ^13.0",
"illuminate/support": "^11.15 || ^12 || ^13.0"
},
"require-dev": {
"ext-pdo_sqlite": "*",
"friendsofphp/php-cs-fixer": "^3",
"illuminate/config": "^11.15 || ^12",
"illuminate/view": "^11.15 || ^12",
"illuminate/config": "^11.15 || ^12 || ^13.0",
"illuminate/view": "^11.15 || ^12 || ^13.0",
"larastan/larastan": "^3.1",
"mockery/mockery": "^1.4",
"orchestra/testbench": "^9.2 || ^10",
"phpunit/phpunit": "^10.5 || ^11.5.3",
"orchestra/testbench": "^9.2 || ^10 || ^11.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^10.5 || ^11.5.3 || ^12.5.12",
"spatie/phpunit-snapshot-assertions": "^4 || ^5",
"vimeo/psalm": "^5.4",
"vlucas/phpdotenv": "^5"
},
"suggest": {
@@ -65,7 +66,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "3.5-dev"
"dev-master": "3.6-dev"
},
"laravel": {
"providers": [
@@ -74,7 +75,8 @@
}
},
"scripts": {
"analyze": "psalm",
"analyze": "phpstan",
"analyze-set-baseline": "phpstan --generate-baseline",
"check-style": [
"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"
@@ -83,9 +85,8 @@
"php-cs-fixer fix",
"php-cs-fixer fix --config=.php-cs-fixer.tests.php"
],
"psalm-set-baseline": "psalm --set-baseline=psalm-baseline.xml",
"test": "phpunit",
"test-ci": "phpunit -d --without-creating-snapshots",
"test-ci": "phpunit",
"test-regenerate": "phpunit -d --update-snapshots"
}
}
+67 -45
View File
@@ -49,17 +49,14 @@ return [
/*
|--------------------------------------------------------------------------
| Factory builders
| Write model query methods
|--------------------------------------------------------------------------
|
| Set to true to generate factory generators for better factory()
| method auto-completion.
|
| Deprecated for Laravel 8 or latest.
| Set to false to disable generated docs for the 'query()', 'newQuery()' and 'newModelQuery()' methods.
|
*/
'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_exists_properties' => false,
/*
|--------------------------------------------------------------------------
@@ -202,29 +201,29 @@ return [
],
/*
|--------------------------------------------------------------------------
| Support for camel cased models
|--------------------------------------------------------------------------
|
| There are some Laravel packages (such as Eloquence) that allow for accessing
| Eloquent model properties via camel case, instead of snake case.
|
| Enabling this option will support these packages by saving all model
| properties as camel case, instead of snake case.
|
| For example, normally you would see this:
|
| * @property \Illuminate\Support\Carbon $created_at
| * @property \Illuminate\Support\Carbon $updated_at
|
| With this enabled, the properties will be this:
|
| * @property \Illuminate\Support\Carbon $createdAt
| * @property \Illuminate\Support\Carbon $updatedAt
|
| Note, it is currently an all-or-nothing option.
|
*/
|--------------------------------------------------------------------------
| Support for camel cased models
|--------------------------------------------------------------------------
|
| There are some Laravel packages (such as Eloquence) that allow for accessing
| Eloquent model properties via camel case, instead of snake case.
|
| Enabling this option will support these packages by saving all model
| properties as camel case, instead of snake case.
|
| For example, normally you would see this:
|
| * @property \Illuminate\Support\Carbon $created_at
| * @property \Illuminate\Support\Carbon $updated_at
|
| With this enabled, the properties will be this:
|
| * @property \Illuminate\Support\Carbon $createdAt
| * @property \Illuminate\Support\Carbon $updatedAt
|
| Note, it is currently an all-or-nothing option.
|
*/
'model_camel_case_properties' => false,
/*
@@ -274,6 +273,20 @@ return [
*/
'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
@@ -323,6 +336,29 @@ return [
'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
@@ -335,18 +371,4 @@ return [
// '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,56 +1,71 @@
<?php
function vsCodeGetTranslationsFromFile($file, $path, $namespace)
function vsCodeGetTranslationsFromFile(Symfony\Component\Finder\SplFileInfo $file, $path, $namespace)
{
$key = pathinfo($file, PATHINFO_FILENAME);
if ($namespace) {
$key = "{$namespace}::{$key}";
if ($file->getExtension() !== 'php') {
return null;
}
$lang = collect(explode(DIRECTORY_SEPARATOR, str_replace($path, '', $file)))
->filter()
->first();
$filePath = $file->getRealPath();
$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 = [];
$inComment = false;
foreach ($fileLines as $index => $line) {
$trimmed = trim($line);
if (substr($trimmed, 0, 2) === '/*') {
if (str_starts_with($trimmed, '/*')) {
$inComment = true;
continue;
}
if ($inComment) {
if (substr($trimmed, -2) !== '*/') {
continue;
if (str_ends_with($trimmed, '*/')) {
$inComment = false;
}
$inComment = false;
}
if (substr($trimmed, 0, 2) === '//') {
continue;
}
if (str_starts_with($trimmed, '//')) {
continue;
}
$lines[] = [$index + 1, $trimmed];
}
return [
'k' => $key,
'k' => $baseKey,
'la' => $lang,
'vs' => collect(Illuminate\Support\Arr::dot((Illuminate\Support\Arr::wrap(__($key, [], $lang)))))
->map(
fn ($value, $key) => vsCodeTranslationValue(
$key,
$value,
str_replace(base_path(DIRECTORY_SEPARATOR), '', $file),
$lines
)
'vs' => collect(Illuminate\Support\Arr::dot($translations))
->map(
fn ($value, $dotKey) => vsCodeTranslationValue(
$dotKey,
$value,
str_replace(base_path(DIRECTORY_SEPARATOR), '', $filePath),
$lines
)
)
->filter(),
];
}
@@ -63,13 +78,12 @@ function vsCodeTranslationValue($key, $value, $file, $lines): ?array
$lineNumber = 1;
$keys = explode('.', $key);
$index = 0;
$currentKey = array_shift($keys);
foreach ($lines as $index => $line) {
foreach ($lines as $line) {
if (
strpos($line[1], '"' . $currentKey . '"', 0) !== false ||
strpos($line[1], "'" . $currentKey . "'", 0) !== false
strpos($line[1], '"' . $currentKey . '"') !== false ||
strpos($line[1], "'" . $currentKey . "'") !== false
) {
$lineNumber = $line[0];
$currentKey = array_shift($keys);
@@ -98,9 +112,9 @@ function vscodeCollectTranslations(string $path, ?string $namespace = null)
return collect();
}
return collect(Illuminate\Support\Facades\File::allFiles($realPath))->map(
fn ($file) => vsCodeGetTranslationsFromFile($file, $path, $namespace)
);
return collect(Illuminate\Support\Facades\File::allFiles($realPath))
->map(fn ($file) => vsCodeGetTranslationsFromFile($file, $path, $namespace))
->filter();
}
$loader = app('translator')->getLoader();
@@ -110,7 +124,6 @@ $reflection = new ReflectionClass($loader);
$property = $reflection->hasProperty('paths')
? $reflection->getProperty('paths')
: $reflection->getProperty('path');
$property->setAccessible(true);
$paths = Illuminate\Support\Arr::wrap($property->getValue($loader));
@@ -125,6 +138,10 @@ $namespaced = collect($namespaces)->flatMap(
$final = [];
foreach ($default->merge($namespaced) as $value) {
if (!isset($value['vs']) || !is_iterable($value['vs'])) {
continue;
}
foreach ($value['vs'] as $key => $v) {
$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_extends_ns
* @var string[] $real_time_facades
* @var bool $include_fluent
* @var string $helpers
*/
@@ -29,7 +30,7 @@ $s3 = $s1 . $s2;
<?php foreach ($namespaces_by_extends_ns as $namespace => $aliases) : ?>
namespace <?= $namespace === '__root' ? '' : trim($namespace, '\\') ?> {
<?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) : ?>
<?= trim($method->getDocComment($s2)) . "\n{$s2}" ?>public static function <?= $method->getName() ?>(<?= $method->getParamsWithDefault() ?>)
{<?php if ($method->getDeclaringClass() !== $method->getRoot()) : ?>
@@ -76,7 +77,7 @@ namespace <?= $namespace === '__root' ? '' : trim($namespace, '\\') ?> {
<?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)); ?>
namespace <?php echo implode('\\', $nested); ?> {
/**
@@ -119,12 +120,3 @@ namespace Illuminate\Support {
}
<?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
/**
* @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 */
// @formatter:off
@@ -35,15 +47,6 @@ namespace PHPSTORM_META {
]));
<?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::partialMock(0), map(["" => "@&\Mockery\MockInterface"]));
override(\Illuminate\Foundation\Testing\Concerns\InteractsWithContainer::instance(0), type(1));
@@ -79,15 +82,15 @@ namespace PHPSTORM_META {
override(\tap(0), type(0));
override(\optional(0), type(0));
<?php if (isset($expectedArgumentSets)): ?>
<?php if ($expectedArgumentSets): ?>
<?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";
} ?><?= var_export($arg, true); ?>,<?php endforeach; ?>);
<?php endforeach; ?>
<?php endif ?>
<?php if (isset($expectedArguments)) : ?>
<?php if ($expectedArguments) : ?>
<?php foreach ($expectedArguments as $arguments) : ?>
<?php
$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\Query\Builder as QueryBuilder;
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\Traits\Macroable;
use ReflectionClass;
use Throwable;
@@ -36,6 +37,7 @@ class Alias
protected $classType = 'class';
protected $short;
protected $namespace = '__root';
protected $parentClass;
protected $root = null;
protected $classes = [];
protected $methods = [];
@@ -46,8 +48,6 @@ class Alias
protected $phpdoc = null;
protected $classAliases = [];
protected $isMacroable = false;
/** @var ConfigRepository */
protected $config;
@@ -62,13 +62,12 @@ class Alias
* @param array $magicMethods
* @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->magicMethods = $magicMethods;
$this->interfaces = $interfaces;
$this->config = $config;
$this->isMacroable = $isMacroable;
// Make the class absolute
$facade = '\\' . ltrim($facade, '\\');
@@ -87,6 +86,7 @@ class Alias
$this->detectNamespace();
$this->detectClassType();
$this->detectExtendsNamespace();
$this->detectParentClass();
if (!empty($this->namespace)) {
try {
@@ -101,7 +101,7 @@ class Alias
}
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;
}
/**
* 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
*
@@ -229,11 +248,17 @@ class Alias
return;
}
$reflection = new \ReflectionMethod($facade, 'fake');
if ($reflection->getNumberOfRequiredParameters() > 0) {
return;
}
$real = $facade::getFacadeRoot();
try {
$facade::fake();
$fake = $facade::getFacadeRoot();
if ($fake !== $real) {
$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
*/
@@ -288,9 +325,8 @@ class Alias
/**
* Get the real root of a facade
*
* @return bool|string
*/
protected function detectRoot()
protected function detectRoot(): void
{
$facade = $this->facade;
@@ -345,11 +381,10 @@ class Alias
$method = new \ReflectionMethod($className, $name);
$class = new ReflectionClass($className);
if (!in_array($magic, $this->usedMethods)) {
if (!isset($this->usedMethods[$magic])) {
if ($class !== $this->root) {
$this->methods[] = new Method(
$method,
$this->alias,
$class,
$magic,
$this->interfaces,
@@ -358,7 +393,7 @@ class Alias
$this->getTemplateNames()
);
}
$this->usedMethods[] = $magic;
$this->usedMethods[$magic] = true;
}
}
}
@@ -366,7 +401,6 @@ class Alias
/**
* Get the methods for one or multiple classes.
*
* @return string
*/
protected function detectMethods()
{
@@ -376,13 +410,12 @@ class Alias
$methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
if ($methods) {
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.
// And don't add the __*() methods
if ($this->extends !== $class && substr($method->name, 0, 2) !== '__') {
$this->methods[] = new Method(
$method,
$this->alias,
$reflection,
$method->name,
$this->interfaces,
@@ -391,18 +424,18 @@ class Alias
$this->getTemplateNames(),
);
}
$this->usedMethods[] = $method->name;
$this->usedMethods[$method->name] = true;
}
}
}
// Check if the class is macroable
// (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();
$macros = isset($properties['macros']) ? $properties['macros'] : [];
foreach ($macros as $macro_name => $macro_func) {
if (!in_array($macro_name, $this->usedMethods)) {
if (!isset($this->usedMethods[$macro_name])) {
try {
$method = $this->getMacroFunction($macro_func);
} catch (Throwable $e) {
@@ -412,14 +445,13 @@ class Alias
// Add macros
$this->methods[] = new Macro(
$method,
$this->alias,
$reflection,
$macro_name,
$this->interfaces,
$this->classAliases,
$this->getReturnTypeNormalizers($reflection)
);
$this->usedMethods[] = $macro_name;
$this->usedMethods[$macro_name] = true;
}
}
}
@@ -557,12 +589,13 @@ class Alias
*/
protected function removeDuplicateMethodsFromPhpDoc()
{
$methodNames = array_map(function (Method $method) {
return $method->getName();
}, $this->getMethods());
$methodNames = [];
foreach ($this->getMethods() as $method) {
$methodNames[$method->getName()] = true;
}
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);
}
}
@@ -11,7 +11,6 @@
namespace Barryvdh\LaravelIdeHelper\Console;
use Barryvdh\LaravelIdeHelper\Factories;
use Dotenv\Parser\Entry;
use Dotenv\Parser\Parser;
use Illuminate\Console\Command;
@@ -108,9 +107,6 @@ class MetaCommand extends Command
*/
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();
$bindings = [];
@@ -149,7 +145,6 @@ class MetaCommand extends Command
$content = $this->view->make('ide-helper::meta', [
'bindings' => $bindings,
'methods' => $this->methods,
'factories' => $factories,
'configMethods' => $this->configMethods,
'configValues' => $configValues,
'expectedArgumentSets' => $this->getExpectedArgumentSets(),
@@ -192,10 +187,31 @@ class MetaCommand extends Command
*/
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.");
};
spl_autoload_register($autoloader);
return $autoloader;
}
@@ -114,6 +114,7 @@ class ModelsCommand extends Command
protected $write_model_magic_where;
protected $write_model_relation_count_properties;
protected $write_model_relation_exists_properties;
protected $properties = [];
protected $methods = [];
protected $write = false;
@@ -122,6 +123,14 @@ class ModelsCommand extends Command
protected $reset;
protected $phpstorm_noinspections;
protected $write_model_external_builder_methods;
/**
* @var array<string, \SplFileObject>
*/
protected $fileCache = [];
/**
* @var array<string, Context>
*/
protected $contextCache = [];
/**
* @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_relation_count_properties =
$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;
//If filename is default and Write is not specified, ask what to do
@@ -410,9 +421,6 @@ class ModelsCommand extends Command
$params = [];
switch ($type) {
case 'encrypted':
$realType = 'mixed';
break;
case 'boolean':
case 'bool':
$realType = 'bool';
@@ -420,6 +428,7 @@ class ModelsCommand extends Command
case 'decimal':
$realType = 'numeric';
break;
case 'encrypted':
case 'string':
case 'hashed':
$realType = 'string';
@@ -453,7 +462,7 @@ class ModelsCommand extends Command
$realType = '\Illuminate\Support\Collection<array-key, mixed>';
break;
case AsArrayObject::class:
$realType = '\ArrayObject<array-key, mixed>';
$realType = '\Illuminate\Database\Eloquent\Casts\ArrayObject<array-key, mixed>';
break;
default:
// In case of an optional custom cast parameter , only evaluate
@@ -475,7 +484,11 @@ class ModelsCommand extends Command
}
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)) {
@@ -521,7 +534,7 @@ class ModelsCommand extends Command
}
if ($isNullable) {
$type .= '|null';
$type = $this->wrapIntersectionType($type) . '|null';
} else {
$type = str_replace($nullString, '', $type);
}
@@ -529,6 +542,22 @@ class ModelsCommand extends Command
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.
*
@@ -579,6 +608,8 @@ class ModelsCommand extends Command
'float', 'real', 'float4',
'double', 'float8' => 'float',
'decimal', 'numeric' => 'numeric',
default => 'string',
};
}
@@ -669,9 +700,11 @@ class ModelsCommand extends Command
$comment = $this->getCommentFromDocBlock($reflection);
$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
$name = Str::camel(substr($method, 5));
$name = $scopeUsingAttribute ? $method : Str::camel(substr($method, 5));
if (!empty($name)) {
$comment = $this->getCommentFromDocBlock($reflection);
$args = $this->getParameters($reflection);
@@ -687,13 +720,16 @@ class ModelsCommand extends Command
);
$this->setMethod($name, $builder . '<static>|' . $modelName, $args, $comment);
}
} elseif (in_array($method, ['query', 'newQuery', 'newModelQuery'])) {
$builder = $this->getClassNameInDestinationFile($model, get_class($model->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()));
$this->setMethod(
$method,
$builder . '<static>|' . $this->getClassNameInDestinationFile($model, get_class($model))
);
$this->setMethod(
$method,
$builder . '<static>|' . $this->getClassNameInDestinationFile($model, get_class($model))
);
}
if ($this->write_model_external_builder_methods) {
$this->writeModelExternalBuilderMethods($model);
@@ -712,14 +748,19 @@ class ModelsCommand extends Command
$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);
$code = '';
$lines = [];
while ($file->key() < $reflection->getEndLine()) {
$code .= $file->current();
$lines[] = $file->current();
$file->next();
}
$code = implode('', $lines);
$code = trim(preg_replace('/\s\s+/', '', $code));
$begin = strpos($code, 'function(');
$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?
);
}
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 (
$relationReturnType === 'morphTo' ||
(
@@ -871,7 +921,6 @@ class ModelsCommand extends Command
if (in_array($relation, ['hasOne', 'hasOneThrough', 'morphOne'], true)) {
$defaultProp = $reflectionObj->getProperty('withDefault');
$defaultProp->setAccessible(true);
return !$defaultProp->getValue($relationObj);
}
@@ -881,7 +930,6 @@ class ModelsCommand extends Command
}
$fkProp = $reflectionObj->getProperty('foreignKey');
$fkProp->setAccessible(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;
}
/**
* 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
*
@@ -914,7 +984,6 @@ class ModelsCommand extends Command
}
$fkProp = $reflectionObj->getProperty('foreignKey');
$fkProp->setAccessible(true);
foreach (Arr::wrap($fkProp->getValue($relationObj)) as $foreignKey) {
if (isset($this->nullableColumns[$foreignKey])) {
@@ -945,7 +1014,7 @@ class ModelsCommand extends Command
if ($type !== null) {
$newType = $this->getTypeOverride($type);
if ($nullable) {
$newType .= '|null';
$newType = $this->wrapIntersectionType($newType) . '|null';
}
$this->properties[$name]['type'] = $newType;
}
@@ -1096,6 +1165,7 @@ class ModelsCommand extends Command
$serializer = new DocBlockSerializer();
$docComment = $serializer->getDocComment($phpdoc);
$mixinClassName = null;
if ($this->write_mixin) {
$phpdocMixin = new DocBlock($reflection, new Context($namespace));
@@ -1132,6 +1202,14 @@ class ModelsCommand extends Command
$replace = "{$modelDocComment}\n";
$pos = strpos($contents, "final class {$classname}") ?: strpos($contents, "class {$classname}");
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);
}
}
@@ -1184,7 +1262,7 @@ class ModelsCommand extends Command
$default = '[]';
} elseif (is_null($default)) {
$default = 'null';
} elseif (is_int($default)) {
} elseif (is_int($default) || is_float($default)) {
//$default = $default;
} elseif ($default instanceof \UnitEnum) {
$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
*/
protected function getRelationTypes(): array
{
$configuredRelations = $this->laravel['config']->get('ide-helper.additional_relation_types', []);
return array_merge(self::RELATION_TYPES, $configuredRelations);
return $this->cachedRelationTypes ??= array_merge(
self::RELATION_TYPES,
$this->laravel['config']->get('ide-helper.additional_relation_types', [])
);
}
/**
@@ -1251,7 +1334,7 @@ class ModelsCommand extends Command
*/
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
{
// Private/protected ReflectionMethods require setAccessible prior to PHP 8.1
$reflectionMethod->setAccessible(true);
/** @var Attribute $attribute */
$attribute = $reflectionMethod->invoke($model);
@@ -1329,11 +1409,7 @@ class ModelsCommand extends Command
*/
protected function getCommentFromDocBlock(\ReflectionMethod $reflection)
{
$phpDocContext = (new ContextFactory())->createFromReflector($reflection);
$context = new Context(
$phpDocContext->getNamespace(),
$phpDocContext->getNamespaceAliases()
);
$context = $this->getDocBlockContext($reflection);
$comment = '';
$phpdoc = new DocBlock($reflection, $context);
@@ -1354,11 +1430,7 @@ class ModelsCommand extends Command
*/
protected function getReturnTypeFromDocBlock(\ReflectionMethod $reflection, ?\Reflector $reflectorForContext = null)
{
$phpDocContext = (new ContextFactory())->createFromReflector($reflectorForContext ?? $reflection);
$context = new Context(
$phpDocContext->getNamespace(),
$phpDocContext->getNamespaceAliases()
);
$context = $this->getDocBlockContext($reflectorForContext ?? $reflection);
$type = null;
$phpdoc = new DocBlock($reflection, $context);
@@ -1376,6 +1448,31 @@ class ModelsCommand extends Command
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
{
$returnType = $reflection->getReturnType();
@@ -1405,7 +1502,7 @@ class ModelsCommand extends Command
if (in_array('Illuminate\\Database\\Eloquent\\SoftDeletes', $traits)) {
$modelName = $this->getClassNameInDestinationFile($model, get_class($model));
$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('onlyTrashed', $builder . '<static>|' . $modelName, []);
}
@@ -1581,9 +1678,10 @@ class ModelsCommand extends Command
*/
protected function getUsedClassNames(ReflectionClass $reflection): array
{
$context = $this->getDocBlockContext($reflection);
$namespaceAliases = array_flip(array_map(function ($alias) {
return ltrim($alias, '\\');
}, (new ContextFactory())->createFromReflector($reflection)->getNamespaceAliases()));
}, $context->getNamespaceAliases()));
$namespaceAliases[$reflection->getName()] = $reflection->getShortName();
return $namespaceAliases;
@@ -1627,7 +1725,8 @@ class ModelsCommand extends Command
$type = implode('|', $types);
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;
} else {
$type .= '|null';
@@ -1645,7 +1744,7 @@ class ModelsCommand extends Command
preg_match(
'/@param ((?:(?:[\w?|\\\\<>])+(?:\[])?)+)/',
$docComment ?? '',
$docComment,
$matches
);
$type = $matches[1] ?? '';
@@ -1700,24 +1799,53 @@ class ModelsCommand extends Command
return $type;
}
protected function extractReflectionTypes(ReflectionType $reflection_type)
protected function extractReflectionTypes(ReflectionType $reflection_type): array
{
if ($reflection_type instanceof ReflectionNamedType) {
$types[] = $this->getReflectionNamedType($reflection_type);
} else {
$types = [];
foreach ($reflection_type->getTypes() as $named_type) {
if ($named_type->getName() === 'null') {
return [$this->getReflectionNamedType($reflection_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($named_type);
$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;
}
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
{
$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 $interfaces = [];
protected $helpers;
protected array $macroableTraits = [];
/**
* @param \Illuminate\Config\Repository $config
@@ -58,9 +57,13 @@ class Generator
// Find the drivers to add to the extra/interfaces
$this->detectDrivers();
$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->interfaces = array_merge($this->interfaces, $this->config->get('ide-helper.interfaces'), []);
$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->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
foreach ($this->interfaces as &$interface) {
$interface = '\\' . ltrim($interface, '\\');
@@ -71,7 +74,7 @@ class Generator
/**
* Generate the helper file contents;
*
* @return string;
* @return string
*/
public function generate()
{
@@ -82,7 +85,6 @@ class Generator
->with('real_time_facades', $this->getRealTimeFacades())
->with('helpers', $this->detectHelpers())
->with('include_fluent', $this->config->get('ide-helper.include_fluent', true))
->with('factories', $this->config->get('ide-helper.include_factory_builders') ? Factories::all() : [])
->render();
}
@@ -360,7 +362,7 @@ class Generator
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) {
$traits = class_uses_recursive($class);
if (isset($traits[Macroable::class])) {
return true;
}
// 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 only classes with the macroable trait
return isset($traits[Macroable::class]);
})
->filter(function ($class) use ($aliases) {
$class = Str::start($class, '\\');
+46 -16
View File
@@ -5,20 +5,16 @@ namespace Barryvdh\LaravelIdeHelper;
use Barryvdh\Reflection\DocBlock;
use Barryvdh\Reflection\DocBlock\Tag;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Collection;
class Macro extends Method
{
protected $macroDefaults = [
\Illuminate\Http\Client\Factory::class => PendingRequest::class,
];
protected static $macroDefaults = [];
/**
* Macro constructor.
*
* @param \ReflectionFunctionAbstract $method
* @param string $alias
* @param \ReflectionClass $class
* @param null $methodName
* @param array $interfaces
@@ -27,16 +23,19 @@ class Macro extends Method
*/
public function __construct(
$method,
$alias,
$class,
$methodName = null,
$interfaces = [],
$classAliases = [],
$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
*/
@@ -74,8 +73,8 @@ class Macro extends Method
$type = $this->concatReflectionTypes($return);
/** @psalm-suppress UndefinedClass */
if (!$return instanceof \ReflectionUnionType) {
/** @phpstan-ignore method.notFound */
$type .= $this->root === "\\{$builder}" && $return->getName() === $builder ? '|static' : '';
$type .= $return->allowsNull() ? '|null' : '';
}
@@ -84,25 +83,56 @@ class Macro extends Method
}
$class = ltrim($this->declaringClassName, '\\');
if (!$this->phpdoc->hasTag('return') && isset($this->macroDefaults[$class])) {
$type = $this->macroDefaults[$class];
if (!$this->phpdoc->hasTag('return') && isset(static::$macroDefaults[$class])) {
$type = static::$macroDefaults[$class];
$this->phpdoc->appendTag(Tag::createInstance("@return {$type}"));
}
}
protected function concatReflectionTypes(?\ReflectionType $type): string
{
/** @psalm-suppress UndefinedClass */
$returnTypes = $type instanceof \ReflectionUnionType
? $type->getTypes()
: [$type];
if ($type instanceof \ReflectionNamedType) {
return $type->getName();
}
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()
->map->getName()
->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()
{
if ($this->method->name === '__invoke') {
+3 -4
View File
@@ -44,7 +44,6 @@ class Method
/**
* @param \ReflectionMethod|\ReflectionFunctionAbstract $method
* @param string $alias
* @param \ReflectionClass $class
* @param string|null $methodName
* @param array $interfaces
@@ -52,7 +51,7 @@ class Method
* @param array $returnTypeNormalizers
* @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->interfaces = $interfaces;
@@ -178,7 +177,7 @@ class 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
*/
public function getParams($implode = true)
@@ -208,7 +207,7 @@ class Method
/**
* 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
*/
public function getParamsWithDefault($implode = true)
@@ -7,8 +7,6 @@ on:
pull_request:
branches:
- "*"
schedule:
- cron: '0 0 * * *'
jobs:
php-tests:
@@ -206,8 +206,9 @@ class Serializer
$text = wordwrap($text, $wrapLength);
}
$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());
@@ -220,6 +221,7 @@ class Serializer
$tagText = wordwrap($tagText, $wrapLength);
}
$tagText = str_replace("\n", "\n{$indent} * ", $tagText);
$tagText = preg_replace('/^(\s*\*)[ \t]+$/m', '$1', $tagText);
$comment .= "{$indent} * {$tagText}\n";
@@ -409,6 +409,6 @@ class Tag implements \Reflector
*/
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_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/nesbot/carbon/bin/carbon');
exit(0);
return include("phpvfscomposer://" . __DIR__ . '/..'.'/nesbot/carbon/bin/carbon');
}
}
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_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations');
exit(0);
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations');
}
}
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_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcbf');
exit(0);
return include("phpvfscomposer://" . __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcbf');
}
}
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_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcs');
exit(0);
return include("phpvfscomposer://" . __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcs');
}
}
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_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/phpunit/phpunit/phpunit');
exit(0);
return include("phpvfscomposer://" . __DIR__ . '/..'.'/phpunit/phpunit/phpunit');
}
}
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_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/psy/psysh/bin/psysh');
exit(0);
return include("phpvfscomposer://" . __DIR__ . '/..'.'/psy/psysh/bin/psysh');
}
}
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_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server');
exit(0);
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server');
}
}
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_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint');
exit(0);
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint');
}
}
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.
## [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
**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
First beta release.
+3 -3
View File
@@ -19,12 +19,12 @@
],
"license": "MIT",
"require": {
"php": "^8.1"
"php": "^8.2"
},
"require-dev": {
"phpunit/phpunit": "^10.1",
"phpunit/phpunit": "^11.5",
"php-coveralls/php-coveralls": "^2.2",
"vimeo/psalm": "6.8.8"
"phpstan/phpstan": "2.1.22"
},
"autoload": {
"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>
+338 -155
View File
@@ -7,15 +7,36 @@ namespace Brick\Math;
use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NegativeNumberException;
use Brick\Math\Exception\RoundingNecessaryException;
use Brick\Math\Internal\Calculator;
use Brick\Math\Internal\CalculatorRegistry;
use InvalidArgumentException;
use LogicException;
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.
@@ -24,20 +45,22 @@ final class BigDecimal extends BigNumber
* No leading zero must be present.
* 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.
*
* This must be zero or more.
*/
private readonly int $scale;
private int $scale;
/**
* Protected constructor. Use a factory method to obtain an instance.
*
* @param string $value The unscaled value, validated.
* @param int $scale The scale, validated.
*
* @pure
*/
protected function __construct(string $value, int $scale = 0)
{
@@ -45,47 +68,45 @@ final class BigDecimal extends BigNumber
$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.
*
* 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 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) {
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.
*
* @psalm-pure
* @pure
*/
public static function zero() : BigDecimal
public static function zero(): BigDecimal
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigDecimal|null $zero
*/
/** @var BigDecimal|null $zero */
static $zero;
if ($zero === null) {
@@ -98,14 +119,11 @@ final class BigDecimal extends BigNumber
/**
* Returns a BigDecimal representing one, with a scale of zero.
*
* @psalm-pure
* @pure
*/
public static function one() : BigDecimal
public static function one(): BigDecimal
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigDecimal|null $one
*/
/** @var BigDecimal|null $one */
static $one;
if ($one === null) {
@@ -118,14 +136,11 @@ final class BigDecimal extends BigNumber
/**
* Returns a BigDecimal representing ten, with a scale of zero.
*
* @psalm-pure
* @pure
*/
public static function ten() : BigDecimal
public static function ten(): BigDecimal
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigDecimal|null $ten
*/
/** @var BigDecimal|null $ten */
static $ten;
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.
*
* @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);
@@ -158,8 +175,8 @@ final class BigDecimal extends BigNumber
[$a, $b] = $this->scaleValues($this, $that);
$value = Calculator::get()->add($a, $b);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
$value = CalculatorRegistry::get()->add($a, $b);
$scale = max($this->scale, $that->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.
*
* @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);
@@ -183,8 +202,8 @@ final class BigDecimal extends BigNumber
[$a, $b] = $this->scaleValues($this, $that);
$value = Calculator::get()->sub($a, $b);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
$value = CalculatorRegistry::get()->sub($a, $b);
$scale = max($this->scale, $that->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.
*
* @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);
@@ -210,7 +231,7 @@ final class BigDecimal extends BigNumber
return $that;
}
$value = Calculator::get()->mul($this->value, $that->value);
$value = CalculatorRegistry::get()->mul($this->value, $that->value);
$scale = $this->scale + $that->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.
*
* @param BigNumber|int|float|string $that The divisor.
* @param int|null $scale The desired scale, or null to use the scale of this number.
* @param RoundingMode $roundingMode An optional rounding mode, defaults to UNNECESSARY.
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
* @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.
*
* @throws \InvalidArgumentException If the scale or rounding mode is invalid.
* @throws MathException If the number is invalid, is zero, or rounding was necessary.
* @throws InvalidArgumentException If the scale is negative.
* @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);
@@ -235,9 +261,15 @@ final class BigDecimal extends BigNumber
}
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;
} 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) {
@@ -247,7 +279,7 @@ final class BigDecimal extends BigNumber
$p = $this->valueWithMinScale($that->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);
}
@@ -257,12 +289,37 @@ final class BigDecimal extends BigNumber
*
* 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.
*
* @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.
*/
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);
@@ -272,13 +329,13 @@ final class BigDecimal extends BigNumber
[, $b] = $this->scaleValues($this, $that);
$d = \rtrim($b, '0');
$scale = \strlen($b) - \strlen($d);
$d = rtrim($b, '0');
$scale = strlen($b) - strlen($d);
$calculator = Calculator::get();
$calculator = CalculatorRegistry::get();
foreach ([5, 2] as $prime) {
for (;;) {
for (; ;) {
$lastDigit = (int) $d[-1];
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`.
*
* @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) {
return BigDecimal::one();
@@ -311,14 +370,14 @@ final class BigDecimal extends BigNumber
}
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.',
$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`.
*
* 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.
*
* @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);
@@ -341,7 +410,7 @@ final class BigDecimal extends BigNumber
$p = $this->valueWithMinScale($that->scale);
$q = $that->valueWithMinScale($this->scale);
$quotient = Calculator::get()->divQ($p, $q);
$quotient = CalculatorRegistry::get()->divQ($p, $q);
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.
*
* 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.
*
* @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);
@@ -366,9 +446,9 @@ final class BigDecimal extends BigNumber
$p = $this->valueWithMinScale($that->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);
}
@@ -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)`.
*
* 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.
*
* @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);
@@ -397,9 +485,9 @@ final class BigDecimal extends BigNumber
$p = $this->valueWithMinScale($that->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);
$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.
* @throws NegativeNumberException If this number 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 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) {
throw new \InvalidArgumentException('Scale cannot be negative.');
throw new InvalidArgumentException('Scale must not be negative.');
}
if ($this->value === '0') {
@@ -428,30 +535,48 @@ final class BigDecimal extends BigNumber
}
$value = $this->value;
$addDigits = 2 * $scale - $this->scale;
$inputScale = $this->scale;
if ($addDigits > 0) {
// add zeros
$value .= \str_repeat('0', $addDigits);
} 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);
if ($inputScale % 2 !== 0) {
$value .= '0';
$inputScale++;
}
$value = Calculator::get()->sqrt($value);
$calculator = CalculatorRegistry::get();
return new BigDecimal($value, $scale);
// 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();
}
// 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');
}
// 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) {
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) {
return $this;
@@ -482,7 +609,7 @@ final class BigDecimal extends BigNumber
if ($scale < 0) {
if ($value !== '0') {
$value .= \str_repeat('0', -$scale);
$value .= str_repeat('0', -$scale);
}
$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.
*
* @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) {
return $this;
}
$trimmedValue = \rtrim($this->value, '0');
$trimmedValue = rtrim($this->value, '0');
if ($trimmedValue === '') {
return BigDecimal::zero();
}
$trimmableZeros = \strlen($this->value) - \strlen($trimmedValue);
$trimmableZeros = strlen($this->value) - strlen($trimmedValue);
if ($trimmableZeros === 0) {
return $this;
@@ -515,30 +659,20 @@ final class BigDecimal extends BigNumber
$trimmableZeros = $this->scale;
}
$value = \substr($this->value, 0, -$trimmableZeros);
$value = substr($this->value, 0, -$trimmableZeros);
$scale = $this->scale - $trimmableZeros;
return new BigDecimal($value, $scale);
}
/**
* Returns the absolute value of this number.
*/
public function abs() : BigDecimal
#[Override]
public function negated(): static
{
return $this->isNegative() ? $this->negated() : $this;
}
/**
* Returns the negated value of this number.
*/
public function negated() : BigDecimal
{
return new BigDecimal(Calculator::get()->neg($this->value), $this->scale);
return new BigDecimal(CalculatorRegistry::get()->neg($this->value), $this->scale);
}
#[Override]
public function compareTo(BigNumber|int|float|string $that) : int
public function compareTo(BigNumber|int|float|string $that): int
{
$that = BigNumber::of($that);
@@ -549,24 +683,30 @@ final class BigDecimal extends BigNumber
if ($that instanceof BigDecimal) {
[$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]
public function getSign() : int
public function getSign(): int
{
return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1);
}
public function getUnscaledValue() : BigInteger
/**
* @pure
*/
public function getUnscaledValue(): BigInteger
{
return self::newBigInteger($this->value);
}
public function getScale() : int
/**
* @pure
*/
public function getScale(): int
{
return $this->scale;
}
@@ -584,6 +724,8 @@ final class BigDecimal extends BigNumber
* 123.456 => 6
* 0.00123 => 3
* 0.0012300 => 5
*
* @pure
*/
public function getPrecision(): int
{
@@ -593,7 +735,7 @@ final class BigDecimal extends BigNumber
return 0;
}
$length = \strlen($value);
$length = strlen($value);
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.
*
* 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) {
return $this->value;
}
$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.
*
* 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) {
return '';
}
$value = $this->getUnscaledValueWithLeadingZeros();
return \substr($value, -$this->scale);
return substr($value, -$this->scale);
}
/**
* 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]
public function toBigInteger() : BigInteger
public function toBigInteger(): BigInteger
{
$zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0);
@@ -649,22 +813,22 @@ final class BigDecimal extends BigNumber
}
#[Override]
public function toBigDecimal() : BigDecimal
public function toBigDecimal(): BigDecimal
{
return $this;
}
#[Override]
public function toBigRational() : BigRational
public function toBigRational(): BigRational
{
$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);
}
#[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) {
return $this;
@@ -674,27 +838,32 @@ final class BigDecimal extends BigNumber
}
#[Override]
public function toInt() : int
public function toInt(): int
{
return $this->toBigInteger()->toInt();
}
#[Override]
public function toFloat() : float
public function toFloat(): float
{
return (float) (string) $this;
return (float) $this->toString();
}
/**
* @return numeric-string
*/
#[Override]
public function __toString() : string
public function toString(): string
{
if ($this->scale === 0) {
/** @var numeric-string */
return $this->value;
}
$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.
*
* @internal
* @psalm-suppress RedundantPropertyInitializationCheck
*
* @param array{value: string, scale: int} $data
*
* @throws \LogicException
* @throws LogicException
*/
public function __unserialize(array $data): void
{
/** @phpstan-ignore isset.initializedProperty */
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->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.
*
* @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;
$b = $y->value;
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) {
$a .= \str_repeat('0', $y->scale - $x->scale);
$a .= str_repeat('0', $y->scale - $x->scale);
}
return [$a, $b];
}
private function valueWithMinScale(int $scale) : string
/**
* @pure
*/
private function valueWithMinScale(int $scale): string
{
$value = $this->value;
if ($this->value !== '0' && $scale > $this->scale) {
$value .= \str_repeat('0', $scale - $this->scale);
$value .= str_repeat('0', $scale - $this->scale);
}
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.
*
* @pure
*/
private function getUnscaledValueWithLeadingZeros() : string
private function getUnscaledValueWithLeadingZeros(): string
{
$value = $this->value;
$targetLength = $this->scale + 1;
$negative = ($value[0] === '-');
$length = \strlen($value);
$length = strlen($value);
if ($negative) {
$length--;
@@ -778,10 +961,10 @@ final class BigDecimal extends BigNumber
}
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) {
$value = '-' . $value;
+504 -215
View File
File diff suppressed because it is too large Load Diff
+549 -352
View File
File diff suppressed because it is too large Load Diff
+274 -92
View File
@@ -8,26 +8,38 @@ use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Exception\RoundingNecessaryException;
use InvalidArgumentException;
use LogicException;
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.
*
* 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.
*/
private readonly BigInteger $numerator;
private BigInteger $numerator;
/**
* The denominator. Always strictly positive.
*/
private readonly BigInteger $denominator;
private BigInteger $denominator;
/**
* 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.
*
* @throws DivisionByZeroException If the denominator is zero.
*
* @pure
*/
protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator)
{
@@ -46,22 +60,40 @@ final class BigRational extends BigNumber
}
if ($denominator->isNegative()) {
$numerator = $numerator->negated();
$numerator = $numerator->negated();
$denominator = $denominator->negated();
}
}
$this->numerator = $numerator;
$this->numerator = $numerator;
$this->denominator = $denominator;
}
/**
* @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]
protected static function from(BigNumber $number): static
{
return $number->toBigRational();
public static function nd(
BigNumber|int|float|string $numerator,
BigNumber|int|float|string $denominator,
): BigRational {
trigger_error(
'The BigRational::nd() method is deprecated, use BigRational::ofFraction() instead.',
E_USER_DEPRECATED,
);
return self::ofFraction($numerator, $denominator);
}
/**
@@ -73,17 +105,16 @@ final class BigRational extends BigNumber
* @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.
* @throws MathException If an argument is not valid, or is not convertible to a BigInteger.
* @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 $denominator,
) : BigRational {
$numerator = BigInteger::of($numerator);
): BigRational {
$numerator = BigInteger::of($numerator);
$denominator = BigInteger::of($denominator);
return new BigRational($numerator, $denominator, true);
@@ -92,14 +123,11 @@ final class BigRational extends BigNumber
/**
* Returns a BigRational representing zero.
*
* @psalm-pure
* @pure
*/
public static function zero() : BigRational
public static function zero(): BigRational
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigRational|null $zero
*/
/** @var BigRational|null $zero */
static $zero;
if ($zero === null) {
@@ -112,14 +140,11 @@ final class BigRational extends BigNumber
/**
* Returns a BigRational representing one.
*
* @psalm-pure
* @pure
*/
public static function one() : BigRational
public static function one(): BigRational
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigRational|null $one
*/
/** @var BigRational|null $one */
static $one;
if ($one === null) {
@@ -132,14 +157,11 @@ final class BigRational extends BigNumber
/**
* Returns a BigRational representing ten.
*
* @psalm-pure
* @pure
*/
public static function ten() : BigRational
public static function ten(): BigRational
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigRational|null $ten
*/
/** @var BigRational|null $ten */
static $ten;
if ($ten === null) {
@@ -149,57 +171,118 @@ final class BigRational extends BigNumber
return $ten;
}
public function getNumerator() : BigInteger
/**
* @pure
*/
public function getNumerator(): BigInteger
{
return $this->numerator;
}
public function getDenominator() : BigInteger
/**
* @pure
*/
public function getDenominator(): BigInteger
{
return $this->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);
}
/**
* 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);
}
/**
* 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);
}
/**
* 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.
*
* @param BigNumber|int|float|string $that The number to add.
*
* @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);
$numerator = $this->numerator->multipliedBy($that->denominator);
$numerator = $numerator->plus($that->numerator->multipliedBy($this->denominator));
$numerator = $this->numerator->multipliedBy($that->denominator);
$numerator = $numerator->plus($that->numerator->multipliedBy($this->denominator));
$denominator = $this->denominator->multipliedBy($that->denominator);
return new BigRational($numerator, $denominator, false);
@@ -211,13 +294,15 @@ final class BigRational extends BigNumber
* @param BigNumber|int|float|string $that The number to subtract.
*
* @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);
$numerator = $this->numerator->multipliedBy($that->denominator);
$numerator = $numerator->minus($that->numerator->multipliedBy($this->denominator));
$numerator = $this->numerator->multipliedBy($that->denominator);
$numerator = $numerator->minus($that->numerator->multipliedBy($this->denominator));
$denominator = $this->denominator->multipliedBy($that->denominator);
return new BigRational($numerator, $denominator, false);
@@ -228,13 +313,15 @@ final class BigRational extends BigNumber
*
* @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);
$numerator = $this->numerator->multipliedBy($that->numerator);
$numerator = $this->numerator->multipliedBy($that->numerator);
$denominator = $this->denominator->multipliedBy($that->denominator);
return new BigRational($numerator, $denominator, false);
@@ -245,13 +332,20 @@ final class BigRational extends BigNumber
*
* @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);
$numerator = $this->numerator->multipliedBy($that->denominator);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
$numerator = $this->numerator->multipliedBy($that->denominator);
$denominator = $this->denominator->multipliedBy($that->numerator);
return new BigRational($numerator, $denominator, true);
@@ -260,14 +354,14 @@ final class BigRational extends BigNumber
/**
* 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) {
$one = BigInteger::one();
return new BigRational($one, $one, false);
return BigRational::one();
}
if ($exponent === 1) {
@@ -277,7 +371,7 @@ final class BigRational extends BigNumber
return new BigRational(
$this->numerator->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.
*
* @throws DivisionByZeroException If the numerator is zero.
*
* @pure
*/
public function reciprocal() : BigRational
public function reciprocal(): BigRational
{
return new BigRational($this->denominator, $this->numerator, true);
}
/**
* Returns the absolute value of this BigRational.
*/
public function abs() : BigRational
{
return new BigRational($this->numerator->abs(), $this->denominator, false);
}
/**
* Returns the negated value of this BigRational.
*/
public function negated() : BigRational
#[Override]
public function negated(): static
{
return new BigRational($this->numerator->negated(), $this->denominator, false);
}
/**
* Returns the simplified value of this BigRational.
*
* @pure
*/
public function simplified() : BigRational
public function simplified(): BigRational
{
$gcd = $this->numerator->gcd($this->denominator);
@@ -323,19 +411,27 @@ final class BigRational extends BigNumber
}
#[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]
public function getSign() : int
public function getSign(): int
{
return $this->numerator->getSign();
}
#[Override]
public function toBigInteger() : BigInteger
public function toBigInteger(): BigInteger
{
$simplified = $this->simplified();
@@ -347,41 +443,59 @@ final class BigRational extends BigNumber
}
#[Override]
public function toBigDecimal() : BigDecimal
public function toBigDecimal(): BigDecimal
{
return $this->numerator->toBigDecimal()->exactlyDividedBy($this->denominator);
return $this->numerator->toBigDecimal()->dividedByExact($this->denominator);
}
#[Override]
public function toBigRational() : BigRational
public function toBigRational(): BigRational
{
return $this;
}
#[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);
}
#[Override]
public function toInt() : int
public function toInt(): int
{
return $this->toBigInteger()->toInt();
}
#[Override]
public function toFloat() : float
public function toFloat(): float
{
$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]
public function __toString() : string
public function toString(): string
{
$numerator = (string) $this->numerator;
$denominator = (string) $this->denominator;
$numerator = $this->numerator->toString();
$denominator = $this->denominator->toString();
if ($denominator === '1') {
return $numerator;
@@ -390,6 +504,67 @@ final class BigRational extends BigNumber
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.
*
@@ -406,19 +581,26 @@ final class BigRational extends BigNumber
* This method is only here to allow unserializing the object and cannot be accessed directly.
*
* @internal
* @psalm-suppress RedundantPropertyInitializationCheck
*
* @param array{numerator: BigInteger, denominator: BigInteger} $data
*
* @throws \LogicException
* @throws LogicException
*/
public function __unserialize(array $data): void
{
/** @phpstan-ignore isset.initializedProperty */
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->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.
*/
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.');
}
/**
* @psalm-pure
* @pure
*/
public static function modulusMustNotBeZero() : DivisionByZeroException
public static function modulusMustNotBeZero(): DivisionByZeroException
{
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.');
}
@@ -6,18 +6,23 @@ namespace Brick\Math\Exception;
use Brick\Math\BigInteger;
use function sprintf;
use const PHP_INT_MAX;
use const PHP_INT_MIN;
/**
* 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.';
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;
use RuntimeException;
/**
* 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.
*/
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;
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.
*/
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.',
$value,
));
@@ -20,22 +28,33 @@ class NumberFormatException extends MathException
/**
* @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) {
$char = \strtoupper(\dechex($ord));
$char = strtoupper(dechex($ord));
if ($ord < 10) {
if ($ord < 16) {
$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.
*/
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.');
}
+193 -157
View File
@@ -7,6 +7,16 @@ namespace Brick\Math\Internal;
use Brick\Math\Exception\RoundingNecessaryException;
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.
*
@@ -17,10 +27,8 @@ use Brick\Math\RoundingMode;
* All methods must return strings respecting this format, unless specified otherwise.
*
* @internal
*
* @psalm-immutable
*/
abstract class Calculator
abstract readonly class Calculator
{
/**
* The maximum exponent value allowed for the pow() method.
@@ -32,94 +40,29 @@ abstract class Calculator
*/
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.
*
* @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.
*
* @pure
*/
final public function neg(string $n) : string
final public function neg(string $n): string
{
if ($n === '0') {
return '0';
}
if ($n[0] === '-') {
return \substr($n, 1);
return substr($n, 1);
}
return '-' . $n;
@@ -128,11 +71,13 @@ abstract class Calculator
/**
* 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);
@@ -144,8 +89,8 @@ abstract class Calculator
return 1;
}
$aLen = \strlen($aDig);
$bLen = \strlen($bDig);
$aLen = strlen($aDig);
$bLen = strlen($bDig);
if ($aLen < $bLen) {
$result = -1;
@@ -160,18 +105,24 @@ abstract class Calculator
/**
* 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.
*
* @pure
*/
abstract public function sub(string $a, string $b) : string;
abstract public function sub(string $a, string $b): string;
/**
* 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.
@@ -180,8 +131,10 @@ abstract class Calculator
* @param string $b The divisor, must not be zero.
*
* @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.
@@ -190,8 +143,10 @@ abstract class Calculator
* @param string $b The divisor, must not be zero.
*
* @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.
@@ -200,8 +155,10 @@ abstract class Calculator
* @param string $b The divisor, must not be zero.
*
* @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.
@@ -210,13 +167,17 @@ abstract class Calculator
* @param int $e The exponent, validated as an integer between 0 and MAX_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.
*
* @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);
}
@@ -229,8 +190,10 @@ abstract class Calculator
* 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.
*
* @pure
*/
public function modInverse(string $x, string $m) : ?string
public function modInverse(string $x, string $m): ?string
{
if ($m === '1') {
return '0';
@@ -254,11 +217,13 @@ abstract class Calculator
/**
* 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 $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.
@@ -267,8 +232,10 @@ abstract class Calculator
* has built-in support for GCD calculations.
*
* @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') {
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') {
return [$b, '0', '1'];
if ($a === '0' || $b === '0') {
return '0';
}
[$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];
return $this->divQ($this->abs($this->mul($a, $b)), $this->gcd($a, $b));
}
/**
@@ -303,8 +272,10 @@ abstract class Calculator
*
* The result is the largest x such that x² ≤ n.
* 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.
@@ -316,10 +287,12 @@ abstract class Calculator
* @param int $base The base of the number, validated from 2 to 36.
*
* @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.
*
* @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] === '-');
if ($negative) {
$number = \substr($number, 1);
$number = substr($number, 1);
}
$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.
*
* @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"
$number = \ltrim($number, $alphabet[0]);
$number = ltrim($number, $alphabet[0]);
if ($number === '') {
return '0';
@@ -379,13 +356,13 @@ abstract class Calculator
$base = (string) $base;
for ($i = \strlen($number) - 1; $i >= 0; $i--) {
$index = \strpos($alphabet, $number[$i]);
for ($i = strlen($number) - 1; $i >= 0; $i--) {
$index = strpos($alphabet, $number[$i]);
if ($index !== 0) {
$result = $this->add($result, ($index === 1)
? $power
: $this->mul($power, (string) $index)
$result = $this->add(
$result,
($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.
*
* @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') {
return $alphabet[0];
@@ -422,7 +401,7 @@ abstract class Calculator
$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 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);
$hasDiscardedFraction = ($remainder !== '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'));
$b = $this->abs($b);
@@ -456,51 +434,57 @@ abstract class Calculator
$increment = false;
switch ($roundingMode) {
case RoundingMode::UNNECESSARY:
case RoundingMode::Unnecessary:
if ($hasDiscardedFraction) {
throw RoundingNecessaryException::roundingNecessary();
}
break;
case RoundingMode::UP:
case RoundingMode::Up:
$increment = $hasDiscardedFraction;
break;
case RoundingMode::DOWN:
case RoundingMode::Down:
break;
case RoundingMode::CEILING:
case RoundingMode::Ceiling:
$increment = $hasDiscardedFraction && $isPositiveOrZero;
break;
case RoundingMode::FLOOR:
case RoundingMode::Floor:
$increment = $hasDiscardedFraction && ! $isPositiveOrZero;
break;
case RoundingMode::HALF_UP:
case RoundingMode::HalfUp:
$increment = $discardedFractionSign() >= 0;
break;
case RoundingMode::HALF_DOWN:
case RoundingMode::HalfDown:
$increment = $discardedFractionSign() > 0;
break;
case RoundingMode::HALF_CEILING:
case RoundingMode::HalfCeiling:
$increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0;
break;
case RoundingMode::HALF_FLOOR:
case RoundingMode::HalfFloor:
$increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
break;
case RoundingMode::HALF_EVEN:
case RoundingMode::HalfEven:
$lastDigit = (int) $quotient[-1];
$lastDigitIsEven = ($lastDigit % 2 === 0);
$increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
break;
default:
throw new \InvalidArgumentException('Invalid rounding mode.');
break;
}
if ($increment) {
@@ -515,8 +499,10 @@ abstract class Calculator
*
* This method can be overridden by the concrete implementation if the underlying library
* 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);
}
@@ -526,8 +512,10 @@ abstract class Calculator
*
* This method can be overridden by the concrete implementation if the underlying library
* 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);
}
@@ -537,33 +525,74 @@ abstract class Calculator
*
* This method can be overridden by the concrete implementation if the underlying library
* 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);
}
/**
* 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.
*
* @param 'and'|'or'|'xor' $operator The operator to use.
* @param string $a The left 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);
$aBin = $this->toBinary($aDig);
$bBin = $this->toBinary($bDig);
$aLen = \strlen($aBin);
$bLen = \strlen($bBin);
$aLen = strlen($aBin);
$bLen = strlen($bBin);
if ($aLen > $bLen) {
$bBin = \str_repeat("\x00", $aLen - $bLen) . $bBin;
$bBin = str_repeat("\x00", $aLen - $bLen) . $bBin;
} elseif ($bLen > $aLen) {
$aBin = \str_repeat("\x00", $bLen - $aLen) . $aBin;
$aBin = str_repeat("\x00", $bLen - $aLen) . $aBin;
}
if ($aNeg) {
@@ -596,18 +625,21 @@ abstract class Calculator
/**
* @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;
for ($i = \strlen($number) - 1; $i >= 0; $i--) {
$byte = \ord($number[$i]);
for ($i = strlen($number) - 1; $i >= 0; $i--) {
$byte = ord($number[$i]);
if (++$byte !== 256) {
$number[$i] = \chr($byte);
$number[$i] = chr($byte);
break;
}
@@ -625,36 +657,40 @@ abstract class Calculator
* Converts a decimal number to a binary string.
*
* @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 = '';
while ($number !== '0') {
[$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.
*
* @param string $bytes The bytes representing the number.
*
* @pure
*/
private function toDecimal(string $bytes) : string
private function toDecimal(string $bytes): string
{
$result = '0';
$power = '1';
for ($i = \strlen($bytes) - 1; $i >= 0; $i--) {
$index = \ord($bytes[$i]);
for ($i = strlen($bytes) - 1; $i >= 0; $i--) {
$index = ord($bytes[$i]);
if ($index !== 0) {
$result = $this->add($result, ($index === 1)
? $power
: $this->mul($power, (string) $index)
$result = $this->add(
$result,
($index === 1) ? $power : $this->mul($power, (string) $index),
);
}
@@ -7,69 +7,79 @@ namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator;
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.
*
* @internal
*
* @psalm-immutable
*/
class BcMathCalculator extends Calculator
final readonly class BcMathCalculator extends Calculator
{
#[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]
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]
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]
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]
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]
public function divQR(string $a, string $b) : array
public function divQR(string $a, string $b): array
{
$q = \bcdiv($a, $b, 0);
$r = \bcmod($a, $b, 0);
$q = bcdiv($a, $b, 0);
$r = bcmod($a, $b, 0);
return [$q, $r];
}
#[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]
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]
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;
use Brick\Math\Internal\Calculator;
use GMP;
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.
*
* @internal
*
* @psalm-immutable
*/
class GmpCalculator extends Calculator
final readonly class GmpCalculator extends Calculator
{
#[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]
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]
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]
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]
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]
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 [
\gmp_strval($q),
\gmp_strval($r)
gmp_strval($q),
gmp_strval($r),
];
}
#[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]
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) {
return null;
}
return \gmp_strval($result);
return gmp_strval($result);
}
#[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]
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]
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]
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]
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]
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]
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]
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 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.
*
* @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.
@@ -24,9 +36,11 @@ class NativeCalculator extends Calculator
* 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)
*/
private readonly int $maxDigits;
private int $maxDigits;
/**
* @pure
*
* @codeCoverageIgnore
*/
public function __construct()
@@ -34,16 +48,15 @@ class NativeCalculator extends Calculator
$this->maxDigits = match (PHP_INT_SIZE) {
4 => 9,
8 => 18,
default => throw new \RuntimeException('The platform is not 32-bit or 64-bit as expected.')
};
}
#[Override]
public function add(string $a, string $b) : string
public function add(string $a, string $b): string
{
/**
* @psalm-var numeric-string $a
* @psalm-var numeric-string $b
* @var numeric-string $a
* @var numeric-string $b
*/
$result = $a + $b;
@@ -71,17 +84,17 @@ class NativeCalculator extends Calculator
}
#[Override]
public function sub(string $a, string $b) : string
public function sub(string $a, string $b): string
{
return $this->add($a, $this->neg($b));
}
#[Override]
public function mul(string $a, string $b) : string
public function mul(string $a, string $b): string
{
/**
* @psalm-var numeric-string $a
* @psalm-var numeric-string $b
* @var numeric-string $a
* @var numeric-string $b
*/
$result = $a * $b;
@@ -121,7 +134,7 @@ class NativeCalculator extends Calculator
}
#[Override]
public function divQ(string $a, string $b) : string
public function divQ(string $a, string $b): string
{
return $this->divQR($a, $b)[0];
}
@@ -133,7 +146,7 @@ class NativeCalculator extends Calculator
}
#[Override]
public function divQR(string $a, string $b) : array
public function divQR(string $a, string $b): array
{
if ($a === '0') {
return ['0', '0'];
@@ -151,11 +164,11 @@ class NativeCalculator extends Calculator
return [$this->neg($a), '0'];
}
/** @psalm-var numeric-string $a */
/** @var numeric-string $a */
$na = $a * 1; // cast to number
if (is_int($na)) {
/** @psalm-var numeric-string $b */
/** @var numeric-string $b */
$nb = $b * 1;
if (is_int($nb)) {
@@ -166,7 +179,7 @@ class NativeCalculator extends Calculator
return [
(string) $q,
(string) $r
(string) $r,
];
}
}
@@ -187,7 +200,7 @@ class NativeCalculator extends Calculator
}
#[Override]
public function pow(string $a, int $e) : string
public function pow(string $a, int $e): string
{
if ($e === 0) {
return '1';
@@ -202,7 +215,6 @@ class NativeCalculator extends Calculator
$aa = $this->mul($a, $a);
/** @psalm-suppress PossiblyInvalidArgument We're sure that $e / 2 is an int now */
$result = $this->pow($aa, $e / 2);
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]
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)
if ($base === '0' && $exp === '0' && $mod === '1') {
return '0';
}
// normalize to Euclidean representative so modPow() stays consistent with mod()
$base = $this->mod($base, $mod);
// special case: the algorithm below fails with power 0 mod 1 (returns 1 instead of 0)
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]
public function sqrt(string $n) : string
public function sqrt(string $n): string
{
if ($n === '0') {
return '0';
}
// initial approximation
$x = \str_repeat('9', \intdiv(\strlen($n), 2) ?: 1);
$x = str_repeat('9', intdiv(strlen($n), 2) ?: 1);
$decreased = false;
for (;;) {
for (; ;) {
$nx = $this->divQ($this->add($x, $this->divQ($n, $x)), '2');
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.
*
* @pure
*/
private function doAdd(string $a, string $b) : string
private function doAdd(string $a, string $b): string
{
[$a, $b, $length] = $this->pad($a, $b);
$carry = 0;
$result = '';
for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) {
for ($i = $length - $this->maxDigits; ; $i -= $this->maxDigits) {
$blockLength = $this->maxDigits;
if ($i < 0) {
$blockLength += $i;
/** @psalm-suppress LoopInvalidation */
$i = 0;
}
/** @psalm-var numeric-string $blockA */
$blockA = \substr($a, $i, $blockLength);
/** @var numeric-string $blockA */
$blockA = substr($a, $i, $blockLength);
/** @psalm-var numeric-string $blockB */
$blockB = \substr($b, $i, $blockLength);
/** @var numeric-string $blockB */
$blockB = substr($b, $i, $blockLength);
$sum = (string) ($blockA + $blockB + $carry);
$sumLength = \strlen($sum);
$sumLength = strlen($sum);
if ($sumLength > $blockLength) {
$sum = \substr($sum, 1);
$sum = substr($sum, 1);
$carry = 1;
} else {
if ($sumLength < $blockLength) {
$sum = \str_repeat('0', $blockLength - $sumLength) . $sum;
$sum = str_repeat('0', $blockLength - $sumLength) . $sum;
}
$carry = 0;
}
@@ -330,8 +341,10 @@ class NativeCalculator extends Calculator
/**
* 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) {
return '0';
@@ -355,20 +368,19 @@ class NativeCalculator extends Calculator
$complement = 10 ** $this->maxDigits;
for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) {
for ($i = $length - $this->maxDigits; ; $i -= $this->maxDigits) {
$blockLength = $this->maxDigits;
if ($i < 0) {
$blockLength += $i;
/** @psalm-suppress LoopInvalidation */
$i = 0;
}
/** @psalm-var numeric-string $blockA */
$blockA = \substr($a, $i, $blockLength);
/** @var numeric-string $blockA */
$blockA = substr($a, $i, $blockLength);
/** @psalm-var numeric-string $blockB */
$blockB = \substr($b, $i, $blockLength);
/** @var numeric-string $blockB */
$blockB = substr($b, $i, $blockLength);
$sum = $blockA - $blockB - $carry;
@@ -380,10 +392,10 @@ class NativeCalculator extends Calculator
}
$sum = (string) $sum;
$sumLength = \strlen($sum);
$sumLength = strlen($sum);
if ($sumLength < $blockLength) {
$sum = \str_repeat('0', $blockLength - $sumLength) . $sum;
$sum = str_repeat('0', $blockLength - $sumLength) . $sum;
}
$result = $sum . $result;
@@ -396,7 +408,7 @@ class NativeCalculator extends Calculator
// Carry cannot be 1 when the loop ends, as a > b
assert($carry === 0);
$result = \ltrim($result, '0');
$result = ltrim($result, '0');
if ($invert) {
$result = $this->neg($result);
@@ -407,48 +419,48 @@ class NativeCalculator extends Calculator
/**
* 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);
$y = \strlen($b);
$x = strlen($a);
$y = strlen($b);
$maxDigits = \intdiv($this->maxDigits, 2);
$maxDigits = intdiv($this->maxDigits, 2);
$complement = 10 ** $maxDigits;
$result = '0';
for ($i = $x - $maxDigits;; $i -= $maxDigits) {
for ($i = $x - $maxDigits; ; $i -= $maxDigits) {
$blockALength = $maxDigits;
if ($i < 0) {
$blockALength += $i;
/** @psalm-suppress LoopInvalidation */
$i = 0;
}
$blockA = (int) \substr($a, $i, $blockALength);
$blockA = (int) substr($a, $i, $blockALength);
$line = '';
$carry = 0;
for ($j = $y - $maxDigits;; $j -= $maxDigits) {
for ($j = $y - $maxDigits; ; $j -= $maxDigits) {
$blockBLength = $maxDigits;
if ($j < 0) {
$blockBLength += $j;
/** @psalm-suppress LoopInvalidation */
$j = 0;
}
$blockB = (int) \substr($b, $j, $blockBLength);
$blockB = (int) substr($b, $j, $blockBLength);
$mul = $blockA * $blockB + $carry;
$value = $mul % $complement;
$carry = ($mul - $value) / $complement;
$value = (string) $value;
$value = \str_pad($value, $maxDigits, '0', STR_PAD_LEFT);
$value = str_pad($value, $maxDigits, '0', STR_PAD_LEFT);
$line = $value . $line;
@@ -461,10 +473,10 @@ class NativeCalculator extends Calculator
$line = $carry . $line;
}
$line = \ltrim($line, '0');
$line = ltrim($line, '0');
if ($line !== '') {
$line .= \str_repeat('0', $x - $blockALength - $i);
$line .= str_repeat('0', $x - $blockALength - $i);
$result = $this->add($result, $line);
}
@@ -480,8 +492,10 @@ class NativeCalculator extends Calculator
* Performs the division of two non-signed large integers.
*
* @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);
@@ -489,8 +503,8 @@ class NativeCalculator extends Calculator
return ['0', $a];
}
$x = \strlen($a);
$y = \strlen($b);
$x = strlen($a);
$y = strlen($b);
// we now know that a >= b && x >= y
@@ -498,24 +512,24 @@ class NativeCalculator extends Calculator
$r = $a; // remainder
$z = $y; // focus length, always $y or $y+1
/** @psalm-var numeric-string $b */
/** @var numeric-string $b */
$nb = $b * 1; // cast to number
// performance optimization in cases where the remainder will never cause int overflow
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++) {
$n = $r * 10 + (int) $a[$i];
/** @psalm-var int $nb */
$q .= \intdiv($n, $nb);
/** @var int $nb */
$q .= intdiv($n, $nb);
$r = $n % $nb;
}
return [\ltrim($q, '0') ?: '0', (string) $r];
return [ltrim($q, '0') ?: '0', (string) $r];
}
for (;;) {
$focus = \substr($a, 0, $z);
for (; ;) {
$focus = substr($a, 0, $z);
$cmp = $this->doCmp($focus, $b);
@@ -527,7 +541,7 @@ class NativeCalculator extends Calculator
$z++;
}
$zeros = \str_repeat('0', $x - $z);
$zeros = str_repeat('0', $x - $z);
$q = $this->add($q, '1' . $zeros);
$a = $this->sub($a, $b . $zeros);
@@ -538,7 +552,7 @@ class NativeCalculator extends Calculator
break;
}
$x = \strlen($a);
$x = strlen($a);
if ($x < $y) { // remainder < dividend
break;
@@ -553,12 +567,14 @@ class NativeCalculator extends Calculator
/**
* 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);
$y = \strlen($b);
$x = strlen($a);
$y = strlen($b);
$cmp = $x <=> $y;
@@ -566,7 +582,7 @@ class NativeCalculator extends Calculator
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.
*
* @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);
$y = \strlen($b);
$x = strlen($a);
$y = strlen($b);
if ($x > $y) {
$b = \str_repeat('0', $x - $y) . $b;
$b = str_repeat('0', $x - $y) . $b;
return [$a, $b, $x];
}
if ($x < $y) {
$a = \str_repeat('0', $y - $x) . $a;
$a = str_repeat('0', $y - $x) . $a;
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;
/**
* Specifies a rounding behavior for numerical operations capable of discarding precision.
*
* 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.
* Specifies rounding behavior by defining how discarded digits affect the returned result when an exact value cannot
* be represented at the requested scale.
*/
enum RoundingMode
{
@@ -21,7 +16,7 @@ enum RoundingMode
* 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.
*/
case UNNECESSARY;
case Unnecessary;
/**
* Rounds away from zero.
@@ -29,7 +24,7 @@ enum RoundingMode
* Always increments the digit prior to a nonzero discarded fraction.
* Note that this rounding mode never decreases the magnitude of the calculated value.
*/
case UP;
case Up;
/**
* Rounds towards zero.
@@ -37,62 +32,112 @@ enum RoundingMode
* 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.
*/
case DOWN;
case Down;
/**
* 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.
*/
case CEILING;
case Ceiling;
/**
* 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.
*/
case FLOOR;
case Floor;
/**
* 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.
*/
case HALF_UP;
case HalfUp;
/**
* 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.
*
* 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.
*
* 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.
*
* Behaves as for HALF_UP if the digit to the left of the discarded fraction is odd;
* behaves as for HALF_DOWN if it's even.
* Behaves as for HalfUp if the digit to the left of the discarded fraction is odd;
* behaves as for HalfDown if it's even.
*
* Note that this is the rounding mode that statistically minimizes
* 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.
*/
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;
}
+72 -65
View File
@@ -42,35 +42,37 @@ namespace Composer\Autoload;
*/
class ClassLoader
{
/** @var ?string */
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array[]
* @psalm-var array<string, array<string, int>>
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, array<int, string>>
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, string>
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* @var array[]
* @psalm-var array<string, array<string, string[]>>
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var array[]
* @psalm-var array<string, string>
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
@@ -78,8 +80,7 @@ class ClassLoader
private $useIncludePath = false;
/**
* @var string[]
* @psalm-var array<string, string>
* @var array<string, string>
*/
private $classMap = array();
@@ -87,29 +88,29 @@ class ClassLoader
private $classMapAuthoritative = false;
/**
* @var bool[]
* @psalm-var array<string, bool>
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var ?string */
/** @var string|null */
private $apcuPrefix;
/**
* @var self[]
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param ?string $vendorDir
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return string[]
* @return array<string, list<string>>
*/
public function getPrefixes()
{
@@ -121,8 +122,7 @@ class ClassLoader
}
/**
* @return array[]
* @psalm-return array<string, array<int, string>>
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
@@ -130,8 +130,7 @@ class ClassLoader
}
/**
* @return array[]
* @psalm-return array<string, string>
* @return list<string>
*/
public function getFallbackDirs()
{
@@ -139,8 +138,7 @@ class ClassLoader
}
/**
* @return array[]
* @psalm-return array<string, string>
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
@@ -148,8 +146,7 @@ class ClassLoader
}
/**
* @return string[] Array of classname => path
* @psalm-return array<string, string>
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
@@ -157,8 +154,7 @@ class ClassLoader
}
/**
* @param string[] $classMap Class to filename map
* @psalm-param array<string, string> $classMap
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
@@ -175,24 +171,25 @@ class ClassLoader
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
$paths
);
}
@@ -201,19 +198,19 @@ class ClassLoader
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
$paths
);
}
}
@@ -222,9 +219,9 @@ class ClassLoader
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
@@ -232,17 +229,18 @@ class ClassLoader
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
$paths
);
}
} 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.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
$paths
);
}
}
@@ -272,8 +270,8 @@ class ClassLoader
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 base directories
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
@@ -290,8 +288,8 @@ class ClassLoader
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
@@ -425,7 +423,8 @@ class ClassLoader
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
$includeFile = self::$includeFile;
$includeFile($file);
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()
{
@@ -555,18 +554,26 @@ class ClassLoader
return false;
}
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
* @private
*/
function includeFile($file)
{
include $file;
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}
+51 -7
View File
@@ -26,12 +26,23 @@ use Composer\Semver\VersionParser;
*/
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
* @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;
/**
* @var bool
*/
private static $installedIsLocalDir;
/**
* @var bool|null
*/
@@ -98,7 +109,7 @@ class InstalledVersions
{
foreach (self::getInstalled() as $installed) {
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)
{
$constraint = $parser->parseConstraints($constraint);
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
@@ -309,6 +320,24 @@ class InstalledVersions
{
self::$installed = $data;
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();
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
/** @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 $vendorDir.'/composer/installed.php';
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,
// 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') {
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 {
self::$installed = array();
}
}
$installed[] = self::$installed;
if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$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(
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
@@ -18,25 +18,29 @@ return array(
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'9c67151ae59aff4788964ce8eb2a0f43' => $vendorDir . '/clue/stream-filter/src/functions_include.php',
'8cff32064859f4559445b89279f3199c' => $vendorDir . '/php-http/message/src/filters.php',
'606a39d89246991a373564698c2d8383' => $vendorDir . '/symfony/polyfill-php85/bootstrap.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',
'35a6ad97d21e794e7e22a17d806652e4' => $vendorDir . '/nunomaduro/termwind/src/Functions.php',
'3bd81c9b8fcc150b69d8b63b4d2ccf23' => $vendorDir . '/spatie/flare-client-php/src/helpers.php',
'2203a247e6fda86070a5e4e07aed533a' => $vendorDir . '/symfony/clock/Resources/now.php',
'09f6b20656683369174dd6fa83b7e5fb' => $vendorDir . '/symfony/polyfill-uuid/bootstrap.php',
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
'8a9dc1de0ca7e01f3e08231539562f61' => $vendorDir . '/aws/aws-sdk-php/src/functions.php',
'47e1160838b5e5a10346ac4084b58c23' => $vendorDir . '/laravel/prompts/src/helpers.php',
'6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
'801c31d8ed748cfa537fa45402288c95' => $vendorDir . '/psy/psysh/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',
'265b4faa2b3a9766332744949e83bf97' => $vendorDir . '/laravel/framework/src/Illuminate/Collections/helpers.php',
'c7a3c339e7e14b60e06a2d7fcce9476b' => $vendorDir . '/laravel/framework/src/Illuminate/Events/functions.php',
'f57d353b41eb2e234b26064d63d8c5dd' => $vendorDir . '/laravel/framework/src/Illuminate/Filesystem/functions.php',
'f0906e6318348a765ffb6eb24e0d0938' => $vendorDir . '/laravel/framework/src/Illuminate/Foundation/helpers.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',
'58571171fd5812e6e447dce228f52f4d' => $vendorDir . '/laravel/framework/src/Illuminate/Support/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'),
'enshrined\\svgSanitize\\' => array($vendorDir . '/enshrined/svg-sanitize/src'),
'Whoops\\' => array($vendorDir . '/filp/whoops/src/Whoops'),
'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'),
'TijsVerkoyen\\CssToInlineStyles\\' => array($vendorDir . '/tijsverkoyen/css-to-inline-styles/src'),
'Tests\\' => array($baseDir . '/tests'),
'Termwind\\' => array($vendorDir . '/nunomaduro/termwind/src'),
'Symfony\\Thanks\\' => array($vendorDir . '/symfony/thanks/src'),
'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\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'),
'Symfony\\Polyfill\\Intl\\Idn\\' => array($vendorDir . '/symfony/polyfill-intl-idn'),
'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\\Contracts\\Translation\\' => array($vendorDir . '/symfony/translation-contracts'),
'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'),
@@ -47,8 +49,8 @@ return array(
'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
'Symfony\\Component\\Clock\\' => array($vendorDir . '/symfony/clock'),
'Symfony\\Component\\Cache\\' => array($vendorDir . '/symfony/cache'),
'Spatie\\LaravelIgnition\\' => array($vendorDir . '/spatie/error-solutions/legacy/laravel-ignition', $vendorDir . '/spatie/laravel-ignition/src'),
'Spatie\\Ignition\\' => array($vendorDir . '/spatie/error-solutions/legacy/ignition', $vendorDir . '/spatie/ignition/src'),
'Spatie\\LaravelIgnition\\' => array($vendorDir . '/spatie/laravel-ignition/src', $vendorDir . '/spatie/error-solutions/legacy/laravel-ignition'),
'Spatie\\Ignition\\' => array($vendorDir . '/spatie/ignition/src', $vendorDir . '/spatie/error-solutions/legacy/ignition'),
'Spatie\\Html\\' => array($vendorDir . '/spatie/laravel-html/src'),
'Spatie\\FlareClient\\' => array($vendorDir . '/spatie/flare-client-php/src'),
'Spatie\\ErrorSolutions\\' => array($vendorDir . '/spatie/error-solutions/src'),
@@ -67,6 +69,7 @@ return array(
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'),
'NunoMaduro\\Collision\\' => array($vendorDir . '/nunomaduro/collision/src'),
'Nette\\' => array($vendorDir . '/nette/schema/src', $vendorDir . '/nette/utils/src'),
'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
'Mockery\\' => array($vendorDir . '/mockery/mockery/library/Mockery'),
'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\\Prompts\\' => array($vendorDir . '/laravel/prompts/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\\' => array($vendorDir . '/laravel/framework/src/Illuminate'),
'Http\\Promise\\' => array($vendorDir . '/php-http/promise/src'),
@@ -105,7 +108,7 @@ return array(
'Faker\\' => array($vendorDir . '/fakerphp/faker/src/Faker'),
'Egulias\\EmailValidator\\' => array($vendorDir . '/egulias/email-validator/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'),
'Dflydev\\DotAccessData\\' => array($vendorDir . '/dflydev/dot-access-data/src'),
'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'),
+10 -17
View File
@@ -33,25 +33,18 @@ class ComposerAutoloaderInitb2555e5ff7197b9e020da74bbd3b7cfa
$loader->register(true);
$includeFiles = \Composer\Autoload\ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa::$files;
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequireb2555e5ff7197b9e020da74bbd3b7cfa($fileIdentifier, $file);
$filesToLoad = \Composer\Autoload\ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa::$files;
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}, null, null);
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
}
}
/**
* @param string $fileIdentifier
* @param string $file
* @return void
*/
function composerRequireb2555e5ff7197b9e020da74bbd3b7cfa($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}
+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.
[![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
------------
+2 -2
View File
@@ -15,7 +15,7 @@
],
"require": {
"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"
},
"require-dev": {
@@ -24,7 +24,7 @@
"phpstan/phpstan-deprecation-rules": "^1 || ^2",
"phpstan/phpstan-strict-rules": "^1.1 || ^2",
"phpstan/phpstan-phpunit": "^1 || ^2",
"symfony/filesystem": "^5.4 || ^6"
"symfony/filesystem": "^5.4 || ^6 || ^7 || ^8"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -94,7 +94,7 @@ class ClassMap implements \Countable
$ambiguousClasses = [];
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, '\\', '/'));
});
if (\count($paths) > 0) {
@@ -157,7 +157,7 @@ class ClassMap implements \Countable
$pathPrefix = rtrim(strtr($pathPrefix, '\\', '/'), '/');
foreach ($this->psrViolations as $path => $violations) {
if ($path === $pathPrefix || 0 === \strpos($path, $pathPrefix.'/')) {
if ($path === $pathPrefix || 0 === strpos($path, $pathPrefix.'/')) {
unset($this->psrViolations[$path]);
}
}
+16 -16
View File
@@ -20,7 +20,6 @@ namespace Composer\ClassMapGenerator;
use Composer\Pcre\Preg;
use Symfony\Component\Finder\Finder;
use Composer\IO\IOInterface;
/**
* ClassMapGenerator
@@ -57,7 +56,7 @@ class ClassMapGenerator
{
$this->extensions = $extensions;
$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
{
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"');
}
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');
}
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');
}
$basePath = $path;
}
if (is_string($path)) {
if (\is_string($path)) {
if (is_file($path)) {
$path = [new \SplFileInfo($path)];
} elseif (is_dir($path) || strpos($path, '*') !== false) {
@@ -144,7 +143,7 @@ class ClassMapGenerator
foreach ($path as $file) {
$filePath = $file->getPathname();
if (!in_array(pathinfo($filePath, PATHINFO_EXTENSION), $this->extensions, true)) {
if (!\in_array(pathinfo($filePath, PATHINFO_EXTENSION), $this->extensions, true)) {
continue;
}
@@ -224,13 +223,18 @@ class ClassMapGenerator
$validClasses = [];
$rejectedClasses = [];
$realSubPath = substr($filePath, strlen($basePath) + 1);
$realSubPath = substr($filePath, \strlen($basePath) + 1);
$dotPosition = strrpos($realSubPath, '.');
$realSubPath = substr($realSubPath, 0, $dotPosition === false ? PHP_INT_MAX : $dotPosition);
foreach ($classes as $class) {
// transform class name to file path and validate
if ('psr-0' === $namespaceType) {
if ('' !== $baseNamespace && !str_starts_with($class, $baseNamespace)) {
$rejectedClasses[] = $class;
continue;
}
$namespaceLength = strrpos($class, '\\');
if (false !== $namespaceLength) {
$namespace = substr($class, 0, $namespaceLength + 1);
@@ -241,7 +245,7 @@ class ClassMapGenerator
$subPath = str_replace('_', DIRECTORY_SEPARATOR, $class);
}
} elseif ('psr-4' === $namespaceType) {
$subNamespace = ('' !== $baseNamespace) ? substr($class, strlen($baseNamespace)) : $class;
$subNamespace = ('' !== $baseNamespace) ? substr($class, \strlen($baseNamespace)) : $class;
$subPath = str_replace('\\', DIRECTORY_SEPARATOR, $subNamespace);
} else {
throw new \InvalidArgumentException('$namespaceType must be "psr-0" or "psr-4"');
@@ -276,11 +280,8 @@ class ClassMapGenerator
* Checks if the given path is absolute
*
* @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;
}
@@ -292,9 +293,8 @@ class ClassMapGenerator
* @see Composer\Util\Filesystem::normalizePath
*
* @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 = [];
$path = strtr($path, '\\', '/');
@@ -330,7 +330,7 @@ class ClassMapGenerator
}
// 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);
}
+28 -28
View File
@@ -24,7 +24,7 @@ class PhpFileCleaner
private static $typeConfig;
/** @var non-empty-string */
private static $restPattern;
private static $rejectChars;
/**
* @readonly
@@ -53,14 +53,14 @@ class PhpFileCleaner
public static function setTypeConfig(array $types): void
{
foreach ($types as $type) {
self::$typeConfig[$type[0]] = array(
self::$typeConfig[$type[0]] = [
'name' => $type,
'length' => \strlen($type),
'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)
@@ -110,6 +110,7 @@ class PhpFileCleaner
$this->skipToNewline();
continue;
}
if ($this->peek('*')) {
$this->skipComment();
continue;
@@ -119,19 +120,18 @@ class PhpFileCleaner
if ($this->maxMatches === 1 && isset(self::$typeConfig[$char])) {
$type = self::$typeConfig[$char];
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)
) {
$clean .= $match[0];
return $clean;
return $clean . $match[0];
}
}
$this->index += 1;
if ($this->match(self::$restPattern, $match)) {
$clean .= $char . $match[0];
$this->index += \strlen($match[0]);
$skip = strcspn($this->contents, self::$rejectChars, $this->index);
if ($skip > 0) {
$clean .= $char . substr($this->contents, $this->index, $skip);
$this->index += $skip;
} else {
$clean .= $char;
}
@@ -155,16 +155,24 @@ class PhpFileCleaner
private function skipString(string $delimiter): void
{
$rejectChars = '\\' . $delimiter;
$this->index += 1;
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))) {
$this->index += 2;
continue;
}
if ($this->contents[$this->index] === $delimiter) {
$this->index += 1;
break;
}
$this->index += 1;
}
}
@@ -173,7 +181,9 @@ class PhpFileCleaner
{
$this->index += 2;
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;
break;
}
@@ -184,12 +194,7 @@ class PhpFileCleaner
private function skipToNewline(): void
{
while ($this->index < $this->len) {
if ($this->contents[$this->index] === "\r" || $this->contents[$this->index] === "\n") {
return;
}
$this->index += 1;
}
$this->index += strcspn($this->contents, "\r\n", $this->index);
}
private function skipHeredoc(string $delimiter): void
@@ -207,27 +212,22 @@ class PhpFileCleaner
continue 2;
case $firstDelimiterChar:
if (
\substr($this->contents, $this->index, $delimiterLength) === $delimiter
substr($this->contents, $this->index, $delimiterLength) === $delimiter
&& $this->match($delimiterPattern)
) {
$this->index += $delimiterLength;
return;
}
break;
}
// skip the rest of the line
while ($this->index < $this->len) {
$this->skipToNewline();
$this->skipToNewline();
// skip newlines
while ($this->index < $this->len && ($this->contents[$this->index] === "\r" || $this->contents[$this->index] === "\n")) {
$this->index += 1;
}
break;
}
// skip newlines
$this->index += strspn($this->contents, "\r\n", $this->index);
}
}
+26 -22
View File
@@ -12,6 +12,7 @@
namespace Composer\ClassMapGenerator;
use RuntimeException;
use Composer\Pcre\Preg;
/**
@@ -23,16 +24,17 @@ class PhpFileParser
* Extract the classes in the given file
*
* @param string $path The file to check
* @throws \RuntimeException
* @throws RuntimeException
* @return list<class-string> The found classes
*/
public static function findClasses(string $path): array
{
$extraTypes = self::getExtraTypes();
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.');
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.');
}
// Use @ here instead of Silencer to actively suppress 'unhelpful' output
// @link https://github.com/composer/composer/pull/4886
$contents = @php_strip_whitespace($path);
@@ -43,24 +45,26 @@ class PhpFileParser
$message = 'File at "%s" is not readable, check its permissions';
} elseif ('' === trim((string) file_get_contents($path))) {
// The input file was really empty and thus contains no classes
return array();
return [];
} else {
$message = 'File at "%s" could not be parsed as PHP, it may be binary or corrupted';
}
$error = error_get_last();
if (isset($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
Preg::matchAllStrictGroups('{\b(?:class|interface|trait'.$extraTypes.')\s}i', $contents, $matches);
if (0 === \count($matches)) {
return array();
if ([] === $matches[0]) {
return [];
}
$p = new PhpFileCleaner($contents, count($matches[0]));
$p = new PhpFileCleaner($contents, \count($matches[0]));
$contents = $p->clean();
unset($p);
@@ -71,22 +75,26 @@ class PhpFileParser
)
}ix', $contents, $matches);
$classes = array();
$classes = [];
$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] !== '') {
$namespace = str_replace(array(' ', "\t", "\r", "\n"), '', (string) $matches['nsname'][$i]) . '\\';
$namespace = str_replace([' ', "\t", "\r", "\n"], '', (string) $matches['nsname'][$i]) . '\\';
} else {
$name = $matches['name'][$i];
assert(is_string($name));
\assert(\is_string($name));
// skip anon classes extending/implementing
if ($name === 'extends' || $name === 'implements') {
if ($name === 'extends') {
continue;
}
if ($name === 'implements') {
continue;
}
if ($name[0] === ':') {
// 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') {
// something like:
// enum Foo: int { HERP = '123'; }
@@ -101,6 +109,7 @@ class PhpFileParser
$name = substr($name, 0, $colonPos);
}
}
/** @var class-string */
$className = ltrim($namespace . $name, '\\');
$classes[] = $className;
@@ -110,22 +119,18 @@ class PhpFileParser
return $classes;
}
/**
* @return string
*/
private static function getExtraTypes(): string
{
static $extraTypes = null;
if (null === $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';
$extraTypesArray = ['enum'];
}
$extraTypesArray = array_filter(explode('|', $extraTypes), function (string $type) {
return $type !== '';
});
PhpFileCleaner::setTypeConfig(array_merge(['class', 'interface', 'trait'], $extraTypesArray));
}
@@ -140,7 +145,6 @@ class PhpFileParser
*
* @see Composer\Util\Filesystem::isReadable
*
* @param string $path
* @return bool
*/
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"
},
"require-dev": {
"phpunit/phpunit": "^8 || ^9",
"phpstan/phpstan": "^1.12 || ^2",
"phpstan/phpstan-strict-rules": "^1 || ^2"
"phpunit/phpunit": "^9",
"phpstan/phpstan": "^2",
"phpstan/phpstan-strict-rules": "^2",
"phpstan/phpstan-deprecation-rules": "^2"
},
"conflict": {
"phpstan/phpstan": "<1.11.10"
"phpstan/phpstan": "<2.2.2"
},
"autoload": {
"psr-4": {
@@ -48,7 +49,10 @@
}
},
"scripts": {
"test": "@php vendor/bin/phpunit",
"test": [
"@php vendor/bin/phpunit",
"@php vendor/bin/phpunit --testsuite phpstan"
],
"phpstan": "@php phpstan analyse"
}
}
@@ -57,18 +57,38 @@ final class PregMatchTypeSpecifyingExtension implements StaticMethodTypeSpecifyi
{
$args = $node->getArgs();
$patternArg = $args[0] ?? null;
$subjectArg = $args[1] ?? null;
$matchesArg = $args[2] ?? null;
$flagsArg = $args[3] ?? null;
$subjectTypes = new SpecifiedTypes();
if ($patternArg === null) {
return $subjectTypes;
}
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);
if ($flagsType === null) {
return new SpecifiedTypes();
return $subjectTypes;
}
if (stripos($methodReflection->getName(), 'matchAll') !== false) {
@@ -78,7 +98,7 @@ final class PregMatchTypeSpecifyingExtension implements StaticMethodTypeSpecifyi
}
if ($matchedType === null) {
return new SpecifiedTypes();
return $subjectTypes;
}
if (
@@ -93,27 +113,13 @@ final class PregMatchTypeSpecifyingExtension implements StaticMethodTypeSpecifyi
$context = $context->negate();
}
// @phpstan-ignore function.alreadyNarrowedType
if (method_exists('PHPStan\Analyser\SpecifiedTypes', 'setRootExpr')) {
$typeSpecifier = $this->typeSpecifier->create(
$matchesArg->value,
$matchedType,
$context,
$scope
)->setRootExpr($node);
return $overwrite ? $typeSpecifier->setAlwaysOverwriteTypes() : $typeSpecifier;
}
// @phpstan-ignore arguments.count
return $this->typeSpecifier->create(
$specifiedTypes = $this->typeSpecifier->create(
$matchesArg->value,
$matchedType,
$context,
// @phpstan-ignore argument.type
$overwrite,
$scope,
$node
);
$scope
)->setRootExpr($node);
return $subjectTypes->unionWith($overwrite ? $specifiedTypes->setAlwaysOverwriteTypes() : $specifiedTypes);
}
}
+2 -2
View File
@@ -395,7 +395,7 @@ class Preg
* @return array<int|string, string>
* @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) {
if (is_string($match) || (is_array($match) && is_string($match[0]))) {
@@ -414,7 +414,7 @@ class Preg
* @return array<int|string, list<string>>
* @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 ($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;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
throw new \RuntimeException(
'Composer detected issues in your platform: ' . implode(' ', $issues)
);
}
+44 -18
View File
@@ -1,41 +1,67 @@
{
"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.",
"keywords": ["php", "strings", "words", "manipulation", "inflector", "inflection", "uppercase", "lowercase", "singular", "plural"],
"homepage": "https://www.doctrine-project.org/projects/inflector.html",
"license": "MIT",
"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"}
"type": "library",
"keywords": [
"php",
"strings",
"words",
"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": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"doctrine/coding-standard": "^11.0",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan-phpunit": "^1.1",
"phpstan/phpstan-strict-rules": "^1.3",
"phpunit/phpunit": "^8.5 || ^9.5",
"vimeo/psalm": "^4.25 || ^5.4"
"doctrine/coding-standard": "^12.0 || ^13.0",
"phpstan/phpstan": "^1.12 || ^2.0",
"phpstan/phpstan-phpunit": "^1.4 || ^2.0",
"phpstan/phpstan-strict-rules": "^1.6 || ^2.0",
"phpunit/phpunit": "^8.5 || ^12.2"
},
"autoload": {
"psr-4": {
"Doctrine\\Inflector\\": "lib/Doctrine/Inflector"
"Doctrine\\Inflector\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Doctrine\\Tests\\Inflector\\": "tests/Doctrine/Tests/Inflector"
"Doctrine\\Tests\\Inflector\\": "tests"
}
},
"config": {
"allow-plugins": {
"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:
- ``Language::ENGLISH``
- ``Language::ESPERANTO``
- ``Language::FRENCH``
- ``Language::NORWEGIAN_BOKMAL``
- ``Language::PORTUGUESE``
@@ -5,7 +5,9 @@ declare(strict_types=1);
namespace Doctrine\Inflector;
use Doctrine\Inflector\Rules\English;
use Doctrine\Inflector\Rules\Esperanto;
use Doctrine\Inflector\Rules\French;
use Doctrine\Inflector\Rules\Italian;
use Doctrine\Inflector\Rules\NorwegianBokmal;
use Doctrine\Inflector\Rules\Portuguese;
use Doctrine\Inflector\Rules\Spanish;
@@ -27,9 +29,15 @@ final class InflectorFactory
case Language::ENGLISH:
return new English\InflectorFactory();
case Language::ESPERANTO:
return new Esperanto\InflectorFactory();
case Language::FRENCH:
return new French\InflectorFactory();
case Language::ITALIAN:
return new Italian\InflectorFactory();
case Language::NORWEGIAN_BOKMAL:
return new NorwegianBokmal\InflectorFactory();
@@ -7,7 +7,9 @@ namespace Doctrine\Inflector;
final class Language
{
public const ENGLISH = 'english';
public const ESPERANTO = 'esperanto';
public const FRENCH = 'french';
public const ITALIAN = 'italian';
public const NORWEGIAN_BOKMAL = 'norwegian-bokmal';
public const PORTUGUESE = 'portuguese';
public const SPANISH = 'spanish';
@@ -92,28 +92,35 @@ class Inflectible
/** @return Substitution[] */
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('avalanche'), new Word('avalanches'));
yield new Substitution(new Word('axis'), 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('blouse'), new Word('blouses'));
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('canvas'), new Word('canvases'));
yield new Substitution(new Word('cave'), new Word('caves'));
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('canvas'), new Word('canvases'));
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('cow'), new Word('cows'));
yield new Substitution(new Word('criterion'), new Word('criteria'));
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('die'), new Word('dice'));
yield new Substitution(new Word('domino'), new Word('dominoes'));
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('foe'), new Word('foes'));
yield new Substitution(new Word('foot'), new Word('feet'));
yield new Substitution(new Word('fungus'), new Word('fungi'));
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('goose'), new Word('geese'));
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('hoax'), new Word('hoaxes'));
yield new Substitution(new Word('hoof'), new Word('hoofs'));
yield new Substitution(new Word('human'), new Word('humans'));
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('move'), new Word('moves'));
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('niveau'), new Word('niveaux'));
yield new Substitution(new Word('nucleus'), new Word('nuclei'));
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('octopus'), new Word('octopuses'));
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('runner-up'), new Word('runners-up'));
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('sieve'), new Word('sieves'));
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('stadium'), new Word('stadiums'));
yield new Substitution(new Word('syllabus'), new Word('syllabi'));
yield new Substitution(new Word('testis'), new Word('testes'));
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('valve'), new Word('valves'));
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('oasis'), new Word('oases'));
yield new Substitution(new Word('valve'), new Word('valves'));
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);
namespace Doctrine\Inflector\Rules\French;
namespace Doctrine\Inflector\Rules\Esperanto;
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('/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('/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