mirror of
https://github.com/linuxserver/Heimdall.git
synced 2026-08-07 07:16:13 +00:00
Merge pull request #1574 from linuxserver/chore/laravel-13-upgrade
Upgrade to Laravel 13, bump to v2.8.0, and remediate vulnerabilities
This commit is contained in:
@@ -29,3 +29,4 @@ yarn-error.log
|
||||
storage/app/public/avatars/*
|
||||
.env
|
||||
.phpunit.result.cache
|
||||
/.phpunit.cache
|
||||
|
||||
@@ -430,7 +430,7 @@ class ItemController extends Controller
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function appload(Request $request): ?string
|
||||
public function appload(Request $request): \Illuminate\Http\JsonResponse|string|null
|
||||
{
|
||||
$output = [];
|
||||
$appid = $request->input('app');
|
||||
|
||||
+4
-4
@@ -12,10 +12,10 @@
|
||||
"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/tinker": "^2.9",
|
||||
"laravel/framework": "^13.0",
|
||||
"laravel/tinker": "^3.0",
|
||||
"laravel/ui": "^4.4",
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"nunomaduro/collision": "^8.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": "^12.0",
|
||||
"squizlabs/php_codesniffer": "3.*",
|
||||
"symfony/thanks": "^1.2",
|
||||
"fakerphp/faker": "^1.23"
|
||||
|
||||
Generated
+1602
-1299
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -5,7 +5,7 @@ use Illuminate\Support\Facades\Facade;
|
||||
|
||||
return [
|
||||
|
||||
'version' => '2.7.7',
|
||||
'version' => '2.8.0',
|
||||
|
||||
'appsource' => env('APP_SOURCE', 'https://appslist.heimdall.site/'),
|
||||
|
||||
|
||||
Generated
+272
-278
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Heimdall",
|
||||
"name": "Heimdall-LS",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
@@ -24,26 +24,14 @@
|
||||
"webpack-cli": "^6.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
@@ -52,30 +40,32 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
|
||||
"integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
|
||||
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
|
||||
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.0",
|
||||
"@babel/helper-compilation-targets": "^7.27.2",
|
||||
"@babel/helper-module-transforms": "^7.27.3",
|
||||
"@babel/helpers": "^7.27.6",
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/traverse": "^7.28.0",
|
||||
"@babel/types": "^7.28.0",
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
"@babel/helper-compilation-targets": "^7.29.7",
|
||||
"@babel/helper-module-transforms": "^7.29.7",
|
||||
"@babel/helpers": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
@@ -91,13 +81,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
|
||||
"integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
|
||||
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/types": "^7.28.0",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"jsesc": "^3.0.2"
|
||||
@@ -119,13 +110,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
|
||||
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
|
||||
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.27.2",
|
||||
"@babel/helper-validator-option": "^7.27.1",
|
||||
"@babel/compat-data": "^7.29.7",
|
||||
"@babel/helper-validator-option": "^7.29.7",
|
||||
"browserslist": "^4.24.0",
|
||||
"lru-cache": "^5.1.1",
|
||||
"semver": "^6.3.1"
|
||||
@@ -189,10 +181,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-globals": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
||||
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
|
||||
"integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -211,27 +204,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-imports": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
|
||||
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
|
||||
"integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.1"
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"version": "7.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
|
||||
"integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
|
||||
"integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.3"
|
||||
"@babel/helper-module-imports": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -253,10 +248,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-plugin-utils": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
|
||||
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
|
||||
"integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -309,28 +305,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-option": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
||||
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
|
||||
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -350,25 +349,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.27.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
|
||||
"integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
|
||||
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.27.6"
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
|
||||
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.0"
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -955,15 +956,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-modules-systemjs": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz",
|
||||
"integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz",
|
||||
"integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.1"
|
||||
"@babel/helper-module-transforms": "^7.29.7",
|
||||
"@babel/helper-plugin-utils": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1491,31 +1493,33 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/parser": "^7.27.2",
|
||||
"@babel/types": "^7.27.1"
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
|
||||
"integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
|
||||
"integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.0",
|
||||
"@babel/helper-globals": "^7.28.0",
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.28.0",
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
"@babel/helper-globals": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1523,13 +1527,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz",
|
||||
"integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1"
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1658,6 +1663,17 @@
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/remapping": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
@@ -2036,15 +2052,6 @@
|
||||
"integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@trysound/sax": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -2915,12 +2922,6 @@
|
||||
"minimalistic-assert": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asn1.js/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/assert": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/assert/-/assert-1.5.1.tgz",
|
||||
@@ -3069,7 +3070,8 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
@@ -3119,10 +3121,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bn.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz",
|
||||
"integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==",
|
||||
"dev": true
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.4.tgz",
|
||||
"integrity": "sha512-QL7sb18rJ1PbdsKsqPA0guxL563vIMwRHgzNrW/uzQuRGN1Cjqd/wonUBAVqHox9KwzHA6vCbM0lXx3k4iQMow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.3",
|
||||
@@ -3163,21 +3166,6 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/body-parser/node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/bonjour-service": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz",
|
||||
@@ -3201,10 +3189,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "1.1.16",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -3730,7 +3719,8 @@
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat/node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
@@ -3858,12 +3848,6 @@
|
||||
"elliptic": "^6.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/create-ecdh/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/create-hash": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
|
||||
@@ -4354,12 +4338,6 @@
|
||||
"randombytes": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/diffie-hellman/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
@@ -4557,12 +4535,6 @@
|
||||
"minimalistic-crypto-utils": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/elliptic/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
@@ -5218,21 +5190,6 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/express/node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -5286,9 +5243,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
||||
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
|
||||
"integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -5299,7 +5256,8 @@
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
]
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fastest-levenshtein": {
|
||||
"version": "1.0.16",
|
||||
@@ -5492,15 +5450,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"dev": true
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -5508,6 +5467,7 @@
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
@@ -6174,10 +6134,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-middleware": {
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
|
||||
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.10.tgz",
|
||||
"integrity": "sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-proxy": "^1.17.8",
|
||||
"http-proxy": "^1.18.1",
|
||||
@@ -6325,10 +6286,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
|
||||
"integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
|
||||
"dev": true
|
||||
"version": "5.1.9",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz",
|
||||
"integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
@@ -6957,10 +6919,21 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
||||
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodeca"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
@@ -7265,13 +7238,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/launch-editor": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz",
|
||||
"integrity": "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==",
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz",
|
||||
"integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picocolors": "^1.0.0",
|
||||
"shell-quote": "^1.8.1"
|
||||
"picocolors": "^1.1.1",
|
||||
"shell-quote": "^1.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/levn": {
|
||||
@@ -7341,10 +7315,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
@@ -7520,12 +7495,6 @@
|
||||
"miller-rabin": "bin/miller-rabin"
|
||||
}
|
||||
},
|
||||
"node_modules/miller-rabin/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
@@ -7620,10 +7589,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
@@ -7660,9 +7630,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"version": "3.3.15",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
|
||||
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -7670,6 +7640,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
@@ -7716,10 +7687,11 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz",
|
||||
"integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==",
|
||||
"dev": true,
|
||||
"license": "(BSD-3-Clause OR GPL-2.0)",
|
||||
"engines": {
|
||||
"node": ">= 6.13.0"
|
||||
}
|
||||
@@ -7966,10 +7938,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/on-headers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
|
||||
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -8241,10 +8214,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"dev": true
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "4.0.0",
|
||||
@@ -8310,10 +8284,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
@@ -8395,9 +8370,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"version": "8.5.16",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
||||
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -8413,8 +8388,9 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"nanoid": "^3.3.12",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -9090,12 +9066,6 @@
|
||||
"safe-buffer": "^5.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/public-encrypt/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
@@ -9103,12 +9073,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||
"version": "6.15.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
|
||||
"integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
"es-define-property": "^1.0.1",
|
||||
"side-channel": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
@@ -9685,6 +9657,16 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
|
||||
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz",
|
||||
@@ -9785,12 +9767,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/serialize-javascript": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
||||
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
|
||||
"version": "7.0.7",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.7.tgz",
|
||||
"integrity": "sha512-YAy8Od6KV+uuwUuU50np8fGB/Aues6Y0nAhA9y/hId74PlKUcme4pXcBD46NWKr1Q4osN/iseZ17YqO1XfmI8g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"randombytes": "^2.1.0"
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-index": {
|
||||
@@ -9998,10 +9981,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz",
|
||||
"integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -10016,14 +10000,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
||||
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"object-inspect": "^1.13.4",
|
||||
"side-channel-list": "^1.0.1",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
@@ -10035,13 +10020,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
"object-inspect": "^1.13.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -10454,17 +10440,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svgo": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz",
|
||||
"integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==",
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.2.tgz",
|
||||
"integrity": "sha512-TyzE4NVGLUFy+H/Uy4N6c3G0HEeprsVfge6Lmq+0FdQQ/zqoVYB62IsBZORsiL+o96s6ff/V6/3UQo/C0cgCAA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@trysound/sax": "0.2.0",
|
||||
"commander": "^7.2.0",
|
||||
"css-select": "^4.1.3",
|
||||
"css-tree": "^1.1.3",
|
||||
"csso": "^4.2.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"sax": "^1.5.0",
|
||||
"stable": "^0.1.8"
|
||||
},
|
||||
"bin": {
|
||||
@@ -10986,12 +10973,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz",
|
||||
"integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
@@ -11680,10 +11672,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
@@ -11725,10 +11718,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
|
||||
"integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
|
||||
@@ -28,5 +28,32 @@
|
||||
"dependencies": {
|
||||
"select2": "~4.0.13",
|
||||
"sortablejs": "^1.15.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@babel/core": "^7.29.7",
|
||||
"@babel/plugin-transform-modules-systemjs": "^7.29.7",
|
||||
"bn.js": "^5.2.4",
|
||||
"brace-expansion": "^1.1.16",
|
||||
"fast-uri": "^3.1.3",
|
||||
"flatted": "^3.4.2",
|
||||
"follow-redirects": "^1.16.0",
|
||||
"http-proxy-middleware": "^2.0.10",
|
||||
"immutable": "^5.1.9",
|
||||
"js-yaml": "^4.3.0",
|
||||
"launch-editor": "^2.14.1",
|
||||
"lodash": "^4.18.1",
|
||||
"minimatch": "^3.1.5",
|
||||
"node-forge": "^1.4.0",
|
||||
"on-headers": "^1.1.0",
|
||||
"path-to-regexp": "^0.1.13",
|
||||
"picomatch": "^2.3.2",
|
||||
"postcss": "^8.5.16",
|
||||
"qs": "^6.15.3",
|
||||
"serialize-javascript": "^7.0.7",
|
||||
"shell-quote": "^1.9.0",
|
||||
"svgo": "^2.8.2",
|
||||
"uuid": "^11.1.1",
|
||||
"ws": "^8.21.0",
|
||||
"yaml": "^1.10.3"
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -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/12.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>
|
||||
|
||||
Vendored
+3
@@ -4484,6 +4484,9 @@ var fetchAppDetails = function fetchAppDetails(appId) {
|
||||
app: appId
|
||||
})
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
return Promise.reject(new Error("Failed to find app id: ".concat(appId)));
|
||||
}
|
||||
return response.json();
|
||||
});
|
||||
};
|
||||
|
||||
Vendored
+10
-1
@@ -92,7 +92,16 @@ const fetchAppDetails = (appId) => {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ app: appId }),
|
||||
}).then((response) => response.json());
|
||||
}).then((response) => {
|
||||
// A missing app now returns a genuine 404 (see ItemController::appload).
|
||||
// fetch() does not reject on 4xx, so surface it as a rejection here to
|
||||
// keep importItems reporting "Failed to find app id" rather than treating
|
||||
// the {"error":...} body as a successful import.
|
||||
if (!response.ok) {
|
||||
return Promise.reject(new Error(`Failed to find app id: ${appId}`));
|
||||
}
|
||||
return response.json();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Item;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* End-to-end coverage for the AJAX POST endpoints that the dashboard relies
|
||||
* on (the routes excluded from CSRF verification: order / appload).
|
||||
*
|
||||
* CSRF itself is disabled while running unit tests, so these tests deliberately
|
||||
* do NOT assert "an un-tokened POST succeeds" (that would be meaningless).
|
||||
* Instead they exercise the controller + routing end-to-end with realistic
|
||||
* input and assert the real, observable behaviour, which is what would break
|
||||
* if the controller or router regressed on a framework upgrade.
|
||||
*/
|
||||
class AjaxPostEndpointsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_order_endpoint_persists_the_new_item_order(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$first = Item::factory()->create(['order' => 5]);
|
||||
$second = Item::factory()->create(['order' => 9]);
|
||||
|
||||
// POST the ids in reverse: index 0 => $second, index 1 => $first.
|
||||
$response = $this->post('/order', [
|
||||
'order' => [$second->id, $first->id],
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$this->assertSame(0, (int) $second->fresh()->order);
|
||||
$this->assertSame(1, (int) $first->fresh()->order);
|
||||
}
|
||||
|
||||
public function test_appload_returns_null_for_the_none_selection(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$response = $this->post('/appload', ['app' => 'null']);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$this->assertSame('', $response->getContent());
|
||||
}
|
||||
|
||||
public function test_appload_surfaces_a_not_found_error_for_an_unknown_app(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$response = $this->post('/appload', ['app' => 'this-app-does-not-exist']);
|
||||
|
||||
// For an unknown app the controller returns a genuine 404 JSON
|
||||
// response. appload() is declared to return
|
||||
// JsonResponse|string|null, so the JsonResponse is served as-is
|
||||
// (correct status + JSON body) rather than being coerced through
|
||||
// Response::__toString() into a raw HTTP message served as a 200.
|
||||
$response->assertStatus(404);
|
||||
$response->assertExactJson(['error' => 'Application not found.']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Controllers\ItemController;
|
||||
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use ReflectionClass;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Guards the CSRF configuration wired up in bootstrap/app.php.
|
||||
*
|
||||
* Laravel's request-forgery middleware self-disables while running unit tests,
|
||||
* so we cannot observe CSRF at request time. Instead we assert the *configuration*
|
||||
* directly: the three excluded URIs really are registered on the framework's
|
||||
* PreventRequestForgery middleware, and the routes those exceptions cover still
|
||||
* resolve to the expected controller actions. Either check would fail if a
|
||||
* framework upgrade renamed / deprecated the exception API (validateCsrfTokens()
|
||||
* now proxies preventRequestForgery()) or changed how the except list is stored,
|
||||
* or if the AJAX routes were dropped.
|
||||
*/
|
||||
class CsrfExceptionsTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function csrfExceptUris(): array
|
||||
{
|
||||
// The except list is stored in the protected static $neverVerify
|
||||
// property that Middleware::validateCsrfTokens(except: [...]) feeds.
|
||||
$reflection = new ReflectionClass(PreventRequestForgery::class);
|
||||
$property = $reflection->getProperty('neverVerify');
|
||||
$property->setAccessible(true);
|
||||
|
||||
return (array) $property->getValue();
|
||||
}
|
||||
|
||||
public function test_ajax_routes_are_registered_as_csrf_exceptions(): void
|
||||
{
|
||||
$except = $this->csrfExceptUris();
|
||||
|
||||
$this->assertContains('order', $except);
|
||||
$this->assertContains('appload', $except);
|
||||
$this->assertContains('test_config', $except);
|
||||
}
|
||||
|
||||
public function test_csrf_excepted_routes_resolve_to_the_expected_actions(): void
|
||||
{
|
||||
$expected = [
|
||||
'items.order' => ['order', 'setOrder'],
|
||||
'appload' => ['appload', 'appload'],
|
||||
'test_config' => ['test_config', 'testConfig'],
|
||||
];
|
||||
|
||||
foreach ($expected as $name => [$uri, $method]) {
|
||||
$route = Route::getRoutes()->getByName($name);
|
||||
|
||||
$this->assertNotNull($route, "Route [{$name}] is not registered.");
|
||||
$this->assertSame($uri, $route->uri());
|
||||
$this->assertContains('POST', $route->methods());
|
||||
$this->assertSame(ItemController::class . '@' . $method, $route->getActionName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Broad "the app still boots and renders on this Laravel version" safety net.
|
||||
*
|
||||
* Beyond the focused per-feature tests, this walks the main GET surface of the
|
||||
* app in one place and asserts every route returns its expected status with no
|
||||
* exception. A framework/PHP upgrade that broke view rendering, routing, the
|
||||
* auth scaffolding or the auth middleware would surface here as a 500 / wrong
|
||||
* status even if a more specific test was missing.
|
||||
*/
|
||||
class RoutesRenderTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_core_get_routes_boot_without_error(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$routes = [
|
||||
'/' => 200,
|
||||
'/login' => 200,
|
||||
'/userselect' => 200,
|
||||
'/settings' => 200,
|
||||
'/items' => 200,
|
||||
'/items/create' => 200,
|
||||
'/tags' => 200,
|
||||
'/health' => 200,
|
||||
'/up' => 200,
|
||||
];
|
||||
|
||||
foreach ($routes as $uri => $expectedStatus) {
|
||||
$response = $this->get($uri);
|
||||
|
||||
$this->assertSame(
|
||||
$expectedStatus,
|
||||
$response->getStatusCode(),
|
||||
"GET {$uri} returned {$response->getStatusCode()}, expected {$expectedStatus}."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_home_redirects_guests_to_login(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
// /home is behind the auth middleware; a guest must be redirected to
|
||||
// the login route (redirectGuestsTo in bootstrap/app.php).
|
||||
$response = $this->get('/home');
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
}
|
||||
|
||||
public function test_search_redirects_to_the_provider(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$response = $this->get('/search?provider=google&q=heimdall');
|
||||
|
||||
$response->assertStatus(302);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Guards the filesystem configuration the icon / avatar upload paths depend on.
|
||||
*
|
||||
* config/filesystems.php explicitly pins the local disk root to
|
||||
* storage_path('app'). Laravel 12 changed the default local root to
|
||||
* storage_path('app/private'); if that pin were lost (or the framework's
|
||||
* default disks stopped being merged in) every stored icon path would silently
|
||||
* point at the wrong directory. These tests fail loudly if that happens.
|
||||
*/
|
||||
class StorageDiskTest extends TestCase
|
||||
{
|
||||
public function test_local_disk_resolves(): void
|
||||
{
|
||||
$this->assertInstanceOf(Filesystem::class, Storage::disk('local'));
|
||||
}
|
||||
|
||||
public function test_public_disk_resolves(): void
|
||||
{
|
||||
// The public disk comes from the framework defaults merged over the
|
||||
// app's partial config/filesystems.php.
|
||||
$this->assertInstanceOf(Filesystem::class, Storage::disk('public'));
|
||||
}
|
||||
|
||||
public function test_local_disk_root_is_pinned_to_storage_app(): void
|
||||
{
|
||||
$this->assertSame(storage_path('app'), config('filesystems.disks.local.root'));
|
||||
|
||||
// The resolved absolute path must live directly under storage/app,
|
||||
// not the Laravel 12 storage/app/private default.
|
||||
$this->assertSame(storage_path('app/icon.png'), Storage::disk('local')->path('icon.png'));
|
||||
}
|
||||
|
||||
public function test_public_disk_root_is_storage_app_public(): void
|
||||
{
|
||||
$this->assertSame(storage_path('app/public'), config('filesystems.disks.public.root'));
|
||||
}
|
||||
|
||||
public function test_public_disk_supports_a_put_exists_get_round_trip(): void
|
||||
{
|
||||
Storage::fake('public');
|
||||
|
||||
$contents = 'icon-bytes';
|
||||
Storage::disk('public')->put('icons/test.png', $contents);
|
||||
|
||||
$this->assertTrue(Storage::disk('public')->exists('icons/test.png'));
|
||||
$this->assertSame($contents, Storage::disk('public')->get('icons/test.png'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\helpers;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Regression coverage for className() in app/Helper.php.
|
||||
*
|
||||
* className() turns a supported-app display name into the PHP class-name
|
||||
* fragment used to resolve enhanced-app classes (see Application::single()).
|
||||
* It relies on a unicode-aware preg_replace, which is exactly the kind of
|
||||
* PCRE behaviour that can change across PHP versions, so the stripping rules
|
||||
* and unicode-safety are pinned here.
|
||||
*/
|
||||
class ClassNameTest extends TestCase
|
||||
{
|
||||
public function test_strips_spaces_and_punctuation(): void
|
||||
{
|
||||
$this->assertSame('HomeAssistant', className('Home Assistant'));
|
||||
$this->assertSame('Pihole', className('Pi-hole'));
|
||||
$this->assertSame('NodeRED', className('Node-RED!'));
|
||||
}
|
||||
|
||||
public function test_keeps_digits(): void
|
||||
{
|
||||
$this->assertSame('App2Go', className('App 2 Go'));
|
||||
}
|
||||
|
||||
public function test_is_unicode_safe(): void
|
||||
{
|
||||
// Letters in other scripts / accented letters must be preserved,
|
||||
// only the separators are removed.
|
||||
$this->assertSame('CaféServer', className('Café Server'));
|
||||
$this->assertSame('中文测试', className('中文 测试'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\helpers;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Regression coverage for the colour brightness globals in app/Helper.php.
|
||||
*
|
||||
* get_brightness() and title_color() drive the automatic black/white tile
|
||||
* text colour on the dashboard. They rely on hexdec(), substr() and integer
|
||||
* maths, all of which are sensitive to PHP behavioural changes, so the
|
||||
* expected luminance values and the black/white threshold are pinned here.
|
||||
*/
|
||||
class ColorHelpersTest extends TestCase
|
||||
{
|
||||
public function test_get_brightness_returns_full_luminance_for_white(): void
|
||||
{
|
||||
$this->assertEqualsWithDelta(255, get_brightness('#ffffff'), 0.0001);
|
||||
}
|
||||
|
||||
public function test_get_brightness_returns_zero_for_black(): void
|
||||
{
|
||||
$this->assertEqualsWithDelta(0, get_brightness('#000000'), 0.0001);
|
||||
}
|
||||
|
||||
public function test_get_brightness_expands_three_char_hex(): void
|
||||
{
|
||||
// #fff / #000 must expand to the six-char form before decoding.
|
||||
$this->assertEqualsWithDelta(255, get_brightness('#fff'), 0.0001);
|
||||
$this->assertEqualsWithDelta(0, get_brightness('#000'), 0.0001);
|
||||
}
|
||||
|
||||
public function test_get_brightness_strips_leading_hash_and_other_non_hex(): void
|
||||
{
|
||||
// A value without the leading # must decode identically.
|
||||
$this->assertEqualsWithDelta(255, get_brightness('ffffff'), 0.0001);
|
||||
|
||||
// Interior non-hex separators (the "other non-hex" in the name) must be
|
||||
// stripped before decoding, so these normalise to ffffff. If the
|
||||
// preg_replace were dropped these would decode to a different value.
|
||||
$this->assertEqualsWithDelta(255, get_brightness('#ff:ff:ff'), 0.0001);
|
||||
$this->assertEqualsWithDelta(255, get_brightness('ff-ff-ff'), 0.0001);
|
||||
}
|
||||
|
||||
public function test_get_brightness_weights_channels_per_luma_formula(): void
|
||||
{
|
||||
// (R*299 + G*587 + B*114) / 1000
|
||||
$this->assertEqualsWithDelta(76.245, get_brightness('#ff0000'), 0.0001);
|
||||
$this->assertEqualsWithDelta(149.685, get_brightness('#00ff00'), 0.0001);
|
||||
$this->assertEqualsWithDelta(29.07, get_brightness('#0000ff'), 0.0001);
|
||||
}
|
||||
|
||||
public function test_title_color_returns_black_for_bright_colours(): void
|
||||
{
|
||||
// Brightness > 130 => dark text.
|
||||
$this->assertSame(' black', title_color('#ffffff'));
|
||||
$this->assertSame(' black', title_color('#00ff00'));
|
||||
}
|
||||
|
||||
public function test_title_color_returns_white_for_dark_colours(): void
|
||||
{
|
||||
// Brightness <= 130 => light text.
|
||||
$this->assertSame(' white', title_color('#000000'));
|
||||
$this->assertSame(' white', title_color('#0000ff'));
|
||||
$this->assertSame(' white', title_color('#ff0000'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\helpers;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Regression coverage for the byte / size formatting globals in app/Helper.php.
|
||||
*
|
||||
* These are plain global functions (no framework involved) so a PHP upgrade
|
||||
* that changes integer/float division, rounding, or numeric-string casting is
|
||||
* exactly the kind of thing that would silently break them. Every assertion
|
||||
* pins a concrete expected string/integer so the behaviour is locked down.
|
||||
*/
|
||||
class SizeHelpersTest extends TestCase
|
||||
{
|
||||
public function test_format_bytes_uses_drive_size_base_1000_by_default(): void
|
||||
{
|
||||
// Default $is_drive_size = true => divide by 1000 (simulated HD size).
|
||||
$this->assertSame('500B', format_bytes(500));
|
||||
$this->assertSame('1KB', format_bytes(1000));
|
||||
$this->assertSame('2KB', format_bytes(2000));
|
||||
$this->assertSame('1MB', format_bytes(1000000));
|
||||
$this->assertSame('1.5MB', format_bytes(1500000));
|
||||
$this->assertSame('1GB', format_bytes(1000000000));
|
||||
$this->assertSame('2.5GB', format_bytes(2500000000));
|
||||
$this->assertSame('1TB', format_bytes(1000000000000));
|
||||
}
|
||||
|
||||
public function test_format_bytes_base_1024_when_not_drive_size(): void
|
||||
{
|
||||
// $is_drive_size = false => divide by 1024 (real byte size).
|
||||
$this->assertSame('1KB', format_bytes(1024, false));
|
||||
$this->assertSame('1MB', format_bytes(1048576, false));
|
||||
$this->assertSame('1GB', format_bytes(1073741824, false));
|
||||
$this->assertSame('1.43MB', format_bytes(1500000, false));
|
||||
}
|
||||
|
||||
public function test_format_bytes_drive_size_flag_changes_the_result(): void
|
||||
{
|
||||
// The same byte count must format differently depending on the base.
|
||||
$this->assertSame('1MB', format_bytes(1000000, true));
|
||||
$this->assertSame('977KB', format_bytes(1000000, false));
|
||||
}
|
||||
|
||||
public function test_format_bytes_caps_at_terabytes(): void
|
||||
{
|
||||
// The unit loop stops at TB (index 4) even for very large inputs.
|
||||
$this->assertSame('5TB', format_bytes(5000000000000));
|
||||
}
|
||||
|
||||
public function test_format_bytes_applies_before_and_after_unit_strings(): void
|
||||
{
|
||||
$this->assertSame('1 KBps', format_bytes(1000, true, ' ', 'ps'));
|
||||
$this->assertSame('1.43 MB/s', format_bytes(1500000, false, ' ', '/s'));
|
||||
}
|
||||
|
||||
public function test_parse_size_resolves_gmk_suffixes_to_bytes(): void
|
||||
{
|
||||
$this->assertSame(1073741824, parse_size('1g'));
|
||||
$this->assertSame(2147483648, parse_size('2G'));
|
||||
$this->assertSame(536870912, parse_size('512m'));
|
||||
$this->assertSame(131072, parse_size('128k'));
|
||||
}
|
||||
|
||||
public function test_parse_size_without_suffix_returns_the_integer_value(): void
|
||||
{
|
||||
$this->assertSame(1024, parse_size('1024'));
|
||||
}
|
||||
}
|
||||
Vendored
+12
-2
@@ -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';
|
||||
|
||||
Vendored
+1
-1
@@ -857,7 +857,7 @@ namespace Aws;
|
||||
*/
|
||||
class Sdk
|
||||
{
|
||||
const VERSION = '3.388.0';
|
||||
const VERSION = '3.388.1';
|
||||
|
||||
/** @var array Arguments for creating clients */
|
||||
private $args;
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+178
-50
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
+3
-1
@@ -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";
|
||||
|
||||
|
||||
+1
-1
@@ -409,6 +409,6 @@ class Tag implements \Reflector
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return "@{$this->getName()} {$this->getContent()}";
|
||||
return trim("@{$this->getName()} {$this->getContent()}");
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+2
-3
@@ -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';
|
||||
|
||||
Vendored
+2
-3
@@ -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';
|
||||
|
||||
Vendored
+2
-3
@@ -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';
|
||||
|
||||
Vendored
+2
-3
@@ -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';
|
||||
|
||||
Vendored
+2
-3
@@ -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';
|
||||
|
||||
Vendored
+2
-3
@@ -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';
|
||||
|
||||
Vendored
+2
-3
@@ -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';
|
||||
|
||||
Vendored
+2
-3
@@ -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';
|
||||
|
||||
Vendored
+301
-1
@@ -2,6 +2,307 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.18.0](https://github.com/brick/math/releases/tag/0.18.0) - 2026-06-14
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
The following breaking change only affects you if you're using named arguments:
|
||||
|
||||
- `BigInteger::fromBytes()` now uses `$bytes` as the parameter name
|
||||
|
||||
🐛 **Bug fixes**
|
||||
|
||||
- `of()` no longer accepts a trailing newline (`\n`) in the input string; such input now throws `NumberFormatException`
|
||||
- `of()` now consistently throws `NumberFormatException` for exponents too large to process, instead of `IntegerOverflowException` in some cases
|
||||
- `BigInteger::fromBase()` now reports the invalid character with its original case in the `NumberFormatException` message, instead of lower-casing it
|
||||
|
||||
⚡️ **Performance improvements**
|
||||
|
||||
- `BigInteger::gcd()` no longer exhausts memory on large inputs when the GMP extension is not installed
|
||||
- `BigInteger::modInverse()` no longer exhausts memory on large inputs, and is faster when the GMP extension is not installed
|
||||
|
||||
👌 **Static analysis improvements**
|
||||
|
||||
- Narrowed parameter and return types with static analysis annotations:
|
||||
- `nthRoot()`'s `$n` is now `positive-int`
|
||||
- `BigInteger::toBase()`, `toArbitraryBase()`, `toBytes()` and `BigRational::toRepeatingDecimalString()` now return `non-empty-string`
|
||||
|
||||
## [0.17.2](https://github.com/brick/math/releases/tag/0.17.2) - 2026-05-25
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- New methods: `BigInteger::nthRoot()` and `BigDecimal::nthRoot()` compute the nth root of a number (#113 by @Pablo1Gustavo)
|
||||
|
||||
## [0.17.1](https://github.com/brick/math/releases/tag/0.17.1) - 2026-04-19
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- More precise `float` approximations in `BigRational::toFloat()`
|
||||
|
||||
## [0.17.0](https://github.com/brick/math/releases/tag/0.17.0) - 2026-03-17
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- Deprecated method `BigDecimal::hasNonZeroFractionalPart()` has been removed, use `! $number->getFractionalPart()->isZero()` instead
|
||||
- Exception constructors and factory methods are now `@internal`
|
||||
|
||||
✨ **Compatibility improvements**
|
||||
|
||||
- `BigDecimal::fromFloatExact()` now supports 32-bit PHP
|
||||
|
||||
## [0.16.2](https://github.com/brick/math/releases/tag/0.16.2) - 2026-03-15
|
||||
|
||||
✨ **New features**
|
||||
|
||||
New methods to create a `BigDecimal` from a `float` value:
|
||||
|
||||
- `BigDecimal::fromFloatExact()`
|
||||
- `BigDecimal::fromFloatShortest()`
|
||||
|
||||
## [0.16.1](https://github.com/brick/math/releases/tag/0.16.1) - 2026-03-09
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- Add `@return non-empty-string` to `toString()`, `jsonSerialize()` and `__toString()` (#111 by @vudaltsov)
|
||||
|
||||
## [0.16.0](https://github.com/brick/math/releases/tag/0.16.0) - 2026-03-06
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- **`BigInteger::getLowestSetBit()` now returns `null` instead of `-1` when the number is zero**
|
||||
- Deprecated method `BigRational::simplified()` has been removed, as it is now a no-op
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- New method: `BigDecimal::getIntegralPart()` returns the integral part as `BigInteger` (this method existed with a different signature in version 0.14, and was removed in 0.15)
|
||||
- New method: `BigDecimal::getFractionalPart()` returns the fractional part as `BigDecimal` (this method existed with a different signature and meaning in version 0.14, and was removed in 0.15)
|
||||
|
||||
🗑️ **Deprecations**
|
||||
|
||||
- Method `BigDecimal::hasNonZeroFractionalPart()` is deprecated, use `->getFractionalPart()->isZero()` instead
|
||||
|
||||
## [0.15.0](https://github.com/brick/math/releases/tag/0.15.0) - 2026-02-20
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- **floating-point inputs are no longer accepted by `of()` and arithmetic methods**, use `of((string) $float)` to get the same behaviour as before (#105)
|
||||
- **`BigRational` is now always simplified to lowest terms:** all operations, including `of()` and `ofFraction()`, now return a fraction in its simplest form (e.g. `2/3` instead of `4/6`)
|
||||
- **`BigDecimal::dividedBy()` now requires the `$scale` parameter**
|
||||
- **`BigInteger::sqrt()` and `BigDecimal::sqrt()` now default to `RoundingMode::Unnecessary`**, explicitly pass `RoundingMode::Down` to get the previous behaviour
|
||||
- **`BigInteger::mod()` now uses Euclidean modulo semantics**: the modulus must be strictly positive, and the result is always non-negative; this change aligns with Java's `BigInteger.mod()` behaviour
|
||||
- **`BigInteger::mod()`, `modInverse()` and `modPow()` now throw `InvalidArgumentException` (instead of `NegativeNumberException`) for negative modulus/exponent arguments**
|
||||
- **`MathException` is now an interface** instead of a class
|
||||
- **`BigDecimal::getPrecision()` now returns `1` for zero values**
|
||||
- `BigNumber::min()`, `max()` and `sum()` now throw an `ArgumentCountError` when called with no arguments (previously threw `InvalidArgumentException`)
|
||||
- `BigInteger::randomBits()` and `randomRange()` now throw `RandomSourceException` when random byte generation fails or returns invalid data
|
||||
|
||||
Deprecated API elements removed:
|
||||
|
||||
- deprecated method `BigInteger::testBit()` has been removed, use `isBitSet()` instead
|
||||
- deprecated method `BigInteger::gcdMultiple()` has been removed, use `gcdAll()` instead
|
||||
- deprecated method `BigDecimal::exactlyDividedBy()` has been removed, use `dividedByExact()` instead
|
||||
- deprecated method `BigDecimal::getIntegralPart()` has been removed (will be re-introduced as returning `BigInteger` in 0.16)
|
||||
- deprecated method `BigDecimal::getFractionalPart()` has been removed (will be re-introduced as returning `BigDecimal` with a different meaning in 0.16)
|
||||
- deprecated method `BigDecimal::stripTrailingZeros()` has been removed, use `strippedOfTrailingZeros()` instead
|
||||
- deprecated method `BigRational::nd()` has been removed, use `ofFraction()` instead
|
||||
- deprecated method `BigRational::quotient()` has been removed, use `getIntegralPart()` instead
|
||||
- deprecated method `BigRational::remainder()` has been removed, use `$number->getNumerator()->remainder($number->getDenominator())` instead
|
||||
- deprecated method `BigRational::quotientAndRemainder()` has been removed, use `$number->getNumerator()->quotientAndRemainder($number->getDenominator())` instead
|
||||
- deprecated `RoundingMode` upper snake case constants (e.g. `HALF_UP`) have been removed, use the pascal case version (e.g. `HalfUp`) instead
|
||||
|
||||
The following breaking changes only affect you if you're using named arguments:
|
||||
|
||||
- `BigInteger::mod()` now uses `$modulus` as the parameter name
|
||||
- `BigInteger::modInverse()` now uses `$modulus` as the parameter name
|
||||
- `BigInteger::modPow()` now uses `$exponent` and `$modulus` as parameter names
|
||||
- `BigInteger::shiftedLeft()` now uses `$bits` as the parameter name
|
||||
- `BigInteger::shiftedRight()` now uses `$bits` as the parameter name
|
||||
- `BigInteger::isBitSet()` now uses `$bitIndex` as the parameter name
|
||||
- `BigInteger::randomBits()` now uses `$bitCount` as the parameter name
|
||||
- `BigDecimal::withPointMovedLeft()` now uses `$places` as the parameter name
|
||||
- `BigDecimal::withPointMovedRight()` now uses `$places` as the parameter name
|
||||
|
||||
The following breaking changes are unlikely to affect you:
|
||||
|
||||
- `DivisionByZeroException::modulusMustNotBeZero()` has been renamed to `zeroModulus()`
|
||||
- `DivisionByZeroException::denominatorMustNotBeZero()` has been renamed to `zeroDenominator()`
|
||||
- `IntegerOverflowException::toIntOverflow()` has been renamed to `integerOutOfRange()`
|
||||
- `RoundingNecessaryException::roundingNecessary()` has been removed
|
||||
|
||||
🗑️ **Deprecations**
|
||||
|
||||
- Method `BigRational::simplified()` is deprecated, as it is now a no-op
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::power()`, `BigDecimal::power()` and `BigRational::power()` no longer enforce an exponent limit
|
||||
- `BigInteger::shiftedLeft()` and `BigInteger::shiftedRight()` no longer enforce a limit on the number of bits
|
||||
- `BigRational::power()` now accepts negative exponents
|
||||
- New exception: `InvalidArgumentException` for invalid argument errors
|
||||
- New exception: `NoInverseException` for modular inverse errors
|
||||
- New exception: `RandomSourceException` for random source errors
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- Narrowed parameter and return types with static analysis annotations (#108 by @simPod)
|
||||
|
||||
## [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 `InvalidArgumentException` 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**
|
||||
|
||||
- All exceptions thrown by the library now implement a common `MathException` interface
|
||||
- `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 +777,3 @@ Added `BigDecimal::divideAndRemainder()`
|
||||
## [0.1.0](https://github.com/brick/math/releases/tag/0.1.0) - 2014-08-31
|
||||
|
||||
First beta release.
|
||||
|
||||
|
||||
Vendored
+265
@@ -0,0 +1,265 @@
|
||||
## Brick\Math
|
||||
|
||||
<img src="https://raw.githubusercontent.com/brick/brick/master/logo.png" alt="" align="left" height="64">
|
||||
|
||||
A PHP library to work with arbitrary precision numbers.
|
||||
|
||||
[](https://github.com/brick/math/actions)
|
||||
[](https://codecov.io/github/brick/math)
|
||||
[](https://packagist.org/packages/brick/math)
|
||||
[](https://packagist.org/packages/brick/math)
|
||||
[](http://opensource.org/licenses/MIT)
|
||||
|
||||
### Installation
|
||||
|
||||
This library is installable via [Composer](https://getcomposer.org/):
|
||||
|
||||
```bash
|
||||
composer require brick/math
|
||||
```
|
||||
|
||||
### Requirements
|
||||
|
||||
This library requires PHP 8.2 or later.
|
||||
|
||||
For PHP 8.1 compatibility, you can use version `0.13`. For PHP 8.0, you can use version `0.11`. For PHP 7.4, you can use version `0.10`. For PHP 7.1, 7.2 & 7.3, you can use version `0.9`. Note that [PHP versions < 8.1 are EOL](http://php.net/supported-versions.php) and not supported anymore. If you're still using one of these PHP versions, you should consider upgrading as soon as possible.
|
||||
|
||||
Although the library can work seamlessly on any PHP installation, it is highly recommended that you install the
|
||||
[GMP](http://php.net/manual/en/book.gmp.php) or [BCMath](http://php.net/manual/en/book.bc.php) extension
|
||||
to speed up calculations. The fastest available calculator implementation will be automatically selected at runtime.
|
||||
|
||||
### Project status & release process
|
||||
|
||||
While this library is still under development, it is well tested and considered stable enough to use in production
|
||||
environments.
|
||||
|
||||
The current releases are numbered `0.x.y`. When a non-breaking change is introduced (adding new methods, optimizing
|
||||
existing code, etc.), `y` is incremented.
|
||||
|
||||
**When a breaking change is introduced, a new `0.x` version cycle is always started.**
|
||||
|
||||
It is therefore safe to lock your project to a given release cycle, such as `^0.17`.
|
||||
|
||||
If you need to upgrade to a newer release cycle, check the [release history](https://github.com/brick/math/releases)
|
||||
for a list of changes introduced by each further `0.x.0` version.
|
||||
|
||||
### Package contents
|
||||
|
||||
This library provides the following public classes in the `Brick\Math` namespace:
|
||||
|
||||
- [BigNumber](https://github.com/brick/math/blob/0.17.2/src/BigNumber.php): base class for `BigInteger`, `BigDecimal` and `BigRational`
|
||||
- [BigInteger](https://github.com/brick/math/blob/0.17.2/src/BigInteger.php): represents an arbitrary-precision integer number.
|
||||
- [BigDecimal](https://github.com/brick/math/blob/0.17.2/src/BigDecimal.php): represents an arbitrary-precision decimal number.
|
||||
- [BigRational](https://github.com/brick/math/blob/0.17.2/src/BigRational.php): represents an arbitrary-precision rational number (fraction), always reduced to lowest terms.
|
||||
- [RoundingMode](https://github.com/brick/math/blob/0.17.2/src/RoundingMode.php): enum representing all available rounding modes.
|
||||
|
||||
And [exceptions](#exceptions) in the `Brick\Math\Exception` namespace.
|
||||
|
||||
### Overview
|
||||
|
||||
#### Instantiation
|
||||
|
||||
The constructors of the classes are not public, you must use a factory method to obtain an instance.
|
||||
|
||||
All classes provide an `of()` factory method that accepts any of the following types:
|
||||
|
||||
- `BigNumber` instances
|
||||
- `int` numbers
|
||||
- `string` representations of integer, decimal and rational numbers
|
||||
|
||||
Example:
|
||||
|
||||
```php
|
||||
BigInteger::of(123546);
|
||||
BigInteger::of('9999999999999999999999999999999999999999999');
|
||||
|
||||
BigDecimal::of('9.99999999999999999999999999999999999999999999');
|
||||
|
||||
BigRational::of('2/3');
|
||||
```
|
||||
|
||||
Note that all `of()` methods accept all the representations above, *as long as it can be safely converted to
|
||||
the current type*:
|
||||
|
||||
```php
|
||||
BigInteger::of('1.00'); // 1
|
||||
BigInteger::of('1.01'); // RoundingNecessaryException
|
||||
|
||||
BigDecimal::of('1/8'); // 0.125
|
||||
BigDecimal::of('1/3'); // RoundingNecessaryException
|
||||
|
||||
BigRational::of('1.1'); // 11/10
|
||||
BigRational::of('1.15'); // 23/20 (reduced to lowest terms)
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The `of()` factory method does not accept `float` values, because casting a float to string can be lossy.
|
||||
> To convert a float to a `BigDecimal`, use one of the dedicated methods:
|
||||
>
|
||||
> ```php
|
||||
> // Exact IEEE-754 representation — the value the float actually holds:
|
||||
> BigDecimal::fromFloatExact(0.1); // 0.1000000000000000055511151231257827021181583404541015625
|
||||
>
|
||||
> // Shortest decimal that round-trips back to the same float:
|
||||
> BigDecimal::fromFloatShortest(0.1); // 0.1
|
||||
> ```
|
||||
|
||||
#### Immutability & chaining
|
||||
|
||||
The `BigInteger`, `BigDecimal` and `BigRational` classes are immutable: their value never changes,
|
||||
so that they can be safely passed around. All methods that return a `BigInteger`, `BigDecimal` or `BigRational`
|
||||
return a new object, leaving the original object unaffected:
|
||||
|
||||
```php
|
||||
$ten = BigInteger::of(10);
|
||||
|
||||
echo $ten->plus(5); // 15
|
||||
echo $ten->multipliedBy(3); // 30
|
||||
```
|
||||
|
||||
The methods can be chained for better readability:
|
||||
|
||||
```php
|
||||
echo BigInteger::of(10)->plus(5)->multipliedBy(3); // 45
|
||||
```
|
||||
|
||||
#### Parameter types
|
||||
|
||||
All methods that accept a number: `plus()`, `minus()`, `multipliedBy()`, etc. accept the same types as `of()`.
|
||||
For example, given the following number:
|
||||
|
||||
```php
|
||||
$integer = BigInteger::of(123);
|
||||
```
|
||||
|
||||
The following lines are equivalent:
|
||||
|
||||
```php
|
||||
$integer->multipliedBy(123);
|
||||
$integer->multipliedBy('123');
|
||||
$integer->multipliedBy($integer);
|
||||
```
|
||||
|
||||
Just like `of()`, other types of `BigNumber` are acceptable, as long as they can be safely converted to the current type:
|
||||
|
||||
```php
|
||||
echo BigInteger::of(2)->multipliedBy(BigDecimal::of('2.0')); // 4
|
||||
echo BigInteger::of(2)->multipliedBy(BigDecimal::of('2.5')); // RoundingNecessaryException
|
||||
echo BigDecimal::of(2.5)->multipliedBy(BigInteger::of(2)); // 5.0
|
||||
```
|
||||
|
||||
#### Division & rounding
|
||||
|
||||
##### BigInteger
|
||||
|
||||
By default, dividing a `BigInteger` returns the exact result of the division, or throws an exception if the remainder
|
||||
of the division is not zero:
|
||||
|
||||
```php
|
||||
echo BigInteger::of(999)->dividedBy(3); // 333
|
||||
echo BigInteger::of(1000)->dividedBy(3); // RoundingNecessaryException
|
||||
```
|
||||
|
||||
You can pass an optional [rounding mode](https://github.com/brick/math/blob/0.17.2/src/RoundingMode.php) to round the result, if necessary:
|
||||
|
||||
```php
|
||||
echo BigInteger::of(1000)->dividedBy(3, RoundingMode::Down); // 333
|
||||
echo BigInteger::of(1000)->dividedBy(3, RoundingMode::Up); // 334
|
||||
```
|
||||
|
||||
If you're into quotients and remainders, there are methods for this, too:
|
||||
|
||||
```php
|
||||
echo BigInteger::of(1000)->quotient(3); // 333
|
||||
echo BigInteger::of(1000)->remainder(3); // 1
|
||||
```
|
||||
|
||||
You can even get both at the same time:
|
||||
|
||||
```php
|
||||
[$quotient, $remainder] = BigInteger::of(1000)->quotientAndRemainder(3);
|
||||
```
|
||||
|
||||
##### BigDecimal
|
||||
|
||||
Dividing a `BigDecimal` always requires a scale to be specified. If the exact result of the division does not fit in
|
||||
the given scale, a [rounding mode](https://github.com/brick/math/blob/0.17.2/src/RoundingMode.php) must be provided.
|
||||
|
||||
```php
|
||||
echo BigDecimal::of(1)->dividedBy('8', 3); // 0.125
|
||||
echo BigDecimal::of(1)->dividedBy('8', 2); // RoundingNecessaryException
|
||||
echo BigDecimal::of(1)->dividedBy('8', 2, RoundingMode::HalfDown); // 0.12
|
||||
echo BigDecimal::of(1)->dividedBy('8', 2, RoundingMode::HalfUp); // 0.13
|
||||
```
|
||||
|
||||
If you know that the division yields a finite number of decimals places, you can use `dividedByExact()`, which will
|
||||
automatically compute the required scale to fit the result, or throw an exception if the division yields an infinite
|
||||
repeating decimal:
|
||||
|
||||
```php
|
||||
echo BigDecimal::of(1)->dividedByExact(256); // 0.00390625
|
||||
echo BigDecimal::of(1)->dividedByExact(11); // RoundingNecessaryException
|
||||
```
|
||||
|
||||
##### BigRational
|
||||
|
||||
The result of the division of a `BigRational` can always be represented exactly:
|
||||
|
||||
```php
|
||||
echo BigRational::of('13/99')->dividedBy('7'); // 13/693
|
||||
echo BigRational::of('13/99')->dividedBy('9/8'); // 104/891
|
||||
```
|
||||
|
||||
#### Bitwise operations
|
||||
|
||||
`BigInteger` supports bitwise operations:
|
||||
|
||||
- `and()`
|
||||
- `or()`
|
||||
- `xor()`
|
||||
- `not()`
|
||||
|
||||
and bit shifting:
|
||||
|
||||
- `shiftedLeft()`
|
||||
- `shiftedRight()`
|
||||
|
||||
#### Exceptions
|
||||
|
||||
All exceptions thrown by this library implement the `MathException` interface.
|
||||
This means that you can safely catch all exceptions thrown by this library using a single catch clause:
|
||||
|
||||
```php
|
||||
use Brick\Math\BigDecimal;
|
||||
use Brick\Math\Exception\MathException;
|
||||
|
||||
try {
|
||||
$number = BigInteger::of(1)->dividedBy(3);
|
||||
} catch (MathException $e) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
If you need more granular control over the exceptions thrown, you can catch the specific exception classes:
|
||||
|
||||
- `DivisionByZeroException`
|
||||
- `IntegerOverflowException`
|
||||
- `InvalidArgumentException`
|
||||
- `NegativeNumberException`
|
||||
- `NoInverseException`
|
||||
- `NumberFormatException`
|
||||
- `RandomSourceException`
|
||||
- `RoundingNecessaryException`
|
||||
- `UnsupportedPlatformException`
|
||||
|
||||
#### Serialization
|
||||
|
||||
`BigInteger`, `BigDecimal` and `BigRational` can be safely serialized on a machine and unserialized on another,
|
||||
even if these machines do not share the same set of PHP extensions.
|
||||
|
||||
For example, serializing on a machine with GMP support and unserializing on a machine that does not have this extension
|
||||
installed will still work as expected.
|
||||
|
||||
#### PHPStan extension
|
||||
|
||||
A third-party [PHPStan extension](https://github.com/simPod/phpstan-brick-math) is available for this library. It provides more specific throw type narrowing for brick/math methods, so that PHPStan can infer the exact exception classes thrown. Note that this extension is not maintained by the author of brick/math.
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
codecov:
|
||||
require_ci_to_pass: true
|
||||
notify:
|
||||
after_n_builds: 6
|
||||
wait_for_ci: true
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
informational: true
|
||||
patch:
|
||||
default:
|
||||
informational: true
|
||||
Vendored
+3
-4
@@ -19,12 +19,11 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.1"
|
||||
"php": "^8.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.1",
|
||||
"php-coveralls/php-coveralls": "^2.2",
|
||||
"vimeo/psalm": "6.8.8"
|
||||
"phpunit/phpunit": "^11.5",
|
||||
"phpstan/phpstan": "2.1.22"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
||||
Vendored
-70
@@ -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>
|
||||
Vendored
+684
-269
File diff suppressed because it is too large
Load Diff
Vendored
+750
-336
File diff suppressed because it is too large
Load Diff
Vendored
+574
-382
File diff suppressed because it is too large
Load Diff
+318
-148
@@ -5,29 +5,38 @@ declare(strict_types=1);
|
||||
namespace Brick\Math;
|
||||
|
||||
use Brick\Math\Exception\DivisionByZeroException;
|
||||
use Brick\Math\Exception\InvalidArgumentException;
|
||||
use Brick\Math\Exception\MathException;
|
||||
use Brick\Math\Exception\NumberFormatException;
|
||||
use Brick\Math\Exception\RoundingNecessaryException;
|
||||
use Brick\Math\Internal\DecimalHelper;
|
||||
use Brick\Math\Internal\Safe;
|
||||
use LogicException;
|
||||
use Override;
|
||||
|
||||
use function max;
|
||||
use function min;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -35,33 +44,34 @@ final class BigRational extends BigNumber
|
||||
* @param BigInteger $numerator The numerator.
|
||||
* @param BigInteger $denominator The denominator.
|
||||
* @param bool $checkDenominator Whether to check the denominator for negative and zero.
|
||||
* @param bool $simplify Whether to simplify the fraction to lowest terms.
|
||||
*
|
||||
* @throws DivisionByZeroException If the denominator is zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator)
|
||||
protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator, bool $simplify)
|
||||
{
|
||||
if ($checkDenominator) {
|
||||
if ($denominator->isZero()) {
|
||||
throw DivisionByZeroException::denominatorMustNotBeZero();
|
||||
throw DivisionByZeroException::zeroDenominator();
|
||||
}
|
||||
|
||||
if ($denominator->isNegative()) {
|
||||
$numerator = $numerator->negated();
|
||||
$numerator = $numerator->negated();
|
||||
$denominator = $denominator->negated();
|
||||
}
|
||||
}
|
||||
|
||||
$this->numerator = $numerator;
|
||||
$this->denominator = $denominator;
|
||||
}
|
||||
if ($simplify) {
|
||||
$gcd = $numerator->gcd($denominator);
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
#[Override]
|
||||
protected static function from(BigNumber $number): static
|
||||
{
|
||||
return $number->toBigRational();
|
||||
$numerator = $numerator->quotient($gcd);
|
||||
$denominator = $denominator->quotient($gcd);
|
||||
}
|
||||
|
||||
$this->numerator = $numerator;
|
||||
$this->denominator = $denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,40 +80,36 @@ final class BigRational extends BigNumber
|
||||
* 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.
|
||||
*
|
||||
* @param BigNumber|int|float|string $numerator The numerator. Must be convertible to a BigInteger.
|
||||
* @param BigNumber|int|float|string $denominator The denominator. Must be convertible to a BigInteger.
|
||||
* @param BigNumber|int|string $numerator The numerator. Must be convertible to a BigInteger.
|
||||
* @param BigNumber|int|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(
|
||||
BigNumber|int|float|string $numerator,
|
||||
BigNumber|int|float|string $denominator,
|
||||
) : BigRational {
|
||||
$numerator = BigInteger::of($numerator);
|
||||
public static function ofFraction(
|
||||
BigNumber|int|string $numerator,
|
||||
BigNumber|int|string $denominator,
|
||||
): BigRational {
|
||||
$numerator = BigInteger::of($numerator);
|
||||
$denominator = BigInteger::of($denominator);
|
||||
|
||||
return new BigRational($numerator, $denominator, true);
|
||||
return new BigRational($numerator, $denominator, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
$zero = new BigRational(BigInteger::zero(), BigInteger::one(), false);
|
||||
$zero = new BigRational(BigInteger::zero(), BigInteger::one(), false, false);
|
||||
}
|
||||
|
||||
return $zero;
|
||||
@@ -112,18 +118,15 @@ 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) {
|
||||
$one = new BigRational(BigInteger::one(), BigInteger::one(), false);
|
||||
$one = new BigRational(BigInteger::one(), BigInteger::one(), false, false);
|
||||
}
|
||||
|
||||
return $one;
|
||||
@@ -132,152 +135,214 @@ 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) {
|
||||
$ten = new BigRational(BigInteger::ten(), BigInteger::one(), false);
|
||||
$ten = new BigRational(BigInteger::ten(), BigInteger::one(), false, false);
|
||||
}
|
||||
|
||||
return $ten;
|
||||
}
|
||||
|
||||
public function getNumerator() : BigInteger
|
||||
/**
|
||||
* Returns the numerator of this rational number.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function getNumerator(): BigInteger
|
||||
{
|
||||
return $this->numerator;
|
||||
}
|
||||
|
||||
public function getDenominator() : BigInteger
|
||||
/**
|
||||
* Returns the denominator of this rational number.
|
||||
*
|
||||
* The denominator is always strictly positive.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function getDenominator(): BigInteger
|
||||
{
|
||||
return $this->denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the quotient of the division of the numerator by the 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()))`. Note that in
|
||||
* this identity, the operand order is significant: the reversed form throws when the fractional part is non-zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function quotient() : BigInteger
|
||||
public function getIntegralPart(): BigInteger
|
||||
{
|
||||
return $this->numerator->quotient($this->denominator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remainder of the division of the numerator by the denominator.
|
||||
*/
|
||||
public function remainder() : BigInteger
|
||||
{
|
||||
return $this->numerator->remainder($this->denominator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the quotient and remainder of the division of the numerator by the denominator.
|
||||
* Returns the fractional part of this rational number.
|
||||
*
|
||||
* @return BigInteger[]
|
||||
* Examples:
|
||||
*
|
||||
* @psalm-return array{BigInteger, BigInteger}
|
||||
* - `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()))`. Note that in
|
||||
* this identity, the operand order is significant: the reversed form throws when the fractional part is non-zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function quotientAndRemainder() : array
|
||||
public function getFractionalPart(): BigRational
|
||||
{
|
||||
return $this->numerator->quotientAndRemainder($this->denominator);
|
||||
return new BigRational($this->numerator->remainder($this->denominator), $this->denominator, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sum of this number and the given one.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The number to add.
|
||||
* @param BigNumber|int|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|string $that): BigRational
|
||||
{
|
||||
$that = BigRational::of($that);
|
||||
|
||||
$numerator = $this->numerator->multipliedBy($that->denominator);
|
||||
$numerator = $numerator->plus($that->numerator->multipliedBy($this->denominator));
|
||||
if ($that->isZero()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($this->isZero()) {
|
||||
return $that;
|
||||
}
|
||||
|
||||
$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);
|
||||
return new BigRational($numerator, $denominator, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the difference of this number and the given one.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The number to subtract.
|
||||
* @param BigNumber|int|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|string $that): BigRational
|
||||
{
|
||||
$that = BigRational::of($that);
|
||||
|
||||
$numerator = $this->numerator->multipliedBy($that->denominator);
|
||||
$numerator = $numerator->minus($that->numerator->multipliedBy($this->denominator));
|
||||
if ($that->isZero()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($this->isZero()) {
|
||||
return $that->negated();
|
||||
}
|
||||
|
||||
$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);
|
||||
return new BigRational($numerator, $denominator, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the product of this number and the given one.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The multiplier.
|
||||
* @param BigNumber|int|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|string $that): BigRational
|
||||
{
|
||||
$that = BigRational::of($that);
|
||||
|
||||
$numerator = $this->numerator->multipliedBy($that->numerator);
|
||||
if ($that->isZero() || $this->isZero()) {
|
||||
return BigRational::zero();
|
||||
}
|
||||
|
||||
$numerator = $this->numerator->multipliedBy($that->numerator);
|
||||
$denominator = $this->denominator->multipliedBy($that->denominator);
|
||||
|
||||
return new BigRational($numerator, $denominator, false);
|
||||
return new BigRational($numerator, $denominator, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result of the division of this number by the given one.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The divisor.
|
||||
* @param BigNumber|int|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|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);
|
||||
return new BigRational($numerator, $denominator, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this number exponentiated to the given value.
|
||||
*
|
||||
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
|
||||
* Unlike BigInteger and BigDecimal, BigRational supports negative exponents:
|
||||
* the result is the reciprocal raised to the absolute value of the exponent.
|
||||
*
|
||||
* @throws DivisionByZeroException If the exponent is negative and this number is zero.
|
||||
*
|
||||
* @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) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($exponent < 0) {
|
||||
if ($this->isZero()) {
|
||||
throw DivisionByZeroException::zeroToNegativePower();
|
||||
}
|
||||
|
||||
return $this->reciprocal()->power(Safe::neg($exponent));
|
||||
}
|
||||
|
||||
return new BigRational(
|
||||
$this->numerator->power($exponent),
|
||||
$this->denominator->power($exponent),
|
||||
false
|
||||
false,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -286,102 +351,137 @@ final class BigRational extends BigNumber
|
||||
*
|
||||
* The reciprocal has the numerator and denominator swapped.
|
||||
*
|
||||
* @throws DivisionByZeroException If the numerator is zero.
|
||||
* @throws DivisionByZeroException If this number is zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function reciprocal() : BigRational
|
||||
public function reciprocal(): BigRational
|
||||
{
|
||||
return new BigRational($this->denominator, $this->numerator, true);
|
||||
}
|
||||
if ($this->isZero()) {
|
||||
throw DivisionByZeroException::reciprocalOfZero();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
return new BigRational($this->numerator->negated(), $this->denominator, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the simplified value of this BigRational.
|
||||
*/
|
||||
public function simplified() : BigRational
|
||||
{
|
||||
$gcd = $this->numerator->gcd($this->denominator);
|
||||
|
||||
$numerator = $this->numerator->quotient($gcd);
|
||||
$denominator = $this->denominator->quotient($gcd);
|
||||
|
||||
return new BigRational($numerator, $denominator, false);
|
||||
return new BigRational($this->denominator, $this->numerator, true, false);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function compareTo(BigNumber|int|float|string $that) : int
|
||||
public function negated(): static
|
||||
{
|
||||
return $this->minus($that)->getSign();
|
||||
return new BigRational($this->numerator->negated(), $this->denominator, false, false);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function getSign() : int
|
||||
public function compareTo(BigNumber|int|string $that): int
|
||||
{
|
||||
$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
|
||||
{
|
||||
return $this->numerator->getSign();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBigInteger() : BigInteger
|
||||
public function toBigInteger(): BigInteger
|
||||
{
|
||||
$simplified = $this->simplified();
|
||||
|
||||
if (! $simplified->denominator->isEqualTo(1)) {
|
||||
throw new RoundingNecessaryException('This rational number cannot be represented as an integer value without rounding.');
|
||||
if ($this->denominator->isEqualTo(1)) {
|
||||
return $this->numerator;
|
||||
}
|
||||
|
||||
return $simplified->numerator;
|
||||
throw RoundingNecessaryException::rationalNotConvertibleToInteger();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBigDecimal() : BigDecimal
|
||||
public function toBigDecimal(): BigDecimal
|
||||
{
|
||||
return $this->numerator->toBigDecimal()->exactlyDividedBy($this->denominator);
|
||||
$scale = DecimalHelper::computeScaleFromReducedFractionDenominator($this->denominator->toString());
|
||||
|
||||
if ($scale === null) {
|
||||
throw RoundingNecessaryException::rationalNotConvertibleToDecimal();
|
||||
}
|
||||
|
||||
return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale)->strippedOfTrailingZeros();
|
||||
}
|
||||
|
||||
#[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
|
||||
{
|
||||
if ($scale < 0) { // @phpstan-ignore smaller.alwaysFalse
|
||||
throw InvalidArgumentException::negativeScale();
|
||||
}
|
||||
|
||||
if ($roundingMode === RoundingMode::Unnecessary) {
|
||||
$requiredScale = DecimalHelper::computeScaleFromReducedFractionDenominator($this->denominator->toString());
|
||||
|
||||
if ($requiredScale === null) {
|
||||
throw RoundingNecessaryException::rationalNotConvertibleToDecimal();
|
||||
}
|
||||
|
||||
if ($requiredScale > $scale) {
|
||||
throw RoundingNecessaryException::rationalScaleTooSmall();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
if ($this->denominator->isEqualTo(1)) {
|
||||
return $this->numerator->toFloat();
|
||||
}
|
||||
|
||||
// Avoid $this->numerator->toFloat() / $this->denominator->toFloat(): converting both operands to float first
|
||||
// adds an extra rounding step before the division and can change the final float. Instead, divide in decimal
|
||||
// first and convert the resulting decimal approximation to float once.
|
||||
|
||||
// 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($this->numerator->abs()->toString()) - strlen($this->denominator->toString());
|
||||
$scale = min(350, max(0, 20 - $magnitude));
|
||||
|
||||
$result = $this->numerator
|
||||
->toBigDecimal()
|
||||
->dividedBy($this->denominator, $scale, RoundingMode::HalfEven)
|
||||
->toFloat();
|
||||
|
||||
// Preserve the sign when the decimal approximation underflows to zero.
|
||||
if ($result === 0.0 && $this->numerator->isNegative()) {
|
||||
return -0.0;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
#[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 +490,69 @@ 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`
|
||||
*
|
||||
* @return non-empty-string
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function toRepeatingDecimalString(): string
|
||||
{
|
||||
if ($this->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 +569,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();
|
||||
}
|
||||
}
|
||||
|
||||
+46
-8
@@ -4,32 +4,70 @@ declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Exception thrown when a division by zero occurs.
|
||||
*/
|
||||
class DivisionByZeroException extends MathException
|
||||
final class DivisionByZeroException extends RuntimeException implements MathException
|
||||
{
|
||||
/**
|
||||
* @psalm-pure
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function divisionByZero() : DivisionByZeroException
|
||||
public function __construct(string $message)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function divisionByZero(): self
|
||||
{
|
||||
return new self('Division by zero.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function modulusMustNotBeZero() : DivisionByZeroException
|
||||
public static function zeroModulus(): self
|
||||
{
|
||||
return new self('The modulus must not be zero.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function denominatorMustNotBeZero() : DivisionByZeroException
|
||||
public static function zeroDenominator(): self
|
||||
{
|
||||
return new self('The denominator of a rational number cannot be zero.');
|
||||
return new self('The denominator of a rational number must not be zero.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function reciprocalOfZero(): self
|
||||
{
|
||||
return new self('The reciprocal of zero is undefined.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function zeroToNegativePower(): self
|
||||
{
|
||||
return new self('Cannot raise zero to a negative power.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,19 +5,52 @@ declare(strict_types=1);
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use Brick\Math\BigInteger;
|
||||
use RuntimeException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
use const PHP_INT_MAX;
|
||||
use const PHP_INT_MIN;
|
||||
|
||||
/**
|
||||
* Exception thrown when an integer overflow occurs.
|
||||
* Exception thrown when a native integer overflow occurs.
|
||||
*/
|
||||
class IntegerOverflowException extends MathException
|
||||
final class IntegerOverflowException extends RuntimeException implements MathException
|
||||
{
|
||||
/**
|
||||
* @psalm-pure
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function toIntOverflow(BigInteger $value) : IntegerOverflowException
|
||||
public function __construct(string $message)
|
||||
{
|
||||
$message = '%s is out of range %d to %d and cannot be represented as an integer.';
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
return new self(\sprintf($message, (string) $value, PHP_INT_MIN, PHP_INT_MAX));
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function integerOutOfRange(BigInteger $value): self
|
||||
{
|
||||
$message = '%s is out of range [%d, %d] and cannot be represented as an integer.';
|
||||
|
||||
return new self(sprintf($message, $value->toString(), PHP_INT_MIN, PHP_INT_MAX));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function nativeIntegerOverflow(string $expression): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Cannot compute %s because the result is outside the native integer range [%d, %d].',
|
||||
$expression,
|
||||
PHP_INT_MIN,
|
||||
PHP_INT_MAX,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Exception thrown when an invalid argument is provided.
|
||||
*/
|
||||
final class InvalidArgumentException extends \InvalidArgumentException implements MathException
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function __construct(string $message)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function baseOutOfRange(int $base): self
|
||||
{
|
||||
return new self(sprintf('Base %d is out of range [2, 36].', $base));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function negativeScale(): self
|
||||
{
|
||||
return new self('The scale must not be negative.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function negativeBitIndex(): self
|
||||
{
|
||||
return new self('The bit index must not be negative.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function negativeBitCount(): self
|
||||
{
|
||||
return new self('The bit count must not be negative.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function alphabetTooShort(): self
|
||||
{
|
||||
return new self('The alphabet must contain at least 2 characters.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function duplicateCharsInAlphabet(): self
|
||||
{
|
||||
return new self('The alphabet must not contain duplicate characters.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function minGreaterThanMax(): self
|
||||
{
|
||||
return new self('The minimum value must be less than or equal to the maximum value.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function cannotConvertFloat(string $type): self
|
||||
{
|
||||
return new self(sprintf('Cannot convert %s to a BigDecimal.', $type));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function negativeExponent(): self
|
||||
{
|
||||
return new self('The exponent must not be negative.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function negativeModulus(): self
|
||||
{
|
||||
return new self('The modulus must not be negative.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function nonPositiveNthRootDegree(): self
|
||||
{
|
||||
return new self('The degree of an nth root must be a positive integer.');
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -4,9 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Base class for all math exceptions.
|
||||
* Base interface for all math exceptions.
|
||||
*/
|
||||
class MathException extends \Exception
|
||||
interface MathException extends Throwable
|
||||
{
|
||||
}
|
||||
|
||||
+52
-1
@@ -4,9 +4,60 @@ declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* 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 RuntimeException implements MathException
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function __construct(string $message)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function squareRootOfNegativeNumber(): self
|
||||
{
|
||||
return new self('Cannot calculate the square root of a negative number.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function nthRootOfNegativeNumber(): self
|
||||
{
|
||||
return new self('Cannot take an even nth root of a negative number.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function toArbitraryBaseOfNegativeNumber(): self
|
||||
{
|
||||
return new self('Cannot convert a negative number to an arbitrary base.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function unsignedBytesOfNegativeNumber(): self
|
||||
{
|
||||
return new self('Cannot convert a negative number to a byte string in unsigned mode.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Exception thrown when attempting to compute a modular inverse that does not exist.
|
||||
*/
|
||||
final class NoInverseException extends RuntimeException implements MathException
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function __construct(string $message)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function noModularInverse(): self
|
||||
{
|
||||
return new self('This number has no multiplicative inverse modulo the given modulus (they are not coprime).');
|
||||
}
|
||||
}
|
||||
+91
-12
@@ -4,38 +4,117 @@ declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
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 RuntimeException implements MathException
|
||||
{
|
||||
public static function invalidFormat(string $value) : self
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function __construct(string $message)
|
||||
{
|
||||
return new self(\sprintf(
|
||||
'The given value "%s" does not represent a valid number.',
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function invalidFormat(string $value): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Value "%s" does not represent a valid number.',
|
||||
$value,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @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),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function charNotValidInBase(string $char, int $base): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Character %s is not valid in base %d.',
|
||||
self::charToString($char),
|
||||
$base,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function emptyNumber(): self
|
||||
{
|
||||
return new self('The number must not be empty.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function emptyByteString(): self
|
||||
{
|
||||
return new self('The byte string must not be empty.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function exponentTooLarge(): self
|
||||
{
|
||||
return new self('The exponent is too large to be represented as an integer.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 . '"';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
use function get_debug_type;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Exception thrown when random byte generation fails.
|
||||
*/
|
||||
final class RandomSourceException extends RuntimeException implements MathException
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function __construct(string $message, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function randomSourceFailure(Throwable $previous): self
|
||||
{
|
||||
return new self('Random byte generation failed.', $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function invalidRandomBytesType(mixed $value): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'The random bytes generator must return a string, got %s.',
|
||||
get_debug_type($value),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function invalidRandomBytesLength(int $expectedLength, int $actualLength): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'The random bytes generator returned %d byte(s), expected %d.',
|
||||
$actualLength,
|
||||
$expectedLength,
|
||||
));
|
||||
}
|
||||
}
|
||||
+148
-4
@@ -4,16 +4,160 @@ declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Exception thrown when a number cannot be represented at the requested scale without rounding.
|
||||
*/
|
||||
class RoundingNecessaryException extends MathException
|
||||
final class RoundingNecessaryException extends RuntimeException implements MathException
|
||||
{
|
||||
/**
|
||||
* @psalm-pure
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function roundingNecessary() : RoundingNecessaryException
|
||||
public function __construct(string $message)
|
||||
{
|
||||
return new self('Rounding is necessary to represent the result of the operation at this scale.');
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function decimalScaleTooSmall(): self
|
||||
{
|
||||
return new self('This decimal number cannot be represented at the requested scale without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function rationalScaleTooSmall(): self
|
||||
{
|
||||
return new self('This rational number cannot be represented at the requested scale without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function integerDivisionNotExact(): self
|
||||
{
|
||||
return new self('The division has a non-zero remainder and cannot be represented as an integer without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function decimalDivisionNotExact(): self
|
||||
{
|
||||
return new self('The division yields a non-terminating decimal expansion and cannot be represented as a decimal without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function decimalDivisionScaleTooSmall(): self
|
||||
{
|
||||
return new self('The division result is exact but cannot be represented at the requested scale without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function integerSquareRootNotExact(): self
|
||||
{
|
||||
return new self('The square root is not exact and cannot be represented as an integer without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function decimalSquareRootNotExact(): self
|
||||
{
|
||||
return new self('The square root is not exact and cannot be represented as a decimal without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function decimalSquareRootScaleTooSmall(): self
|
||||
{
|
||||
return new self('The square root is exact but cannot be represented at the requested scale without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function integerNthRootNotExact(): self
|
||||
{
|
||||
return new self('The nth root is not exact and cannot be represented as an integer without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function decimalNthRootNotExact(): self
|
||||
{
|
||||
return new self('The nth root is not exact and cannot be represented as a decimal without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function decimalNthRootScaleTooSmall(): self
|
||||
{
|
||||
return new self('The nth root is exact but cannot be represented at the requested scale without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function decimalNotConvertibleToInteger(): self
|
||||
{
|
||||
return new self('This decimal number cannot be represented as an integer without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function rationalNotConvertibleToInteger(): self
|
||||
{
|
||||
return new self('This rational number cannot be represented as an integer without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function rationalNotConvertibleToDecimal(): self
|
||||
{
|
||||
return new self('This rational number has a non-terminating decimal expansion and cannot be represented as a decimal without rounding.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Exception thrown when the current PHP platform does not support a required feature.
|
||||
*/
|
||||
final class UnsupportedPlatformException extends RuntimeException implements MathException
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function __construct(string $message)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function unsupportedFloatFormat(): self
|
||||
{
|
||||
return new self('Unsupported float format: expected IEEE-754 double.');
|
||||
}
|
||||
}
|
||||
+263
-174
@@ -4,9 +4,19 @@ declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal;
|
||||
|
||||
use Brick\Math\Exception\RoundingNecessaryException;
|
||||
use Brick\Math\RoundingMode;
|
||||
|
||||
use function chr;
|
||||
use function intdiv;
|
||||
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,109 +27,37 @@ 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.
|
||||
*/
|
||||
public const MAX_POWER = 1_000_000;
|
||||
|
||||
/**
|
||||
* The alphabet for converting from and to base 2 to 36, lowercase.
|
||||
*/
|
||||
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 +66,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 +84,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 +100,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 +126,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 +138,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,23 +150,29 @@ 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.
|
||||
*
|
||||
* @param string $a The base number.
|
||||
* @param int $e The exponent, validated as an integer between 0 and MAX_POWER.
|
||||
* @param int $e The exponent, validated as a non-negative integer.
|
||||
*
|
||||
* @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 +185,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 +212,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,35 +227,35 @@ 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);
|
||||
while ($b !== '0') {
|
||||
[$a, $b] = [$b, $this->divR($a, $b)];
|
||||
}
|
||||
|
||||
if ($b === '0') {
|
||||
return $this->abs($a);
|
||||
}
|
||||
|
||||
return $this->gcd($b, $this->divR($a, $b));
|
||||
return $this->abs($a);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 +263,68 @@ 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;
|
||||
|
||||
/**
|
||||
* Returns the integer nth root of the given number, truncated toward zero.
|
||||
*
|
||||
* If $n is non-negative, the result is the largest x such that x^$k ≤ $n (floor).
|
||||
* If $n is negative, $k MUST be odd, and the result is the negation of the floor root of |$n|
|
||||
* (i.e., truncation toward zero: the smallest x such that x^$k ≥ $n).
|
||||
*
|
||||
* The caller MUST guarantee that $k ≥ 1 and that $n is non-negative when $k is even.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for nth root calculations.
|
||||
*
|
||||
* @param string $n The number. May be negative only when $k is odd.
|
||||
* @param int $k The root degree. Must be strictly positive.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function nthRoot(string $n, int $k): string
|
||||
{
|
||||
if ($n === '0') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
$negative = ($n[0] === '-');
|
||||
$m = $negative ? substr($n, 1) : $n;
|
||||
|
||||
if ($m === '1') {
|
||||
return $negative ? '-1' : '1';
|
||||
}
|
||||
|
||||
// Initial overshoot: 10^ceil(strlen(m)/k) is strictly greater than the true root.
|
||||
// Newton-Raphson requires starting above the true root to converge monotonically down.
|
||||
$x = '1' . str_repeat('0', intdiv(strlen($m) - 1, $k) + 1);
|
||||
|
||||
$kStr = (string) $k;
|
||||
$kMinusOneStr = (string) ($k - 1);
|
||||
|
||||
// Newton-Raphson recurrence for integer nth root:
|
||||
// x_{i+1} = floor(((k-1) * x_i + floor(m / x_i^{k-1})) / k)
|
||||
for (; ;) {
|
||||
$nx = $this->divQ(
|
||||
$this->add(
|
||||
$this->mul($kMinusOneStr, $x),
|
||||
$this->divQ($m, $this->pow($x, $k - 1)),
|
||||
),
|
||||
$kStr,
|
||||
);
|
||||
|
||||
if ($this->cmp($nx, $x) >= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
$x = $nx;
|
||||
}
|
||||
|
||||
return $negative ? $this->neg($x) : $x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a number from an arbitrary base.
|
||||
@@ -316,10 +336,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 +354,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 +383,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 +405,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 +431,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,31 +450,29 @@ abstract class Calculator
|
||||
$result .= $alphabet[$remainder];
|
||||
}
|
||||
|
||||
return \strrev($result);
|
||||
return strrev($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a rounded division.
|
||||
*
|
||||
* Rounding is performed when the remainder of the division is not zero.
|
||||
* When the remainder of the division is not zero, rounding is performed according to the rounding mode provided,
|
||||
* unless RoundingMode::Unnecessary is used, in which case the method returns null.
|
||||
*
|
||||
* @param string $a The dividend.
|
||||
* @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.
|
||||
*
|
||||
* @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 +482,57 @@ abstract class Calculator
|
||||
$increment = false;
|
||||
|
||||
switch ($roundingMode) {
|
||||
case RoundingMode::UNNECESSARY:
|
||||
case RoundingMode::Unnecessary:
|
||||
if ($hasDiscardedFraction) {
|
||||
throw RoundingNecessaryException::roundingNecessary();
|
||||
return null;
|
||||
}
|
||||
|
||||
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 +547,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 +560,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 +573,79 @@ 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,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $a Must be non-negative.
|
||||
* @param string $b Must be non-negative.
|
||||
*
|
||||
* @return array{string, string} GCD, X
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private function gcdExtended(string $a, string $b): array
|
||||
{
|
||||
// Iterative extended Euclidean algorithm; recursion would exhaust memory on large inputs.
|
||||
[$r0, $r1] = [$a, $b];
|
||||
[$x0, $x1] = ['1', '0'];
|
||||
|
||||
while ($r1 !== '0') {
|
||||
[$q, $r] = $this->divQR($r0, $r1);
|
||||
|
||||
[$r0, $r1] = [$r1, $r];
|
||||
[$x0, $x1] = [$x1, $this->sub($x0, $this->mul($q, $x1))];
|
||||
}
|
||||
|
||||
return [$r0, $x0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +678,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 +710,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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+46
-22
@@ -7,69 +7,93 @@ 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);
|
||||
if ($e === 0) {
|
||||
return '1';
|
||||
}
|
||||
|
||||
// bcpow() allocates memory proportional to the exponent even when the base is trivial,
|
||||
// exhausting memory on 32-bit builds at large exponents.
|
||||
if ($a === '0' || $a === '1') {
|
||||
return $a;
|
||||
}
|
||||
|
||||
if ($a === '-1') {
|
||||
return $e % 2 === 0 ? '1' : '-1';
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+80
-38
@@ -5,121 +5,163 @@ 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_root;
|
||||
use function gmp_sqrt;
|
||||
use function gmp_strval;
|
||||
use function gmp_sub;
|
||||
use function gmp_xor;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function nthRoot(string $n, int $k): string
|
||||
{
|
||||
// Delegate on the absolute value and re-apply the sign ourselves so the
|
||||
// truncation-toward-zero convention matches the shared Newton-Raphson fallback
|
||||
// bit-for-bit, regardless of any PHP/GMP behaviour changes for negative inputs.
|
||||
if ($n[0] === '-') {
|
||||
return '-' . gmp_strval(gmp_root(substr($n, 1), $k));
|
||||
}
|
||||
|
||||
return gmp_strval(gmp_root($n, $k));
|
||||
}
|
||||
}
|
||||
|
||||
+102
-84
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal;
|
||||
|
||||
use Brick\Math\RoundingMode;
|
||||
|
||||
use function ltrim;
|
||||
use function rtrim;
|
||||
use function str_pad;
|
||||
use function str_repeat;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
|
||||
use const STR_PAD_LEFT;
|
||||
|
||||
/**
|
||||
* Shared helper for decimal operations.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class DecimalHelper
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the scale needed to represent the exact decimal result of a reduced fraction.
|
||||
*
|
||||
* Returns null if the denominator has prime factors other than 2 or 5.
|
||||
*
|
||||
* @param string $denominator The denominator of the reduced fraction. Must be strictly positive.
|
||||
*
|
||||
* @return non-negative-int|null
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function computeScaleFromReducedFractionDenominator(string $denominator): ?int
|
||||
{
|
||||
$calculator = CalculatorRegistry::get();
|
||||
|
||||
$d = rtrim($denominator, '0');
|
||||
|
||||
/** @var non-negative-int $scale rtrim can only shorten a string */
|
||||
$scale = strlen($denominator) - strlen($d);
|
||||
|
||||
foreach ([5, 2] as $prime) {
|
||||
for (; ;) {
|
||||
$lastDigit = (int) $d[-1];
|
||||
|
||||
if ($lastDigit % $prime !== 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
$d = $calculator->divQ($d, (string) $prime);
|
||||
$scale++;
|
||||
}
|
||||
}
|
||||
|
||||
return $d === '1' ? $scale : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales an unscaled decimal value to the requested scale.
|
||||
*
|
||||
* Returns null when rounding is necessary and the rounding mode is Unnecessary.
|
||||
*
|
||||
* @param string $value The unscaled value.
|
||||
* @param int $currentScale The current scale.
|
||||
* @param int $targetScale The target scale.
|
||||
* @param RoundingMode $roundingMode The rounding mode.
|
||||
*
|
||||
* @return string|null The unscaled value at the target scale, or null if RoundingMode::Unnecessary is used and rounding is necessary.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function scale(string $value, int $currentScale, int $targetScale, RoundingMode $roundingMode): ?string
|
||||
{
|
||||
$scaled = self::tryScaleExactly($value, $currentScale, $targetScale);
|
||||
|
||||
if ($scaled !== null) {
|
||||
return $scaled;
|
||||
}
|
||||
|
||||
if ($roundingMode === RoundingMode::Unnecessary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$divisor = '1' . str_repeat('0', $currentScale - $targetScale);
|
||||
|
||||
return CalculatorRegistry::get()->divRound($value, $divisor, $roundingMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds leading zeros if necessary to represent the full decimal number.
|
||||
*
|
||||
* @param string $value The unscaled value.
|
||||
* @param int $scale The current scale.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function padUnscaledValue(string $value, int $scale): string
|
||||
{
|
||||
$targetLength = $scale + 1;
|
||||
$negative = ($value[0] === '-');
|
||||
$length = strlen($value);
|
||||
|
||||
if ($negative) {
|
||||
$length--;
|
||||
}
|
||||
|
||||
if ($length >= $targetLength) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($negative) {
|
||||
$value = substr($value, 1);
|
||||
}
|
||||
|
||||
$value = str_pad($value, $targetLength, '0', STR_PAD_LEFT);
|
||||
|
||||
if ($negative) {
|
||||
$value = '-' . $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to scale exactly without rounding, returning null when rounding would be required.
|
||||
*
|
||||
* @param string $value The unscaled value.
|
||||
* @param int $currentScale The current scale.
|
||||
* @param int $targetScale The target scale.
|
||||
*
|
||||
* @return string|null The unscaled value at the target scale, or null if rounding would be required.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function tryScaleExactly(string $value, int $currentScale, int $targetScale): ?string
|
||||
{
|
||||
if ($value === '0' || $targetScale === $currentScale) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($targetScale > $currentScale) {
|
||||
return $value . str_repeat('0', $targetScale - $currentScale);
|
||||
}
|
||||
|
||||
$negative = ($value[0] === '-');
|
||||
if ($negative) {
|
||||
$value = substr($value, 1);
|
||||
}
|
||||
|
||||
$value = self::padUnscaledValue($value, $currentScale);
|
||||
$discardedDigits = $currentScale - $targetScale;
|
||||
|
||||
if (substr($value, -$discardedDigits) !== str_repeat('0', $discardedDigits)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = substr($value, 0, -$discardedDigits);
|
||||
$value = ltrim($value, '0');
|
||||
|
||||
if ($value === '') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
if ($negative) {
|
||||
$value = '-' . $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal;
|
||||
|
||||
use Brick\Math\Exception\IntegerOverflowException;
|
||||
|
||||
use function is_int;
|
||||
use function sprintf;
|
||||
|
||||
use const PHP_INT_MIN;
|
||||
|
||||
/**
|
||||
* Helpers for arithmetic operations that throw on native integer overflow.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Safe
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
public static function add(int $a, int $b): int
|
||||
{
|
||||
$result = $a + $b;
|
||||
|
||||
if (is_int($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// @phpstan-ignore deadCode.unreachable
|
||||
throw IntegerOverflowException::nativeIntegerOverflow(sprintf('%d + %d', $a, $b));
|
||||
}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
public static function sub(int $a, int $b): int
|
||||
{
|
||||
$result = $a - $b;
|
||||
|
||||
if (is_int($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// @phpstan-ignore deadCode.unreachable
|
||||
throw IntegerOverflowException::nativeIntegerOverflow(sprintf('%d - %d', $a, $b));
|
||||
}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
public static function mul(int $a, int $b): int
|
||||
{
|
||||
$result = $a * $b;
|
||||
|
||||
if (is_int($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// @phpstan-ignore deadCode.unreachable
|
||||
throw IntegerOverflowException::nativeIntegerOverflow(sprintf('%d * %d', $a, $b));
|
||||
}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
public static function neg(int $value): int
|
||||
{
|
||||
if ($value === PHP_INT_MIN) {
|
||||
throw IntegerOverflowException::nativeIntegerOverflow(sprintf('-(%d)', $value));
|
||||
}
|
||||
|
||||
return -$value;
|
||||
}
|
||||
}
|
||||
+21
-26
@@ -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,62 @@ 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.
|
||||
* It is sometimes known as "Banker's rounding", and is the default rounding mode in IEEE 754.
|
||||
*/
|
||||
case HALF_EVEN;
|
||||
case HalfEven;
|
||||
}
|
||||
|
||||
Vendored
+72
-65
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
+840
-177
File diff suppressed because it is too large
Load Diff
Vendored
+8
-4
@@ -8,9 +8,10 @@ $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',
|
||||
'606a39d89246991a373564698c2d8383' => $vendorDir . '/symfony/polyfill-php85/bootstrap.php',
|
||||
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
|
||||
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
|
||||
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
|
||||
@@ -19,24 +20,27 @@ return array(
|
||||
'9c67151ae59aff4788964ce8eb2a0f43' => $vendorDir . '/clue/stream-filter/src/functions_include.php',
|
||||
'8cff32064859f4559445b89279f3199c' => $vendorDir . '/php-http/message/src/filters.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',
|
||||
'9d2b9fc6db0f153a0a149fefb182415e' => $vendorDir . '/symfony/polyfill-php84/bootstrap.php',
|
||||
'2c2415ec15363ede1bff13a287462ba1' => $vendorDir . '/symfony/polyfill-php86/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',
|
||||
|
||||
Vendored
+9
-6
@@ -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\\Php83\\' => array($vendorDir . '/symfony/polyfill-php83'),
|
||||
'Symfony\\Polyfill\\Php86\\' => array($vendorDir . '/symfony/polyfill-php86'),
|
||||
'Symfony\\Polyfill\\Php85\\' => array($vendorDir . '/symfony/polyfill-php85'),
|
||||
'Symfony\\Polyfill\\Php84\\' => array($vendorDir . '/symfony/polyfill-php84'),
|
||||
'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'),
|
||||
|
||||
Vendored
+10
-17
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+1013
-329
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -3,8 +3,7 @@ composer/class-map-generator
|
||||
|
||||
Utilities to generate class maps and scan PHP code.
|
||||
|
||||
[](https://github.com/composer/class-map-generator/actions)
|
||||
|
||||
[](https://github.com/composer/class-map-generator/actions)
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
+2
-2
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Vendored
+1713
-1408
File diff suppressed because it is too large
Load Diff
Vendored
+387
-364
File diff suppressed because it is too large
Load Diff
Vendored
+9
-5
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user