feat: commit vendor folder directly to bypass air-gapped build restrictions
This commit is contained in:
@@ -4,7 +4,6 @@
|
|||||||
/public/uploadFiles
|
/public/uploadFiles
|
||||||
/public/downloadFiles
|
/public/downloadFiles
|
||||||
/storage/*.key
|
/storage/*.key
|
||||||
/vendor
|
|
||||||
.env
|
.env
|
||||||
.env.backup
|
.env.backup
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
|
|||||||
+7
-7
@@ -40,18 +40,18 @@ RUN echo "deb [trusted=yes] http://10.15.39.186:8081/repository/debian-proxy/ bu
|
|||||||
|
|
||||||
WORKDIR /var/www/html
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
# Salin source code aplikasi
|
# Salin source code aplikasi (Pastikan folder 'vendor' ikut tersalin ke sini)
|
||||||
COPY . /var/www/html
|
COPY . /var/www/html
|
||||||
|
|
||||||
# Install Composer dan arahkan Packagist ke Nexus
|
# =========================================================
|
||||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
|
# PERHATIAN: Blok 'RUN composer install' DIHAPUS.
|
||||||
&& composer config repo.packagist composer http://10.15.39.186:8081/repository/composer-proxy/ \
|
# Pastikan folder vendor/ sudah di-commit ke Git!
|
||||||
&& composer config secure-http false \
|
# =========================================================
|
||||||
&& composer install --prefer-dist --no-dev --optimize-autoloader --no-interaction
|
|
||||||
|
|
||||||
# Atur hak akses folder web, storage, dan cache Laravel
|
# Atur hak akses folder web, storage, dan cache Laravel
|
||||||
RUN chown -R www-data:www-data /var/www/html \
|
RUN chown -R www-data:www-data /var/www/html \
|
||||||
&& chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache
|
&& chmod -R 775 /var/www/html/storage \
|
||||||
|
&& chmod -R 775 /var/www/html/bootstrap/cache
|
||||||
|
|
||||||
# Konfigurasi Nginx
|
# Konfigurasi Nginx
|
||||||
RUN echo 'server { \
|
RUN echo 'server { \
|
||||||
|
|||||||
Vendored
+19
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2013-2017 Alexander <iam.asm89@gmail.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
Vendored
+85
@@ -0,0 +1,85 @@
|
|||||||
|
# Stack/Cors
|
||||||
|
|
||||||
|
Library and middleware enabling cross-origin resource sharing for your
|
||||||
|
http-{foundation,kernel} using application. It attempts to implement the
|
||||||
|
[W3C Recommendation] for cross-origin resource sharing.
|
||||||
|
|
||||||
|
[W3C Recommendation]: http://www.w3.org/TR/cors/
|
||||||
|
|
||||||
|
Build status: 
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Require `asm89/stack-cors` using composer.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
This package can be used as a library or as [stack middleware].
|
||||||
|
|
||||||
|
[stack middleware]: http://stackphp.com/
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Option | Description | Default value |
|
||||||
|
|------------------------|------------------------------------------------------------|---------------|
|
||||||
|
| `allowedMethods` | Matches the request method. | `[]` |
|
||||||
|
| `allowedOrigins` | Matches the request origin. | `[]` |
|
||||||
|
| `allowedOriginsPatterns` | Matches the request origin with `preg_match`. | `[]` |
|
||||||
|
| `allowedHeaders` | Sets the Access-Control-Allow-Headers response header. | `[]` |
|
||||||
|
| `exposedHeaders` | Sets the Access-Control-Expose-Headers response header. | `false` |
|
||||||
|
| `maxAge` | Sets the Access-Control-Max-Age response header.<br/>Set to `null` to omit the header/use browser default. | `0` |
|
||||||
|
| `supportsCredentials` | Sets the Access-Control-Allow-Credentials header. | `false` |
|
||||||
|
|
||||||
|
The _allowedMethods_ and _allowedHeaders_ options are case-insensitive.
|
||||||
|
|
||||||
|
You don't need to provide both _allowedOrigins_ and _allowedOriginsPatterns_. If one of the strings passed matches, it is considered a valid origin.
|
||||||
|
|
||||||
|
If `['*']` is provided to _allowedMethods_, _allowedOrigins_ or _allowedHeaders_ all methods / origins / headers are allowed.
|
||||||
|
|
||||||
|
If _supportsCredentials_ is `true`, you must [explicitly set](https://fetch.spec.whatwg.org/#cors-protocol-and-credentials) `allowedHeaders` for any headers which are not CORS safelisted.
|
||||||
|
|
||||||
|
### Example: using the library
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Asm89\Stack\CorsService;
|
||||||
|
|
||||||
|
$cors = new CorsService([
|
||||||
|
'allowedHeaders' => ['x-allowed-header', 'x-other-allowed-header'],
|
||||||
|
'allowedMethods' => ['DELETE', 'GET', 'POST', 'PUT'],
|
||||||
|
'allowedOrigins' => ['http://localhost'],
|
||||||
|
'allowedOriginsPatterns' => ['/localhost:\d/'],
|
||||||
|
'exposedHeaders' => false,
|
||||||
|
'maxAge' => 600,
|
||||||
|
'supportsCredentials' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$cors->addActualRequestHeaders(Response $response, $origin);
|
||||||
|
$cors->handlePreflightRequest(Request $request);
|
||||||
|
$cors->isActualRequestAllowed(Request $request);
|
||||||
|
$cors->isCorsRequest(Request $request);
|
||||||
|
$cors->isPreflightRequest(Request $request);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: using the stack middleware
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Asm89\Stack\Cors;
|
||||||
|
|
||||||
|
$app = new Cors($app, [
|
||||||
|
// you can use ['*'] to allow any headers
|
||||||
|
'allowedHeaders' => ['x-allowed-header', 'x-other-allowed-header'],
|
||||||
|
// you can use ['*'] to allow any methods
|
||||||
|
'allowedMethods' => ['DELETE', 'GET', 'POST', 'PUT'],
|
||||||
|
// you can use ['*'] to allow requests from any origin
|
||||||
|
'allowedOrigins' => ['localhost'],
|
||||||
|
// you can enter regexes that are matched to the origin request header
|
||||||
|
'allowedOriginsPatterns' => ['/localhost:\d/'],
|
||||||
|
'exposedHeaders' => false,
|
||||||
|
'maxAge' => 600,
|
||||||
|
'supportsCredentials' => false,
|
||||||
|
]);
|
||||||
|
```
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "asm89/stack-cors",
|
||||||
|
"description": "Cross-origin resource sharing library and stack middleware",
|
||||||
|
"keywords": ["stack", "cors"],
|
||||||
|
"homepage": "https://github.com/asm89/stack-cors",
|
||||||
|
"type": "library",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Alexander",
|
||||||
|
"email": "iam.asm89@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^7.3|^8.0",
|
||||||
|
"symfony/http-foundation": "^5.3|^6|^7",
|
||||||
|
"symfony/http-kernel": "^5.3|^6|^7"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^9",
|
||||||
|
"squizlabs/php_codesniffer": "^3.5"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Asm89\\Stack\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Asm89\\Stack\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "phpunit",
|
||||||
|
"check-style": "phpcs -p --standard=PSR12 --exclude=Generic.Files.LineLength --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src",
|
||||||
|
"fix-style": "phpcbf -p --standard=PSR12 --exclude=Generic.Files.LineLength --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.2-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimum-stability": "beta",
|
||||||
|
"prefer-stable": true
|
||||||
|
}
|
||||||
Vendored
+61
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of asm89/stack-cors.
|
||||||
|
*
|
||||||
|
* (c) Alexander <iam.asm89@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Asm89\Stack;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
class Cors implements HttpKernelInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var \Symfony\Component\HttpKernel\HttpKernelInterface
|
||||||
|
*/
|
||||||
|
private $app;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Asm89\Stack\CorsService
|
||||||
|
*/
|
||||||
|
private $cors;
|
||||||
|
|
||||||
|
private $defaultOptions = [
|
||||||
|
'allowedHeaders' => [],
|
||||||
|
'allowedMethods' => [],
|
||||||
|
'allowedOrigins' => [],
|
||||||
|
'allowedOriginsPatterns' => [],
|
||||||
|
'exposedHeaders' => [],
|
||||||
|
'maxAge' => 0,
|
||||||
|
'supportsCredentials' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(HttpKernelInterface $app, array $options = [])
|
||||||
|
{
|
||||||
|
$this->app = $app;
|
||||||
|
$this->cors = new CorsService(array_merge($this->defaultOptions, $options));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response
|
||||||
|
{
|
||||||
|
if ($this->cors->isPreflightRequest($request)) {
|
||||||
|
$response = $this->cors->handlePreflightRequest($request);
|
||||||
|
return $this->cors->varyHeader($response, 'Access-Control-Request-Method');
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->app->handle($request, $type, $catch);
|
||||||
|
|
||||||
|
if ($request->getMethod() === 'OPTIONS') {
|
||||||
|
$this->cors->varyHeader($response, 'Access-Control-Request-Method');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->cors->addActualRequestHeaders($response, $request);
|
||||||
|
}
|
||||||
|
}
|
||||||
+225
@@ -0,0 +1,225 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of asm89/stack-cors.
|
||||||
|
*
|
||||||
|
* (c) Alexander <iam.asm89@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Asm89\Stack;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class CorsService
|
||||||
|
{
|
||||||
|
private $options;
|
||||||
|
|
||||||
|
public function __construct(array $options = [])
|
||||||
|
{
|
||||||
|
$this->options = $this->normalizeOptions($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeOptions(array $options = []): array
|
||||||
|
{
|
||||||
|
$options += [
|
||||||
|
'allowedOrigins' => [],
|
||||||
|
'allowedOriginsPatterns' => [],
|
||||||
|
'supportsCredentials' => false,
|
||||||
|
'allowedHeaders' => [],
|
||||||
|
'exposedHeaders' => [],
|
||||||
|
'allowedMethods' => [],
|
||||||
|
'maxAge' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
// normalize array('*') to true
|
||||||
|
if (in_array('*', $options['allowedOrigins'])) {
|
||||||
|
$options['allowedOrigins'] = true;
|
||||||
|
}
|
||||||
|
if (in_array('*', $options['allowedHeaders'])) {
|
||||||
|
$options['allowedHeaders'] = true;
|
||||||
|
} else {
|
||||||
|
$options['allowedHeaders'] = array_map('strtolower', $options['allowedHeaders']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array('*', $options['allowedMethods'])) {
|
||||||
|
$options['allowedMethods'] = true;
|
||||||
|
} else {
|
||||||
|
$options['allowedMethods'] = array_map('strtoupper', $options['allowedMethods']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use isOriginAllowed
|
||||||
|
*/
|
||||||
|
public function isActualRequestAllowed(Request $request): bool
|
||||||
|
{
|
||||||
|
return $this->isOriginAllowed($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isCorsRequest(Request $request): bool
|
||||||
|
{
|
||||||
|
return $request->headers->has('Origin');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isPreflightRequest(Request $request): bool
|
||||||
|
{
|
||||||
|
return $request->getMethod() === 'OPTIONS' && $request->headers->has('Access-Control-Request-Method');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handlePreflightRequest(Request $request): Response
|
||||||
|
{
|
||||||
|
$response = new Response();
|
||||||
|
|
||||||
|
$response->setStatusCode(204);
|
||||||
|
|
||||||
|
return $this->addPreflightRequestHeaders($response, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addPreflightRequestHeaders(Response $response, Request $request): Response
|
||||||
|
{
|
||||||
|
$this->configureAllowedOrigin($response, $request);
|
||||||
|
|
||||||
|
if ($response->headers->has('Access-Control-Allow-Origin')) {
|
||||||
|
$this->configureAllowCredentials($response, $request);
|
||||||
|
|
||||||
|
$this->configureAllowedMethods($response, $request);
|
||||||
|
|
||||||
|
$this->configureAllowedHeaders($response, $request);
|
||||||
|
|
||||||
|
$this->configureMaxAge($response, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isOriginAllowed(Request $request): bool
|
||||||
|
{
|
||||||
|
if ($this->options['allowedOrigins'] === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$request->headers->has('Origin')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$origin = $request->headers->get('Origin');
|
||||||
|
|
||||||
|
if (in_array($origin, $this->options['allowedOrigins'])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->options['allowedOriginsPatterns'] as $pattern) {
|
||||||
|
if (preg_match($pattern, $origin)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addActualRequestHeaders(Response $response, Request $request): Response
|
||||||
|
{
|
||||||
|
$this->configureAllowedOrigin($response, $request);
|
||||||
|
|
||||||
|
if ($response->headers->has('Access-Control-Allow-Origin')) {
|
||||||
|
$this->configureAllowCredentials($response, $request);
|
||||||
|
|
||||||
|
$this->configureExposedHeaders($response, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function configureAllowedOrigin(Response $response, Request $request)
|
||||||
|
{
|
||||||
|
if ($this->options['allowedOrigins'] === true && !$this->options['supportsCredentials']) {
|
||||||
|
// Safe+cacheable, allow everything
|
||||||
|
$response->headers->set('Access-Control-Allow-Origin', '*');
|
||||||
|
} elseif ($this->isSingleOriginAllowed()) {
|
||||||
|
// Single origins can be safely set
|
||||||
|
$response->headers->set('Access-Control-Allow-Origin', array_values($this->options['allowedOrigins'])[0]);
|
||||||
|
} else {
|
||||||
|
// For dynamic headers, set the requested Origin header when set and allowed
|
||||||
|
if ($this->isCorsRequest($request) && $this->isOriginAllowed($request)) {
|
||||||
|
$response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->varyHeader($response, 'Origin');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isSingleOriginAllowed(): bool
|
||||||
|
{
|
||||||
|
if ($this->options['allowedOrigins'] === true || !empty($this->options['allowedOriginsPatterns'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count($this->options['allowedOrigins']) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function configureAllowedMethods(Response $response, Request $request)
|
||||||
|
{
|
||||||
|
if ($this->options['allowedMethods'] === true) {
|
||||||
|
$allowMethods = strtoupper($request->headers->get('Access-Control-Request-Method'));
|
||||||
|
$this->varyHeader($response, 'Access-Control-Request-Method');
|
||||||
|
} else {
|
||||||
|
$allowMethods = implode(', ', $this->options['allowedMethods']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->headers->set('Access-Control-Allow-Methods', $allowMethods);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function configureAllowedHeaders(Response $response, Request $request)
|
||||||
|
{
|
||||||
|
if ($this->options['allowedHeaders'] === true) {
|
||||||
|
$allowHeaders = $request->headers->get('Access-Control-Request-Headers');
|
||||||
|
$this->varyHeader($response, 'Access-Control-Request-Headers');
|
||||||
|
} else {
|
||||||
|
$allowHeaders = implode(', ', $this->options['allowedHeaders']);
|
||||||
|
}
|
||||||
|
$response->headers->set('Access-Control-Allow-Headers', $allowHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function configureAllowCredentials(Response $response, Request $request)
|
||||||
|
{
|
||||||
|
if ($this->options['supportsCredentials']) {
|
||||||
|
$response->headers->set('Access-Control-Allow-Credentials', 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function configureExposedHeaders(Response $response, Request $request)
|
||||||
|
{
|
||||||
|
if ($this->options['exposedHeaders']) {
|
||||||
|
$response->headers->set('Access-Control-Expose-Headers', implode(', ', $this->options['exposedHeaders']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function configureMaxAge(Response $response, Request $request)
|
||||||
|
{
|
||||||
|
if ($this->options['maxAge'] !== null) {
|
||||||
|
$response->headers->set('Access-Control-Max-Age', (int) $this->options['maxAge']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function varyHeader(Response $response, $header): Response
|
||||||
|
{
|
||||||
|
if (!$response->headers->has('Vary')) {
|
||||||
|
$response->headers->set('Vary', $header);
|
||||||
|
} elseif (!in_array($header, explode(', ', $response->headers->get('Vary')))) {
|
||||||
|
$response->headers->set('Vary', $response->headers->get('Vary') . ', ' . $header);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isSameHost(Request $request): bool
|
||||||
|
{
|
||||||
|
return $request->headers->get('Origin') === $request->getSchemeAndHttpHost();
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+22
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload.php @generated by Composer
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 50600) {
|
||||||
|
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';
|
||||||
|
|
||||||
|
return ComposerAutoloaderInit2093020b165c40a9b39e5bea40080e73::getLoader();
|
||||||
Vendored
+119
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy PHP file generated by Composer
|
||||||
|
*
|
||||||
|
* This file includes the referenced bin path (../nesbot/carbon/bin/carbon)
|
||||||
|
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||||
|
*
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer;
|
||||||
|
|
||||||
|
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||||
|
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 80000) {
|
||||||
|
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BinProxyWrapper
|
||||||
|
{
|
||||||
|
private $handle;
|
||||||
|
private $position;
|
||||||
|
private $realpath;
|
||||||
|
|
||||||
|
public function stream_open($path, $mode, $options, &$opened_path)
|
||||||
|
{
|
||||||
|
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||||
|
$opened_path = substr($path, 17);
|
||||||
|
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||||
|
$opened_path = $this->realpath;
|
||||||
|
$this->handle = fopen($this->realpath, $mode);
|
||||||
|
$this->position = 0;
|
||||||
|
|
||||||
|
return (bool) $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_read($count)
|
||||||
|
{
|
||||||
|
$data = fread($this->handle, $count);
|
||||||
|
|
||||||
|
if ($this->position === 0) {
|
||||||
|
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->position += strlen($data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_cast($castAs)
|
||||||
|
{
|
||||||
|
return $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_close()
|
||||||
|
{
|
||||||
|
fclose($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_lock($operation)
|
||||||
|
{
|
||||||
|
return $operation ? flock($this->handle, $operation) : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_seek($offset, $whence)
|
||||||
|
{
|
||||||
|
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||||
|
$this->position = ftell($this->handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_tell()
|
||||||
|
{
|
||||||
|
return $this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_eof()
|
||||||
|
{
|
||||||
|
return feof($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_stat()
|
||||||
|
{
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_set_option($option, $arg1, $arg2)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url_stat($path, $flags)
|
||||||
|
{
|
||||||
|
$path = substr($path, 17);
|
||||||
|
if (file_exists($path)) {
|
||||||
|
return stat($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||||
|
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||||
|
) {
|
||||||
|
return include("phpvfscomposer://" . __DIR__ . '/..'.'/nesbot/carbon/bin/carbon');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return include __DIR__ . '/..'.'/nesbot/carbon/bin/carbon';
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
setlocal DISABLEDELAYEDEXPANSION
|
||||||
|
SET BIN_TARGET=%~dp0/carbon
|
||||||
|
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||||
|
php "%BIN_TARGET%" %*
|
||||||
Vendored
+119
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy PHP file generated by Composer
|
||||||
|
*
|
||||||
|
* This file includes the referenced bin path (../symfony/error-handler/Resources/bin/patch-type-declarations)
|
||||||
|
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||||
|
*
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer;
|
||||||
|
|
||||||
|
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||||
|
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 80000) {
|
||||||
|
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BinProxyWrapper
|
||||||
|
{
|
||||||
|
private $handle;
|
||||||
|
private $position;
|
||||||
|
private $realpath;
|
||||||
|
|
||||||
|
public function stream_open($path, $mode, $options, &$opened_path)
|
||||||
|
{
|
||||||
|
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||||
|
$opened_path = substr($path, 17);
|
||||||
|
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||||
|
$opened_path = $this->realpath;
|
||||||
|
$this->handle = fopen($this->realpath, $mode);
|
||||||
|
$this->position = 0;
|
||||||
|
|
||||||
|
return (bool) $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_read($count)
|
||||||
|
{
|
||||||
|
$data = fread($this->handle, $count);
|
||||||
|
|
||||||
|
if ($this->position === 0) {
|
||||||
|
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->position += strlen($data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_cast($castAs)
|
||||||
|
{
|
||||||
|
return $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_close()
|
||||||
|
{
|
||||||
|
fclose($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_lock($operation)
|
||||||
|
{
|
||||||
|
return $operation ? flock($this->handle, $operation) : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_seek($offset, $whence)
|
||||||
|
{
|
||||||
|
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||||
|
$this->position = ftell($this->handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_tell()
|
||||||
|
{
|
||||||
|
return $this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_eof()
|
||||||
|
{
|
||||||
|
return feof($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_stat()
|
||||||
|
{
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_set_option($option, $arg1, $arg2)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url_stat($path, $flags)
|
||||||
|
{
|
||||||
|
$path = substr($path, 17);
|
||||||
|
if (file_exists($path)) {
|
||||||
|
return stat($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||||
|
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||||
|
) {
|
||||||
|
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return include __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations';
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
setlocal DISABLEDELAYEDEXPANSION
|
||||||
|
SET BIN_TARGET=%~dp0/patch-type-declarations
|
||||||
|
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||||
|
php "%BIN_TARGET%" %*
|
||||||
Vendored
+119
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy PHP file generated by Composer
|
||||||
|
*
|
||||||
|
* This file includes the referenced bin path (../nikic/php-parser/bin/php-parse)
|
||||||
|
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||||
|
*
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer;
|
||||||
|
|
||||||
|
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||||
|
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 80000) {
|
||||||
|
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BinProxyWrapper
|
||||||
|
{
|
||||||
|
private $handle;
|
||||||
|
private $position;
|
||||||
|
private $realpath;
|
||||||
|
|
||||||
|
public function stream_open($path, $mode, $options, &$opened_path)
|
||||||
|
{
|
||||||
|
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||||
|
$opened_path = substr($path, 17);
|
||||||
|
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||||
|
$opened_path = $this->realpath;
|
||||||
|
$this->handle = fopen($this->realpath, $mode);
|
||||||
|
$this->position = 0;
|
||||||
|
|
||||||
|
return (bool) $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_read($count)
|
||||||
|
{
|
||||||
|
$data = fread($this->handle, $count);
|
||||||
|
|
||||||
|
if ($this->position === 0) {
|
||||||
|
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->position += strlen($data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_cast($castAs)
|
||||||
|
{
|
||||||
|
return $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_close()
|
||||||
|
{
|
||||||
|
fclose($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_lock($operation)
|
||||||
|
{
|
||||||
|
return $operation ? flock($this->handle, $operation) : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_seek($offset, $whence)
|
||||||
|
{
|
||||||
|
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||||
|
$this->position = ftell($this->handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_tell()
|
||||||
|
{
|
||||||
|
return $this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_eof()
|
||||||
|
{
|
||||||
|
return feof($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_stat()
|
||||||
|
{
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_set_option($option, $arg1, $arg2)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url_stat($path, $flags)
|
||||||
|
{
|
||||||
|
$path = substr($path, 17);
|
||||||
|
if (file_exists($path)) {
|
||||||
|
return stat($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||||
|
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||||
|
) {
|
||||||
|
return include("phpvfscomposer://" . __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return include __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse';
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
setlocal DISABLEDELAYEDEXPANSION
|
||||||
|
SET BIN_TARGET=%~dp0/php-parse
|
||||||
|
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||||
|
php "%BIN_TARGET%" %*
|
||||||
Vendored
+119
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy PHP file generated by Composer
|
||||||
|
*
|
||||||
|
* This file includes the referenced bin path (../psy/psysh/bin/psysh)
|
||||||
|
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||||
|
*
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer;
|
||||||
|
|
||||||
|
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||||
|
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 80000) {
|
||||||
|
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BinProxyWrapper
|
||||||
|
{
|
||||||
|
private $handle;
|
||||||
|
private $position;
|
||||||
|
private $realpath;
|
||||||
|
|
||||||
|
public function stream_open($path, $mode, $options, &$opened_path)
|
||||||
|
{
|
||||||
|
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||||
|
$opened_path = substr($path, 17);
|
||||||
|
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||||
|
$opened_path = $this->realpath;
|
||||||
|
$this->handle = fopen($this->realpath, $mode);
|
||||||
|
$this->position = 0;
|
||||||
|
|
||||||
|
return (bool) $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_read($count)
|
||||||
|
{
|
||||||
|
$data = fread($this->handle, $count);
|
||||||
|
|
||||||
|
if ($this->position === 0) {
|
||||||
|
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->position += strlen($data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_cast($castAs)
|
||||||
|
{
|
||||||
|
return $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_close()
|
||||||
|
{
|
||||||
|
fclose($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_lock($operation)
|
||||||
|
{
|
||||||
|
return $operation ? flock($this->handle, $operation) : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_seek($offset, $whence)
|
||||||
|
{
|
||||||
|
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||||
|
$this->position = ftell($this->handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_tell()
|
||||||
|
{
|
||||||
|
return $this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_eof()
|
||||||
|
{
|
||||||
|
return feof($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_stat()
|
||||||
|
{
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_set_option($option, $arg1, $arg2)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url_stat($path, $flags)
|
||||||
|
{
|
||||||
|
$path = substr($path, 17);
|
||||||
|
if (file_exists($path)) {
|
||||||
|
return stat($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||||
|
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||||
|
) {
|
||||||
|
return include("phpvfscomposer://" . __DIR__ . '/..'.'/psy/psysh/bin/psysh');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return include __DIR__ . '/..'.'/psy/psysh/bin/psysh';
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
setlocal DISABLEDELAYEDEXPANSION
|
||||||
|
SET BIN_TARGET=%~dp0/psysh
|
||||||
|
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||||
|
php "%BIN_TARGET%" %*
|
||||||
Vendored
+119
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy PHP file generated by Composer
|
||||||
|
*
|
||||||
|
* This file includes the referenced bin path (../symfony/var-dumper/Resources/bin/var-dump-server)
|
||||||
|
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||||
|
*
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer;
|
||||||
|
|
||||||
|
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||||
|
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 80000) {
|
||||||
|
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BinProxyWrapper
|
||||||
|
{
|
||||||
|
private $handle;
|
||||||
|
private $position;
|
||||||
|
private $realpath;
|
||||||
|
|
||||||
|
public function stream_open($path, $mode, $options, &$opened_path)
|
||||||
|
{
|
||||||
|
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||||
|
$opened_path = substr($path, 17);
|
||||||
|
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||||
|
$opened_path = $this->realpath;
|
||||||
|
$this->handle = fopen($this->realpath, $mode);
|
||||||
|
$this->position = 0;
|
||||||
|
|
||||||
|
return (bool) $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_read($count)
|
||||||
|
{
|
||||||
|
$data = fread($this->handle, $count);
|
||||||
|
|
||||||
|
if ($this->position === 0) {
|
||||||
|
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->position += strlen($data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_cast($castAs)
|
||||||
|
{
|
||||||
|
return $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_close()
|
||||||
|
{
|
||||||
|
fclose($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_lock($operation)
|
||||||
|
{
|
||||||
|
return $operation ? flock($this->handle, $operation) : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_seek($offset, $whence)
|
||||||
|
{
|
||||||
|
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||||
|
$this->position = ftell($this->handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_tell()
|
||||||
|
{
|
||||||
|
return $this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_eof()
|
||||||
|
{
|
||||||
|
return feof($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_stat()
|
||||||
|
{
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_set_option($option, $arg1, $arg2)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url_stat($path, $flags)
|
||||||
|
{
|
||||||
|
$path = substr($path, 17);
|
||||||
|
if (file_exists($path)) {
|
||||||
|
return stat($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||||
|
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||||
|
) {
|
||||||
|
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return include __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server';
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
setlocal DISABLEDELAYEDEXPANSION
|
||||||
|
SET BIN_TARGET=%~dp0/var-dump-server
|
||||||
|
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||||
|
php "%BIN_TARGET%" %*
|
||||||
Vendored
+415
@@ -0,0 +1,415 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.9.3](https://github.com/brick/math/releases/tag/0.9.3) - 2021-08-15
|
||||||
|
|
||||||
|
🚀 **Compatibility with PHP 8.1**
|
||||||
|
|
||||||
|
- Support for custom object serialization; this removes a warning on PHP 8.1 due to the `Serializable` interface being deprecated (thanks @TRowbotham)
|
||||||
|
|
||||||
|
## [0.9.2](https://github.com/brick/math/releases/tag/0.9.2) - 2021-01-20
|
||||||
|
|
||||||
|
🐛 **Bug fix**
|
||||||
|
|
||||||
|
- Incorrect results could be returned when using the BCMath calculator, with a default scale set with `bcscale()`, on PHP >= 7.2 (#55).
|
||||||
|
|
||||||
|
## [0.9.1](https://github.com/brick/math/releases/tag/0.9.1) - 2020-08-19
|
||||||
|
|
||||||
|
✨ New features
|
||||||
|
|
||||||
|
- `BigInteger::not()` returns the bitwise `NOT` value
|
||||||
|
|
||||||
|
🐛 **Bug fixes**
|
||||||
|
|
||||||
|
- `BigInteger::toBytes()` could return an incorrect binary representation for some numbers
|
||||||
|
- The bitwise operations `and()`, `or()`, `xor()` on `BigInteger` could return an incorrect result when the GMP extension is not available
|
||||||
|
|
||||||
|
## [0.9.0](https://github.com/brick/math/releases/tag/0.9.0) - 2020-08-18
|
||||||
|
|
||||||
|
👌 **Improvements**
|
||||||
|
|
||||||
|
- `BigNumber::of()` now accepts `.123` and `123.` formats, both of which return a `BigDecimal`
|
||||||
|
|
||||||
|
💥 **Breaking changes**
|
||||||
|
|
||||||
|
- Deprecated method `BigInteger::powerMod()` has been removed - use `modPow()` instead
|
||||||
|
- Deprecated method `BigInteger::parse()` has been removed - use `fromBase()` instead
|
||||||
|
|
||||||
|
## [0.8.17](https://github.com/brick/math/releases/tag/0.8.17) - 2020-08-19
|
||||||
|
|
||||||
|
🐛 **Bug fix**
|
||||||
|
|
||||||
|
- `BigInteger::toBytes()` could return an incorrect binary representation for some numbers
|
||||||
|
- The bitwise operations `and()`, `or()`, `xor()` on `BigInteger` could return an incorrect result when the GMP extension is not available
|
||||||
|
|
||||||
|
## [0.8.16](https://github.com/brick/math/releases/tag/0.8.16) - 2020-08-18
|
||||||
|
|
||||||
|
🚑 **Critical fix**
|
||||||
|
|
||||||
|
- This version reintroduces the deprecated `BigInteger::parse()` method, that has been removed by mistake in version `0.8.9` and should have lasted for the whole `0.8` release cycle.
|
||||||
|
|
||||||
|
✨ **New features**
|
||||||
|
|
||||||
|
- `BigInteger::modInverse()` calculates a modular multiplicative inverse
|
||||||
|
- `BigInteger::fromBytes()` creates a `BigInteger` from a byte string
|
||||||
|
- `BigInteger::toBytes()` converts a `BigInteger` to a byte string
|
||||||
|
- `BigInteger::randomBits()` creates a pseudo-random `BigInteger` of a given bit length
|
||||||
|
- `BigInteger::randomRange()` creates a pseudo-random `BigInteger` between two bounds
|
||||||
|
|
||||||
|
💩 **Deprecations**
|
||||||
|
|
||||||
|
- `BigInteger::powerMod()` is now deprecated in favour of `modPow()`
|
||||||
|
|
||||||
|
## [0.8.15](https://github.com/brick/math/releases/tag/0.8.15) - 2020-04-15
|
||||||
|
|
||||||
|
🐛 **Fixes**
|
||||||
|
|
||||||
|
- added missing `ext-json` requirement, due to `BigNumber` implementing `JsonSerializable`
|
||||||
|
|
||||||
|
⚡️ **Optimizations**
|
||||||
|
|
||||||
|
- additional optimization in `BigInteger::remainder()`
|
||||||
|
|
||||||
|
## [0.8.14](https://github.com/brick/math/releases/tag/0.8.14) - 2020-02-18
|
||||||
|
|
||||||
|
✨ **New features**
|
||||||
|
|
||||||
|
- `BigInteger::getLowestSetBit()` returns the index of the rightmost one bit
|
||||||
|
|
||||||
|
## [0.8.13](https://github.com/brick/math/releases/tag/0.8.13) - 2020-02-16
|
||||||
|
|
||||||
|
✨ **New features**
|
||||||
|
|
||||||
|
- `BigInteger::isEven()` tests whether the number is even
|
||||||
|
- `BigInteger::isOdd()` tests whether the number is odd
|
||||||
|
- `BigInteger::testBit()` tests if a bit is set
|
||||||
|
- `BigInteger::getBitLength()` returns the number of bits in the minimal representation of the number
|
||||||
|
|
||||||
|
## [0.8.12](https://github.com/brick/math/releases/tag/0.8.12) - 2020-02-03
|
||||||
|
|
||||||
|
🛠️ **Maintenance release**
|
||||||
|
|
||||||
|
Classes are now annotated for better static analysis with [psalm](https://psalm.dev/).
|
||||||
|
|
||||||
|
This is a maintenance release: no bug fixes, no new features, no breaking changes.
|
||||||
|
|
||||||
|
## [0.8.11](https://github.com/brick/math/releases/tag/0.8.11) - 2020-01-23
|
||||||
|
|
||||||
|
✨ **New feature**
|
||||||
|
|
||||||
|
`BigInteger::powerMod()` performs a power-with-modulo operation. Useful for crypto.
|
||||||
|
|
||||||
|
## [0.8.10](https://github.com/brick/math/releases/tag/0.8.10) - 2020-01-21
|
||||||
|
|
||||||
|
✨ **New feature**
|
||||||
|
|
||||||
|
`BigInteger::mod()` returns the **modulo** of two numbers. The *modulo* differs from the *remainder* when the signs of the operands are different.
|
||||||
|
|
||||||
|
## [0.8.9](https://github.com/brick/math/releases/tag/0.8.9) - 2020-01-08
|
||||||
|
|
||||||
|
⚡️ **Performance improvements**
|
||||||
|
|
||||||
|
A few additional optimizations in `BigInteger` and `BigDecimal` when one of the operands can be returned as is. Thanks to @tomtomsen in #24.
|
||||||
|
|
||||||
|
## [0.8.8](https://github.com/brick/math/releases/tag/0.8.8) - 2019-04-25
|
||||||
|
|
||||||
|
🐛 **Bug fixes**
|
||||||
|
|
||||||
|
- `BigInteger::toBase()` could return an empty string for zero values (BCMath & Native calculators only, GMP calculator unaffected)
|
||||||
|
|
||||||
|
✨ **New features**
|
||||||
|
|
||||||
|
- `BigInteger::toArbitraryBase()` converts a number to an arbitrary base, using a custom alphabet
|
||||||
|
- `BigInteger::fromArbitraryBase()` converts a string in an arbitrary base, using a custom alphabet, back to a number
|
||||||
|
|
||||||
|
These methods can be used as the foundation to convert strings between different bases/alphabets, using BigInteger as an intermediate representation.
|
||||||
|
|
||||||
|
💩 **Deprecations**
|
||||||
|
|
||||||
|
- `BigInteger::parse()` is now deprecated in favour of `fromBase()`
|
||||||
|
|
||||||
|
`BigInteger::fromBase()` works the same way as `parse()`, with 2 minor differences:
|
||||||
|
|
||||||
|
- the `$base` parameter is required, it does not default to `10`
|
||||||
|
- it throws a `NumberFormatException` instead of an `InvalidArgumentException` when the number is malformed
|
||||||
|
|
||||||
|
## [0.8.7](https://github.com/brick/math/releases/tag/0.8.7) - 2019-04-20
|
||||||
|
|
||||||
|
**Improvements**
|
||||||
|
|
||||||
|
- Safer conversion from `float` when using custom locales
|
||||||
|
- **Much faster** `NativeCalculator` implementation 🚀
|
||||||
|
|
||||||
|
You can expect **at least a 3x performance improvement** for common arithmetic operations when using the library on systems without GMP or BCMath; it gets exponentially faster on multiplications with a high number of digits. This is due to calculations now being performed on whole blocks of digits (the block size depending on the platform, 32-bit or 64-bit) instead of digit-by-digit as before.
|
||||||
|
|
||||||
|
## [0.8.6](https://github.com/brick/math/releases/tag/0.8.6) - 2019-04-11
|
||||||
|
|
||||||
|
**New method**
|
||||||
|
|
||||||
|
`BigNumber::sum()` returns the sum of one or more numbers.
|
||||||
|
|
||||||
|
## [0.8.5](https://github.com/brick/math/releases/tag/0.8.5) - 2019-02-12
|
||||||
|
|
||||||
|
**Bug fix**: `of()` factory methods could fail when passing a `float` in environments using a `LC_NUMERIC` locale with a decimal separator other than `'.'` (#20).
|
||||||
|
|
||||||
|
Thanks @manowark 👍
|
||||||
|
|
||||||
|
## [0.8.4](https://github.com/brick/math/releases/tag/0.8.4) - 2018-12-07
|
||||||
|
|
||||||
|
**New method**
|
||||||
|
|
||||||
|
`BigDecimal::sqrt()` calculates the square root of a decimal number, to a given scale.
|
||||||
|
|
||||||
|
## [0.8.3](https://github.com/brick/math/releases/tag/0.8.3) - 2018-12-06
|
||||||
|
|
||||||
|
**New method**
|
||||||
|
|
||||||
|
`BigInteger::sqrt()` calculates the square root of a number (thanks @peter279k).
|
||||||
|
|
||||||
|
**New exception**
|
||||||
|
|
||||||
|
`NegativeNumberException` is thrown when calling `sqrt()` on a negative number.
|
||||||
|
|
||||||
|
## [0.8.2](https://github.com/brick/math/releases/tag/0.8.2) - 2018-11-08
|
||||||
|
|
||||||
|
**Performance update**
|
||||||
|
|
||||||
|
- Further improvement of `toInt()` performance
|
||||||
|
- `NativeCalculator` can now perform some multiplications more efficiently
|
||||||
|
|
||||||
|
## [0.8.1](https://github.com/brick/math/releases/tag/0.8.1) - 2018-11-07
|
||||||
|
|
||||||
|
Performance optimization of `toInt()` methods.
|
||||||
|
|
||||||
|
## [0.8.0](https://github.com/brick/math/releases/tag/0.8.0) - 2018-10-13
|
||||||
|
|
||||||
|
**Breaking changes**
|
||||||
|
|
||||||
|
The following deprecated methods have been removed. Use the new method name instead:
|
||||||
|
|
||||||
|
| Method removed | Replacement method |
|
||||||
|
| --- | --- |
|
||||||
|
| `BigDecimal::getIntegral()` | `BigDecimal::getIntegralPart()` |
|
||||||
|
| `BigDecimal::getFraction()` | `BigDecimal::getFractionalPart()` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**New features**
|
||||||
|
|
||||||
|
`BigInteger` has been augmented with 5 new methods for bitwise operations:
|
||||||
|
|
||||||
|
| New method | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| `and()` | performs a bitwise `AND` operation on two numbers |
|
||||||
|
| `or()` | performs a bitwise `OR` operation on two numbers |
|
||||||
|
| `xor()` | performs a bitwise `XOR` operation on two numbers |
|
||||||
|
| `shiftedLeft()` | returns the number shifted left by a number of bits |
|
||||||
|
| `shiftedRight()` | returns the number shifted right by a number of bits |
|
||||||
|
|
||||||
|
Thanks to @DASPRiD 👍
|
||||||
|
|
||||||
|
## [0.7.3](https://github.com/brick/math/releases/tag/0.7.3) - 2018-08-20
|
||||||
|
|
||||||
|
**New method:** `BigDecimal::hasNonZeroFractionalPart()`
|
||||||
|
|
||||||
|
**Renamed/deprecated methods:**
|
||||||
|
|
||||||
|
- `BigDecimal::getIntegral()` has been renamed to `getIntegralPart()` and is now deprecated
|
||||||
|
- `BigDecimal::getFraction()` has been renamed to `getFractionalPart()` and is now deprecated
|
||||||
|
|
||||||
|
## [0.7.2](https://github.com/brick/math/releases/tag/0.7.2) - 2018-07-21
|
||||||
|
|
||||||
|
**Performance update**
|
||||||
|
|
||||||
|
`BigInteger::parse()` and `toBase()` now use GMP's built-in base conversion features when available.
|
||||||
|
|
||||||
|
## [0.7.1](https://github.com/brick/math/releases/tag/0.7.1) - 2018-03-01
|
||||||
|
|
||||||
|
This is a maintenance release, no code has been changed.
|
||||||
|
|
||||||
|
- When installed with `--no-dev`, the autoloader does not autoload tests anymore
|
||||||
|
- Tests and other files unnecessary for production are excluded from the dist package
|
||||||
|
|
||||||
|
This will help make installations more compact.
|
||||||
|
|
||||||
|
## [0.7.0](https://github.com/brick/math/releases/tag/0.7.0) - 2017-10-02
|
||||||
|
|
||||||
|
Methods renamed:
|
||||||
|
|
||||||
|
- `BigNumber:sign()` has been renamed to `getSign()`
|
||||||
|
- `BigDecimal::unscaledValue()` has been renamed to `getUnscaledValue()`
|
||||||
|
- `BigDecimal::scale()` has been renamed to `getScale()`
|
||||||
|
- `BigDecimal::integral()` has been renamed to `getIntegral()`
|
||||||
|
- `BigDecimal::fraction()` has been renamed to `getFraction()`
|
||||||
|
- `BigRational::numerator()` has been renamed to `getNumerator()`
|
||||||
|
- `BigRational::denominator()` has been renamed to `getDenominator()`
|
||||||
|
|
||||||
|
Classes renamed:
|
||||||
|
|
||||||
|
- `ArithmeticException` has been renamed to `MathException`
|
||||||
|
|
||||||
|
## [0.6.2](https://github.com/brick/math/releases/tag/0.6.2) - 2017-10-02
|
||||||
|
|
||||||
|
The base class for all exceptions is now `MathException`.
|
||||||
|
`ArithmeticException` has been deprecated, and will be removed in 0.7.0.
|
||||||
|
|
||||||
|
## [0.6.1](https://github.com/brick/math/releases/tag/0.6.1) - 2017-10-02
|
||||||
|
|
||||||
|
A number of methods have been renamed:
|
||||||
|
|
||||||
|
- `BigNumber:sign()` is deprecated; use `getSign()` instead
|
||||||
|
- `BigDecimal::unscaledValue()` is deprecated; use `getUnscaledValue()` instead
|
||||||
|
- `BigDecimal::scale()` is deprecated; use `getScale()` instead
|
||||||
|
- `BigDecimal::integral()` is deprecated; use `getIntegral()` instead
|
||||||
|
- `BigDecimal::fraction()` is deprecated; use `getFraction()` instead
|
||||||
|
- `BigRational::numerator()` is deprecated; use `getNumerator()` instead
|
||||||
|
- `BigRational::denominator()` is deprecated; use `getDenominator()` instead
|
||||||
|
|
||||||
|
The old methods will be removed in version 0.7.0.
|
||||||
|
|
||||||
|
## [0.6.0](https://github.com/brick/math/releases/tag/0.6.0) - 2017-08-25
|
||||||
|
|
||||||
|
- Minimum PHP version is now [7.1](https://gophp71.org/); for PHP 5.6 and PHP 7.0 support, use version `0.5`
|
||||||
|
- Deprecated method `BigDecimal::withScale()` has been removed; use `toScale()` instead
|
||||||
|
- Method `BigNumber::toInteger()` has been renamed to `toInt()`
|
||||||
|
|
||||||
|
## [0.5.4](https://github.com/brick/math/releases/tag/0.5.4) - 2016-10-17
|
||||||
|
|
||||||
|
`BigNumber` classes now implement [JsonSerializable](http://php.net/manual/en/class.jsonserializable.php).
|
||||||
|
The JSON output is always a string.
|
||||||
|
|
||||||
|
## [0.5.3](https://github.com/brick/math/releases/tag/0.5.3) - 2016-03-31
|
||||||
|
|
||||||
|
This is a bugfix release. Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
|
||||||
|
|
||||||
|
## [0.5.2](https://github.com/brick/math/releases/tag/0.5.2) - 2015-08-06
|
||||||
|
|
||||||
|
The `$scale` parameter of `BigDecimal::dividedBy()` is now optional again.
|
||||||
|
|
||||||
|
## [0.5.1](https://github.com/brick/math/releases/tag/0.5.1) - 2015-07-05
|
||||||
|
|
||||||
|
**New method: `BigNumber::toScale()`**
|
||||||
|
|
||||||
|
This allows to convert any `BigNumber` to a `BigDecimal` with a given scale, using rounding if necessary.
|
||||||
|
|
||||||
|
## [0.5.0](https://github.com/brick/math/releases/tag/0.5.0) - 2015-07-04
|
||||||
|
|
||||||
|
**New features**
|
||||||
|
- Common `BigNumber` interface for all classes, with the following methods:
|
||||||
|
- `sign()` and derived methods (`isZero()`, `isPositive()`, ...)
|
||||||
|
- `compareTo()` and derived methods (`isEqualTo()`, `isGreaterThan()`, ...) that work across different `BigNumber` types
|
||||||
|
- `toBigInteger()`, `toBigDecimal()`, `toBigRational`() conversion methods
|
||||||
|
- `toInteger()` and `toFloat()` conversion methods to native types
|
||||||
|
- Unified `of()` behaviour: every class now accepts any type of number, provided that it can be safely converted to the current type
|
||||||
|
- New method: `BigDecimal::exactlyDividedBy()`; this method automatically computes the scale of the result, provided that the division yields a finite number of digits
|
||||||
|
- New methods: `BigRational::quotient()` and `remainder()`
|
||||||
|
- Fine-grained exceptions: `DivisionByZeroException`, `RoundingNecessaryException`, `NumberFormatException`
|
||||||
|
- Factory methods `zero()`, `one()` and `ten()` available in all classes
|
||||||
|
- Rounding mode reintroduced in `BigInteger::dividedBy()`
|
||||||
|
|
||||||
|
This release also comes with many performance improvements.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Breaking changes**
|
||||||
|
- `BigInteger`:
|
||||||
|
- `getSign()` is renamed to `sign()`
|
||||||
|
- `toString()` is renamed to `toBase()`
|
||||||
|
- `BigInteger::dividedBy()` now throws an exception by default if the remainder is not zero; use `quotient()` to get the previous behaviour
|
||||||
|
- `BigDecimal`:
|
||||||
|
- `getSign()` is renamed to `sign()`
|
||||||
|
- `getUnscaledValue()` is renamed to `unscaledValue()`
|
||||||
|
- `getScale()` is renamed to `scale()`
|
||||||
|
- `getIntegral()` is renamed to `integral()`
|
||||||
|
- `getFraction()` is renamed to `fraction()`
|
||||||
|
- `divideAndRemainder()` is renamed to `quotientAndRemainder()`
|
||||||
|
- `dividedBy()` now takes a **mandatory** `$scale` parameter **before** the rounding mode
|
||||||
|
- `toBigInteger()` does not accept a `$roundingMode` parameter any more
|
||||||
|
- `toBigRational()` does not simplify the fraction any more; explicitly add `->simplified()` to get the previous behaviour
|
||||||
|
- `BigRational`:
|
||||||
|
- `getSign()` is renamed to `sign()`
|
||||||
|
- `getNumerator()` is renamed to `numerator()`
|
||||||
|
- `getDenominator()` is renamed to `denominator()`
|
||||||
|
- `of()` is renamed to `nd()`, while `parse()` is renamed to `of()`
|
||||||
|
- Miscellaneous:
|
||||||
|
- `ArithmeticException` is moved to an `Exception\` sub-namespace
|
||||||
|
- `of()` factory methods now throw `NumberFormatException` instead of `InvalidArgumentException`
|
||||||
|
|
||||||
|
## [0.4.3](https://github.com/brick/math/releases/tag/0.4.3) - 2016-03-31
|
||||||
|
|
||||||
|
Backport of two bug fixes from the 0.5 branch:
|
||||||
|
- `BigInteger::parse()` did not always throw `InvalidArgumentException` as expected
|
||||||
|
- Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
|
||||||
|
|
||||||
|
## [0.4.2](https://github.com/brick/math/releases/tag/0.4.2) - 2015-06-16
|
||||||
|
|
||||||
|
New method: `BigDecimal::stripTrailingZeros()`
|
||||||
|
|
||||||
|
## [0.4.1](https://github.com/brick/math/releases/tag/0.4.1) - 2015-06-12
|
||||||
|
|
||||||
|
Introducing a `BigRational` class, to perform calculations on fractions of any size.
|
||||||
|
|
||||||
|
## [0.4.0](https://github.com/brick/math/releases/tag/0.4.0) - 2015-06-12
|
||||||
|
|
||||||
|
Rounding modes have been removed from `BigInteger`, and are now a concept specific to `BigDecimal`.
|
||||||
|
|
||||||
|
`BigInteger::dividedBy()` now always returns the quotient of the division.
|
||||||
|
|
||||||
|
## [0.3.5](https://github.com/brick/math/releases/tag/0.3.5) - 2016-03-31
|
||||||
|
|
||||||
|
Backport of two bug fixes from the 0.5 branch:
|
||||||
|
|
||||||
|
- `BigInteger::parse()` did not always throw `InvalidArgumentException` as expected
|
||||||
|
- Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
|
||||||
|
|
||||||
|
## [0.3.4](https://github.com/brick/math/releases/tag/0.3.4) - 2015-06-11
|
||||||
|
|
||||||
|
New methods:
|
||||||
|
- `BigInteger::remainder()` returns the remainder of a division only
|
||||||
|
- `BigInteger::gcd()` returns the greatest common divisor of two numbers
|
||||||
|
|
||||||
|
## [0.3.3](https://github.com/brick/math/releases/tag/0.3.3) - 2015-06-07
|
||||||
|
|
||||||
|
Fix `toString()` not handling negative numbers.
|
||||||
|
|
||||||
|
## [0.3.2](https://github.com/brick/math/releases/tag/0.3.2) - 2015-06-07
|
||||||
|
|
||||||
|
`BigInteger` and `BigDecimal` now have a `getSign()` method that returns:
|
||||||
|
- `-1` if the number is negative
|
||||||
|
- `0` if the number is zero
|
||||||
|
- `1` if the number is positive
|
||||||
|
|
||||||
|
## [0.3.1](https://github.com/brick/math/releases/tag/0.3.1) - 2015-06-05
|
||||||
|
|
||||||
|
Minor performance improvements
|
||||||
|
|
||||||
|
## [0.3.0](https://github.com/brick/math/releases/tag/0.3.0) - 2015-06-04
|
||||||
|
|
||||||
|
The `$roundingMode` and `$scale` parameters have been swapped in `BigDecimal::dividedBy()`.
|
||||||
|
|
||||||
|
## [0.2.2](https://github.com/brick/math/releases/tag/0.2.2) - 2015-06-04
|
||||||
|
|
||||||
|
Stronger immutability guarantee for `BigInteger` and `BigDecimal`.
|
||||||
|
|
||||||
|
So far, it would have been possible to break immutability of these classes by calling the `unserialize()` internal function. This release fixes that.
|
||||||
|
|
||||||
|
## [0.2.1](https://github.com/brick/math/releases/tag/0.2.1) - 2015-06-02
|
||||||
|
|
||||||
|
Added `BigDecimal::divideAndRemainder()`
|
||||||
|
|
||||||
|
## [0.2.0](https://github.com/brick/math/releases/tag/0.2.0) - 2015-05-22
|
||||||
|
|
||||||
|
- `min()` and `max()` do not accept an `array` any more, but a variable number of parameters
|
||||||
|
- **minimum PHP version is now 5.6**
|
||||||
|
- continuous integration with PHP 7
|
||||||
|
|
||||||
|
## [0.1.1](https://github.com/brick/math/releases/tag/0.1.1) - 2014-09-01
|
||||||
|
|
||||||
|
- Added `BigInteger::power()`
|
||||||
|
- Added HHVM support
|
||||||
|
|
||||||
|
## [0.1.0](https://github.com/brick/math/releases/tag/0.1.0) - 2014-08-31
|
||||||
|
|
||||||
|
First beta release.
|
||||||
|
|
||||||
Vendored
+20
@@ -0,0 +1,20 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2013-present Benjamin Morel
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
Vendored
+17
@@ -0,0 +1,17 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Only the last two release streams are supported.
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 0.9.x | :white_check_mark: |
|
||||||
|
| 0.8.x | :white_check_mark: |
|
||||||
|
| < 0.8 | :x: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
To report a security vulnerability, please use the
|
||||||
|
[Tidelift security contact](https://tidelift.com/security).
|
||||||
|
Tidelift will coordinate the fix and disclosure.
|
||||||
Vendored
+35
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "brick/math",
|
||||||
|
"description": "Arbitrary-precision arithmetic library",
|
||||||
|
"type": "library",
|
||||||
|
"keywords": [
|
||||||
|
"Brick",
|
||||||
|
"Math",
|
||||||
|
"Arbitrary-precision",
|
||||||
|
"Arithmetic",
|
||||||
|
"BigInteger",
|
||||||
|
"BigDecimal",
|
||||||
|
"BigRational",
|
||||||
|
"Bignum"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1 || ^8.0",
|
||||||
|
"ext-json": "*"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0",
|
||||||
|
"php-coveralls/php-coveralls": "^2.2",
|
||||||
|
"vimeo/psalm": "4.9.2"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Brick\\Math\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Brick\\Math\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+895
@@ -0,0 +1,895 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Brick\Math;
|
||||||
|
|
||||||
|
use Brick\Math\Exception\DivisionByZeroException;
|
||||||
|
use Brick\Math\Exception\MathException;
|
||||||
|
use Brick\Math\Exception\NegativeNumberException;
|
||||||
|
use Brick\Math\Internal\Calculator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable, arbitrary-precision signed decimal numbers.
|
||||||
|
*
|
||||||
|
* @psalm-immutable
|
||||||
|
*/
|
||||||
|
final class BigDecimal extends BigNumber
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The unscaled value of this decimal number.
|
||||||
|
*
|
||||||
|
* This is a string of digits with an optional leading minus sign.
|
||||||
|
* No leading zero must be present.
|
||||||
|
* No leading minus sign must be present if the value is 0.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scale (number of digits after the decimal point) of this decimal number.
|
||||||
|
*
|
||||||
|
* This must be zero or more.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $scale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protected constructor. Use a factory method to obtain an instance.
|
||||||
|
*
|
||||||
|
* @param string $value The unscaled value, validated.
|
||||||
|
* @param int $scale The scale, validated.
|
||||||
|
*/
|
||||||
|
protected function __construct(string $value, int $scale = 0)
|
||||||
|
{
|
||||||
|
$this->value = $value;
|
||||||
|
$this->scale = $scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a BigDecimal of the given value.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $value
|
||||||
|
*
|
||||||
|
* @return BigDecimal
|
||||||
|
*
|
||||||
|
* @throws MathException If the value cannot be converted to a BigDecimal.
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function of($value) : BigNumber
|
||||||
|
{
|
||||||
|
return parent::of($value)->toBigDecimal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a BigDecimal from an unscaled value and a scale.
|
||||||
|
*
|
||||||
|
* Example: `(12345, 3)` will result in the BigDecimal `12.345`.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $value The unscaled value. Must be convertible to a BigInteger.
|
||||||
|
* @param int $scale The scale of the number, positive or zero.
|
||||||
|
*
|
||||||
|
* @return BigDecimal
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException If the scale is negative.
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function ofUnscaledValue($value, int $scale = 0) : BigDecimal
|
||||||
|
{
|
||||||
|
if ($scale < 0) {
|
||||||
|
throw new \InvalidArgumentException('The scale cannot be negative.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BigDecimal((string) BigInteger::of($value), $scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a BigDecimal representing zero, with a scale of zero.
|
||||||
|
*
|
||||||
|
* @return BigDecimal
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function zero() : BigDecimal
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @psalm-suppress ImpureStaticVariable
|
||||||
|
* @var BigDecimal|null $zero
|
||||||
|
*/
|
||||||
|
static $zero;
|
||||||
|
|
||||||
|
if ($zero === null) {
|
||||||
|
$zero = new BigDecimal('0');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a BigDecimal representing one, with a scale of zero.
|
||||||
|
*
|
||||||
|
* @return BigDecimal
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function one() : BigDecimal
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @psalm-suppress ImpureStaticVariable
|
||||||
|
* @var BigDecimal|null $one
|
||||||
|
*/
|
||||||
|
static $one;
|
||||||
|
|
||||||
|
if ($one === null) {
|
||||||
|
$one = new BigDecimal('1');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $one;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a BigDecimal representing ten, with a scale of zero.
|
||||||
|
*
|
||||||
|
* @return BigDecimal
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function ten() : BigDecimal
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @psalm-suppress ImpureStaticVariable
|
||||||
|
* @var BigDecimal|null $ten
|
||||||
|
*/
|
||||||
|
static $ten;
|
||||||
|
|
||||||
|
if ($ten === null) {
|
||||||
|
$ten = new BigDecimal('10');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ten;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the sum of this number and the given one.
|
||||||
|
*
|
||||||
|
* The result has a scale of `max($this->scale, $that->scale)`.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigDecimal.
|
||||||
|
*
|
||||||
|
* @return BigDecimal The result.
|
||||||
|
*
|
||||||
|
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
|
||||||
|
*/
|
||||||
|
public function plus($that) : BigDecimal
|
||||||
|
{
|
||||||
|
$that = BigDecimal::of($that);
|
||||||
|
|
||||||
|
if ($that->value === '0' && $that->scale <= $this->scale) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->value === '0' && $this->scale <= $that->scale) {
|
||||||
|
return $that;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$a, $b] = $this->scaleValues($this, $that);
|
||||||
|
|
||||||
|
$value = Calculator::get()->add($a, $b);
|
||||||
|
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
|
||||||
|
|
||||||
|
return new BigDecimal($value, $scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the difference of this number and the given one.
|
||||||
|
*
|
||||||
|
* The result has a scale of `max($this->scale, $that->scale)`.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigDecimal.
|
||||||
|
*
|
||||||
|
* @return BigDecimal The result.
|
||||||
|
*
|
||||||
|
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
|
||||||
|
*/
|
||||||
|
public function minus($that) : BigDecimal
|
||||||
|
{
|
||||||
|
$that = BigDecimal::of($that);
|
||||||
|
|
||||||
|
if ($that->value === '0' && $that->scale <= $this->scale) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$a, $b] = $this->scaleValues($this, $that);
|
||||||
|
|
||||||
|
$value = Calculator::get()->sub($a, $b);
|
||||||
|
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
|
||||||
|
|
||||||
|
return new BigDecimal($value, $scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the product of this number and the given one.
|
||||||
|
*
|
||||||
|
* The result has a scale of `$this->scale + $that->scale`.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigDecimal.
|
||||||
|
*
|
||||||
|
* @return BigDecimal The result.
|
||||||
|
*
|
||||||
|
* @throws MathException If the multiplier is not a valid number, or is not convertible to a BigDecimal.
|
||||||
|
*/
|
||||||
|
public function multipliedBy($that) : BigDecimal
|
||||||
|
{
|
||||||
|
$that = BigDecimal::of($that);
|
||||||
|
|
||||||
|
if ($that->value === '1' && $that->scale === 0) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->value === '1' && $this->scale === 0) {
|
||||||
|
return $that;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = Calculator::get()->mul($this->value, $that->value);
|
||||||
|
$scale = $this->scale + $that->scale;
|
||||||
|
|
||||||
|
return new BigDecimal($value, $scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the result of the division of this number by the given one, at the given scale.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $that The divisor.
|
||||||
|
* @param int|null $scale The desired scale, or null to use the scale of this number.
|
||||||
|
* @param int $roundingMode An optional rounding mode.
|
||||||
|
*
|
||||||
|
* @return BigDecimal
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException If the scale or rounding mode is invalid.
|
||||||
|
* @throws MathException If the number is invalid, is zero, or rounding was necessary.
|
||||||
|
*/
|
||||||
|
public function dividedBy($that, ?int $scale = null, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
|
||||||
|
{
|
||||||
|
$that = BigDecimal::of($that);
|
||||||
|
|
||||||
|
if ($that->isZero()) {
|
||||||
|
throw DivisionByZeroException::divisionByZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scale === null) {
|
||||||
|
$scale = $this->scale;
|
||||||
|
} elseif ($scale < 0) {
|
||||||
|
throw new \InvalidArgumentException('Scale cannot be negative.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($that->value === '1' && $that->scale === 0 && $scale === $this->scale) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$p = $this->valueWithMinScale($that->scale + $scale);
|
||||||
|
$q = $that->valueWithMinScale($this->scale - $scale);
|
||||||
|
|
||||||
|
$result = Calculator::get()->divRound($p, $q, $roundingMode);
|
||||||
|
|
||||||
|
return new BigDecimal($result, $scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the exact result of the division of this number by the given one.
|
||||||
|
*
|
||||||
|
* The scale of the result is automatically calculated to fit all the fraction digits.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
|
||||||
|
*
|
||||||
|
* @return BigDecimal The result.
|
||||||
|
*
|
||||||
|
* @throws MathException If the divisor is not a valid number, is not convertible to a BigDecimal, is zero,
|
||||||
|
* or the result yields an infinite number of digits.
|
||||||
|
*/
|
||||||
|
public function exactlyDividedBy($that) : BigDecimal
|
||||||
|
{
|
||||||
|
$that = BigDecimal::of($that);
|
||||||
|
|
||||||
|
if ($that->value === '0') {
|
||||||
|
throw DivisionByZeroException::divisionByZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
[, $b] = $this->scaleValues($this, $that);
|
||||||
|
|
||||||
|
$d = \rtrim($b, '0');
|
||||||
|
$scale = \strlen($b) - \strlen($d);
|
||||||
|
|
||||||
|
$calculator = Calculator::get();
|
||||||
|
|
||||||
|
foreach ([5, 2] as $prime) {
|
||||||
|
for (;;) {
|
||||||
|
$lastDigit = (int) $d[-1];
|
||||||
|
|
||||||
|
if ($lastDigit % $prime !== 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$d = $calculator->divQ($d, (string) $prime);
|
||||||
|
$scale++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->dividedBy($that, $scale)->stripTrailingZeros();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns this number exponentiated to the given value.
|
||||||
|
*
|
||||||
|
* The result has a scale of `$this->scale * $exponent`.
|
||||||
|
*
|
||||||
|
* @param int $exponent The exponent.
|
||||||
|
*
|
||||||
|
* @return BigDecimal The result.
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
|
||||||
|
*/
|
||||||
|
public function power(int $exponent) : BigDecimal
|
||||||
|
{
|
||||||
|
if ($exponent === 0) {
|
||||||
|
return BigDecimal::one();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($exponent === 1) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($exponent < 0 || $exponent > Calculator::MAX_POWER) {
|
||||||
|
throw new \InvalidArgumentException(\sprintf(
|
||||||
|
'The exponent %d is not in the range 0 to %d.',
|
||||||
|
$exponent,
|
||||||
|
Calculator::MAX_POWER
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BigDecimal(Calculator::get()->pow($this->value, $exponent), $this->scale * $exponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the quotient of the division of this number by this given one.
|
||||||
|
*
|
||||||
|
* The quotient has a scale of `0`.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
|
||||||
|
*
|
||||||
|
* @return BigDecimal The quotient.
|
||||||
|
*
|
||||||
|
* @throws MathException If the divisor is not a valid decimal number, or is zero.
|
||||||
|
*/
|
||||||
|
public function quotient($that) : BigDecimal
|
||||||
|
{
|
||||||
|
$that = BigDecimal::of($that);
|
||||||
|
|
||||||
|
if ($that->isZero()) {
|
||||||
|
throw DivisionByZeroException::divisionByZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
$p = $this->valueWithMinScale($that->scale);
|
||||||
|
$q = $that->valueWithMinScale($this->scale);
|
||||||
|
|
||||||
|
$quotient = Calculator::get()->divQ($p, $q);
|
||||||
|
|
||||||
|
return new BigDecimal($quotient, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the remainder of the division of this number by this given one.
|
||||||
|
*
|
||||||
|
* The remainder has a scale of `max($this->scale, $that->scale)`.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
|
||||||
|
*
|
||||||
|
* @return BigDecimal The remainder.
|
||||||
|
*
|
||||||
|
* @throws MathException If the divisor is not a valid decimal number, or is zero.
|
||||||
|
*/
|
||||||
|
public function remainder($that) : BigDecimal
|
||||||
|
{
|
||||||
|
$that = BigDecimal::of($that);
|
||||||
|
|
||||||
|
if ($that->isZero()) {
|
||||||
|
throw DivisionByZeroException::divisionByZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
$p = $this->valueWithMinScale($that->scale);
|
||||||
|
$q = $that->valueWithMinScale($this->scale);
|
||||||
|
|
||||||
|
$remainder = Calculator::get()->divR($p, $q);
|
||||||
|
|
||||||
|
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
|
||||||
|
|
||||||
|
return new BigDecimal($remainder, $scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the quotient and remainder of the division of this number by the given one.
|
||||||
|
*
|
||||||
|
* The quotient has a scale of `0`, and the remainder has a scale of `max($this->scale, $that->scale)`.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
|
||||||
|
*
|
||||||
|
* @return BigDecimal[] An array containing the quotient and the remainder.
|
||||||
|
*
|
||||||
|
* @throws MathException If the divisor is not a valid decimal number, or is zero.
|
||||||
|
*/
|
||||||
|
public function quotientAndRemainder($that) : array
|
||||||
|
{
|
||||||
|
$that = BigDecimal::of($that);
|
||||||
|
|
||||||
|
if ($that->isZero()) {
|
||||||
|
throw DivisionByZeroException::divisionByZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
$p = $this->valueWithMinScale($that->scale);
|
||||||
|
$q = $that->valueWithMinScale($this->scale);
|
||||||
|
|
||||||
|
[$quotient, $remainder] = Calculator::get()->divQR($p, $q);
|
||||||
|
|
||||||
|
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
|
||||||
|
|
||||||
|
$quotient = new BigDecimal($quotient, 0);
|
||||||
|
$remainder = new BigDecimal($remainder, $scale);
|
||||||
|
|
||||||
|
return [$quotient, $remainder];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the square root of this number, rounded down to the given number of decimals.
|
||||||
|
*
|
||||||
|
* @param int $scale
|
||||||
|
*
|
||||||
|
* @return BigDecimal
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException If the scale is negative.
|
||||||
|
* @throws NegativeNumberException If this number is negative.
|
||||||
|
*/
|
||||||
|
public function sqrt(int $scale) : BigDecimal
|
||||||
|
{
|
||||||
|
if ($scale < 0) {
|
||||||
|
throw new \InvalidArgumentException('Scale cannot be negative.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->value === '0') {
|
||||||
|
return new BigDecimal('0', $scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->value[0] === '-') {
|
||||||
|
throw new NegativeNumberException('Cannot calculate the square root of a negative number.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $this->value;
|
||||||
|
$addDigits = 2 * $scale - $this->scale;
|
||||||
|
|
||||||
|
if ($addDigits > 0) {
|
||||||
|
// add zeros
|
||||||
|
$value .= \str_repeat('0', $addDigits);
|
||||||
|
} elseif ($addDigits < 0) {
|
||||||
|
// trim digits
|
||||||
|
if (-$addDigits >= \strlen($this->value)) {
|
||||||
|
// requesting a scale too low, will always yield a zero result
|
||||||
|
return new BigDecimal('0', $scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = \substr($value, 0, $addDigits);
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = Calculator::get()->sqrt($value);
|
||||||
|
|
||||||
|
return new BigDecimal($value, $scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of this BigDecimal with the decimal point moved $n places to the left.
|
||||||
|
*
|
||||||
|
* @param int $n
|
||||||
|
*
|
||||||
|
* @return BigDecimal
|
||||||
|
*/
|
||||||
|
public function withPointMovedLeft(int $n) : BigDecimal
|
||||||
|
{
|
||||||
|
if ($n === 0) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($n < 0) {
|
||||||
|
return $this->withPointMovedRight(-$n);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BigDecimal($this->value, $this->scale + $n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of this BigDecimal with the decimal point moved $n places to the right.
|
||||||
|
*
|
||||||
|
* @param int $n
|
||||||
|
*
|
||||||
|
* @return BigDecimal
|
||||||
|
*/
|
||||||
|
public function withPointMovedRight(int $n) : BigDecimal
|
||||||
|
{
|
||||||
|
if ($n === 0) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($n < 0) {
|
||||||
|
return $this->withPointMovedLeft(-$n);
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $this->value;
|
||||||
|
$scale = $this->scale - $n;
|
||||||
|
|
||||||
|
if ($scale < 0) {
|
||||||
|
if ($value !== '0') {
|
||||||
|
$value .= \str_repeat('0', -$scale);
|
||||||
|
}
|
||||||
|
$scale = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BigDecimal($value, $scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of this BigDecimal with any trailing zeros removed from the fractional part.
|
||||||
|
*
|
||||||
|
* @return BigDecimal
|
||||||
|
*/
|
||||||
|
public function stripTrailingZeros() : BigDecimal
|
||||||
|
{
|
||||||
|
if ($this->scale === 0) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$trimmedValue = \rtrim($this->value, '0');
|
||||||
|
|
||||||
|
if ($trimmedValue === '') {
|
||||||
|
return BigDecimal::zero();
|
||||||
|
}
|
||||||
|
|
||||||
|
$trimmableZeros = \strlen($this->value) - \strlen($trimmedValue);
|
||||||
|
|
||||||
|
if ($trimmableZeros === 0) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($trimmableZeros > $this->scale) {
|
||||||
|
$trimmableZeros = $this->scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = \substr($this->value, 0, -$trimmableZeros);
|
||||||
|
$scale = $this->scale - $trimmableZeros;
|
||||||
|
|
||||||
|
return new BigDecimal($value, $scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the absolute value of this number.
|
||||||
|
*
|
||||||
|
* @return BigDecimal
|
||||||
|
*/
|
||||||
|
public function abs() : BigDecimal
|
||||||
|
{
|
||||||
|
return $this->isNegative() ? $this->negated() : $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the negated value of this number.
|
||||||
|
*
|
||||||
|
* @return BigDecimal
|
||||||
|
*/
|
||||||
|
public function negated() : BigDecimal
|
||||||
|
{
|
||||||
|
return new BigDecimal(Calculator::get()->neg($this->value), $this->scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function compareTo($that) : int
|
||||||
|
{
|
||||||
|
$that = BigNumber::of($that);
|
||||||
|
|
||||||
|
if ($that instanceof BigInteger) {
|
||||||
|
$that = $that->toBigDecimal();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($that instanceof BigDecimal) {
|
||||||
|
[$a, $b] = $this->scaleValues($this, $that);
|
||||||
|
|
||||||
|
return Calculator::get()->cmp($a, $b);
|
||||||
|
}
|
||||||
|
|
||||||
|
return - $that->compareTo($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getSign() : int
|
||||||
|
{
|
||||||
|
return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return BigInteger
|
||||||
|
*/
|
||||||
|
public function getUnscaledValue() : BigInteger
|
||||||
|
{
|
||||||
|
return BigInteger::create($this->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getScale() : int
|
||||||
|
{
|
||||||
|
return $this->scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string representing the integral part of this decimal number.
|
||||||
|
*
|
||||||
|
* Example: `-123.456` => `-123`.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getIntegralPart() : string
|
||||||
|
{
|
||||||
|
if ($this->scale === 0) {
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $this->getUnscaledValueWithLeadingZeros();
|
||||||
|
|
||||||
|
return \substr($value, 0, -$this->scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string representing the fractional part of this decimal number.
|
||||||
|
*
|
||||||
|
* If the scale is zero, an empty string is returned.
|
||||||
|
*
|
||||||
|
* Examples: `-123.456` => '456', `123` => ''.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getFractionalPart() : string
|
||||||
|
{
|
||||||
|
if ($this->scale === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $this->getUnscaledValueWithLeadingZeros();
|
||||||
|
|
||||||
|
return \substr($value, -$this->scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether this decimal number has a non-zero fractional part.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasNonZeroFractionalPart() : bool
|
||||||
|
{
|
||||||
|
return $this->getFractionalPart() !== \str_repeat('0', $this->scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function toBigInteger() : BigInteger
|
||||||
|
{
|
||||||
|
$zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0);
|
||||||
|
|
||||||
|
return BigInteger::create($zeroScaleDecimal->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function toBigDecimal() : BigDecimal
|
||||||
|
{
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function toBigRational() : BigRational
|
||||||
|
{
|
||||||
|
$numerator = BigInteger::create($this->value);
|
||||||
|
$denominator = BigInteger::create('1' . \str_repeat('0', $this->scale));
|
||||||
|
|
||||||
|
return BigRational::create($numerator, $denominator, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
|
||||||
|
{
|
||||||
|
if ($scale === $this->scale) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->dividedBy(BigDecimal::one(), $scale, $roundingMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function toInt() : int
|
||||||
|
{
|
||||||
|
return $this->toBigInteger()->toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function toFloat() : float
|
||||||
|
{
|
||||||
|
return (float) (string) $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function __toString() : string
|
||||||
|
{
|
||||||
|
if ($this->scale === 0) {
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $this->getUnscaledValueWithLeadingZeros();
|
||||||
|
|
||||||
|
return \substr($value, 0, -$this->scale) . '.' . \substr($value, -$this->scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is required for serializing the object and SHOULD NOT be accessed directly.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @return array{value: string, scale: int}
|
||||||
|
*/
|
||||||
|
public function __serialize(): array
|
||||||
|
{
|
||||||
|
return ['value' => $this->value, 'scale' => $this->scale];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is only here to allow unserializing the object and cannot be accessed directly.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
* @psalm-suppress RedundantPropertyInitializationCheck
|
||||||
|
*
|
||||||
|
* @param array{value: string, scale: int} $data
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws \LogicException
|
||||||
|
*/
|
||||||
|
public function __unserialize(array $data): void
|
||||||
|
{
|
||||||
|
if (isset($this->value)) {
|
||||||
|
throw new \LogicException('__unserialize() is an internal function, it must not be called directly.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->value = $data['value'];
|
||||||
|
$this->scale = $data['scale'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is required by interface Serializable and SHOULD NOT be accessed directly.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function serialize() : string
|
||||||
|
{
|
||||||
|
return $this->value . ':' . $this->scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is only here to implement interface Serializable and cannot be accessed directly.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
* @psalm-suppress RedundantPropertyInitializationCheck
|
||||||
|
*
|
||||||
|
* @param string $value
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws \LogicException
|
||||||
|
*/
|
||||||
|
public function unserialize($value) : void
|
||||||
|
{
|
||||||
|
if (isset($this->value)) {
|
||||||
|
throw new \LogicException('unserialize() is an internal function, it must not be called directly.');
|
||||||
|
}
|
||||||
|
|
||||||
|
[$value, $scale] = \explode(':', $value);
|
||||||
|
|
||||||
|
$this->value = $value;
|
||||||
|
$this->scale = (int) $scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puts the internal values of the given decimal numbers on the same scale.
|
||||||
|
*
|
||||||
|
* @param BigDecimal $x The first decimal number.
|
||||||
|
* @param BigDecimal $y The second decimal number.
|
||||||
|
*
|
||||||
|
* @return array{string, string} The scaled integer values of $x and $y.
|
||||||
|
*/
|
||||||
|
private function scaleValues(BigDecimal $x, BigDecimal $y) : array
|
||||||
|
{
|
||||||
|
$a = $x->value;
|
||||||
|
$b = $y->value;
|
||||||
|
|
||||||
|
if ($b !== '0' && $x->scale > $y->scale) {
|
||||||
|
$b .= \str_repeat('0', $x->scale - $y->scale);
|
||||||
|
} elseif ($a !== '0' && $x->scale < $y->scale) {
|
||||||
|
$a .= \str_repeat('0', $y->scale - $x->scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$a, $b];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $scale
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function valueWithMinScale(int $scale) : string
|
||||||
|
{
|
||||||
|
$value = $this->value;
|
||||||
|
|
||||||
|
if ($this->value !== '0' && $scale > $this->scale) {
|
||||||
|
$value .= \str_repeat('0', $scale - $this->scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds leading zeros if necessary to the unscaled value to represent the full decimal number.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getUnscaledValueWithLeadingZeros() : string
|
||||||
|
{
|
||||||
|
$value = $this->value;
|
||||||
|
$targetLength = $this->scale + 1;
|
||||||
|
$negative = ($value[0] === '-');
|
||||||
|
$length = \strlen($value);
|
||||||
|
|
||||||
|
if ($negative) {
|
||||||
|
$length--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($length >= $targetLength) {
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($negative) {
|
||||||
|
$value = \substr($value, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = \str_pad($value, $targetLength, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
if ($negative) {
|
||||||
|
$value = '-' . $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+1184
File diff suppressed because it is too large
Load Diff
Vendored
+572
@@ -0,0 +1,572 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Brick\Math;
|
||||||
|
|
||||||
|
use Brick\Math\Exception\DivisionByZeroException;
|
||||||
|
use Brick\Math\Exception\MathException;
|
||||||
|
use Brick\Math\Exception\NumberFormatException;
|
||||||
|
use Brick\Math\Exception\RoundingNecessaryException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common interface for arbitrary-precision rational numbers.
|
||||||
|
*
|
||||||
|
* @psalm-immutable
|
||||||
|
*/
|
||||||
|
abstract class BigNumber implements \Serializable, \JsonSerializable
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The regular expression used to parse integer, decimal and rational numbers.
|
||||||
|
*/
|
||||||
|
private const PARSE_REGEXP =
|
||||||
|
'/^' .
|
||||||
|
'(?<sign>[\-\+])?' .
|
||||||
|
'(?:' .
|
||||||
|
'(?:' .
|
||||||
|
'(?<integral>[0-9]+)?' .
|
||||||
|
'(?<point>\.)?' .
|
||||||
|
'(?<fractional>[0-9]+)?' .
|
||||||
|
'(?:[eE](?<exponent>[\-\+]?[0-9]+))?' .
|
||||||
|
')|(?:' .
|
||||||
|
'(?<numerator>[0-9]+)' .
|
||||||
|
'\/?' .
|
||||||
|
'(?<denominator>[0-9]+)' .
|
||||||
|
')' .
|
||||||
|
')' .
|
||||||
|
'$/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a BigNumber of the given value.
|
||||||
|
*
|
||||||
|
* The concrete return type is dependent on the given value, with the following rules:
|
||||||
|
*
|
||||||
|
* - BigNumber instances are returned as is
|
||||||
|
* - integer numbers are returned as BigInteger
|
||||||
|
* - floating point numbers are converted to a string then parsed as such
|
||||||
|
* - strings containing a `/` character are returned as BigRational
|
||||||
|
* - strings containing a `.` character or using an exponential notation are returned as BigDecimal
|
||||||
|
* - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $value
|
||||||
|
*
|
||||||
|
* @return BigNumber
|
||||||
|
*
|
||||||
|
* @throws NumberFormatException If the format of the number is not valid.
|
||||||
|
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function of($value) : BigNumber
|
||||||
|
{
|
||||||
|
if ($value instanceof BigNumber) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\is_int($value)) {
|
||||||
|
return new BigInteger((string) $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @psalm-suppress RedundantCastGivenDocblockType We cannot trust the untyped $value here! */
|
||||||
|
$value = \is_float($value) ? self::floatToString($value) : (string) $value;
|
||||||
|
|
||||||
|
$throw = static function() use ($value) : void {
|
||||||
|
throw new NumberFormatException(\sprintf(
|
||||||
|
'The given value "%s" does not represent a valid number.',
|
||||||
|
$value
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (\preg_match(self::PARSE_REGEXP, $value, $matches) !== 1) {
|
||||||
|
$throw();
|
||||||
|
}
|
||||||
|
|
||||||
|
$getMatch = static function(string $value) use ($matches) : ?string {
|
||||||
|
return isset($matches[$value]) && $matches[$value] !== '' ? $matches[$value] : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
$sign = $getMatch('sign');
|
||||||
|
$numerator = $getMatch('numerator');
|
||||||
|
$denominator = $getMatch('denominator');
|
||||||
|
|
||||||
|
if ($numerator !== null) {
|
||||||
|
assert($denominator !== null);
|
||||||
|
|
||||||
|
if ($sign !== null) {
|
||||||
|
$numerator = $sign . $numerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
$numerator = self::cleanUp($numerator);
|
||||||
|
$denominator = self::cleanUp($denominator);
|
||||||
|
|
||||||
|
if ($denominator === '0') {
|
||||||
|
throw DivisionByZeroException::denominatorMustNotBeZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BigRational(
|
||||||
|
new BigInteger($numerator),
|
||||||
|
new BigInteger($denominator),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$point = $getMatch('point');
|
||||||
|
$integral = $getMatch('integral');
|
||||||
|
$fractional = $getMatch('fractional');
|
||||||
|
$exponent = $getMatch('exponent');
|
||||||
|
|
||||||
|
if ($integral === null && $fractional === null) {
|
||||||
|
$throw();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($integral === null) {
|
||||||
|
$integral = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($point !== null || $exponent !== null) {
|
||||||
|
$fractional = ($fractional ?? '');
|
||||||
|
$exponent = ($exponent !== null) ? (int) $exponent : 0;
|
||||||
|
|
||||||
|
if ($exponent === PHP_INT_MIN || $exponent === PHP_INT_MAX) {
|
||||||
|
throw new NumberFormatException('Exponent too large.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$unscaledValue = self::cleanUp(($sign ?? ''). $integral . $fractional);
|
||||||
|
|
||||||
|
$scale = \strlen($fractional) - $exponent;
|
||||||
|
|
||||||
|
if ($scale < 0) {
|
||||||
|
if ($unscaledValue !== '0') {
|
||||||
|
$unscaledValue .= \str_repeat('0', - $scale);
|
||||||
|
}
|
||||||
|
$scale = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BigDecimal($unscaledValue, $scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
$integral = self::cleanUp(($sign ?? '') . $integral);
|
||||||
|
|
||||||
|
return new BigInteger($integral);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely converts float to string, avoiding locale-dependent issues.
|
||||||
|
*
|
||||||
|
* @see https://github.com/brick/math/pull/20
|
||||||
|
*
|
||||||
|
* @param float $float
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
* @psalm-suppress ImpureFunctionCall
|
||||||
|
*/
|
||||||
|
private static function floatToString(float $float) : string
|
||||||
|
{
|
||||||
|
$currentLocale = \setlocale(LC_NUMERIC, '0');
|
||||||
|
\setlocale(LC_NUMERIC, 'C');
|
||||||
|
|
||||||
|
$result = (string) $float;
|
||||||
|
|
||||||
|
\setlocale(LC_NUMERIC, $currentLocale);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy method to access protected constructors from sibling classes.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @param mixed ...$args The arguments to the constructor.
|
||||||
|
*
|
||||||
|
* @return static
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
* @psalm-suppress TooManyArguments
|
||||||
|
* @psalm-suppress UnsafeInstantiation
|
||||||
|
*/
|
||||||
|
protected static function create(... $args) : BigNumber
|
||||||
|
{
|
||||||
|
return new static(... $args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the minimum of the given values.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
|
||||||
|
* to an instance of the class this method is called on.
|
||||||
|
*
|
||||||
|
* @return static The minimum value.
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException If no values are given.
|
||||||
|
* @throws MathException If an argument is not valid.
|
||||||
|
*
|
||||||
|
* @psalm-suppress LessSpecificReturnStatement
|
||||||
|
* @psalm-suppress MoreSpecificReturnType
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function min(...$values) : BigNumber
|
||||||
|
{
|
||||||
|
$min = null;
|
||||||
|
|
||||||
|
foreach ($values as $value) {
|
||||||
|
$value = static::of($value);
|
||||||
|
|
||||||
|
if ($min === null || $value->isLessThan($min)) {
|
||||||
|
$min = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($min === null) {
|
||||||
|
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $min;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the maximum of the given values.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
|
||||||
|
* to an instance of the class this method is called on.
|
||||||
|
*
|
||||||
|
* @return static The maximum value.
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException If no values are given.
|
||||||
|
* @throws MathException If an argument is not valid.
|
||||||
|
*
|
||||||
|
* @psalm-suppress LessSpecificReturnStatement
|
||||||
|
* @psalm-suppress MoreSpecificReturnType
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function max(...$values) : BigNumber
|
||||||
|
{
|
||||||
|
$max = null;
|
||||||
|
|
||||||
|
foreach ($values as $value) {
|
||||||
|
$value = static::of($value);
|
||||||
|
|
||||||
|
if ($max === null || $value->isGreaterThan($max)) {
|
||||||
|
$max = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($max === null) {
|
||||||
|
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $max;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the sum of the given values.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string ...$values The numbers to add. All the numbers need to be convertible
|
||||||
|
* to an instance of the class this method is called on.
|
||||||
|
*
|
||||||
|
* @return static The sum.
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException If no values are given.
|
||||||
|
* @throws MathException If an argument is not valid.
|
||||||
|
*
|
||||||
|
* @psalm-suppress LessSpecificReturnStatement
|
||||||
|
* @psalm-suppress MoreSpecificReturnType
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function sum(...$values) : BigNumber
|
||||||
|
{
|
||||||
|
/** @var BigNumber|null $sum */
|
||||||
|
$sum = null;
|
||||||
|
|
||||||
|
foreach ($values as $value) {
|
||||||
|
$value = static::of($value);
|
||||||
|
|
||||||
|
$sum = $sum === null ? $value : self::add($sum, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sum === null) {
|
||||||
|
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException.
|
||||||
|
*
|
||||||
|
* @todo This could be better resolved by creating an abstract protected method in BigNumber, and leaving to
|
||||||
|
* concrete classes the responsibility to perform the addition themselves or delegate it to the given number,
|
||||||
|
* depending on their ability to perform the operation. This will also require a version bump because we're
|
||||||
|
* potentially breaking custom BigNumber implementations (if any...)
|
||||||
|
*
|
||||||
|
* @param BigNumber $a
|
||||||
|
* @param BigNumber $b
|
||||||
|
*
|
||||||
|
* @return BigNumber
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
private static function add(BigNumber $a, BigNumber $b) : BigNumber
|
||||||
|
{
|
||||||
|
if ($a instanceof BigRational) {
|
||||||
|
return $a->plus($b);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($b instanceof BigRational) {
|
||||||
|
return $b->plus($a);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($a instanceof BigDecimal) {
|
||||||
|
return $a->plus($b);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($b instanceof BigDecimal) {
|
||||||
|
return $b->plus($a);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var BigInteger $a */
|
||||||
|
|
||||||
|
return $a->plus($b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes optional leading zeros and + sign from the given number.
|
||||||
|
*
|
||||||
|
* @param string $number The number, validated as a non-empty string of digits with optional leading sign.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
private static function cleanUp(string $number) : string
|
||||||
|
{
|
||||||
|
$firstChar = $number[0];
|
||||||
|
|
||||||
|
if ($firstChar === '+' || $firstChar === '-') {
|
||||||
|
$number = \substr($number, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$number = \ltrim($number, '0');
|
||||||
|
|
||||||
|
if ($number === '') {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($firstChar === '-') {
|
||||||
|
return '-' . $number;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this number is equal to the given one.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $that
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isEqualTo($that) : bool
|
||||||
|
{
|
||||||
|
return $this->compareTo($that) === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this number is strictly lower than the given one.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $that
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isLessThan($that) : bool
|
||||||
|
{
|
||||||
|
return $this->compareTo($that) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this number is lower than or equal to the given one.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $that
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isLessThanOrEqualTo($that) : bool
|
||||||
|
{
|
||||||
|
return $this->compareTo($that) <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this number is strictly greater than the given one.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $that
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isGreaterThan($that) : bool
|
||||||
|
{
|
||||||
|
return $this->compareTo($that) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this number is greater than or equal to the given one.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $that
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isGreaterThanOrEqualTo($that) : bool
|
||||||
|
{
|
||||||
|
return $this->compareTo($that) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this number equals zero.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isZero() : bool
|
||||||
|
{
|
||||||
|
return $this->getSign() === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this number is strictly negative.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isNegative() : bool
|
||||||
|
{
|
||||||
|
return $this->getSign() < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this number is negative or zero.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isNegativeOrZero() : bool
|
||||||
|
{
|
||||||
|
return $this->getSign() <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this number is strictly positive.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isPositive() : bool
|
||||||
|
{
|
||||||
|
return $this->getSign() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this number is positive or zero.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isPositiveOrZero() : bool
|
||||||
|
{
|
||||||
|
return $this->getSign() >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the sign of this number.
|
||||||
|
*
|
||||||
|
* @return int -1 if the number is negative, 0 if zero, 1 if positive.
|
||||||
|
*/
|
||||||
|
abstract public function getSign() : int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares this number to the given one.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $that
|
||||||
|
*
|
||||||
|
* @return int [-1,0,1] If `$this` is lower than, equal to, or greater than `$that`.
|
||||||
|
*
|
||||||
|
* @throws MathException If the number is not valid.
|
||||||
|
*/
|
||||||
|
abstract public function compareTo($that) : int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this number to a BigInteger.
|
||||||
|
*
|
||||||
|
* @return BigInteger The converted number.
|
||||||
|
*
|
||||||
|
* @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding.
|
||||||
|
*/
|
||||||
|
abstract public function toBigInteger() : BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this number to a BigDecimal.
|
||||||
|
*
|
||||||
|
* @return BigDecimal The converted number.
|
||||||
|
*
|
||||||
|
* @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding.
|
||||||
|
*/
|
||||||
|
abstract public function toBigDecimal() : BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this number to a BigRational.
|
||||||
|
*
|
||||||
|
* @return BigRational The converted number.
|
||||||
|
*/
|
||||||
|
abstract public function toBigRational() : BigRational;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this number to a BigDecimal with the given scale, using rounding if necessary.
|
||||||
|
*
|
||||||
|
* @param int $scale The scale of the resulting `BigDecimal`.
|
||||||
|
* @param int $roundingMode A `RoundingMode` constant.
|
||||||
|
*
|
||||||
|
* @return BigDecimal
|
||||||
|
*
|
||||||
|
* @throws RoundingNecessaryException If this number cannot be converted to the given scale without rounding.
|
||||||
|
* This only applies when RoundingMode::UNNECESSARY is used.
|
||||||
|
*/
|
||||||
|
abstract public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the exact value of this number as a native integer.
|
||||||
|
*
|
||||||
|
* If this number cannot be converted to a native integer without losing precision, an exception is thrown.
|
||||||
|
* Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit.
|
||||||
|
*
|
||||||
|
* @return int The converted value.
|
||||||
|
*
|
||||||
|
* @throws MathException If this number cannot be exactly converted to a native integer.
|
||||||
|
*/
|
||||||
|
abstract public function toInt() : int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an approximation of this number as a floating-point value.
|
||||||
|
*
|
||||||
|
* Note that this method can discard information as the precision of a floating-point value
|
||||||
|
* is inherently limited.
|
||||||
|
*
|
||||||
|
* If the number is greater than the largest representable floating point number, positive infinity is returned.
|
||||||
|
* If the number is less than the smallest representable floating point number, negative infinity is returned.
|
||||||
|
*
|
||||||
|
* @return float The converted value.
|
||||||
|
*/
|
||||||
|
abstract public function toFloat() : float;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string representation of this number.
|
||||||
|
*
|
||||||
|
* The output of this method can be parsed by the `of()` factory method;
|
||||||
|
* this will yield an object equal to this one, without any information loss.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
abstract public function __toString() : string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function jsonSerialize() : string
|
||||||
|
{
|
||||||
|
return $this->__toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
+523
@@ -0,0 +1,523 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Brick\Math;
|
||||||
|
|
||||||
|
use Brick\Math\Exception\DivisionByZeroException;
|
||||||
|
use Brick\Math\Exception\MathException;
|
||||||
|
use Brick\Math\Exception\NumberFormatException;
|
||||||
|
use Brick\Math\Exception\RoundingNecessaryException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An arbitrarily large rational number.
|
||||||
|
*
|
||||||
|
* This class is immutable.
|
||||||
|
*
|
||||||
|
* @psalm-immutable
|
||||||
|
*/
|
||||||
|
final class BigRational extends BigNumber
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The numerator.
|
||||||
|
*
|
||||||
|
* @var BigInteger
|
||||||
|
*/
|
||||||
|
private $numerator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The denominator. Always strictly positive.
|
||||||
|
*
|
||||||
|
* @var BigInteger
|
||||||
|
*/
|
||||||
|
private $denominator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protected constructor. Use a factory method to obtain an instance.
|
||||||
|
*
|
||||||
|
* @param BigInteger $numerator The numerator.
|
||||||
|
* @param BigInteger $denominator The denominator.
|
||||||
|
* @param bool $checkDenominator Whether to check the denominator for negative and zero.
|
||||||
|
*
|
||||||
|
* @throws DivisionByZeroException If the denominator is zero.
|
||||||
|
*/
|
||||||
|
protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator)
|
||||||
|
{
|
||||||
|
if ($checkDenominator) {
|
||||||
|
if ($denominator->isZero()) {
|
||||||
|
throw DivisionByZeroException::denominatorMustNotBeZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($denominator->isNegative()) {
|
||||||
|
$numerator = $numerator->negated();
|
||||||
|
$denominator = $denominator->negated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->numerator = $numerator;
|
||||||
|
$this->denominator = $denominator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a BigRational of the given value.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $value
|
||||||
|
*
|
||||||
|
* @return BigRational
|
||||||
|
*
|
||||||
|
* @throws MathException If the value cannot be converted to a BigRational.
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function of($value) : BigNumber
|
||||||
|
{
|
||||||
|
return parent::of($value)->toBigRational();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a BigRational out of a numerator and a denominator.
|
||||||
|
*
|
||||||
|
* If the denominator is negative, the signs of both the numerator and the denominator
|
||||||
|
* will be inverted to ensure that the denominator is always positive.
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* @return BigRational
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function nd($numerator, $denominator) : BigRational
|
||||||
|
{
|
||||||
|
$numerator = BigInteger::of($numerator);
|
||||||
|
$denominator = BigInteger::of($denominator);
|
||||||
|
|
||||||
|
return new BigRational($numerator, $denominator, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a BigRational representing zero.
|
||||||
|
*
|
||||||
|
* @return BigRational
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function zero() : BigRational
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @psalm-suppress ImpureStaticVariable
|
||||||
|
* @var BigRational|null $zero
|
||||||
|
*/
|
||||||
|
static $zero;
|
||||||
|
|
||||||
|
if ($zero === null) {
|
||||||
|
$zero = new BigRational(BigInteger::zero(), BigInteger::one(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a BigRational representing one.
|
||||||
|
*
|
||||||
|
* @return BigRational
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function one() : BigRational
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @psalm-suppress ImpureStaticVariable
|
||||||
|
* @var BigRational|null $one
|
||||||
|
*/
|
||||||
|
static $one;
|
||||||
|
|
||||||
|
if ($one === null) {
|
||||||
|
$one = new BigRational(BigInteger::one(), BigInteger::one(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $one;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a BigRational representing ten.
|
||||||
|
*
|
||||||
|
* @return BigRational
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function ten() : BigRational
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @psalm-suppress ImpureStaticVariable
|
||||||
|
* @var BigRational|null $ten
|
||||||
|
*/
|
||||||
|
static $ten;
|
||||||
|
|
||||||
|
if ($ten === null) {
|
||||||
|
$ten = new BigRational(BigInteger::ten(), BigInteger::one(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ten;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return BigInteger
|
||||||
|
*/
|
||||||
|
public function getNumerator() : BigInteger
|
||||||
|
{
|
||||||
|
return $this->numerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return BigInteger
|
||||||
|
*/
|
||||||
|
public function getDenominator() : BigInteger
|
||||||
|
{
|
||||||
|
return $this->denominator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the quotient of the division of the numerator by the denominator.
|
||||||
|
*
|
||||||
|
* @return BigInteger
|
||||||
|
*/
|
||||||
|
public function quotient() : BigInteger
|
||||||
|
{
|
||||||
|
return $this->numerator->quotient($this->denominator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the remainder of the division of the numerator by the denominator.
|
||||||
|
*
|
||||||
|
* @return BigInteger
|
||||||
|
*/
|
||||||
|
public function remainder() : BigInteger
|
||||||
|
{
|
||||||
|
return $this->numerator->remainder($this->denominator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the quotient and remainder of the division of the numerator by the denominator.
|
||||||
|
*
|
||||||
|
* @return BigInteger[]
|
||||||
|
*/
|
||||||
|
public function quotientAndRemainder() : array
|
||||||
|
{
|
||||||
|
return $this->numerator->quotientAndRemainder($this->denominator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the sum of this number and the given one.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $that The number to add.
|
||||||
|
*
|
||||||
|
* @return BigRational The result.
|
||||||
|
*
|
||||||
|
* @throws MathException If the number is not valid.
|
||||||
|
*/
|
||||||
|
public function plus($that) : BigRational
|
||||||
|
{
|
||||||
|
$that = BigRational::of($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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the difference of this number and the given one.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $that The number to subtract.
|
||||||
|
*
|
||||||
|
* @return BigRational The result.
|
||||||
|
*
|
||||||
|
* @throws MathException If the number is not valid.
|
||||||
|
*/
|
||||||
|
public function minus($that) : BigRational
|
||||||
|
{
|
||||||
|
$that = BigRational::of($that);
|
||||||
|
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the product of this number and the given one.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $that The multiplier.
|
||||||
|
*
|
||||||
|
* @return BigRational The result.
|
||||||
|
*
|
||||||
|
* @throws MathException If the multiplier is not a valid number.
|
||||||
|
*/
|
||||||
|
public function multipliedBy($that) : BigRational
|
||||||
|
{
|
||||||
|
$that = BigRational::of($that);
|
||||||
|
|
||||||
|
$numerator = $this->numerator->multipliedBy($that->numerator);
|
||||||
|
$denominator = $this->denominator->multipliedBy($that->denominator);
|
||||||
|
|
||||||
|
return new BigRational($numerator, $denominator, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the result of the division of this number by the given one.
|
||||||
|
*
|
||||||
|
* @param BigNumber|int|float|string $that The divisor.
|
||||||
|
*
|
||||||
|
* @return BigRational The result.
|
||||||
|
*
|
||||||
|
* @throws MathException If the divisor is not a valid number, or is zero.
|
||||||
|
*/
|
||||||
|
public function dividedBy($that) : BigRational
|
||||||
|
{
|
||||||
|
$that = BigRational::of($that);
|
||||||
|
|
||||||
|
$numerator = $this->numerator->multipliedBy($that->denominator);
|
||||||
|
$denominator = $this->denominator->multipliedBy($that->numerator);
|
||||||
|
|
||||||
|
return new BigRational($numerator, $denominator, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns this number exponentiated to the given value.
|
||||||
|
*
|
||||||
|
* @param int $exponent The exponent.
|
||||||
|
*
|
||||||
|
* @return BigRational The result.
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
|
||||||
|
*/
|
||||||
|
public function power(int $exponent) : BigRational
|
||||||
|
{
|
||||||
|
if ($exponent === 0) {
|
||||||
|
$one = BigInteger::one();
|
||||||
|
|
||||||
|
return new BigRational($one, $one, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($exponent === 1) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BigRational(
|
||||||
|
$this->numerator->power($exponent),
|
||||||
|
$this->denominator->power($exponent),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the reciprocal of this BigRational.
|
||||||
|
*
|
||||||
|
* The reciprocal has the numerator and denominator swapped.
|
||||||
|
*
|
||||||
|
* @return BigRational
|
||||||
|
*
|
||||||
|
* @throws DivisionByZeroException If the numerator is zero.
|
||||||
|
*/
|
||||||
|
public function reciprocal() : BigRational
|
||||||
|
{
|
||||||
|
return new BigRational($this->denominator, $this->numerator, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the absolute value of this BigRational.
|
||||||
|
*
|
||||||
|
* @return BigRational
|
||||||
|
*/
|
||||||
|
public function abs() : BigRational
|
||||||
|
{
|
||||||
|
return new BigRational($this->numerator->abs(), $this->denominator, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the negated value of this BigRational.
|
||||||
|
*
|
||||||
|
* @return BigRational
|
||||||
|
*/
|
||||||
|
public function negated() : BigRational
|
||||||
|
{
|
||||||
|
return new BigRational($this->numerator->negated(), $this->denominator, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the simplified value of this BigRational.
|
||||||
|
*
|
||||||
|
* @return 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function compareTo($that) : int
|
||||||
|
{
|
||||||
|
return $this->minus($that)->getSign();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getSign() : int
|
||||||
|
{
|
||||||
|
return $this->numerator->getSign();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $simplified->numerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function toBigDecimal() : BigDecimal
|
||||||
|
{
|
||||||
|
return $this->numerator->toBigDecimal()->exactlyDividedBy($this->denominator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function toBigRational() : BigRational
|
||||||
|
{
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
|
||||||
|
{
|
||||||
|
return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function toInt() : int
|
||||||
|
{
|
||||||
|
return $this->toBigInteger()->toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function toFloat() : float
|
||||||
|
{
|
||||||
|
return $this->numerator->toFloat() / $this->denominator->toFloat();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function __toString() : string
|
||||||
|
{
|
||||||
|
$numerator = (string) $this->numerator;
|
||||||
|
$denominator = (string) $this->denominator;
|
||||||
|
|
||||||
|
if ($denominator === '1') {
|
||||||
|
return $numerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->numerator . '/' . $this->denominator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is required for serializing the object and SHOULD NOT be accessed directly.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @return array{numerator: BigInteger, denominator: BigInteger}
|
||||||
|
*/
|
||||||
|
public function __serialize(): array
|
||||||
|
{
|
||||||
|
return ['numerator' => $this->numerator, 'denominator' => $this->denominator];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws \LogicException
|
||||||
|
*/
|
||||||
|
public function __unserialize(array $data): void
|
||||||
|
{
|
||||||
|
if (isset($this->numerator)) {
|
||||||
|
throw new \LogicException('__unserialize() is an internal function, it must not be called directly.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->numerator = $data['numerator'];
|
||||||
|
$this->denominator = $data['denominator'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is required by interface Serializable and SHOULD NOT be accessed directly.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function serialize() : string
|
||||||
|
{
|
||||||
|
return $this->numerator . '/' . $this->denominator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is only here to implement interface Serializable and cannot be accessed directly.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
* @psalm-suppress RedundantPropertyInitializationCheck
|
||||||
|
*
|
||||||
|
* @param string $value
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws \LogicException
|
||||||
|
*/
|
||||||
|
public function unserialize($value) : void
|
||||||
|
{
|
||||||
|
if (isset($this->numerator)) {
|
||||||
|
throw new \LogicException('unserialize() is an internal function, it must not be called directly.');
|
||||||
|
}
|
||||||
|
|
||||||
|
[$numerator, $denominator] = \explode('/', $value);
|
||||||
|
|
||||||
|
$this->numerator = BigInteger::of($numerator);
|
||||||
|
$this->denominator = BigInteger::of($denominator);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Brick\Math\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when a division by zero occurs.
|
||||||
|
*/
|
||||||
|
class DivisionByZeroException extends MathException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return DivisionByZeroException
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function divisionByZero() : DivisionByZeroException
|
||||||
|
{
|
||||||
|
return new self('Division by zero.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return DivisionByZeroException
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function modulusMustNotBeZero() : DivisionByZeroException
|
||||||
|
{
|
||||||
|
return new self('The modulus must not be zero.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return DivisionByZeroException
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function denominatorMustNotBeZero() : DivisionByZeroException
|
||||||
|
{
|
||||||
|
return new self('The denominator of a rational number cannot be zero.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Brick\Math\Exception;
|
||||||
|
|
||||||
|
use Brick\Math\BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when an integer overflow occurs.
|
||||||
|
*/
|
||||||
|
class IntegerOverflowException extends MathException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param BigInteger $value
|
||||||
|
*
|
||||||
|
* @return IntegerOverflowException
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function toIntOverflow(BigInteger $value) : IntegerOverflowException
|
||||||
|
{
|
||||||
|
$message = '%s is out of range %d to %d and cannot be represented as an integer.';
|
||||||
|
|
||||||
|
return new self(\sprintf($message, (string) $value, PHP_INT_MIN, PHP_INT_MAX));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Brick\Math\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for all math exceptions.
|
||||||
|
*
|
||||||
|
* This class is abstract to ensure that only fine-grained exceptions are thrown throughout the code.
|
||||||
|
*/
|
||||||
|
class MathException extends \RuntimeException
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Brick\Math\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when attempting to perform an unsupported operation, such as a square root, on a negative number.
|
||||||
|
*/
|
||||||
|
class NegativeNumberException extends MathException
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Brick\Math\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when attempting to create a number from a string with an invalid format.
|
||||||
|
*/
|
||||||
|
class NumberFormatException extends MathException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string $char The failing character.
|
||||||
|
*
|
||||||
|
* @return NumberFormatException
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function charNotInAlphabet(string $char) : self
|
||||||
|
{
|
||||||
|
$ord = \ord($char);
|
||||||
|
|
||||||
|
if ($ord < 32 || $ord > 126) {
|
||||||
|
$char = \strtoupper(\dechex($ord));
|
||||||
|
|
||||||
|
if ($ord < 10) {
|
||||||
|
$char = '0' . $char;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$char = '"' . $char . '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self(sprintf('Char %s is not a valid character in the given alphabet.', $char));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Brick\Math\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when a number cannot be represented at the requested scale without rounding.
|
||||||
|
*/
|
||||||
|
class RoundingNecessaryException extends MathException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return RoundingNecessaryException
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function roundingNecessary() : RoundingNecessaryException
|
||||||
|
{
|
||||||
|
return new self('Rounding is necessary to represent the result of the operation at this scale.');
|
||||||
|
}
|
||||||
|
}
|
||||||
+756
@@ -0,0 +1,756 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Brick\Math\Internal;
|
||||||
|
|
||||||
|
use Brick\Math\Exception\RoundingNecessaryException;
|
||||||
|
use Brick\Math\RoundingMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs basic operations on arbitrary size integers.
|
||||||
|
*
|
||||||
|
* Unless otherwise specified, all parameters must be validated as non-empty strings of digits,
|
||||||
|
* without leading zero, and with an optional leading minus sign if the number is not zero.
|
||||||
|
*
|
||||||
|
* Any other parameter format will lead to undefined behaviour.
|
||||||
|
* All methods must return strings respecting this format, unless specified otherwise.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @psalm-immutable
|
||||||
|
*/
|
||||||
|
abstract class Calculator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The maximum exponent value allowed for the pow() method.
|
||||||
|
*/
|
||||||
|
public const MAX_POWER = 1000000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The alphabet for converting from and to base 2 to 36, lowercase.
|
||||||
|
*/
|
||||||
|
public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Calculator instance in use.
|
||||||
|
*
|
||||||
|
* @var Calculator|null
|
||||||
|
*/
|
||||||
|
private static $instance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @return Calculator
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
*
|
||||||
|
* @return Calculator
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @param string $a The first operand.
|
||||||
|
* @param string $b The second operand.
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* @param string $n The number.
|
||||||
|
*
|
||||||
|
* @return string The absolute value.
|
||||||
|
*/
|
||||||
|
final public function abs(string $n) : string
|
||||||
|
{
|
||||||
|
return ($n[0] === '-') ? \substr($n, 1) : $n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Negates a number.
|
||||||
|
*
|
||||||
|
* @param string $n The number.
|
||||||
|
*
|
||||||
|
* @return string The negated value.
|
||||||
|
*/
|
||||||
|
final public function neg(string $n) : string
|
||||||
|
{
|
||||||
|
if ($n === '0') {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($n[0] === '-') {
|
||||||
|
return \substr($n, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '-' . $n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two numbers.
|
||||||
|
*
|
||||||
|
* @param string $a The first number.
|
||||||
|
* @param string $b The second number.
|
||||||
|
*
|
||||||
|
* @return int [-1, 0, 1] If the first number is less than, equal to, or greater than the second number.
|
||||||
|
*/
|
||||||
|
final public function cmp(string $a, string $b) : int
|
||||||
|
{
|
||||||
|
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||||
|
|
||||||
|
if ($aNeg && ! $bNeg) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bNeg && ! $aNeg) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$aLen = \strlen($aDig);
|
||||||
|
$bLen = \strlen($bDig);
|
||||||
|
|
||||||
|
if ($aLen < $bLen) {
|
||||||
|
$result = -1;
|
||||||
|
} elseif ($aLen > $bLen) {
|
||||||
|
$result = 1;
|
||||||
|
} else {
|
||||||
|
$result = $aDig <=> $bDig;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $aNeg ? -$result : $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds two numbers.
|
||||||
|
*
|
||||||
|
* @param string $a The augend.
|
||||||
|
* @param string $b The addend.
|
||||||
|
*
|
||||||
|
* @return string The sum.
|
||||||
|
*/
|
||||||
|
abstract public function add(string $a, string $b) : string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subtracts two numbers.
|
||||||
|
*
|
||||||
|
* @param string $a The minuend.
|
||||||
|
* @param string $b The subtrahend.
|
||||||
|
*
|
||||||
|
* @return string The difference.
|
||||||
|
*/
|
||||||
|
abstract public function sub(string $a, string $b) : string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiplies two numbers.
|
||||||
|
*
|
||||||
|
* @param string $a The multiplicand.
|
||||||
|
* @param string $b The multiplier.
|
||||||
|
*
|
||||||
|
* @return string The product.
|
||||||
|
*/
|
||||||
|
abstract public function mul(string $a, string $b) : string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the quotient of the division of two numbers.
|
||||||
|
*
|
||||||
|
* @param string $a The dividend.
|
||||||
|
* @param string $b The divisor, must not be zero.
|
||||||
|
*
|
||||||
|
* @return string The quotient.
|
||||||
|
*/
|
||||||
|
abstract public function divQ(string $a, string $b) : string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the remainder of the division of two numbers.
|
||||||
|
*
|
||||||
|
* @param string $a The dividend.
|
||||||
|
* @param string $b The divisor, must not be zero.
|
||||||
|
*
|
||||||
|
* @return string The remainder.
|
||||||
|
*/
|
||||||
|
abstract public function divR(string $a, string $b) : string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the quotient and remainder of the division of two numbers.
|
||||||
|
*
|
||||||
|
* @param string $a The dividend.
|
||||||
|
* @param string $b The divisor, must not be zero.
|
||||||
|
*
|
||||||
|
* @return string[] An array containing the quotient and remainder.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @return string The power.
|
||||||
|
*/
|
||||||
|
abstract public function pow(string $a, int $e) : string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $a
|
||||||
|
* @param string $b The modulus; must not be zero.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function mod(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
return $this->divR($this->add($this->divR($a, $b), $b), $b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the modular multiplicative inverse of $x modulo $m.
|
||||||
|
*
|
||||||
|
* If $x has no multiplicative inverse mod m, this method must return null.
|
||||||
|
*
|
||||||
|
* This method can be overridden by the concrete implementation if the underlying library has built-in support.
|
||||||
|
*
|
||||||
|
* @param string $x
|
||||||
|
* @param string $m The modulus; must not be negative or zero.
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function modInverse(string $x, string $m) : ?string
|
||||||
|
{
|
||||||
|
if ($m === '1') {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
$modVal = $x;
|
||||||
|
|
||||||
|
if ($x[0] === '-' || ($this->cmp($this->abs($x), $m) >= 0)) {
|
||||||
|
$modVal = $this->mod($x, $m);
|
||||||
|
}
|
||||||
|
|
||||||
|
$x = '0';
|
||||||
|
$y = '0';
|
||||||
|
$g = $this->gcdExtended($modVal, $m, $x, $y);
|
||||||
|
|
||||||
|
if ($g !== '1') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->mod($this->add($this->mod($x, $m), $m), $m);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raises a number into power with modulo.
|
||||||
|
*
|
||||||
|
* @param string $base The base number; must be positive or zero.
|
||||||
|
* @param string $exp The exponent; must be positive or zero.
|
||||||
|
* @param string $mod The modulus; must be strictly positive.
|
||||||
|
*
|
||||||
|
* @return string The power.
|
||||||
|
*/
|
||||||
|
abstract public function modPow(string $base, string $exp, string $mod) : string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the greatest common divisor of the two numbers.
|
||||||
|
*
|
||||||
|
* This method can be overridden by the concrete implementation if the underlying library
|
||||||
|
* has built-in support for GCD calculations.
|
||||||
|
*
|
||||||
|
* @param string $a The first number.
|
||||||
|
* @param string $b The second number.
|
||||||
|
*
|
||||||
|
* @return string The GCD, always positive, or zero if both arguments are zero.
|
||||||
|
*/
|
||||||
|
public function gcd(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
if ($a === '0') {
|
||||||
|
return $this->abs($b);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($b === '0') {
|
||||||
|
return $this->abs($a);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->gcd($b, $this->divR($a, $b));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function gcdExtended(string $a, string $b, string &$x, string &$y) : string
|
||||||
|
{
|
||||||
|
if ($a === '0') {
|
||||||
|
$x = '0';
|
||||||
|
$y = '1';
|
||||||
|
|
||||||
|
return $b;
|
||||||
|
}
|
||||||
|
|
||||||
|
$x1 = '0';
|
||||||
|
$y1 = '0';
|
||||||
|
|
||||||
|
$gcd = $this->gcdExtended($this->mod($b, $a), $a, $x1, $y1);
|
||||||
|
|
||||||
|
$x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1));
|
||||||
|
$y = $x1;
|
||||||
|
|
||||||
|
return $gcd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the square root of the given number, rounded down.
|
||||||
|
*
|
||||||
|
* The result is the largest x such that x² ≤ n.
|
||||||
|
* The input MUST NOT be negative.
|
||||||
|
*
|
||||||
|
* @param string $n The number.
|
||||||
|
*
|
||||||
|
* @return string The square root.
|
||||||
|
*/
|
||||||
|
abstract public function sqrt(string $n) : string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a number from an arbitrary base.
|
||||||
|
*
|
||||||
|
* This method can be overridden by the concrete implementation if the underlying library
|
||||||
|
* has built-in support for base conversion.
|
||||||
|
*
|
||||||
|
* @param string $number The number, positive or zero, non-empty, case-insensitively validated for the given base.
|
||||||
|
* @param int $base The base of the number, validated from 2 to 36.
|
||||||
|
*
|
||||||
|
* @return string The converted number, following the Calculator conventions.
|
||||||
|
*/
|
||||||
|
public function fromBase(string $number, int $base) : string
|
||||||
|
{
|
||||||
|
return $this->fromArbitraryBase(\strtolower($number), self::ALPHABET, $base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a number to an arbitrary base.
|
||||||
|
*
|
||||||
|
* This method can be overridden by the concrete implementation if the underlying library
|
||||||
|
* has built-in support for base conversion.
|
||||||
|
*
|
||||||
|
* @param string $number The number to convert, following the Calculator conventions.
|
||||||
|
* @param int $base The base to convert to, validated from 2 to 36.
|
||||||
|
*
|
||||||
|
* @return string The converted number, lowercase.
|
||||||
|
*/
|
||||||
|
public function toBase(string $number, int $base) : string
|
||||||
|
{
|
||||||
|
$negative = ($number[0] === '-');
|
||||||
|
|
||||||
|
if ($negative) {
|
||||||
|
$number = \substr($number, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$number = $this->toArbitraryBase($number, self::ALPHABET, $base);
|
||||||
|
|
||||||
|
if ($negative) {
|
||||||
|
return '-' . $number;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a non-negative number in an arbitrary base using a custom alphabet, to base 10.
|
||||||
|
*
|
||||||
|
* @param string $number The number to convert, validated as a non-empty string,
|
||||||
|
* containing only chars in the given alphabet/base.
|
||||||
|
* @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
final public function fromArbitraryBase(string $number, string $alphabet, int $base) : string
|
||||||
|
{
|
||||||
|
// remove leading "zeros"
|
||||||
|
$number = \ltrim($number, $alphabet[0]);
|
||||||
|
|
||||||
|
if ($number === '') {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// optimize for "one"
|
||||||
|
if ($number === $alphabet[1]) {
|
||||||
|
return '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = '0';
|
||||||
|
$power = '1';
|
||||||
|
|
||||||
|
$base = (string) $base;
|
||||||
|
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($i !== 0) {
|
||||||
|
$power = $this->mul($power, $base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a non-negative number to an arbitrary base using a custom alphabet.
|
||||||
|
*
|
||||||
|
* @param string $number The number to convert, positive or zero, following the Calculator conventions.
|
||||||
|
* @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
|
||||||
|
* @param int $base The base to convert to, validated from 2 to alphabet length.
|
||||||
|
*
|
||||||
|
* @return string The converted number in the given alphabet.
|
||||||
|
*/
|
||||||
|
final public function toArbitraryBase(string $number, string $alphabet, int $base) : string
|
||||||
|
{
|
||||||
|
if ($number === '0') {
|
||||||
|
return $alphabet[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$base = (string) $base;
|
||||||
|
$result = '';
|
||||||
|
|
||||||
|
while ($number !== '0') {
|
||||||
|
[$number, $remainder] = $this->divQR($number, $base);
|
||||||
|
$remainder = (int) $remainder;
|
||||||
|
|
||||||
|
$result .= $alphabet[$remainder];
|
||||||
|
}
|
||||||
|
|
||||||
|
return \strrev($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a rounded division.
|
||||||
|
*
|
||||||
|
* Rounding is performed when the remainder of the division is not zero.
|
||||||
|
*
|
||||||
|
* @param string $a The dividend.
|
||||||
|
* @param string $b The divisor, must not be zero.
|
||||||
|
* @param int $roundingMode The rounding mode.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException If the rounding mode is invalid.
|
||||||
|
* @throws RoundingNecessaryException If RoundingMode::UNNECESSARY is provided but rounding is necessary.
|
||||||
|
*/
|
||||||
|
final public function divRound(string $a, string $b, int $roundingMode) : string
|
||||||
|
{
|
||||||
|
[$quotient, $remainder] = $this->divQR($a, $b);
|
||||||
|
|
||||||
|
$hasDiscardedFraction = ($remainder !== '0');
|
||||||
|
$isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-');
|
||||||
|
|
||||||
|
$discardedFractionSign = function() use ($remainder, $b) : int {
|
||||||
|
$r = $this->abs($this->mul($remainder, '2'));
|
||||||
|
$b = $this->abs($b);
|
||||||
|
|
||||||
|
return $this->cmp($r, $b);
|
||||||
|
};
|
||||||
|
|
||||||
|
$increment = false;
|
||||||
|
|
||||||
|
switch ($roundingMode) {
|
||||||
|
case RoundingMode::UNNECESSARY:
|
||||||
|
if ($hasDiscardedFraction) {
|
||||||
|
throw RoundingNecessaryException::roundingNecessary();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RoundingMode::UP:
|
||||||
|
$increment = $hasDiscardedFraction;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RoundingMode::DOWN:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RoundingMode::CEILING:
|
||||||
|
$increment = $hasDiscardedFraction && $isPositiveOrZero;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RoundingMode::FLOOR:
|
||||||
|
$increment = $hasDiscardedFraction && ! $isPositiveOrZero;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RoundingMode::HALF_UP:
|
||||||
|
$increment = $discardedFractionSign() >= 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RoundingMode::HALF_DOWN:
|
||||||
|
$increment = $discardedFractionSign() > 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RoundingMode::HALF_CEILING:
|
||||||
|
$increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RoundingMode::HALF_FLOOR:
|
||||||
|
$increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RoundingMode::HALF_EVEN:
|
||||||
|
$lastDigit = (int) $quotient[-1];
|
||||||
|
$lastDigitIsEven = ($lastDigit % 2 === 0);
|
||||||
|
$increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new \InvalidArgumentException('Invalid rounding mode.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($increment) {
|
||||||
|
return $this->add($quotient, $isPositiveOrZero ? '1' : '-1');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $quotient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates bitwise AND of two numbers.
|
||||||
|
*
|
||||||
|
* This method can be overridden by the concrete implementation if the underlying library
|
||||||
|
* has built-in support for bitwise operations.
|
||||||
|
*
|
||||||
|
* @param string $a
|
||||||
|
* @param string $b
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function and(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
return $this->bitwise('and', $a, $b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates bitwise OR of two numbers.
|
||||||
|
*
|
||||||
|
* This method can be overridden by the concrete implementation if the underlying library
|
||||||
|
* has built-in support for bitwise operations.
|
||||||
|
*
|
||||||
|
* @param string $a
|
||||||
|
* @param string $b
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function or(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
return $this->bitwise('or', $a, $b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates bitwise XOR of two numbers.
|
||||||
|
*
|
||||||
|
* This method can be overridden by the concrete implementation if the underlying library
|
||||||
|
* has built-in support for bitwise operations.
|
||||||
|
*
|
||||||
|
* @param string $a
|
||||||
|
* @param string $b
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function xor(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
return $this->bitwise('xor', $a, $b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a bitwise operation on a decimal number.
|
||||||
|
*
|
||||||
|
* @param string $operator The operator to use, must be "and", "or" or "xor".
|
||||||
|
* @param string $a The left operand.
|
||||||
|
* @param string $b The right operand.
|
||||||
|
*
|
||||||
|
* @return 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);
|
||||||
|
|
||||||
|
if ($aLen > $bLen) {
|
||||||
|
$bBin = \str_repeat("\x00", $aLen - $bLen) . $bBin;
|
||||||
|
} elseif ($bLen > $aLen) {
|
||||||
|
$aBin = \str_repeat("\x00", $bLen - $aLen) . $aBin;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($aNeg) {
|
||||||
|
$aBin = $this->twosComplement($aBin);
|
||||||
|
}
|
||||||
|
if ($bNeg) {
|
||||||
|
$bBin = $this->twosComplement($bBin);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($operator) {
|
||||||
|
case 'and':
|
||||||
|
$value = $aBin & $bBin;
|
||||||
|
$negative = ($aNeg and $bNeg);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'or':
|
||||||
|
$value = $aBin | $bBin;
|
||||||
|
$negative = ($aNeg or $bNeg);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'xor':
|
||||||
|
$value = $aBin ^ $bBin;
|
||||||
|
$negative = ($aNeg xor $bNeg);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
default:
|
||||||
|
throw new \InvalidArgumentException('Invalid bitwise operator.');
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($negative) {
|
||||||
|
$value = $this->twosComplement($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->toDecimal($value);
|
||||||
|
|
||||||
|
return $negative ? $this->neg($result) : $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $number A positive, binary number.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function twosComplement(string $number) : string
|
||||||
|
{
|
||||||
|
$xor = \str_repeat("\xff", \strlen($number));
|
||||||
|
|
||||||
|
$number ^= $xor;
|
||||||
|
|
||||||
|
for ($i = \strlen($number) - 1; $i >= 0; $i--) {
|
||||||
|
$byte = \ord($number[$i]);
|
||||||
|
|
||||||
|
if (++$byte !== 256) {
|
||||||
|
$number[$i] = \chr($byte);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$number[$i] = "\x00";
|
||||||
|
|
||||||
|
if ($i === 0) {
|
||||||
|
$number = "\x01" . $number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a decimal number to a binary string.
|
||||||
|
*
|
||||||
|
* @param string $number The number to convert, positive or zero, only digits.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function toBinary(string $number) : string
|
||||||
|
{
|
||||||
|
$result = '';
|
||||||
|
|
||||||
|
while ($number !== '0') {
|
||||||
|
[$number, $remainder] = $this->divQR($number, '256');
|
||||||
|
$result .= \chr((int) $remainder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return \strrev($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the positive decimal representation of a binary number.
|
||||||
|
*
|
||||||
|
* @param string $bytes The bytes representing the number.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function toDecimal(string $bytes) : string
|
||||||
|
{
|
||||||
|
$result = '0';
|
||||||
|
$power = '1';
|
||||||
|
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($i !== 0) {
|
||||||
|
$power = $this->mul($power, '256');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Brick\Math\Internal\Calculator;
|
||||||
|
|
||||||
|
use Brick\Math\Internal\Calculator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculator implementation built around the bcmath library.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @psalm-immutable
|
||||||
|
*/
|
||||||
|
class BcMathCalculator extends Calculator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function add(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
return \bcadd($a, $b, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function sub(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
return \bcsub($a, $b, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function mul(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
return \bcmul($a, $b, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @psalm-suppress InvalidNullableReturnType
|
||||||
|
* @psalm-suppress NullableReturnStatement
|
||||||
|
*/
|
||||||
|
public function divQ(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
return \bcdiv($a, $b, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @psalm-suppress InvalidNullableReturnType
|
||||||
|
* @psalm-suppress NullableReturnStatement
|
||||||
|
*/
|
||||||
|
public function divR(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
if (version_compare(PHP_VERSION, '7.2') >= 0) {
|
||||||
|
return \bcmod($a, $b, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return \bcmod($a, $b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function divQR(string $a, string $b) : array
|
||||||
|
{
|
||||||
|
$q = \bcdiv($a, $b, 0);
|
||||||
|
|
||||||
|
if (version_compare(PHP_VERSION, '7.2') >= 0) {
|
||||||
|
$r = \bcmod($a, $b, 0);
|
||||||
|
} else {
|
||||||
|
$r = \bcmod($a, $b);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert($q !== null);
|
||||||
|
assert($r !== null);
|
||||||
|
|
||||||
|
return [$q, $r];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function pow(string $a, int $e) : string
|
||||||
|
{
|
||||||
|
return \bcpow($a, (string) $e, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @psalm-suppress InvalidNullableReturnType
|
||||||
|
* @psalm-suppress NullableReturnStatement
|
||||||
|
*/
|
||||||
|
public function modPow(string $base, string $exp, string $mod) : string
|
||||||
|
{
|
||||||
|
return \bcpowmod($base, $exp, $mod, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* @psalm-suppress NullableReturnStatement
|
||||||
|
* @psalm-suppress InvalidNullableReturnType
|
||||||
|
*/
|
||||||
|
public function sqrt(string $n) : string
|
||||||
|
{
|
||||||
|
return \bcsqrt($n, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Brick\Math\Internal\Calculator;
|
||||||
|
|
||||||
|
use Brick\Math\Internal\Calculator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculator implementation built around the GMP library.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @psalm-immutable
|
||||||
|
*/
|
||||||
|
class GmpCalculator extends Calculator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function add(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
return \gmp_strval(\gmp_add($a, $b));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function sub(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
return \gmp_strval(\gmp_sub($a, $b));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function mul(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
return \gmp_strval(\gmp_mul($a, $b));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function divQ(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
return \gmp_strval(\gmp_div_q($a, $b));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function divR(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
return \gmp_strval(\gmp_div_r($a, $b));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function divQR(string $a, string $b) : array
|
||||||
|
{
|
||||||
|
[$q, $r] = \gmp_div_qr($a, $b);
|
||||||
|
|
||||||
|
return [
|
||||||
|
\gmp_strval($q),
|
||||||
|
\gmp_strval($r)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function pow(string $a, int $e) : string
|
||||||
|
{
|
||||||
|
return \gmp_strval(\gmp_pow($a, $e));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function modInverse(string $x, string $m) : ?string
|
||||||
|
{
|
||||||
|
$result = \gmp_invert($x, $m);
|
||||||
|
|
||||||
|
if ($result === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return \gmp_strval($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function modPow(string $base, string $exp, string $mod) : string
|
||||||
|
{
|
||||||
|
return \gmp_strval(\gmp_powm($base, $exp, $mod));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function gcd(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
return \gmp_strval(\gmp_gcd($a, $b));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function fromBase(string $number, int $base) : string
|
||||||
|
{
|
||||||
|
return \gmp_strval(\gmp_init($number, $base));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function toBase(string $number, int $base) : string
|
||||||
|
{
|
||||||
|
return \gmp_strval($number, $base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function and(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
return \gmp_strval(\gmp_and($a, $b));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function or(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
return \gmp_strval(\gmp_or($a, $b));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function xor(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
return \gmp_strval(\gmp_xor($a, $b));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function sqrt(string $n) : string
|
||||||
|
{
|
||||||
|
return \gmp_strval(\gmp_sqrt($n));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,634 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Brick\Math\Internal\Calculator;
|
||||||
|
|
||||||
|
use Brick\Math\Internal\Calculator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculator implementation using only native PHP code.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @psalm-immutable
|
||||||
|
*/
|
||||||
|
class NativeCalculator extends Calculator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The max number of digits the platform can natively add, subtract, multiply or divide without overflow.
|
||||||
|
* For multiplication, this represents the max sum of the lengths of both operands.
|
||||||
|
*
|
||||||
|
* For addition, it is assumed that an extra digit can hold a carry (1) without overflowing.
|
||||||
|
* 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)
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $maxDigits;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class constructor.
|
||||||
|
*
|
||||||
|
* @codeCoverageIgnore
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
switch (PHP_INT_SIZE) {
|
||||||
|
case 4:
|
||||||
|
$this->maxDigits = 9;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 8:
|
||||||
|
$this->maxDigits = 18;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new \RuntimeException('The platform is not 32-bit or 64-bit as expected.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function add(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @psalm-var numeric-string $a
|
||||||
|
* @psalm-var numeric-string $b
|
||||||
|
*/
|
||||||
|
$result = $a + $b;
|
||||||
|
|
||||||
|
if (is_int($result)) {
|
||||||
|
return (string) $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($a === '0') {
|
||||||
|
return $b;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($b === '0') {
|
||||||
|
return $a;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||||
|
|
||||||
|
$result = $aNeg === $bNeg ? $this->doAdd($aDig, $bDig) : $this->doSub($aDig, $bDig);
|
||||||
|
|
||||||
|
if ($aNeg) {
|
||||||
|
$result = $this->neg($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function sub(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
return $this->add($a, $this->neg($b));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function mul(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @psalm-var numeric-string $a
|
||||||
|
* @psalm-var numeric-string $b
|
||||||
|
*/
|
||||||
|
$result = $a * $b;
|
||||||
|
|
||||||
|
if (is_int($result)) {
|
||||||
|
return (string) $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($a === '0' || $b === '0') {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($a === '1') {
|
||||||
|
return $b;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($b === '1') {
|
||||||
|
return $a;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($a === '-1') {
|
||||||
|
return $this->neg($b);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($b === '-1') {
|
||||||
|
return $this->neg($a);
|
||||||
|
}
|
||||||
|
|
||||||
|
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||||
|
|
||||||
|
$result = $this->doMul($aDig, $bDig);
|
||||||
|
|
||||||
|
if ($aNeg !== $bNeg) {
|
||||||
|
$result = $this->neg($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function divQ(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
return $this->divQR($a, $b)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function divR(string $a, string $b): string
|
||||||
|
{
|
||||||
|
return $this->divQR($a, $b)[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function divQR(string $a, string $b) : array
|
||||||
|
{
|
||||||
|
if ($a === '0') {
|
||||||
|
return ['0', '0'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($a === $b) {
|
||||||
|
return ['1', '0'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($b === '1') {
|
||||||
|
return [$a, '0'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($b === '-1') {
|
||||||
|
return [$this->neg($a), '0'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @psalm-var numeric-string $a */
|
||||||
|
$na = $a * 1; // cast to number
|
||||||
|
|
||||||
|
if (is_int($na)) {
|
||||||
|
/** @psalm-var numeric-string $b */
|
||||||
|
$nb = $b * 1;
|
||||||
|
|
||||||
|
if (is_int($nb)) {
|
||||||
|
// the only division that may overflow is PHP_INT_MIN / -1,
|
||||||
|
// which cannot happen here as we've already handled a divisor of -1 above.
|
||||||
|
$r = $na % $nb;
|
||||||
|
$q = ($na - $r) / $nb;
|
||||||
|
|
||||||
|
assert(is_int($q));
|
||||||
|
|
||||||
|
return [
|
||||||
|
(string) $q,
|
||||||
|
(string) $r
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||||
|
|
||||||
|
[$q, $r] = $this->doDiv($aDig, $bDig);
|
||||||
|
|
||||||
|
if ($aNeg !== $bNeg) {
|
||||||
|
$q = $this->neg($q);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($aNeg) {
|
||||||
|
$r = $this->neg($r);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$q, $r];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function pow(string $a, int $e) : string
|
||||||
|
{
|
||||||
|
if ($e === 0) {
|
||||||
|
return '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($e === 1) {
|
||||||
|
return $a;
|
||||||
|
}
|
||||||
|
|
||||||
|
$odd = $e % 2;
|
||||||
|
$e -= $odd;
|
||||||
|
|
||||||
|
$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) {
|
||||||
|
$result = $this->mul($result, $a);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Algorithm from: https://www.geeksforgeeks.org/modular-exponentiation-power-in-modular-arithmetic/
|
||||||
|
*
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
// special case: the algorithm below fails with power 0 mod 1 (returns 1 instead of 0)
|
||||||
|
if ($exp === '0' && $mod === '1') {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
$x = $base;
|
||||||
|
|
||||||
|
$res = '1';
|
||||||
|
|
||||||
|
// numbers are positive, so we can use remainder instead of modulo
|
||||||
|
$x = $this->divR($x, $mod);
|
||||||
|
|
||||||
|
while ($exp !== '0') {
|
||||||
|
if (in_array($exp[-1], ['1', '3', '5', '7', '9'])) { // odd
|
||||||
|
$res = $this->divR($this->mul($res, $x), $mod);
|
||||||
|
}
|
||||||
|
|
||||||
|
$exp = $this->divQ($exp, '2');
|
||||||
|
$x = $this->divR($this->mul($x, $x), $mod);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapted from https://cp-algorithms.com/num_methods/roots_newton.html
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function sqrt(string $n) : string
|
||||||
|
{
|
||||||
|
if ($n === '0') {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// initial approximation
|
||||||
|
$x = \str_repeat('9', \intdiv(\strlen($n), 2) ?: 1);
|
||||||
|
|
||||||
|
$decreased = false;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
$nx = $this->divQ($this->add($x, $this->divQ($n, $x)), '2');
|
||||||
|
|
||||||
|
if ($x === $nx || $this->cmp($nx, $x) > 0 && $decreased) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decreased = $this->cmp($nx, $x) < 0;
|
||||||
|
$x = $nx;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $x;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the addition of two non-signed large integers.
|
||||||
|
*
|
||||||
|
* @param string $a The first operand.
|
||||||
|
* @param string $b The second operand.
|
||||||
|
*
|
||||||
|
* @return 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) {
|
||||||
|
$blockLength = $this->maxDigits;
|
||||||
|
|
||||||
|
if ($i < 0) {
|
||||||
|
$blockLength += $i;
|
||||||
|
/** @psalm-suppress LoopInvalidation */
|
||||||
|
$i = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @psalm-var numeric-string $blockA */
|
||||||
|
$blockA = \substr($a, $i, $blockLength);
|
||||||
|
|
||||||
|
/** @psalm-var numeric-string $blockB */
|
||||||
|
$blockB = \substr($b, $i, $blockLength);
|
||||||
|
|
||||||
|
$sum = (string) ($blockA + $blockB + $carry);
|
||||||
|
$sumLength = \strlen($sum);
|
||||||
|
|
||||||
|
if ($sumLength > $blockLength) {
|
||||||
|
$sum = \substr($sum, 1);
|
||||||
|
$carry = 1;
|
||||||
|
} else {
|
||||||
|
if ($sumLength < $blockLength) {
|
||||||
|
$sum = \str_repeat('0', $blockLength - $sumLength) . $sum;
|
||||||
|
}
|
||||||
|
$carry = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $sum . $result;
|
||||||
|
|
||||||
|
if ($i === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($carry === 1) {
|
||||||
|
$result = '1' . $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the subtraction of two non-signed large integers.
|
||||||
|
*
|
||||||
|
* @param string $a The first operand.
|
||||||
|
* @param string $b The second operand.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function doSub(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
if ($a === $b) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that we always subtract to a positive result: biggest minus smallest.
|
||||||
|
$cmp = $this->doCmp($a, $b);
|
||||||
|
|
||||||
|
$invert = ($cmp === -1);
|
||||||
|
|
||||||
|
if ($invert) {
|
||||||
|
$c = $a;
|
||||||
|
$a = $b;
|
||||||
|
$b = $c;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$a, $b, $length] = $this->pad($a, $b);
|
||||||
|
|
||||||
|
$carry = 0;
|
||||||
|
$result = '';
|
||||||
|
|
||||||
|
$complement = 10 ** $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);
|
||||||
|
|
||||||
|
/** @psalm-var numeric-string $blockB */
|
||||||
|
$blockB = \substr($b, $i, $blockLength);
|
||||||
|
|
||||||
|
$sum = $blockA - $blockB - $carry;
|
||||||
|
|
||||||
|
if ($sum < 0) {
|
||||||
|
$sum += $complement;
|
||||||
|
$carry = 1;
|
||||||
|
} else {
|
||||||
|
$carry = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sum = (string) $sum;
|
||||||
|
$sumLength = \strlen($sum);
|
||||||
|
|
||||||
|
if ($sumLength < $blockLength) {
|
||||||
|
$sum = \str_repeat('0', $blockLength - $sumLength) . $sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $sum . $result;
|
||||||
|
|
||||||
|
if ($i === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carry cannot be 1 when the loop ends, as a > b
|
||||||
|
assert($carry === 0);
|
||||||
|
|
||||||
|
$result = \ltrim($result, '0');
|
||||||
|
|
||||||
|
if ($invert) {
|
||||||
|
$result = $this->neg($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the multiplication of two non-signed large integers.
|
||||||
|
*
|
||||||
|
* @param string $a The first operand.
|
||||||
|
* @param string $b The second operand.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function doMul(string $a, string $b) : string
|
||||||
|
{
|
||||||
|
$x = \strlen($a);
|
||||||
|
$y = \strlen($b);
|
||||||
|
|
||||||
|
$maxDigits = \intdiv($this->maxDigits, 2);
|
||||||
|
$complement = 10 ** $maxDigits;
|
||||||
|
|
||||||
|
$result = '0';
|
||||||
|
|
||||||
|
for ($i = $x - $maxDigits;; $i -= $maxDigits) {
|
||||||
|
$blockALength = $maxDigits;
|
||||||
|
|
||||||
|
if ($i < 0) {
|
||||||
|
$blockALength += $i;
|
||||||
|
/** @psalm-suppress LoopInvalidation */
|
||||||
|
$i = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$blockA = (int) \substr($a, $i, $blockALength);
|
||||||
|
|
||||||
|
$line = '';
|
||||||
|
$carry = 0;
|
||||||
|
|
||||||
|
for ($j = $y - $maxDigits;; $j -= $maxDigits) {
|
||||||
|
$blockBLength = $maxDigits;
|
||||||
|
|
||||||
|
if ($j < 0) {
|
||||||
|
$blockBLength += $j;
|
||||||
|
/** @psalm-suppress LoopInvalidation */
|
||||||
|
$j = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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);
|
||||||
|
|
||||||
|
$line = $value . $line;
|
||||||
|
|
||||||
|
if ($j === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($carry !== 0) {
|
||||||
|
$line = $carry . $line;
|
||||||
|
}
|
||||||
|
|
||||||
|
$line = \ltrim($line, '0');
|
||||||
|
|
||||||
|
if ($line !== '') {
|
||||||
|
$line .= \str_repeat('0', $x - $blockALength - $i);
|
||||||
|
$result = $this->add($result, $line);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($i === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the division of two non-signed large integers.
|
||||||
|
*
|
||||||
|
* @param string $a The first operand.
|
||||||
|
* @param string $b The second operand.
|
||||||
|
*
|
||||||
|
* @return string[] The quotient and remainder.
|
||||||
|
*/
|
||||||
|
private function doDiv(string $a, string $b) : array
|
||||||
|
{
|
||||||
|
$cmp = $this->doCmp($a, $b);
|
||||||
|
|
||||||
|
if ($cmp === -1) {
|
||||||
|
return ['0', $a];
|
||||||
|
}
|
||||||
|
|
||||||
|
$x = \strlen($a);
|
||||||
|
$y = \strlen($b);
|
||||||
|
|
||||||
|
// we now know that a >= b && x >= y
|
||||||
|
|
||||||
|
$q = '0'; // quotient
|
||||||
|
$r = $a; // remainder
|
||||||
|
$z = $y; // focus length, always $y or $y+1
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
$focus = \substr($a, 0, $z);
|
||||||
|
|
||||||
|
$cmp = $this->doCmp($focus, $b);
|
||||||
|
|
||||||
|
if ($cmp === -1) {
|
||||||
|
if ($z === $x) { // remainder < dividend
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$z++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$zeros = \str_repeat('0', $x - $z);
|
||||||
|
|
||||||
|
$q = $this->add($q, '1' . $zeros);
|
||||||
|
$a = $this->sub($a, $b . $zeros);
|
||||||
|
|
||||||
|
$r = $a;
|
||||||
|
|
||||||
|
if ($r === '0') { // remainder == 0
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$x = \strlen($a);
|
||||||
|
|
||||||
|
if ($x < $y) { // remainder < dividend
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$z = $y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$q, $r];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two non-signed large numbers.
|
||||||
|
*
|
||||||
|
* @param string $a The first operand.
|
||||||
|
* @param string $b The second operand.
|
||||||
|
*
|
||||||
|
* @return int [-1, 0, 1]
|
||||||
|
*/
|
||||||
|
private function doCmp(string $a, string $b) : int
|
||||||
|
{
|
||||||
|
$x = \strlen($a);
|
||||||
|
$y = \strlen($b);
|
||||||
|
|
||||||
|
$cmp = $x <=> $y;
|
||||||
|
|
||||||
|
if ($cmp !== 0) {
|
||||||
|
return $cmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return \strcmp($a, $b) <=> 0; // enforce [-1, 0, 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pads the left of one of the given numbers with zeros if necessary to make both numbers the same length.
|
||||||
|
*
|
||||||
|
* The numbers must only consist of digits, without leading minus sign.
|
||||||
|
*
|
||||||
|
* @param string $a The first operand.
|
||||||
|
* @param string $b The second operand.
|
||||||
|
*
|
||||||
|
* @return array{string, string, int}
|
||||||
|
*/
|
||||||
|
private function pad(string $a, string $b) : array
|
||||||
|
{
|
||||||
|
$x = \strlen($a);
|
||||||
|
$y = \strlen($b);
|
||||||
|
|
||||||
|
if ($x > $y) {
|
||||||
|
$b = \str_repeat('0', $x - $y) . $b;
|
||||||
|
|
||||||
|
return [$a, $b, $x];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($x < $y) {
|
||||||
|
$a = \str_repeat('0', $y - $x) . $a;
|
||||||
|
|
||||||
|
return [$a, $b, $y];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$a, $b, $x];
|
||||||
|
}
|
||||||
|
}
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
final class RoundingMode
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Private constructor. This class is not instantiable.
|
||||||
|
*
|
||||||
|
* @codeCoverageIgnore
|
||||||
|
*/
|
||||||
|
private function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the requested operation has an exact result, hence no rounding is necessary.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
public const UNNECESSARY = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rounds away from zero.
|
||||||
|
*
|
||||||
|
* Always increments the digit prior to a nonzero discarded fraction.
|
||||||
|
* Note that this rounding mode never decreases the magnitude of the calculated value.
|
||||||
|
*/
|
||||||
|
public const UP = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rounds towards zero.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
public const DOWN = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rounds towards positive infinity.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
public const CEILING = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rounds towards negative infinity.
|
||||||
|
*
|
||||||
|
* If the result is positive, behave as for DOWN; if negative, behave as for UP.
|
||||||
|
* Note that this rounding mode never increases the calculated value.
|
||||||
|
*/
|
||||||
|
public const FLOOR = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* Note that this is the rounding mode commonly taught at school.
|
||||||
|
*/
|
||||||
|
public const HALF_UP = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
public const HALF_DOWN = 6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
public const HALF_CEILING = 7;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
public const HALF_FLOOR = 8;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
public const HALF_EVEN = 9;
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Carbon
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# carbonphp/carbon-doctrine-types
|
||||||
|
|
||||||
|
Types to use Carbon in Doctrine
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
[Check how to use in the official Carbon documentation](https://carbon.nesbot.com/symfony/)
|
||||||
|
|
||||||
|
This package is an externalization of [src/Carbon/Doctrine](https://github.com/briannesbitt/Carbon/tree/2.71.0/src/Carbon/Doctrine)
|
||||||
|
from `nestbot/carbon` package.
|
||||||
|
|
||||||
|
Externalization allows to better deal with different versions of dbal. With
|
||||||
|
version 4.0 of dbal, it no longer sustainable to be compatible with all version
|
||||||
|
using a single code.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "carbonphp/carbon-doctrine-types",
|
||||||
|
"description": "Types to use Carbon in Doctrine",
|
||||||
|
"type": "library",
|
||||||
|
"keywords": [
|
||||||
|
"date",
|
||||||
|
"time",
|
||||||
|
"DateTime",
|
||||||
|
"Carbon",
|
||||||
|
"Doctrine"
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^7.4 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"doctrine/dbal": "^3.7.0",
|
||||||
|
"nesbot/carbon": "^2.71.0 || ^3.0.0",
|
||||||
|
"phpunit/phpunit": "^10.3"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"doctrine/dbal": "<3.7.0 || >=4.0.0"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Carbon\\Doctrine\\": "src/Carbon/Doctrine/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "KyleKatarn",
|
||||||
|
"email": "kylekatarnls@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"minimum-stability": "dev"
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Carbon\Doctrine;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||||
|
|
||||||
|
interface CarbonDoctrineType
|
||||||
|
{
|
||||||
|
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform);
|
||||||
|
|
||||||
|
public function convertToPHPValue($value, AbstractPlatform $platform);
|
||||||
|
|
||||||
|
public function convertToDatabaseValue($value, AbstractPlatform $platform);
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Carbon\Doctrine;
|
||||||
|
|
||||||
|
class CarbonImmutableType extends DateTimeImmutableType implements CarbonDoctrineType
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Carbon\Doctrine;
|
||||||
|
|
||||||
|
class CarbonType extends DateTimeType implements CarbonDoctrineType
|
||||||
|
{
|
||||||
|
}
|
||||||
+141
@@ -0,0 +1,141 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Carbon\Doctrine;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Carbon\CarbonInterface;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||||
|
use Doctrine\DBAL\Platforms\DB2Platform;
|
||||||
|
use Doctrine\DBAL\Platforms\OraclePlatform;
|
||||||
|
use Doctrine\DBAL\Platforms\SqlitePlatform;
|
||||||
|
use Doctrine\DBAL\Platforms\SQLServerPlatform;
|
||||||
|
use Doctrine\DBAL\Types\ConversionException;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T of CarbonInterface
|
||||||
|
*/
|
||||||
|
trait CarbonTypeConverter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* This property differentiates types installed by carbonphp/carbon-doctrine-types
|
||||||
|
* from the ones embedded previously in nesbot/carbon source directly.
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
public bool $external = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return class-string<T>
|
||||||
|
*/
|
||||||
|
protected function getCarbonClassName(): string
|
||||||
|
{
|
||||||
|
return Carbon::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
|
||||||
|
{
|
||||||
|
$precision = min(
|
||||||
|
$fieldDeclaration['precision'] ?? DateTimeDefaultPrecision::get(),
|
||||||
|
$this->getMaximumPrecision($platform),
|
||||||
|
);
|
||||||
|
|
||||||
|
$type = parent::getSQLDeclaration($fieldDeclaration, $platform);
|
||||||
|
|
||||||
|
if (!$precision) {
|
||||||
|
return $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($type, '(')) {
|
||||||
|
return preg_replace('/\(\d+\)/', "($precision)", $type);
|
||||||
|
}
|
||||||
|
|
||||||
|
[$before, $after] = explode(' ', "$type ");
|
||||||
|
|
||||||
|
return trim("$before($precision) $after");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||||
|
*
|
||||||
|
* @return T|null
|
||||||
|
*/
|
||||||
|
public function convertToPHPValue($value, AbstractPlatform $platform)
|
||||||
|
{
|
||||||
|
$class = $this->getCarbonClassName();
|
||||||
|
|
||||||
|
if ($value === null || is_a($value, $class)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof DateTimeInterface) {
|
||||||
|
return $class::instance($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$date = null;
|
||||||
|
$error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$date = $class::parse($value);
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
$error = $exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$date) {
|
||||||
|
throw ConversionException::conversionFailedFormat(
|
||||||
|
$value,
|
||||||
|
$this->getTypeName(),
|
||||||
|
'Y-m-d H:i:s.u or any format supported by '.$class.'::parse()',
|
||||||
|
$error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||||
|
*/
|
||||||
|
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof DateTimeInterface) {
|
||||||
|
return $value->format('Y-m-d H:i:s.u');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ConversionException::conversionFailedInvalidType(
|
||||||
|
$value,
|
||||||
|
$this->getTypeName(),
|
||||||
|
['null', 'DateTime', 'Carbon']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTypeName(): string
|
||||||
|
{
|
||||||
|
$chunks = explode('\\', static::class);
|
||||||
|
$type = preg_replace('/Type$/', '', end($chunks));
|
||||||
|
|
||||||
|
return strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $type));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getMaximumPrecision(AbstractPlatform $platform): int
|
||||||
|
{
|
||||||
|
if ($platform instanceof DB2Platform) {
|
||||||
|
return 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($platform instanceof OraclePlatform) {
|
||||||
|
return 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($platform instanceof SQLServerPlatform || $platform instanceof SqlitePlatform) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Carbon\Doctrine;
|
||||||
|
|
||||||
|
class DateTimeDefaultPrecision
|
||||||
|
{
|
||||||
|
private static $precision = 6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the default Doctrine datetime and datetime_immutable precision.
|
||||||
|
*
|
||||||
|
* @param int $precision
|
||||||
|
*/
|
||||||
|
public static function set(int $precision): void
|
||||||
|
{
|
||||||
|
self::$precision = $precision;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default Doctrine datetime and datetime_immutable precision.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public static function get(): int
|
||||||
|
{
|
||||||
|
return self::$precision;
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Carbon\Doctrine;
|
||||||
|
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
use Doctrine\DBAL\Types\VarDateTimeImmutableType;
|
||||||
|
|
||||||
|
class DateTimeImmutableType extends VarDateTimeImmutableType implements CarbonDoctrineType
|
||||||
|
{
|
||||||
|
/** @use CarbonTypeConverter<CarbonImmutable> */
|
||||||
|
use CarbonTypeConverter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return class-string<CarbonImmutable>
|
||||||
|
*/
|
||||||
|
protected function getCarbonClassName(): string
|
||||||
|
{
|
||||||
|
return CarbonImmutable::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Carbon\Doctrine;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Doctrine\DBAL\Types\VarDateTimeType;
|
||||||
|
|
||||||
|
class DateTimeType extends VarDateTimeType implements CarbonDoctrineType
|
||||||
|
{
|
||||||
|
/** @use CarbonTypeConverter<Carbon> */
|
||||||
|
use CarbonTypeConverter;
|
||||||
|
}
|
||||||
Vendored
+579
@@ -0,0 +1,579 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Autoload;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
|
||||||
|
*
|
||||||
|
* $loader = new \Composer\Autoload\ClassLoader();
|
||||||
|
*
|
||||||
|
* // register classes with namespaces
|
||||||
|
* $loader->add('Symfony\Component', __DIR__.'/component');
|
||||||
|
* $loader->add('Symfony', __DIR__.'/framework');
|
||||||
|
*
|
||||||
|
* // activate the autoloader
|
||||||
|
* $loader->register();
|
||||||
|
*
|
||||||
|
* // to enable searching the include path (eg. for PEAR packages)
|
||||||
|
* $loader->setUseIncludePath(true);
|
||||||
|
*
|
||||||
|
* In this example, if you try to use a class in the Symfony\Component
|
||||||
|
* namespace or one of its children (Symfony\Component\Console for instance),
|
||||||
|
* the autoloader will first look for the class under the component/
|
||||||
|
* directory, and it will then fallback to the framework/ directory if not
|
||||||
|
* found before giving up.
|
||||||
|
*
|
||||||
|
* This class is loosely based on the Symfony UniversalClassLoader.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
* @see https://www.php-fig.org/psr/psr-0/
|
||||||
|
* @see https://www.php-fig.org/psr/psr-4/
|
||||||
|
*/
|
||||||
|
class ClassLoader
|
||||||
|
{
|
||||||
|
/** @var \Closure(string):void */
|
||||||
|
private static $includeFile;
|
||||||
|
|
||||||
|
/** @var string|null */
|
||||||
|
private $vendorDir;
|
||||||
|
|
||||||
|
// PSR-4
|
||||||
|
/**
|
||||||
|
* @var array<string, array<string, int>>
|
||||||
|
*/
|
||||||
|
private $prefixLengthsPsr4 = array();
|
||||||
|
/**
|
||||||
|
* @var array<string, list<string>>
|
||||||
|
*/
|
||||||
|
private $prefixDirsPsr4 = array();
|
||||||
|
/**
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
private $fallbackDirsPsr4 = array();
|
||||||
|
|
||||||
|
// PSR-0
|
||||||
|
/**
|
||||||
|
* 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 list<string>
|
||||||
|
*/
|
||||||
|
private $fallbackDirsPsr0 = array();
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
private $useIncludePath = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
private $classMap = array();
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
private $classMapAuthoritative = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, bool>
|
||||||
|
*/
|
||||||
|
private $missingClasses = array();
|
||||||
|
|
||||||
|
/** @var string|null */
|
||||||
|
private $apcuPrefix;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, self>
|
||||||
|
*/
|
||||||
|
private static $registeredLoaders = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|null $vendorDir
|
||||||
|
*/
|
||||||
|
public function __construct($vendorDir = null)
|
||||||
|
{
|
||||||
|
$this->vendorDir = $vendorDir;
|
||||||
|
self::initializeIncludeClosure();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, list<string>>
|
||||||
|
*/
|
||||||
|
public function getPrefixes()
|
||||||
|
{
|
||||||
|
if (!empty($this->prefixesPsr0)) {
|
||||||
|
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, list<string>>
|
||||||
|
*/
|
||||||
|
public function getPrefixesPsr4()
|
||||||
|
{
|
||||||
|
return $this->prefixDirsPsr4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function getFallbackDirs()
|
||||||
|
{
|
||||||
|
return $this->fallbackDirsPsr0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function getFallbackDirsPsr4()
|
||||||
|
{
|
||||||
|
return $this->fallbackDirsPsr4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string> Array of classname => path
|
||||||
|
*/
|
||||||
|
public function getClassMap()
|
||||||
|
{
|
||||||
|
return $this->classMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, string> $classMap Class to filename map
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addClassMap(array $classMap)
|
||||||
|
{
|
||||||
|
if ($this->classMap) {
|
||||||
|
$this->classMap = array_merge($this->classMap, $classMap);
|
||||||
|
} else {
|
||||||
|
$this->classMap = $classMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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(
|
||||||
|
$paths,
|
||||||
|
$this->fallbackDirsPsr0
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->fallbackDirsPsr0 = array_merge(
|
||||||
|
$this->fallbackDirsPsr0,
|
||||||
|
$paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$first = $prefix[0];
|
||||||
|
if (!isset($this->prefixesPsr0[$first][$prefix])) {
|
||||||
|
$this->prefixesPsr0[$first][$prefix] = $paths;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($prepend) {
|
||||||
|
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||||
|
$paths,
|
||||||
|
$this->prefixesPsr0[$first][$prefix]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||||
|
$this->prefixesPsr0[$first][$prefix],
|
||||||
|
$paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 list<string>|string $paths The PSR-4 base directories
|
||||||
|
* @param bool $prepend Whether to prepend the directories
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addPsr4($prefix, $paths, $prepend = false)
|
||||||
|
{
|
||||||
|
$paths = (array) $paths;
|
||||||
|
if (!$prefix) {
|
||||||
|
// Register directories for the root namespace.
|
||||||
|
if ($prepend) {
|
||||||
|
$this->fallbackDirsPsr4 = array_merge(
|
||||||
|
$paths,
|
||||||
|
$this->fallbackDirsPsr4
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->fallbackDirsPsr4 = array_merge(
|
||||||
|
$this->fallbackDirsPsr4,
|
||||||
|
$paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
|
||||||
|
// Register directories for a new namespace.
|
||||||
|
$length = strlen($prefix);
|
||||||
|
if ('\\' !== $prefix[$length - 1]) {
|
||||||
|
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||||
|
}
|
||||||
|
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||||
|
$this->prefixDirsPsr4[$prefix] = $paths;
|
||||||
|
} elseif ($prepend) {
|
||||||
|
// Prepend directories for an already registered namespace.
|
||||||
|
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||||
|
$paths,
|
||||||
|
$this->prefixDirsPsr4[$prefix]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Append directories for an already registered namespace.
|
||||||
|
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||||
|
$this->prefixDirsPsr4[$prefix],
|
||||||
|
$paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 list<string>|string $paths The PSR-0 base directories
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function set($prefix, $paths)
|
||||||
|
{
|
||||||
|
if (!$prefix) {
|
||||||
|
$this->fallbackDirsPsr0 = (array) $paths;
|
||||||
|
} else {
|
||||||
|
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 list<string>|string $paths The PSR-4 base directories
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setPsr4($prefix, $paths)
|
||||||
|
{
|
||||||
|
if (!$prefix) {
|
||||||
|
$this->fallbackDirsPsr4 = (array) $paths;
|
||||||
|
} else {
|
||||||
|
$length = strlen($prefix);
|
||||||
|
if ('\\' !== $prefix[$length - 1]) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns on searching the include path for class files.
|
||||||
|
*
|
||||||
|
* @param bool $useIncludePath
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setUseIncludePath($useIncludePath)
|
||||||
|
{
|
||||||
|
$this->useIncludePath = $useIncludePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can be used to check if the autoloader uses the include path to check
|
||||||
|
* for classes.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function getUseIncludePath()
|
||||||
|
{
|
||||||
|
return $this->useIncludePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns off searching the prefix and fallback directories for classes
|
||||||
|
* that have not been registered with the class map.
|
||||||
|
*
|
||||||
|
* @param bool $classMapAuthoritative
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setClassMapAuthoritative($classMapAuthoritative)
|
||||||
|
{
|
||||||
|
$this->classMapAuthoritative = $classMapAuthoritative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should class lookup fail if not found in the current class map?
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isClassMapAuthoritative()
|
||||||
|
{
|
||||||
|
return $this->classMapAuthoritative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
|
||||||
|
*
|
||||||
|
* @param string|null $apcuPrefix
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setApcuPrefix($apcuPrefix)
|
||||||
|
{
|
||||||
|
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The APCu prefix in use, or null if APCu caching is not enabled.
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getApcuPrefix()
|
||||||
|
{
|
||||||
|
return $this->apcuPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers this instance as an autoloader.
|
||||||
|
*
|
||||||
|
* @param bool $prepend Whether to prepend the autoloader or not
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register($prepend = false)
|
||||||
|
{
|
||||||
|
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
|
||||||
|
|
||||||
|
if (null === $this->vendorDir) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($prepend) {
|
||||||
|
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
|
||||||
|
} else {
|
||||||
|
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||||
|
self::$registeredLoaders[$this->vendorDir] = $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters this instance as an autoloader.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function unregister()
|
||||||
|
{
|
||||||
|
spl_autoload_unregister(array($this, 'loadClass'));
|
||||||
|
|
||||||
|
if (null !== $this->vendorDir) {
|
||||||
|
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the given class or interface.
|
||||||
|
*
|
||||||
|
* @param string $class The name of the class
|
||||||
|
* @return true|null True if loaded, null otherwise
|
||||||
|
*/
|
||||||
|
public function loadClass($class)
|
||||||
|
{
|
||||||
|
if ($file = $this->findFile($class)) {
|
||||||
|
$includeFile = self::$includeFile;
|
||||||
|
$includeFile($file);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the path to the file where the class is defined.
|
||||||
|
*
|
||||||
|
* @param string $class The name of the class
|
||||||
|
*
|
||||||
|
* @return string|false The path if found, false otherwise
|
||||||
|
*/
|
||||||
|
public function findFile($class)
|
||||||
|
{
|
||||||
|
// class map lookup
|
||||||
|
if (isset($this->classMap[$class])) {
|
||||||
|
return $this->classMap[$class];
|
||||||
|
}
|
||||||
|
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (null !== $this->apcuPrefix) {
|
||||||
|
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
|
||||||
|
if ($hit) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $this->findFileWithExtension($class, '.php');
|
||||||
|
|
||||||
|
// Search for Hack files if we are running on HHVM
|
||||||
|
if (false === $file && defined('HHVM_VERSION')) {
|
||||||
|
$file = $this->findFileWithExtension($class, '.hh');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $this->apcuPrefix) {
|
||||||
|
apcu_add($this->apcuPrefix.$class, $file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false === $file) {
|
||||||
|
// Remember that this class does not exist.
|
||||||
|
$this->missingClasses[$class] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the currently registered loaders keyed by their corresponding vendor directories.
|
||||||
|
*
|
||||||
|
* @return array<string, self>
|
||||||
|
*/
|
||||||
|
public static function getRegisteredLoaders()
|
||||||
|
{
|
||||||
|
return self::$registeredLoaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $class
|
||||||
|
* @param string $ext
|
||||||
|
* @return string|false
|
||||||
|
*/
|
||||||
|
private function findFileWithExtension($class, $ext)
|
||||||
|
{
|
||||||
|
// PSR-4 lookup
|
||||||
|
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
|
||||||
|
|
||||||
|
$first = $class[0];
|
||||||
|
if (isset($this->prefixLengthsPsr4[$first])) {
|
||||||
|
$subPath = $class;
|
||||||
|
while (false !== $lastPos = strrpos($subPath, '\\')) {
|
||||||
|
$subPath = substr($subPath, 0, $lastPos);
|
||||||
|
$search = $subPath . '\\';
|
||||||
|
if (isset($this->prefixDirsPsr4[$search])) {
|
||||||
|
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
|
||||||
|
foreach ($this->prefixDirsPsr4[$search] as $dir) {
|
||||||
|
if (file_exists($file = $dir . $pathEnd)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-4 fallback dirs
|
||||||
|
foreach ($this->fallbackDirsPsr4 as $dir) {
|
||||||
|
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-0 lookup
|
||||||
|
if (false !== $pos = strrpos($class, '\\')) {
|
||||||
|
// namespaced class name
|
||||||
|
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
|
||||||
|
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
|
||||||
|
} else {
|
||||||
|
// PEAR-like class name
|
||||||
|
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($this->prefixesPsr0[$first])) {
|
||||||
|
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
|
||||||
|
if (0 === strpos($class, $prefix)) {
|
||||||
|
foreach ($dirs as $dir) {
|
||||||
|
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-0 fallback dirs
|
||||||
|
foreach ($this->fallbackDirsPsr0 as $dir) {
|
||||||
|
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-0 include paths.
|
||||||
|
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+396
@@ -0,0 +1,396 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer;
|
||||||
|
|
||||||
|
use Composer\Autoload\ClassLoader;
|
||||||
|
use Composer\Semver\VersionParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is copied in every Composer installed project and available to all
|
||||||
|
*
|
||||||
|
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
|
||||||
|
*
|
||||||
|
* To require its presence, you can require `composer-runtime-api ^2.0`
|
||||||
|
*
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
private static $canGetVendors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array[]
|
||||||
|
* @psalm-var array<string, 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[]}>}>
|
||||||
|
*/
|
||||||
|
private static $installedByVendor = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all package names which are present, either by being installed, replaced or provided
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
* @psalm-return list<string>
|
||||||
|
*/
|
||||||
|
public static function getInstalledPackages()
|
||||||
|
{
|
||||||
|
$packages = array();
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
$packages[] = array_keys($installed['versions']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (1 === \count($packages)) {
|
||||||
|
return $packages[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all package names with a specific type e.g. 'library'
|
||||||
|
*
|
||||||
|
* @param string $type
|
||||||
|
* @return string[]
|
||||||
|
* @psalm-return list<string>
|
||||||
|
*/
|
||||||
|
public static function getInstalledPackagesByType($type)
|
||||||
|
{
|
||||||
|
$packagesByType = array();
|
||||||
|
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
foreach ($installed['versions'] as $name => $package) {
|
||||||
|
if (isset($package['type']) && $package['type'] === $type) {
|
||||||
|
$packagesByType[] = $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $packagesByType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the given package is installed
|
||||||
|
*
|
||||||
|
* This also returns true if the package name is provided or replaced by another package
|
||||||
|
*
|
||||||
|
* @param string $packageName
|
||||||
|
* @param bool $includeDevRequirements
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isInstalled($packageName, $includeDevRequirements = true)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (isset($installed['versions'][$packageName])) {
|
||||||
|
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the given package satisfies a version constraint
|
||||||
|
*
|
||||||
|
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
|
||||||
|
*
|
||||||
|
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
|
||||||
|
*
|
||||||
|
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
|
||||||
|
* @param string $packageName
|
||||||
|
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function satisfies(VersionParser $parser, $packageName, $constraint)
|
||||||
|
{
|
||||||
|
$constraint = $parser->parseConstraints((string) $constraint);
|
||||||
|
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
|
||||||
|
|
||||||
|
return $provided->matches($constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a version constraint representing all the range(s) which are installed for a given package
|
||||||
|
*
|
||||||
|
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
|
||||||
|
* whether a given version of a package is installed, and not just whether it exists
|
||||||
|
*
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string Version constraint usable with composer/semver
|
||||||
|
*/
|
||||||
|
public static function getVersionRanges($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ranges = array();
|
||||||
|
if (isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||||
|
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
|
||||||
|
}
|
||||||
|
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
|
||||||
|
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
|
||||||
|
}
|
||||||
|
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
|
||||||
|
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
|
||||||
|
}
|
||||||
|
if (array_key_exists('provided', $installed['versions'][$packageName])) {
|
||||||
|
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' || ', $ranges);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||||
|
*/
|
||||||
|
public static function getVersion($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($installed['versions'][$packageName]['version'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed['versions'][$packageName]['version'];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||||
|
*/
|
||||||
|
public static function getPrettyVersion($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed['versions'][$packageName]['pretty_version'];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
|
||||||
|
*/
|
||||||
|
public static function getReference($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($installed['versions'][$packageName]['reference'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed['versions'][$packageName]['reference'];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
|
||||||
|
*/
|
||||||
|
public static function getInstallPath($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
|
||||||
|
*/
|
||||||
|
public static function getRootPackage()
|
||||||
|
{
|
||||||
|
$installed = self::getInstalled();
|
||||||
|
|
||||||
|
return $installed[0]['root'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the raw installed.php data for custom implementations
|
||||||
|
*
|
||||||
|
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
|
||||||
|
* @return array[]
|
||||||
|
* @psalm-return 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[]}>}
|
||||||
|
*/
|
||||||
|
public static function getRawData()
|
||||||
|
{
|
||||||
|
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
|
||||||
|
|
||||||
|
if (null === self::$installed) {
|
||||||
|
// 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 = include __DIR__ . '/installed.php';
|
||||||
|
} else {
|
||||||
|
self::$installed = array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$installed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the raw data of all installed.php which are currently loaded for custom implementations
|
||||||
|
*
|
||||||
|
* @return array[]
|
||||||
|
* @psalm-return list<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[]}>}>
|
||||||
|
*/
|
||||||
|
public static function getAllRawData()
|
||||||
|
{
|
||||||
|
return self::getInstalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lets you reload the static array from another file
|
||||||
|
*
|
||||||
|
* This is only useful for complex integrations in which a project needs to use
|
||||||
|
* this class but then also needs to execute another project's autoloader in process,
|
||||||
|
* and wants to ensure both projects have access to their version of installed.php.
|
||||||
|
*
|
||||||
|
* A typical case would be PHPUnit, where it would need to make sure it reads all
|
||||||
|
* the data it needs from this class, then call reload() with
|
||||||
|
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
|
||||||
|
* the project in which it runs can then also use this class safely, without
|
||||||
|
* interference between PHPUnit's dependencies and the project's dependencies.
|
||||||
|
*
|
||||||
|
* @param array[] $data A vendor/composer/installed.php data set
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @psalm-param 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[]}>} $data
|
||||||
|
*/
|
||||||
|
public static function reload($data)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array[]
|
||||||
|
* @psalm-return list<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[]}>}>
|
||||||
|
*/
|
||||||
|
private static function getInstalled()
|
||||||
|
{
|
||||||
|
if (null === self::$canGetVendors) {
|
||||||
|
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
|
||||||
|
}
|
||||||
|
|
||||||
|
$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')) {
|
||||||
|
/** @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === self::$installed) {
|
||||||
|
// 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') {
|
||||||
|
/** @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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::$installed !== array() && !$copiedLocalDir) {
|
||||||
|
$installed[] = self::$installed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+21
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
Copyright (c) Nils Adermann, Jordi Boggiano
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
+4521
File diff suppressed because it is too large
Load Diff
Vendored
+37
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_files.php @generated by Composer
|
||||||
|
|
||||||
|
$vendorDir = dirname(__DIR__);
|
||||||
|
$baseDir = dirname($vendorDir);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
|
||||||
|
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
|
||||||
|
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
|
||||||
|
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
|
||||||
|
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
|
||||||
|
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
|
||||||
|
'25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
|
||||||
|
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
|
||||||
|
'0d59ee240a4cd96ddbb4ff164fccea4d' => $vendorDir . '/symfony/polyfill-php73/bootstrap.php',
|
||||||
|
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
|
||||||
|
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
|
||||||
|
'23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php',
|
||||||
|
'2cffec82183ee1cea088009cef9a6fc3' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php',
|
||||||
|
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
|
||||||
|
'def43f6c87e4f8dfd0c9e1b1bab14fe8' => $vendorDir . '/symfony/polyfill-iconv/bootstrap.php',
|
||||||
|
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
|
||||||
|
'538ca81a9a966a6716601ecf48f4eaef' => $vendorDir . '/opis/closure/functions.php',
|
||||||
|
'801c31d8ed748cfa537fa45402288c95' => $vendorDir . '/psy/psysh/src/functions.php',
|
||||||
|
'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php',
|
||||||
|
'2c102faa651ef8ea5874edb585946bce' => $vendorDir . '/swiftmailer/swiftmailer/lib/swift_required.php',
|
||||||
|
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
|
||||||
|
'265b4faa2b3a9766332744949e83bf97' => $vendorDir . '/laravel/framework/src/Illuminate/Collections/helpers.php',
|
||||||
|
'c7a3c339e7e14b60e06a2d7fcce9476b' => $vendorDir . '/laravel/framework/src/Illuminate/Events/functions.php',
|
||||||
|
'f0906e6318348a765ffb6eb24e0d0938' => $vendorDir . '/laravel/framework/src/Illuminate/Foundation/helpers.php',
|
||||||
|
'58571171fd5812e6e447dce228f52f4d' => $vendorDir . '/laravel/framework/src/Illuminate/Support/helpers.php',
|
||||||
|
'377b22b161c09ed6e5152de788ca020a' => $vendorDir . '/spatie/laravel-permission/src/helpers.php',
|
||||||
|
'8d3b2199cd129cab173c6d6470bfbcb5' => $vendorDir . '/yajra/laravel-oci8/src/helper.php',
|
||||||
|
'b4e3f29b106af37a2bb239f73cdf68c7' => $baseDir . '/app/helpers.php',
|
||||||
|
);
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_namespaces.php @generated by Composer
|
||||||
|
|
||||||
|
$vendorDir = dirname(__DIR__);
|
||||||
|
$baseDir = dirname($vendorDir);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'HTMLPurifier' => array($vendorDir . '/ezyang/htmlpurifier/library'),
|
||||||
|
);
|
||||||
Vendored
+89
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_psr4.php @generated by Composer
|
||||||
|
|
||||||
|
$vendorDir = dirname(__DIR__);
|
||||||
|
$baseDir = dirname($vendorDir);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'voku\\' => array($vendorDir . '/voku/portable-ascii/src/voku'),
|
||||||
|
'ZipStream\\' => array($vendorDir . '/maennchen/zipstream-php/src'),
|
||||||
|
'Yajra\\' => array($vendorDir . '/yajra/laravel-oci8/src', $vendorDir . '/yajra/laravel-pdo-via-oci8/src'),
|
||||||
|
'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'),
|
||||||
|
'TijsVerkoyen\\CssToInlineStyles\\' => array($vendorDir . '/tijsverkoyen/css-to-inline-styles/src'),
|
||||||
|
'Symfony\\Polyfill\\Php81\\' => array($vendorDir . '/symfony/polyfill-php81'),
|
||||||
|
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
|
||||||
|
'Symfony\\Polyfill\\Php73\\' => array($vendorDir . '/symfony/polyfill-php73'),
|
||||||
|
'Symfony\\Polyfill\\Php72\\' => array($vendorDir . '/symfony/polyfill-php72'),
|
||||||
|
'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\\Iconv\\' => array($vendorDir . '/symfony/polyfill-iconv'),
|
||||||
|
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
|
||||||
|
'Symfony\\Contracts\\Translation\\' => array($vendorDir . '/symfony/translation-contracts'),
|
||||||
|
'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'),
|
||||||
|
'Symfony\\Contracts\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher-contracts'),
|
||||||
|
'Symfony\\Component\\VarDumper\\' => array($vendorDir . '/symfony/var-dumper'),
|
||||||
|
'Symfony\\Component\\Translation\\' => array($vendorDir . '/symfony/translation'),
|
||||||
|
'Symfony\\Component\\String\\' => array($vendorDir . '/symfony/string'),
|
||||||
|
'Symfony\\Component\\Routing\\' => array($vendorDir . '/symfony/routing'),
|
||||||
|
'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'),
|
||||||
|
'Symfony\\Component\\Mime\\' => array($vendorDir . '/symfony/mime'),
|
||||||
|
'Symfony\\Component\\HttpKernel\\' => array($vendorDir . '/symfony/http-kernel'),
|
||||||
|
'Symfony\\Component\\HttpFoundation\\' => array($vendorDir . '/symfony/http-foundation'),
|
||||||
|
'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'),
|
||||||
|
'Symfony\\Component\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher'),
|
||||||
|
'Symfony\\Component\\ErrorHandler\\' => array($vendorDir . '/symfony/error-handler'),
|
||||||
|
'Symfony\\Component\\CssSelector\\' => array($vendorDir . '/symfony/css-selector'),
|
||||||
|
'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
|
||||||
|
'Spatie\\Permission\\' => array($vendorDir . '/spatie/laravel-permission/src'),
|
||||||
|
'Ramsey\\Uuid\\' => array($vendorDir . '/ramsey/uuid/src'),
|
||||||
|
'Ramsey\\Collection\\' => array($vendorDir . '/ramsey/collection/src'),
|
||||||
|
'Psy\\' => array($vendorDir . '/psy/psysh/src'),
|
||||||
|
'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
|
||||||
|
'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
|
||||||
|
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
|
||||||
|
'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
|
||||||
|
'Psr\\EventDispatcher\\' => array($vendorDir . '/psr/event-dispatcher/src'),
|
||||||
|
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
|
||||||
|
'Psr\\Clock\\' => array($vendorDir . '/psr/clock/src'),
|
||||||
|
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
|
||||||
|
'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'),
|
||||||
|
'PhpOffice\\PhpSpreadsheet\\' => array($vendorDir . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet'),
|
||||||
|
'PHPJasper\\' => array($vendorDir . '/geekcom/phpjasper/src'),
|
||||||
|
'Opis\\Closure\\' => array($vendorDir . '/opis/closure/src'),
|
||||||
|
'MyCLabs\\Enum\\' => array($vendorDir . '/myclabs/php-enum/src'),
|
||||||
|
'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
|
||||||
|
'Matrix\\' => array($vendorDir . '/markbaker/matrix/classes/src'),
|
||||||
|
'Maatwebsite\\Excel\\' => array($vendorDir . '/maatwebsite/excel/src'),
|
||||||
|
'League\\MimeTypeDetection\\' => array($vendorDir . '/league/mime-type-detection/src'),
|
||||||
|
'League\\Flysystem\\' => array($vendorDir . '/league/flysystem/src'),
|
||||||
|
'League\\Config\\' => array($vendorDir . '/league/config/src'),
|
||||||
|
'League\\CommonMark\\' => array($vendorDir . '/league/commonmark/src'),
|
||||||
|
'Laravel\\Tinker\\' => array($vendorDir . '/laravel/tinker/src'),
|
||||||
|
'Laravel\\SerializableClosure\\' => array($vendorDir . '/laravel/serializable-closure/src'),
|
||||||
|
'Laravel\\Sanctum\\' => array($vendorDir . '/laravel/sanctum/src'),
|
||||||
|
'Illuminate\\Support\\' => array($vendorDir . '/laravel/framework/src/Illuminate/Macroable', $vendorDir . '/laravel/framework/src/Illuminate/Collections'),
|
||||||
|
'Illuminate\\' => array($vendorDir . '/laravel/framework/src/Illuminate'),
|
||||||
|
'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
|
||||||
|
'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
|
||||||
|
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
|
||||||
|
'GrahamCampbell\\ResultType\\' => array($vendorDir . '/graham-campbell/result-type/src'),
|
||||||
|
'Fruitcake\\Cors\\' => array($vendorDir . '/fruitcake/laravel-cors/src'),
|
||||||
|
'Egulias\\EmailValidator\\' => array($vendorDir . '/egulias/email-validator/src'),
|
||||||
|
'Dotenv\\' => array($vendorDir . '/vlucas/phpdotenv/src'),
|
||||||
|
'Doctrine\\Inflector\\' => array($vendorDir . '/doctrine/inflector/lib/Doctrine/Inflector'),
|
||||||
|
'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/lib/Doctrine/Common/Lexer'),
|
||||||
|
'Dflydev\\DotAccessData\\' => array($vendorDir . '/dflydev/dot-access-data/src'),
|
||||||
|
'Database\\Seeders\\' => array($baseDir . '/database/seeders'),
|
||||||
|
'Database\\Factories\\' => array($baseDir . '/database/factories'),
|
||||||
|
'Cron\\' => array($vendorDir . '/dragonmantank/cron-expression/src/Cron'),
|
||||||
|
'Composer\\Semver\\' => array($vendorDir . '/composer/semver/src'),
|
||||||
|
'Complex\\' => array($vendorDir . '/markbaker/complex/classes/src'),
|
||||||
|
'Carbon\\Doctrine\\' => array($vendorDir . '/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine'),
|
||||||
|
'Carbon\\' => array($vendorDir . '/nesbot/carbon/src/Carbon'),
|
||||||
|
'Brick\\Math\\' => array($vendorDir . '/brick/math/src'),
|
||||||
|
'Asm89\\Stack\\' => array($vendorDir . '/asm89/stack-cors/src'),
|
||||||
|
'App\\' => array($baseDir . '/app'),
|
||||||
|
);
|
||||||
Vendored
+50
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_real.php @generated by Composer
|
||||||
|
|
||||||
|
class ComposerAutoloaderInit2093020b165c40a9b39e5bea40080e73
|
||||||
|
{
|
||||||
|
private static $loader;
|
||||||
|
|
||||||
|
public static function loadClassLoader($class)
|
||||||
|
{
|
||||||
|
if ('Composer\Autoload\ClassLoader' === $class) {
|
||||||
|
require __DIR__ . '/ClassLoader.php';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \Composer\Autoload\ClassLoader
|
||||||
|
*/
|
||||||
|
public static function getLoader()
|
||||||
|
{
|
||||||
|
if (null !== self::$loader) {
|
||||||
|
return self::$loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
require __DIR__ . '/platform_check.php';
|
||||||
|
|
||||||
|
spl_autoload_register(array('ComposerAutoloaderInit2093020b165c40a9b39e5bea40080e73', 'loadClassLoader'), true, true);
|
||||||
|
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
|
||||||
|
spl_autoload_unregister(array('ComposerAutoloaderInit2093020b165c40a9b39e5bea40080e73', 'loadClassLoader'));
|
||||||
|
|
||||||
|
require __DIR__ . '/autoload_static.php';
|
||||||
|
call_user_func(\Composer\Autoload\ComposerStaticInit2093020b165c40a9b39e5bea40080e73::getInitializer($loader));
|
||||||
|
|
||||||
|
$loader->register(true);
|
||||||
|
|
||||||
|
$filesToLoad = \Composer\Autoload\ComposerStaticInit2093020b165c40a9b39e5bea40080e73::$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+5041
File diff suppressed because it is too large
Load Diff
Vendored
+6625
File diff suppressed because it is too large
Load Diff
Vendored
+1038
File diff suppressed because it is too large
Load Diff
Vendored
+25
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// platform_check.php @generated by Composer
|
||||||
|
|
||||||
|
$issues = array();
|
||||||
|
|
||||||
|
if (!(PHP_VERSION_ID >= 70400)) {
|
||||||
|
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.4.0". You are running ' . PHP_VERSION . '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($issues) {
|
||||||
|
if (!headers_sent()) {
|
||||||
|
header('HTTP/1.1 500 Internal Server Error');
|
||||||
|
}
|
||||||
|
if (!ini_get('display_errors')) {
|
||||||
|
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||||
|
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
|
||||||
|
} elseif (!headers_sent()) {
|
||||||
|
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Composer detected issues in your platform: ' . implode(' ', $issues)
|
||||||
|
);
|
||||||
|
}
|
||||||
Vendored
+224
@@ -0,0 +1,224 @@
|
|||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
### [3.4.2] 2024-07-12
|
||||||
|
|
||||||
|
* Fixed PHP 5.3 syntax error
|
||||||
|
|
||||||
|
### [3.4.1] 2024-07-12
|
||||||
|
|
||||||
|
* Fixed normalizeStability's return type to enforce valid stabilities
|
||||||
|
|
||||||
|
### [3.4.0] 2023-08-31
|
||||||
|
|
||||||
|
* Support larger major version numbers (#149)
|
||||||
|
|
||||||
|
### [3.3.2] 2022-04-01
|
||||||
|
|
||||||
|
* Fixed handling of non-string values (#134)
|
||||||
|
|
||||||
|
### [3.3.1] 2022-03-16
|
||||||
|
|
||||||
|
* Fixed possible cache key clash in the CompilingMatcher memoization (#132)
|
||||||
|
|
||||||
|
### [3.3.0] 2022-03-15
|
||||||
|
|
||||||
|
* Improved performance of CompilingMatcher by memoizing more (#131)
|
||||||
|
* Added CompilingMatcher::clear to clear all memoization caches
|
||||||
|
|
||||||
|
### [3.2.9] 2022-02-04
|
||||||
|
|
||||||
|
* Revert #129 (Fixed MultiConstraint with MatchAllConstraint) which caused regressions
|
||||||
|
|
||||||
|
### [3.2.8] 2022-02-04
|
||||||
|
|
||||||
|
* Updates to latest phpstan / CI by @Seldaek in https://github.com/composer/semver/pull/130
|
||||||
|
* Fixed MultiConstraint with MatchAllConstraint by @Toflar in https://github.com/composer/semver/pull/129
|
||||||
|
|
||||||
|
### [3.2.7] 2022-01-04
|
||||||
|
|
||||||
|
* Fixed: typo in type definition of Intervals class causing issues with Psalm scanning vendors
|
||||||
|
|
||||||
|
### [3.2.6] 2021-10-25
|
||||||
|
|
||||||
|
* Fixed: type improvements to parseStability
|
||||||
|
|
||||||
|
### [3.2.5] 2021-05-24
|
||||||
|
|
||||||
|
* Fixed: issue comparing disjunctive MultiConstraints to conjunctive ones (#127)
|
||||||
|
* Fixed: added complete type information using phpstan annotations
|
||||||
|
|
||||||
|
### [3.2.4] 2020-11-13
|
||||||
|
|
||||||
|
* Fixed: code clean-up
|
||||||
|
|
||||||
|
### [3.2.3] 2020-11-12
|
||||||
|
|
||||||
|
* Fixed: constraints in the form of `X || Y, >=Y.1` and other such complex constructs were in some cases being optimized into a more restrictive constraint
|
||||||
|
|
||||||
|
### [3.2.2] 2020-10-14
|
||||||
|
|
||||||
|
* Fixed: internal code cleanups
|
||||||
|
|
||||||
|
### [3.2.1] 2020-09-27
|
||||||
|
|
||||||
|
* Fixed: accidental validation of broken constraints combining ^/~ and wildcards, and -dev suffix allowing weird cases
|
||||||
|
* Fixed: normalization of beta0 and such which was dropping the 0
|
||||||
|
|
||||||
|
### [3.2.0] 2020-09-09
|
||||||
|
|
||||||
|
* Added: support for `x || @dev`, not very useful but seen in the wild and failed to validate with 1.5.2/1.6.0
|
||||||
|
* Added: support for `foobar-dev` being equal to `dev-foobar`, dev-foobar is the official way to write it but we need to support the other for BC and convenience
|
||||||
|
|
||||||
|
### [3.1.0] 2020-09-08
|
||||||
|
|
||||||
|
* Added: support for constraints like `^2.x-dev` and `~2.x-dev`, not very useful but seen in the wild and failed to validate with 3.0.1
|
||||||
|
* Fixed: invalid aliases will no longer throw, unless explicitly validated by Composer in the root package
|
||||||
|
|
||||||
|
### [3.0.1] 2020-09-08
|
||||||
|
|
||||||
|
* Fixed: handling of some invalid -dev versions which were seen as valid
|
||||||
|
|
||||||
|
### [3.0.0] 2020-05-26
|
||||||
|
|
||||||
|
* Break: Renamed `EmptyConstraint`, replace it with `MatchAllConstraint`
|
||||||
|
* Break: Unlikely to affect anyone but strictly speaking a breaking change, `*.*` and such variants will not match all `dev-*` versions anymore, only `*` does
|
||||||
|
* Break: ConstraintInterface is now considered internal/private and not meant to be implemented by third parties anymore
|
||||||
|
* Added `Intervals` class to check if a constraint is a subsets of another one, and allow compacting complex MultiConstraints into simpler ones
|
||||||
|
* Added `CompilingMatcher` class to speed up constraint matching against simple Constraint instances
|
||||||
|
* Added `MatchAllConstraint` and `MatchNoneConstraint` which match everything and nothing
|
||||||
|
* Added more advanced optimization of contiguous constraints inside MultiConstraint
|
||||||
|
* Added tentative support for PHP 8
|
||||||
|
* Fixed ConstraintInterface::matches to be commutative in all cases
|
||||||
|
|
||||||
|
### [2.0.0] 2020-04-21
|
||||||
|
|
||||||
|
* Break: `dev-master`, `dev-trunk` and `dev-default` now normalize to `dev-master`, `dev-trunk` and `dev-default` instead of `9999999-dev` in 1.x
|
||||||
|
* Break: Removed the deprecated `AbstractConstraint`
|
||||||
|
* Added `getUpperBound` and `getLowerBound` to ConstraintInterface. They return `Composer\Semver\Constraint\Bound` instances
|
||||||
|
* Added `MultiConstraint::create` to create the most-optimal form of ConstraintInterface from an array of constraint strings
|
||||||
|
|
||||||
|
### [1.7.2] 2020-12-03
|
||||||
|
|
||||||
|
* Fixed: Allow installing on php 8
|
||||||
|
|
||||||
|
### [1.7.1] 2020-09-27
|
||||||
|
|
||||||
|
* Fixed: accidental validation of broken constraints combining ^/~ and wildcards, and -dev suffix allowing weird cases
|
||||||
|
* Fixed: normalization of beta0 and such which was dropping the 0
|
||||||
|
|
||||||
|
### [1.7.0] 2020-09-09
|
||||||
|
|
||||||
|
* Added: support for `x || @dev`, not very useful but seen in the wild and failed to validate with 1.5.2/1.6.0
|
||||||
|
* Added: support for `foobar-dev` being equal to `dev-foobar`, dev-foobar is the official way to write it but we need to support the other for BC and convenience
|
||||||
|
|
||||||
|
### [1.6.0] 2020-09-08
|
||||||
|
|
||||||
|
* Added: support for constraints like `^2.x-dev` and `~2.x-dev`, not very useful but seen in the wild and failed to validate with 1.5.2
|
||||||
|
* Fixed: invalid aliases will no longer throw, unless explicitly validated by Composer in the root package
|
||||||
|
|
||||||
|
### [1.5.2] 2020-09-08
|
||||||
|
|
||||||
|
* Fixed: handling of some invalid -dev versions which were seen as valid
|
||||||
|
* Fixed: some doctypes
|
||||||
|
|
||||||
|
### [1.5.1] 2020-01-13
|
||||||
|
|
||||||
|
* Fixed: Parsing of aliased version was not validating the alias to be a valid version
|
||||||
|
|
||||||
|
### [1.5.0] 2019-03-19
|
||||||
|
|
||||||
|
* Added: some support for date versions (e.g. 201903) in `~` operator
|
||||||
|
* Fixed: support for stabilities in `~` operator was inconsistent
|
||||||
|
|
||||||
|
### [1.4.2] 2016-08-30
|
||||||
|
|
||||||
|
* Fixed: collapsing of complex constraints lead to buggy constraints
|
||||||
|
|
||||||
|
### [1.4.1] 2016-06-02
|
||||||
|
|
||||||
|
* Changed: branch-like requirements no longer strip build metadata - [composer/semver#38](https://github.com/composer/semver/pull/38).
|
||||||
|
|
||||||
|
### [1.4.0] 2016-03-30
|
||||||
|
|
||||||
|
* Added: getters on MultiConstraint - [composer/semver#35](https://github.com/composer/semver/pull/35).
|
||||||
|
|
||||||
|
### [1.3.0] 2016-02-25
|
||||||
|
|
||||||
|
* Fixed: stability parsing - [composer/composer#1234](https://github.com/composer/composer/issues/4889).
|
||||||
|
* Changed: collapse contiguous constraints when possible.
|
||||||
|
|
||||||
|
### [1.2.0] 2015-11-10
|
||||||
|
|
||||||
|
* Changed: allow multiple numerical identifiers in 'pre-release' version part.
|
||||||
|
* Changed: add more 'v' prefix support.
|
||||||
|
|
||||||
|
### [1.1.0] 2015-11-03
|
||||||
|
|
||||||
|
* Changed: dropped redundant `test` namespace.
|
||||||
|
* Changed: minor adjustment in datetime parsing normalization.
|
||||||
|
* Changed: `ConstraintInterface` relaxed, setPrettyString is not required anymore.
|
||||||
|
* Changed: `AbstractConstraint` marked deprecated, will be removed in 2.0.
|
||||||
|
* Changed: `Constraint` is now extensible.
|
||||||
|
|
||||||
|
### [1.0.0] 2015-09-21
|
||||||
|
|
||||||
|
* Break: `VersionConstraint` renamed to `Constraint`.
|
||||||
|
* Break: `SpecificConstraint` renamed to `AbstractConstraint`.
|
||||||
|
* Break: `LinkConstraintInterface` renamed to `ConstraintInterface`.
|
||||||
|
* Break: `VersionParser::parseNameVersionPairs` was removed.
|
||||||
|
* Changed: `VersionParser::parseConstraints` allows (but ignores) build metadata now.
|
||||||
|
* Changed: `VersionParser::parseConstraints` allows (but ignores) prefixing numeric versions with a 'v' now.
|
||||||
|
* Changed: Fixed namespace(s) of test files.
|
||||||
|
* Changed: `Comparator::compare` no longer throws `InvalidArgumentException`.
|
||||||
|
* Changed: `Constraint` now throws `InvalidArgumentException`.
|
||||||
|
|
||||||
|
### [0.1.0] 2015-07-23
|
||||||
|
|
||||||
|
* Added: `Composer\Semver\Comparator`, various methods to compare versions.
|
||||||
|
* Added: various documents such as README.md, LICENSE, etc.
|
||||||
|
* Added: configuration files for Git, Travis, php-cs-fixer, phpunit.
|
||||||
|
* Break: the following namespaces were renamed:
|
||||||
|
- Namespace: `Composer\Package\Version` -> `Composer\Semver`
|
||||||
|
- Namespace: `Composer\Package\LinkConstraint` -> `Composer\Semver\Constraint`
|
||||||
|
- Namespace: `Composer\Test\Package\Version` -> `Composer\Test\Semver`
|
||||||
|
- Namespace: `Composer\Test\Package\LinkConstraint` -> `Composer\Test\Semver\Constraint`
|
||||||
|
* Changed: code style using php-cs-fixer.
|
||||||
|
|
||||||
|
[3.4.2]: https://github.com/composer/semver/compare/3.4.1...3.4.2
|
||||||
|
[3.4.1]: https://github.com/composer/semver/compare/3.4.0...3.4.1
|
||||||
|
[3.4.0]: https://github.com/composer/semver/compare/3.3.2...3.4.0
|
||||||
|
[3.3.2]: https://github.com/composer/semver/compare/3.3.1...3.3.2
|
||||||
|
[3.3.1]: https://github.com/composer/semver/compare/3.3.0...3.3.1
|
||||||
|
[3.3.0]: https://github.com/composer/semver/compare/3.2.9...3.3.0
|
||||||
|
[3.2.9]: https://github.com/composer/semver/compare/3.2.8...3.2.9
|
||||||
|
[3.2.8]: https://github.com/composer/semver/compare/3.2.7...3.2.8
|
||||||
|
[3.2.7]: https://github.com/composer/semver/compare/3.2.6...3.2.7
|
||||||
|
[3.2.6]: https://github.com/composer/semver/compare/3.2.5...3.2.6
|
||||||
|
[3.2.5]: https://github.com/composer/semver/compare/3.2.4...3.2.5
|
||||||
|
[3.2.4]: https://github.com/composer/semver/compare/3.2.3...3.2.4
|
||||||
|
[3.2.3]: https://github.com/composer/semver/compare/3.2.2...3.2.3
|
||||||
|
[3.2.2]: https://github.com/composer/semver/compare/3.2.1...3.2.2
|
||||||
|
[3.2.1]: https://github.com/composer/semver/compare/3.2.0...3.2.1
|
||||||
|
[3.2.0]: https://github.com/composer/semver/compare/3.1.0...3.2.0
|
||||||
|
[3.1.0]: https://github.com/composer/semver/compare/3.0.1...3.1.0
|
||||||
|
[3.0.1]: https://github.com/composer/semver/compare/3.0.0...3.0.1
|
||||||
|
[3.0.0]: https://github.com/composer/semver/compare/2.0.0...3.0.0
|
||||||
|
[2.0.0]: https://github.com/composer/semver/compare/1.5.1...2.0.0
|
||||||
|
[1.7.2]: https://github.com/composer/semver/compare/1.7.1...1.7.2
|
||||||
|
[1.7.1]: https://github.com/composer/semver/compare/1.7.0...1.7.1
|
||||||
|
[1.7.0]: https://github.com/composer/semver/compare/1.6.0...1.7.0
|
||||||
|
[1.6.0]: https://github.com/composer/semver/compare/1.5.2...1.6.0
|
||||||
|
[1.5.2]: https://github.com/composer/semver/compare/1.5.1...1.5.2
|
||||||
|
[1.5.1]: https://github.com/composer/semver/compare/1.5.0...1.5.1
|
||||||
|
[1.5.0]: https://github.com/composer/semver/compare/1.4.2...1.5.0
|
||||||
|
[1.4.2]: https://github.com/composer/semver/compare/1.4.1...1.4.2
|
||||||
|
[1.4.1]: https://github.com/composer/semver/compare/1.4.0...1.4.1
|
||||||
|
[1.4.0]: https://github.com/composer/semver/compare/1.3.0...1.4.0
|
||||||
|
[1.3.0]: https://github.com/composer/semver/compare/1.2.0...1.3.0
|
||||||
|
[1.2.0]: https://github.com/composer/semver/compare/1.1.0...1.2.0
|
||||||
|
[1.1.0]: https://github.com/composer/semver/compare/1.0.0...1.1.0
|
||||||
|
[1.0.0]: https://github.com/composer/semver/compare/0.1.0...1.0.0
|
||||||
|
[0.1.0]: https://github.com/composer/semver/compare/5e0b9a4da...0.1.0
|
||||||
Vendored
+19
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (C) 2015 Composer
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
Vendored
+99
@@ -0,0 +1,99 @@
|
|||||||
|
composer/semver
|
||||||
|
===============
|
||||||
|
|
||||||
|
Semver (Semantic Versioning) library that offers utilities, version constraint parsing and validation.
|
||||||
|
|
||||||
|
Originally written as part of [composer/composer](https://github.com/composer/composer),
|
||||||
|
now extracted and made available as a stand-alone library.
|
||||||
|
|
||||||
|
[](https://github.com/composer/semver/actions/workflows/continuous-integration.yml)
|
||||||
|
[](https://github.com/composer/semver/actions/workflows/lint.yml)
|
||||||
|
[](https://github.com/composer/semver/actions/workflows/phpstan.yml)
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Install the latest version with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require composer/semver
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
|
* PHP 5.3.2 is required but using the latest version of PHP is highly recommended.
|
||||||
|
|
||||||
|
|
||||||
|
Version Comparison
|
||||||
|
------------------
|
||||||
|
|
||||||
|
For details on how versions are compared, refer to the [Versions](https://getcomposer.org/doc/articles/versions.md)
|
||||||
|
article in the documentation section of the [getcomposer.org](https://getcomposer.org) website.
|
||||||
|
|
||||||
|
|
||||||
|
Basic usage
|
||||||
|
-----------
|
||||||
|
|
||||||
|
### Comparator
|
||||||
|
|
||||||
|
The [`Composer\Semver\Comparator`](https://github.com/composer/semver/blob/main/src/Comparator.php) class provides the following methods for comparing versions:
|
||||||
|
|
||||||
|
* greaterThan($v1, $v2)
|
||||||
|
* greaterThanOrEqualTo($v1, $v2)
|
||||||
|
* lessThan($v1, $v2)
|
||||||
|
* lessThanOrEqualTo($v1, $v2)
|
||||||
|
* equalTo($v1, $v2)
|
||||||
|
* notEqualTo($v1, $v2)
|
||||||
|
|
||||||
|
Each function takes two version strings as arguments and returns a boolean. For example:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Composer\Semver\Comparator;
|
||||||
|
|
||||||
|
Comparator::greaterThan('1.25.0', '1.24.0'); // 1.25.0 > 1.24.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Semver
|
||||||
|
|
||||||
|
The [`Composer\Semver\Semver`](https://github.com/composer/semver/blob/main/src/Semver.php) class provides the following methods:
|
||||||
|
|
||||||
|
* satisfies($version, $constraints)
|
||||||
|
* satisfiedBy(array $versions, $constraint)
|
||||||
|
* sort($versions)
|
||||||
|
* rsort($versions)
|
||||||
|
|
||||||
|
### Intervals
|
||||||
|
|
||||||
|
The [`Composer\Semver\Intervals`](https://github.com/composer/semver/blob/main/src/Intervals.php) static class provides
|
||||||
|
a few utilities to work with complex constraints or read version intervals from a constraint:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Composer\Semver\Intervals;
|
||||||
|
|
||||||
|
// Checks whether $candidate is a subset of $constraint
|
||||||
|
Intervals::isSubsetOf(ConstraintInterface $candidate, ConstraintInterface $constraint);
|
||||||
|
|
||||||
|
// Checks whether $a and $b have any intersection, equivalent to $a->matches($b)
|
||||||
|
Intervals::haveIntersections(ConstraintInterface $a, ConstraintInterface $b);
|
||||||
|
|
||||||
|
// Optimizes a complex multi constraint by merging all intervals down to the smallest
|
||||||
|
// possible multi constraint. The drawbacks are this is not very fast, and the resulting
|
||||||
|
// multi constraint will have no human readable prettyConstraint configured on it
|
||||||
|
Intervals::compactConstraint(ConstraintInterface $constraint);
|
||||||
|
|
||||||
|
// Creates an array of numeric intervals and branch constraints representing a given constraint
|
||||||
|
Intervals::get(ConstraintInterface $constraint);
|
||||||
|
|
||||||
|
// Clears the memoization cache when you are done processing constraints
|
||||||
|
Intervals::clear()
|
||||||
|
```
|
||||||
|
|
||||||
|
See the class docblocks for more details.
|
||||||
|
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
composer/semver is licensed under the MIT License, see the LICENSE file for details.
|
||||||
Vendored
+59
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"name": "composer/semver",
|
||||||
|
"description": "Semver library that offers utilities, version constraint parsing and validation.",
|
||||||
|
"type": "library",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"semver",
|
||||||
|
"semantic",
|
||||||
|
"versioning",
|
||||||
|
"validation"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Nils Adermann",
|
||||||
|
"email": "naderman@naderman.de",
|
||||||
|
"homepage": "http://www.naderman.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jordi Boggiano",
|
||||||
|
"email": "j.boggiano@seld.be",
|
||||||
|
"homepage": "http://seld.be"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Rob Bast",
|
||||||
|
"email": "rob.bast@gmail.com",
|
||||||
|
"homepage": "http://robbast.nl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"irc": "ircs://irc.libera.chat:6697/composer",
|
||||||
|
"issues": "https://github.com/composer/semver/issues"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^5.3.2 || ^7.0 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/phpunit-bridge": "^4.2 || ^5",
|
||||||
|
"phpstan/phpstan": "^1.4"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Composer\\Semver\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Composer\\Semver\\": "tests"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "3.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT=1 vendor/bin/simple-phpunit",
|
||||||
|
"phpstan": "@php vendor/bin/phpstan analyse"
|
||||||
|
}
|
||||||
|
}
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/semver.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Semver;
|
||||||
|
|
||||||
|
use Composer\Semver\Constraint\Constraint;
|
||||||
|
|
||||||
|
class Comparator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Evaluates the expression: $version1 > $version2.
|
||||||
|
*
|
||||||
|
* @param string $version1
|
||||||
|
* @param string $version2
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function greaterThan($version1, $version2)
|
||||||
|
{
|
||||||
|
return self::compare($version1, '>', $version2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates the expression: $version1 >= $version2.
|
||||||
|
*
|
||||||
|
* @param string $version1
|
||||||
|
* @param string $version2
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function greaterThanOrEqualTo($version1, $version2)
|
||||||
|
{
|
||||||
|
return self::compare($version1, '>=', $version2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates the expression: $version1 < $version2.
|
||||||
|
*
|
||||||
|
* @param string $version1
|
||||||
|
* @param string $version2
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function lessThan($version1, $version2)
|
||||||
|
{
|
||||||
|
return self::compare($version1, '<', $version2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates the expression: $version1 <= $version2.
|
||||||
|
*
|
||||||
|
* @param string $version1
|
||||||
|
* @param string $version2
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function lessThanOrEqualTo($version1, $version2)
|
||||||
|
{
|
||||||
|
return self::compare($version1, '<=', $version2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates the expression: $version1 == $version2.
|
||||||
|
*
|
||||||
|
* @param string $version1
|
||||||
|
* @param string $version2
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function equalTo($version1, $version2)
|
||||||
|
{
|
||||||
|
return self::compare($version1, '==', $version2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates the expression: $version1 != $version2.
|
||||||
|
*
|
||||||
|
* @param string $version1
|
||||||
|
* @param string $version2
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function notEqualTo($version1, $version2)
|
||||||
|
{
|
||||||
|
return self::compare($version1, '!=', $version2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates the expression: $version1 $operator $version2.
|
||||||
|
*
|
||||||
|
* @param string $version1
|
||||||
|
* @param string $operator
|
||||||
|
* @param string $version2
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*
|
||||||
|
* @phpstan-param Constraint::STR_OP_* $operator
|
||||||
|
*/
|
||||||
|
public static function compare($version1, $operator, $version2)
|
||||||
|
{
|
||||||
|
$constraint = new Constraint($operator, $version2);
|
||||||
|
|
||||||
|
return $constraint->matchSpecific(new Constraint('==', $version1), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/semver.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Semver;
|
||||||
|
|
||||||
|
use Composer\Semver\Constraint\Constraint;
|
||||||
|
use Composer\Semver\Constraint\ConstraintInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to evaluate constraint by compiling and reusing the code to evaluate
|
||||||
|
*/
|
||||||
|
class CompilingMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
* @phpstan-var array<string, callable>
|
||||||
|
*/
|
||||||
|
private static $compiledCheckerCache = array();
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
* @phpstan-var array<string, bool>
|
||||||
|
*/
|
||||||
|
private static $resultCache = array();
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
private static $enabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @phpstan-var array<Constraint::OP_*, Constraint::STR_OP_*>
|
||||||
|
*/
|
||||||
|
private static $transOpInt = array(
|
||||||
|
Constraint::OP_EQ => Constraint::STR_OP_EQ,
|
||||||
|
Constraint::OP_LT => Constraint::STR_OP_LT,
|
||||||
|
Constraint::OP_LE => Constraint::STR_OP_LE,
|
||||||
|
Constraint::OP_GT => Constraint::STR_OP_GT,
|
||||||
|
Constraint::OP_GE => Constraint::STR_OP_GE,
|
||||||
|
Constraint::OP_NE => Constraint::STR_OP_NE,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the memoization cache once you are done
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function clear()
|
||||||
|
{
|
||||||
|
self::$resultCache = array();
|
||||||
|
self::$compiledCheckerCache = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates the expression: $constraint match $operator $version
|
||||||
|
*
|
||||||
|
* @param ConstraintInterface $constraint
|
||||||
|
* @param int $operator
|
||||||
|
* @phpstan-param Constraint::OP_* $operator
|
||||||
|
* @param string $version
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public static function match(ConstraintInterface $constraint, $operator, $version)
|
||||||
|
{
|
||||||
|
$resultCacheKey = $operator.$constraint.';'.$version;
|
||||||
|
|
||||||
|
if (isset(self::$resultCache[$resultCacheKey])) {
|
||||||
|
return self::$resultCache[$resultCacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::$enabled === null) {
|
||||||
|
self::$enabled = !\in_array('eval', explode(',', (string) ini_get('disable_functions')), true);
|
||||||
|
}
|
||||||
|
if (!self::$enabled) {
|
||||||
|
return self::$resultCache[$resultCacheKey] = $constraint->matches(new Constraint(self::$transOpInt[$operator], $version));
|
||||||
|
}
|
||||||
|
|
||||||
|
$cacheKey = $operator.$constraint;
|
||||||
|
if (!isset(self::$compiledCheckerCache[$cacheKey])) {
|
||||||
|
$code = $constraint->compile($operator);
|
||||||
|
self::$compiledCheckerCache[$cacheKey] = $function = eval('return function($v, $b){return '.$code.';};');
|
||||||
|
} else {
|
||||||
|
$function = self::$compiledCheckerCache[$cacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$resultCache[$resultCacheKey] = $function($version, strpos($version, 'dev-') === 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/semver.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Semver\Constraint;
|
||||||
|
|
||||||
|
class Bound
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $version;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private $isInclusive;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $version
|
||||||
|
* @param bool $isInclusive
|
||||||
|
*/
|
||||||
|
public function __construct($version, $isInclusive)
|
||||||
|
{
|
||||||
|
$this->version = $version;
|
||||||
|
$this->isInclusive = $isInclusive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getVersion()
|
||||||
|
{
|
||||||
|
return $this->version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isInclusive()
|
||||||
|
{
|
||||||
|
return $this->isInclusive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isZero()
|
||||||
|
{
|
||||||
|
return $this->getVersion() === '0.0.0.0-dev' && $this->isInclusive();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isPositiveInfinity()
|
||||||
|
{
|
||||||
|
return $this->getVersion() === PHP_INT_MAX.'.0.0.0' && !$this->isInclusive();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares a bound to another with a given operator.
|
||||||
|
*
|
||||||
|
* @param Bound $other
|
||||||
|
* @param string $operator
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function compareTo(Bound $other, $operator)
|
||||||
|
{
|
||||||
|
if (!\in_array($operator, array('<', '>'), true)) {
|
||||||
|
throw new \InvalidArgumentException('Does not support any other operator other than > or <.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If they are the same it doesn't matter
|
||||||
|
if ($this == $other) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$compareResult = version_compare($this->getVersion(), $other->getVersion());
|
||||||
|
|
||||||
|
// Not the same version means we don't need to check if the bounds are inclusive or not
|
||||||
|
if (0 !== $compareResult) {
|
||||||
|
return (('>' === $operator) ? 1 : -1) === $compareResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Question we're answering here is "am I higher than $other?"
|
||||||
|
return '>' === $operator ? $other->isInclusive() : !$other->isInclusive();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return sprintf(
|
||||||
|
'%s [%s]',
|
||||||
|
$this->getVersion(),
|
||||||
|
$this->isInclusive() ? 'inclusive' : 'exclusive'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function zero()
|
||||||
|
{
|
||||||
|
return new Bound('0.0.0.0-dev', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function positiveInfinity()
|
||||||
|
{
|
||||||
|
return new Bound(PHP_INT_MAX.'.0.0.0', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,435 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/semver.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Semver\Constraint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a constraint.
|
||||||
|
*/
|
||||||
|
class Constraint implements ConstraintInterface
|
||||||
|
{
|
||||||
|
/* operator integer values */
|
||||||
|
const OP_EQ = 0;
|
||||||
|
const OP_LT = 1;
|
||||||
|
const OP_LE = 2;
|
||||||
|
const OP_GT = 3;
|
||||||
|
const OP_GE = 4;
|
||||||
|
const OP_NE = 5;
|
||||||
|
|
||||||
|
/* operator string values */
|
||||||
|
const STR_OP_EQ = '==';
|
||||||
|
const STR_OP_EQ_ALT = '=';
|
||||||
|
const STR_OP_LT = '<';
|
||||||
|
const STR_OP_LE = '<=';
|
||||||
|
const STR_OP_GT = '>';
|
||||||
|
const STR_OP_GE = '>=';
|
||||||
|
const STR_OP_NE = '!=';
|
||||||
|
const STR_OP_NE_ALT = '<>';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator to integer translation table.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
* @phpstan-var array<self::STR_OP_*, self::OP_*>
|
||||||
|
*/
|
||||||
|
private static $transOpStr = array(
|
||||||
|
'=' => self::OP_EQ,
|
||||||
|
'==' => self::OP_EQ,
|
||||||
|
'<' => self::OP_LT,
|
||||||
|
'<=' => self::OP_LE,
|
||||||
|
'>' => self::OP_GT,
|
||||||
|
'>=' => self::OP_GE,
|
||||||
|
'<>' => self::OP_NE,
|
||||||
|
'!=' => self::OP_NE,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integer to operator translation table.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
* @phpstan-var array<self::OP_*, self::STR_OP_*>
|
||||||
|
*/
|
||||||
|
private static $transOpInt = array(
|
||||||
|
self::OP_EQ => '==',
|
||||||
|
self::OP_LT => '<',
|
||||||
|
self::OP_LE => '<=',
|
||||||
|
self::OP_GT => '>',
|
||||||
|
self::OP_GE => '>=',
|
||||||
|
self::OP_NE => '!=',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
* @phpstan-var self::OP_*
|
||||||
|
*/
|
||||||
|
protected $operator;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
protected $version;
|
||||||
|
|
||||||
|
/** @var string|null */
|
||||||
|
protected $prettyString;
|
||||||
|
|
||||||
|
/** @var Bound */
|
||||||
|
protected $lowerBound;
|
||||||
|
|
||||||
|
/** @var Bound */
|
||||||
|
protected $upperBound;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets operator and version to compare with.
|
||||||
|
*
|
||||||
|
* @param string $operator
|
||||||
|
* @param string $version
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException if invalid operator is given.
|
||||||
|
*
|
||||||
|
* @phpstan-param self::STR_OP_* $operator
|
||||||
|
*/
|
||||||
|
public function __construct($operator, $version)
|
||||||
|
{
|
||||||
|
if (!isset(self::$transOpStr[$operator])) {
|
||||||
|
throw new \InvalidArgumentException(sprintf(
|
||||||
|
'Invalid operator "%s" given, expected one of: %s',
|
||||||
|
$operator,
|
||||||
|
implode(', ', self::getSupportedOperators())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->operator = self::$transOpStr[$operator];
|
||||||
|
$this->version = $version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getVersion()
|
||||||
|
{
|
||||||
|
return $this->version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
* @phpstan-return self::STR_OP_*
|
||||||
|
*/
|
||||||
|
public function getOperator()
|
||||||
|
{
|
||||||
|
return self::$transOpInt[$this->operator];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ConstraintInterface $provider
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function matches(ConstraintInterface $provider)
|
||||||
|
{
|
||||||
|
if ($provider instanceof self) {
|
||||||
|
return $this->matchSpecific($provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
// turn matching around to find a match
|
||||||
|
return $provider->matches($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function setPrettyString($prettyString)
|
||||||
|
{
|
||||||
|
$this->prettyString = $prettyString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function getPrettyString()
|
||||||
|
{
|
||||||
|
if ($this->prettyString) {
|
||||||
|
return $this->prettyString;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->__toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all supported comparison operators.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*
|
||||||
|
* @phpstan-return list<self::STR_OP_*>
|
||||||
|
*/
|
||||||
|
public static function getSupportedOperators()
|
||||||
|
{
|
||||||
|
return array_keys(self::$transOpStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $operator
|
||||||
|
* @return int
|
||||||
|
*
|
||||||
|
* @phpstan-param self::STR_OP_* $operator
|
||||||
|
* @phpstan-return self::OP_*
|
||||||
|
*/
|
||||||
|
public static function getOperatorConstant($operator)
|
||||||
|
{
|
||||||
|
return self::$transOpStr[$operator];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $a
|
||||||
|
* @param string $b
|
||||||
|
* @param string $operator
|
||||||
|
* @param bool $compareBranches
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException if invalid operator is given.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*
|
||||||
|
* @phpstan-param self::STR_OP_* $operator
|
||||||
|
*/
|
||||||
|
public function versionCompare($a, $b, $operator, $compareBranches = false)
|
||||||
|
{
|
||||||
|
if (!isset(self::$transOpStr[$operator])) {
|
||||||
|
throw new \InvalidArgumentException(sprintf(
|
||||||
|
'Invalid operator "%s" given, expected one of: %s',
|
||||||
|
$operator,
|
||||||
|
implode(', ', self::getSupportedOperators())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$aIsBranch = strpos($a, 'dev-') === 0;
|
||||||
|
$bIsBranch = strpos($b, 'dev-') === 0;
|
||||||
|
|
||||||
|
if ($operator === '!=' && ($aIsBranch || $bIsBranch)) {
|
||||||
|
return $a !== $b;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($aIsBranch && $bIsBranch) {
|
||||||
|
return $operator === '==' && $a === $b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// when branches are not comparable, we make sure dev branches never match anything
|
||||||
|
if (!$compareBranches && ($aIsBranch || $bIsBranch)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return \version_compare($a, $b, $operator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function compile($otherOperator)
|
||||||
|
{
|
||||||
|
if (strpos($this->version, 'dev-') === 0) {
|
||||||
|
if (self::OP_EQ === $this->operator) {
|
||||||
|
if (self::OP_EQ === $otherOperator) {
|
||||||
|
return sprintf('$b && $v === %s', \var_export($this->version, true));
|
||||||
|
}
|
||||||
|
if (self::OP_NE === $otherOperator) {
|
||||||
|
return sprintf('!$b || $v !== %s', \var_export($this->version, true));
|
||||||
|
}
|
||||||
|
return 'false';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::OP_NE === $this->operator) {
|
||||||
|
if (self::OP_EQ === $otherOperator) {
|
||||||
|
return sprintf('!$b || $v !== %s', \var_export($this->version, true));
|
||||||
|
}
|
||||||
|
if (self::OP_NE === $otherOperator) {
|
||||||
|
return 'true';
|
||||||
|
}
|
||||||
|
return '!$b';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'false';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::OP_EQ === $this->operator) {
|
||||||
|
if (self::OP_EQ === $otherOperator) {
|
||||||
|
return sprintf('\version_compare($v, %s, \'==\')', \var_export($this->version, true));
|
||||||
|
}
|
||||||
|
if (self::OP_NE === $otherOperator) {
|
||||||
|
return sprintf('$b || \version_compare($v, %s, \'!=\')', \var_export($this->version, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf('!$b && \version_compare(%s, $v, \'%s\')', \var_export($this->version, true), self::$transOpInt[$otherOperator]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::OP_NE === $this->operator) {
|
||||||
|
if (self::OP_EQ === $otherOperator) {
|
||||||
|
return sprintf('$b || (!$b && \version_compare($v, %s, \'!=\'))', \var_export($this->version, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::OP_NE === $otherOperator) {
|
||||||
|
return 'true';
|
||||||
|
}
|
||||||
|
return '!$b';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::OP_LT === $this->operator || self::OP_LE === $this->operator) {
|
||||||
|
if (self::OP_LT === $otherOperator || self::OP_LE === $otherOperator) {
|
||||||
|
return '!$b';
|
||||||
|
}
|
||||||
|
} else { // $this->operator must be self::OP_GT || self::OP_GE here
|
||||||
|
if (self::OP_GT === $otherOperator || self::OP_GE === $otherOperator) {
|
||||||
|
return '!$b';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::OP_NE === $otherOperator) {
|
||||||
|
return 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
$codeComparison = sprintf('\version_compare($v, %s, \'%s\')', \var_export($this->version, true), self::$transOpInt[$this->operator]);
|
||||||
|
if ($this->operator === self::OP_LE) {
|
||||||
|
if ($otherOperator === self::OP_GT) {
|
||||||
|
return sprintf('!$b && \version_compare($v, %s, \'!=\') && ', \var_export($this->version, true)) . $codeComparison;
|
||||||
|
}
|
||||||
|
} elseif ($this->operator === self::OP_GE) {
|
||||||
|
if ($otherOperator === self::OP_LT) {
|
||||||
|
return sprintf('!$b && \version_compare($v, %s, \'!=\') && ', \var_export($this->version, true)) . $codeComparison;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf('!$b && %s', $codeComparison);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Constraint $provider
|
||||||
|
* @param bool $compareBranches
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function matchSpecific(Constraint $provider, $compareBranches = false)
|
||||||
|
{
|
||||||
|
$noEqualOp = str_replace('=', '', self::$transOpInt[$this->operator]);
|
||||||
|
$providerNoEqualOp = str_replace('=', '', self::$transOpInt[$provider->operator]);
|
||||||
|
|
||||||
|
$isEqualOp = self::OP_EQ === $this->operator;
|
||||||
|
$isNonEqualOp = self::OP_NE === $this->operator;
|
||||||
|
$isProviderEqualOp = self::OP_EQ === $provider->operator;
|
||||||
|
$isProviderNonEqualOp = self::OP_NE === $provider->operator;
|
||||||
|
|
||||||
|
// '!=' operator is match when other operator is not '==' operator or version is not match
|
||||||
|
// these kinds of comparisons always have a solution
|
||||||
|
if ($isNonEqualOp || $isProviderNonEqualOp) {
|
||||||
|
if ($isNonEqualOp && !$isProviderNonEqualOp && !$isProviderEqualOp && strpos($provider->version, 'dev-') === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isProviderNonEqualOp && !$isNonEqualOp && !$isEqualOp && strpos($this->version, 'dev-') === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$isEqualOp && !$isProviderEqualOp) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return $this->versionCompare($provider->version, $this->version, '!=', $compareBranches);
|
||||||
|
}
|
||||||
|
|
||||||
|
// an example for the condition is <= 2.0 & < 1.0
|
||||||
|
// these kinds of comparisons always have a solution
|
||||||
|
if ($this->operator !== self::OP_EQ && $noEqualOp === $providerNoEqualOp) {
|
||||||
|
return !(strpos($this->version, 'dev-') === 0 || strpos($provider->version, 'dev-') === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$version1 = $isEqualOp ? $this->version : $provider->version;
|
||||||
|
$version2 = $isEqualOp ? $provider->version : $this->version;
|
||||||
|
$operator = $isEqualOp ? $provider->operator : $this->operator;
|
||||||
|
|
||||||
|
if ($this->versionCompare($version1, $version2, self::$transOpInt[$operator], $compareBranches)) {
|
||||||
|
// special case, e.g. require >= 1.0 and provide < 1.0
|
||||||
|
// 1.0 >= 1.0 but 1.0 is outside of the provided interval
|
||||||
|
|
||||||
|
return !(self::$transOpInt[$provider->operator] === $providerNoEqualOp
|
||||||
|
&& self::$transOpInt[$this->operator] !== $noEqualOp
|
||||||
|
&& \version_compare($provider->version, $this->version, '=='));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return self::$transOpInt[$this->operator] . ' ' . $this->version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function getLowerBound()
|
||||||
|
{
|
||||||
|
$this->extractBounds();
|
||||||
|
|
||||||
|
return $this->lowerBound;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function getUpperBound()
|
||||||
|
{
|
||||||
|
$this->extractBounds();
|
||||||
|
|
||||||
|
return $this->upperBound;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function extractBounds()
|
||||||
|
{
|
||||||
|
if (null !== $this->lowerBound) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Branches
|
||||||
|
if (strpos($this->version, 'dev-') === 0) {
|
||||||
|
$this->lowerBound = Bound::zero();
|
||||||
|
$this->upperBound = Bound::positiveInfinity();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($this->operator) {
|
||||||
|
case self::OP_EQ:
|
||||||
|
$this->lowerBound = new Bound($this->version, true);
|
||||||
|
$this->upperBound = new Bound($this->version, true);
|
||||||
|
break;
|
||||||
|
case self::OP_LT:
|
||||||
|
$this->lowerBound = Bound::zero();
|
||||||
|
$this->upperBound = new Bound($this->version, false);
|
||||||
|
break;
|
||||||
|
case self::OP_LE:
|
||||||
|
$this->lowerBound = Bound::zero();
|
||||||
|
$this->upperBound = new Bound($this->version, true);
|
||||||
|
break;
|
||||||
|
case self::OP_GT:
|
||||||
|
$this->lowerBound = new Bound($this->version, false);
|
||||||
|
$this->upperBound = Bound::positiveInfinity();
|
||||||
|
break;
|
||||||
|
case self::OP_GE:
|
||||||
|
$this->lowerBound = new Bound($this->version, true);
|
||||||
|
$this->upperBound = Bound::positiveInfinity();
|
||||||
|
break;
|
||||||
|
case self::OP_NE:
|
||||||
|
$this->lowerBound = Bound::zero();
|
||||||
|
$this->upperBound = Bound::positiveInfinity();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/semver.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Semver\Constraint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DO NOT IMPLEMENT this interface. It is only meant for usage as a type hint
|
||||||
|
* in libraries relying on composer/semver but creating your own constraint class
|
||||||
|
* that implements this interface is not a supported use case and will cause the
|
||||||
|
* composer/semver components to return unexpected results.
|
||||||
|
*/
|
||||||
|
interface ConstraintInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Checks whether the given constraint intersects in any way with this constraint
|
||||||
|
*
|
||||||
|
* @param ConstraintInterface $provider
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function matches(ConstraintInterface $provider);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a compiled version of the constraint for the given operator
|
||||||
|
* The compiled version must be a PHP expression.
|
||||||
|
* Executor of compile version must provide 2 variables:
|
||||||
|
* - $v = the string version to compare with
|
||||||
|
* - $b = whether or not the version is a non-comparable branch (starts with "dev-")
|
||||||
|
*
|
||||||
|
* @see Constraint::OP_* for the list of available operators.
|
||||||
|
* @example return '!$b && version_compare($v, '1.0', '>')';
|
||||||
|
*
|
||||||
|
* @param int $otherOperator one Constraint::OP_*
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
* @phpstan-param Constraint::OP_* $otherOperator
|
||||||
|
*/
|
||||||
|
public function compile($otherOperator);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Bound
|
||||||
|
*/
|
||||||
|
public function getUpperBound();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Bound
|
||||||
|
*/
|
||||||
|
public function getLowerBound();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getPrettyString();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|null $prettyString
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setPrettyString($prettyString);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function __toString();
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/semver.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Semver\Constraint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the absence of a constraint.
|
||||||
|
*
|
||||||
|
* This constraint matches everything.
|
||||||
|
*/
|
||||||
|
class MatchAllConstraint implements ConstraintInterface
|
||||||
|
{
|
||||||
|
/** @var string|null */
|
||||||
|
protected $prettyString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ConstraintInterface $provider
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function matches(ConstraintInterface $provider)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function compile($otherOperator)
|
||||||
|
{
|
||||||
|
return 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function setPrettyString($prettyString)
|
||||||
|
{
|
||||||
|
$this->prettyString = $prettyString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function getPrettyString()
|
||||||
|
{
|
||||||
|
if ($this->prettyString) {
|
||||||
|
return $this->prettyString;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return '*';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function getUpperBound()
|
||||||
|
{
|
||||||
|
return Bound::positiveInfinity();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function getLowerBound()
|
||||||
|
{
|
||||||
|
return Bound::zero();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/semver.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Semver\Constraint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blackhole of constraints, nothing escapes it
|
||||||
|
*/
|
||||||
|
class MatchNoneConstraint implements ConstraintInterface
|
||||||
|
{
|
||||||
|
/** @var string|null */
|
||||||
|
protected $prettyString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ConstraintInterface $provider
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function matches(ConstraintInterface $provider)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function compile($otherOperator)
|
||||||
|
{
|
||||||
|
return 'false';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function setPrettyString($prettyString)
|
||||||
|
{
|
||||||
|
$this->prettyString = $prettyString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function getPrettyString()
|
||||||
|
{
|
||||||
|
if ($this->prettyString) {
|
||||||
|
return $this->prettyString;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return '[]';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function getUpperBound()
|
||||||
|
{
|
||||||
|
return new Bound('0.0.0.0-dev', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function getLowerBound()
|
||||||
|
{
|
||||||
|
return new Bound('0.0.0.0-dev', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/semver.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Semver\Constraint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a conjunctive or disjunctive set of constraints.
|
||||||
|
*/
|
||||||
|
class MultiConstraint implements ConstraintInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ConstraintInterface[]
|
||||||
|
* @phpstan-var non-empty-array<ConstraintInterface>
|
||||||
|
*/
|
||||||
|
protected $constraints;
|
||||||
|
|
||||||
|
/** @var string|null */
|
||||||
|
protected $prettyString;
|
||||||
|
|
||||||
|
/** @var string|null */
|
||||||
|
protected $string;
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
protected $conjunctive;
|
||||||
|
|
||||||
|
/** @var Bound|null */
|
||||||
|
protected $lowerBound;
|
||||||
|
|
||||||
|
/** @var Bound|null */
|
||||||
|
protected $upperBound;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ConstraintInterface[] $constraints A set of constraints
|
||||||
|
* @param bool $conjunctive Whether the constraints should be treated as conjunctive or disjunctive
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException If less than 2 constraints are passed
|
||||||
|
*/
|
||||||
|
public function __construct(array $constraints, $conjunctive = true)
|
||||||
|
{
|
||||||
|
if (\count($constraints) < 2) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
'Must provide at least two constraints for a MultiConstraint. Use '.
|
||||||
|
'the regular Constraint class for one constraint only or MatchAllConstraint for none. You may use '.
|
||||||
|
'MultiConstraint::create() which optimizes and handles those cases automatically.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->constraints = $constraints;
|
||||||
|
$this->conjunctive = $conjunctive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ConstraintInterface[]
|
||||||
|
*/
|
||||||
|
public function getConstraints()
|
||||||
|
{
|
||||||
|
return $this->constraints;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isConjunctive()
|
||||||
|
{
|
||||||
|
return $this->conjunctive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isDisjunctive()
|
||||||
|
{
|
||||||
|
return !$this->conjunctive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function compile($otherOperator)
|
||||||
|
{
|
||||||
|
$parts = array();
|
||||||
|
foreach ($this->constraints as $constraint) {
|
||||||
|
$code = $constraint->compile($otherOperator);
|
||||||
|
if ($code === 'true') {
|
||||||
|
if (!$this->conjunctive) {
|
||||||
|
return 'true';
|
||||||
|
}
|
||||||
|
} elseif ($code === 'false') {
|
||||||
|
if ($this->conjunctive) {
|
||||||
|
return 'false';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$parts[] = '('.$code.')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$parts) {
|
||||||
|
return $this->conjunctive ? 'true' : 'false';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->conjunctive ? implode('&&', $parts) : implode('||', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ConstraintInterface $provider
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function matches(ConstraintInterface $provider)
|
||||||
|
{
|
||||||
|
if (false === $this->conjunctive) {
|
||||||
|
foreach ($this->constraints as $constraint) {
|
||||||
|
if ($provider->matches($constraint)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// when matching a conjunctive and a disjunctive multi constraint we have to iterate over the disjunctive one
|
||||||
|
// otherwise we'd return true if different parts of the disjunctive constraint match the conjunctive one
|
||||||
|
// which would lead to incorrect results, e.g. [>1 and <2] would match [<1 or >2] although they do not intersect
|
||||||
|
if ($provider instanceof MultiConstraint && $provider->isDisjunctive()) {
|
||||||
|
return $provider->matches($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->constraints as $constraint) {
|
||||||
|
if (!$provider->matches($constraint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function setPrettyString($prettyString)
|
||||||
|
{
|
||||||
|
$this->prettyString = $prettyString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function getPrettyString()
|
||||||
|
{
|
||||||
|
if ($this->prettyString) {
|
||||||
|
return $this->prettyString;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
if ($this->string !== null) {
|
||||||
|
return $this->string;
|
||||||
|
}
|
||||||
|
|
||||||
|
$constraints = array();
|
||||||
|
foreach ($this->constraints as $constraint) {
|
||||||
|
$constraints[] = (string) $constraint;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->string = '[' . implode($this->conjunctive ? ' ' : ' || ', $constraints) . ']';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function getLowerBound()
|
||||||
|
{
|
||||||
|
$this->extractBounds();
|
||||||
|
|
||||||
|
if (null === $this->lowerBound) {
|
||||||
|
throw new \LogicException('extractBounds should have populated the lowerBound property');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->lowerBound;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function getUpperBound()
|
||||||
|
{
|
||||||
|
$this->extractBounds();
|
||||||
|
|
||||||
|
if (null === $this->upperBound) {
|
||||||
|
throw new \LogicException('extractBounds should have populated the upperBound property');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->upperBound;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to optimize the constraints as much as possible, meaning
|
||||||
|
* reducing/collapsing congruent constraints etc.
|
||||||
|
* Does not necessarily return a MultiConstraint instance if
|
||||||
|
* things can be reduced to a simple constraint
|
||||||
|
*
|
||||||
|
* @param ConstraintInterface[] $constraints A set of constraints
|
||||||
|
* @param bool $conjunctive Whether the constraints should be treated as conjunctive or disjunctive
|
||||||
|
*
|
||||||
|
* @return ConstraintInterface
|
||||||
|
*/
|
||||||
|
public static function create(array $constraints, $conjunctive = true)
|
||||||
|
{
|
||||||
|
if (0 === \count($constraints)) {
|
||||||
|
return new MatchAllConstraint();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (1 === \count($constraints)) {
|
||||||
|
return $constraints[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$optimized = self::optimizeConstraints($constraints, $conjunctive);
|
||||||
|
if ($optimized !== null) {
|
||||||
|
list($constraints, $conjunctive) = $optimized;
|
||||||
|
if (\count($constraints) === 1) {
|
||||||
|
return $constraints[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self($constraints, $conjunctive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ConstraintInterface[] $constraints
|
||||||
|
* @param bool $conjunctive
|
||||||
|
* @return ?array
|
||||||
|
*
|
||||||
|
* @phpstan-return array{0: list<ConstraintInterface>, 1: bool}|null
|
||||||
|
*/
|
||||||
|
private static function optimizeConstraints(array $constraints, $conjunctive)
|
||||||
|
{
|
||||||
|
// parse the two OR groups and if they are contiguous we collapse
|
||||||
|
// them into one constraint
|
||||||
|
// [>= 1 < 2] || [>= 2 < 3] || [>= 3 < 4] => [>= 1 < 4]
|
||||||
|
if (!$conjunctive) {
|
||||||
|
$left = $constraints[0];
|
||||||
|
$mergedConstraints = array();
|
||||||
|
$optimized = false;
|
||||||
|
for ($i = 1, $l = \count($constraints); $i < $l; $i++) {
|
||||||
|
$right = $constraints[$i];
|
||||||
|
if (
|
||||||
|
$left instanceof self
|
||||||
|
&& $left->conjunctive
|
||||||
|
&& $right instanceof self
|
||||||
|
&& $right->conjunctive
|
||||||
|
&& \count($left->constraints) === 2
|
||||||
|
&& \count($right->constraints) === 2
|
||||||
|
&& ($left0 = (string) $left->constraints[0])
|
||||||
|
&& $left0[0] === '>' && $left0[1] === '='
|
||||||
|
&& ($left1 = (string) $left->constraints[1])
|
||||||
|
&& $left1[0] === '<'
|
||||||
|
&& ($right0 = (string) $right->constraints[0])
|
||||||
|
&& $right0[0] === '>' && $right0[1] === '='
|
||||||
|
&& ($right1 = (string) $right->constraints[1])
|
||||||
|
&& $right1[0] === '<'
|
||||||
|
&& substr($left1, 2) === substr($right0, 3)
|
||||||
|
) {
|
||||||
|
$optimized = true;
|
||||||
|
$left = new MultiConstraint(
|
||||||
|
array(
|
||||||
|
$left->constraints[0],
|
||||||
|
$right->constraints[1],
|
||||||
|
),
|
||||||
|
true);
|
||||||
|
} else {
|
||||||
|
$mergedConstraints[] = $left;
|
||||||
|
$left = $right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($optimized) {
|
||||||
|
$mergedConstraints[] = $left;
|
||||||
|
return array($mergedConstraints, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Here's the place to put more optimizations
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function extractBounds()
|
||||||
|
{
|
||||||
|
if (null !== $this->lowerBound) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->constraints as $constraint) {
|
||||||
|
if (null === $this->lowerBound || null === $this->upperBound) {
|
||||||
|
$this->lowerBound = $constraint->getLowerBound();
|
||||||
|
$this->upperBound = $constraint->getUpperBound();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($constraint->getLowerBound()->compareTo($this->lowerBound, $this->isConjunctive() ? '>' : '<')) {
|
||||||
|
$this->lowerBound = $constraint->getLowerBound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($constraint->getUpperBound()->compareTo($this->upperBound, $this->isConjunctive() ? '<' : '>')) {
|
||||||
|
$this->upperBound = $constraint->getUpperBound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/semver.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Semver;
|
||||||
|
|
||||||
|
use Composer\Semver\Constraint\Constraint;
|
||||||
|
|
||||||
|
class Interval
|
||||||
|
{
|
||||||
|
/** @var Constraint */
|
||||||
|
private $start;
|
||||||
|
/** @var Constraint */
|
||||||
|
private $end;
|
||||||
|
|
||||||
|
public function __construct(Constraint $start, Constraint $end)
|
||||||
|
{
|
||||||
|
$this->start = $start;
|
||||||
|
$this->end = $end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Constraint
|
||||||
|
*/
|
||||||
|
public function getStart()
|
||||||
|
{
|
||||||
|
return $this->start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Constraint
|
||||||
|
*/
|
||||||
|
public function getEnd()
|
||||||
|
{
|
||||||
|
return $this->end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Constraint
|
||||||
|
*/
|
||||||
|
public static function fromZero()
|
||||||
|
{
|
||||||
|
static $zero;
|
||||||
|
|
||||||
|
if (null === $zero) {
|
||||||
|
$zero = new Constraint('>=', '0.0.0.0-dev');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Constraint
|
||||||
|
*/
|
||||||
|
public static function untilPositiveInfinity()
|
||||||
|
{
|
||||||
|
static $positiveInfinity;
|
||||||
|
|
||||||
|
if (null === $positiveInfinity) {
|
||||||
|
$positiveInfinity = new Constraint('<', PHP_INT_MAX.'.0.0.0');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $positiveInfinity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function any()
|
||||||
|
{
|
||||||
|
return new self(self::fromZero(), self::untilPositiveInfinity());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{'names': string[], 'exclude': bool}
|
||||||
|
*/
|
||||||
|
public static function anyDev()
|
||||||
|
{
|
||||||
|
// any == exclude nothing
|
||||||
|
return array('names' => array(), 'exclude' => true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{'names': string[], 'exclude': bool}
|
||||||
|
*/
|
||||||
|
public static function noDev()
|
||||||
|
{
|
||||||
|
// nothing == no names included
|
||||||
|
return array('names' => array(), 'exclude' => false);
|
||||||
|
}
|
||||||
|
}
|
||||||
+478
@@ -0,0 +1,478 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/semver.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Semver;
|
||||||
|
|
||||||
|
use Composer\Semver\Constraint\Constraint;
|
||||||
|
use Composer\Semver\Constraint\ConstraintInterface;
|
||||||
|
use Composer\Semver\Constraint\MatchAllConstraint;
|
||||||
|
use Composer\Semver\Constraint\MatchNoneConstraint;
|
||||||
|
use Composer\Semver\Constraint\MultiConstraint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class generating intervals from constraints
|
||||||
|
*
|
||||||
|
* This contains utilities for:
|
||||||
|
*
|
||||||
|
* - compacting an existing constraint which can be used to combine several into one
|
||||||
|
* by creating a MultiConstraint out of the many constraints you have.
|
||||||
|
*
|
||||||
|
* - checking whether one subset is a subset of another.
|
||||||
|
*
|
||||||
|
* Note: You should call clear to free memoization memory usage when you are done using this class
|
||||||
|
*/
|
||||||
|
class Intervals
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @phpstan-var array<string, array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}}>
|
||||||
|
*/
|
||||||
|
private static $intervalsCache = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @phpstan-var array<string, int>
|
||||||
|
*/
|
||||||
|
private static $opSortOrder = array(
|
||||||
|
'>=' => -3,
|
||||||
|
'<' => -2,
|
||||||
|
'>' => 2,
|
||||||
|
'<=' => 3,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the memoization cache once you are done
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function clear()
|
||||||
|
{
|
||||||
|
self::$intervalsCache = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether $candidate is a subset of $constraint
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isSubsetOf(ConstraintInterface $candidate, ConstraintInterface $constraint)
|
||||||
|
{
|
||||||
|
if ($constraint instanceof MatchAllConstraint) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($candidate instanceof MatchNoneConstraint || $constraint instanceof MatchNoneConstraint) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$intersectionIntervals = self::get(new MultiConstraint(array($candidate, $constraint), true));
|
||||||
|
$candidateIntervals = self::get($candidate);
|
||||||
|
if (\count($intersectionIntervals['numeric']) !== \count($candidateIntervals['numeric'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($intersectionIntervals['numeric'] as $index => $interval) {
|
||||||
|
if (!isset($candidateIntervals['numeric'][$index])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((string) $candidateIntervals['numeric'][$index]->getStart() !== (string) $interval->getStart()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((string) $candidateIntervals['numeric'][$index]->getEnd() !== (string) $interval->getEnd()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($intersectionIntervals['branches']['exclude'] !== $candidateIntervals['branches']['exclude']) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (\count($intersectionIntervals['branches']['names']) !== \count($candidateIntervals['branches']['names'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
foreach ($intersectionIntervals['branches']['names'] as $index => $name) {
|
||||||
|
if ($name !== $candidateIntervals['branches']['names'][$index]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether $a and $b have any intersection, equivalent to $a->matches($b)
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function haveIntersections(ConstraintInterface $a, ConstraintInterface $b)
|
||||||
|
{
|
||||||
|
if ($a instanceof MatchAllConstraint || $b instanceof MatchAllConstraint) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($a instanceof MatchNoneConstraint || $b instanceof MatchNoneConstraint) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$intersectionIntervals = self::generateIntervals(new MultiConstraint(array($a, $b), true), true);
|
||||||
|
|
||||||
|
return \count($intersectionIntervals['numeric']) > 0 || $intersectionIntervals['branches']['exclude'] || \count($intersectionIntervals['branches']['names']) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to optimize a MultiConstraint
|
||||||
|
*
|
||||||
|
* When merging MultiConstraints together they can get very large, this will
|
||||||
|
* compact it by looking at the real intervals covered by all the constraints
|
||||||
|
* and then creates a new constraint containing only the smallest amount of rules
|
||||||
|
* to match the same intervals.
|
||||||
|
*
|
||||||
|
* @return ConstraintInterface
|
||||||
|
*/
|
||||||
|
public static function compactConstraint(ConstraintInterface $constraint)
|
||||||
|
{
|
||||||
|
if (!$constraint instanceof MultiConstraint) {
|
||||||
|
return $constraint;
|
||||||
|
}
|
||||||
|
|
||||||
|
$intervals = self::generateIntervals($constraint);
|
||||||
|
$constraints = array();
|
||||||
|
$hasNumericMatchAll = false;
|
||||||
|
|
||||||
|
if (\count($intervals['numeric']) === 1 && (string) $intervals['numeric'][0]->getStart() === (string) Interval::fromZero() && (string) $intervals['numeric'][0]->getEnd() === (string) Interval::untilPositiveInfinity()) {
|
||||||
|
$constraints[] = $intervals['numeric'][0]->getStart();
|
||||||
|
$hasNumericMatchAll = true;
|
||||||
|
} else {
|
||||||
|
$unEqualConstraints = array();
|
||||||
|
for ($i = 0, $count = \count($intervals['numeric']); $i < $count; $i++) {
|
||||||
|
$interval = $intervals['numeric'][$i];
|
||||||
|
|
||||||
|
// if current interval ends with < N and next interval begins with > N we can swap this out for != N
|
||||||
|
// but this needs to happen as a conjunctive expression together with the start of the current interval
|
||||||
|
// and end of next interval, so [>=M, <N] || [>N, <P] => [>=M, !=N, <P] but M/P can be skipped if
|
||||||
|
// they are zero/+inf
|
||||||
|
if ($interval->getEnd()->getOperator() === '<' && $i+1 < $count) {
|
||||||
|
$nextInterval = $intervals['numeric'][$i+1];
|
||||||
|
if ($interval->getEnd()->getVersion() === $nextInterval->getStart()->getVersion() && $nextInterval->getStart()->getOperator() === '>') {
|
||||||
|
// only add a start if we didn't already do so, can be skipped if we're looking at second
|
||||||
|
// interval in [>=M, <N] || [>N, <P] || [>P, <Q] where unEqualConstraints currently contains
|
||||||
|
// [>=M, !=N] already and we only want to add !=P right now
|
||||||
|
if (\count($unEqualConstraints) === 0 && (string) $interval->getStart() !== (string) Interval::fromZero()) {
|
||||||
|
$unEqualConstraints[] = $interval->getStart();
|
||||||
|
}
|
||||||
|
$unEqualConstraints[] = new Constraint('!=', $interval->getEnd()->getVersion());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\count($unEqualConstraints) > 0) {
|
||||||
|
// this is where the end of the following interval of a != constraint is added as explained above
|
||||||
|
if ((string) $interval->getEnd() !== (string) Interval::untilPositiveInfinity()) {
|
||||||
|
$unEqualConstraints[] = $interval->getEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
// count is 1 if entire constraint is just one != expression
|
||||||
|
if (\count($unEqualConstraints) > 1) {
|
||||||
|
$constraints[] = new MultiConstraint($unEqualConstraints, true);
|
||||||
|
} else {
|
||||||
|
$constraints[] = $unEqualConstraints[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$unEqualConstraints = array();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert back >= x - <= x intervals to == x
|
||||||
|
if ($interval->getStart()->getVersion() === $interval->getEnd()->getVersion() && $interval->getStart()->getOperator() === '>=' && $interval->getEnd()->getOperator() === '<=') {
|
||||||
|
$constraints[] = new Constraint('==', $interval->getStart()->getVersion());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((string) $interval->getStart() === (string) Interval::fromZero()) {
|
||||||
|
$constraints[] = $interval->getEnd();
|
||||||
|
} elseif ((string) $interval->getEnd() === (string) Interval::untilPositiveInfinity()) {
|
||||||
|
$constraints[] = $interval->getStart();
|
||||||
|
} else {
|
||||||
|
$constraints[] = new MultiConstraint(array($interval->getStart(), $interval->getEnd()), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$devConstraints = array();
|
||||||
|
|
||||||
|
if (0 === \count($intervals['branches']['names'])) {
|
||||||
|
if ($intervals['branches']['exclude']) {
|
||||||
|
if ($hasNumericMatchAll) {
|
||||||
|
return new MatchAllConstraint;
|
||||||
|
}
|
||||||
|
// otherwise constraint should contain a != operator and already cover this
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
foreach ($intervals['branches']['names'] as $branchName) {
|
||||||
|
if ($intervals['branches']['exclude']) {
|
||||||
|
$devConstraints[] = new Constraint('!=', $branchName);
|
||||||
|
} else {
|
||||||
|
$devConstraints[] = new Constraint('==', $branchName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// excluded branches, e.g. != dev-foo are conjunctive with the interval, so
|
||||||
|
// > 2.0 != dev-foo must return a conjunctive constraint
|
||||||
|
if ($intervals['branches']['exclude']) {
|
||||||
|
if (\count($constraints) > 1) {
|
||||||
|
return new MultiConstraint(array_merge(
|
||||||
|
array(new MultiConstraint($constraints, false)),
|
||||||
|
$devConstraints
|
||||||
|
), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\count($constraints) === 1 && (string)$constraints[0] === (string)Interval::fromZero()) {
|
||||||
|
if (\count($devConstraints) > 1) {
|
||||||
|
return new MultiConstraint($devConstraints, true);
|
||||||
|
}
|
||||||
|
return $devConstraints[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MultiConstraint(array_merge($constraints, $devConstraints), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise devConstraints contains a list of == operators for branches which are disjunctive with the
|
||||||
|
// rest of the constraint
|
||||||
|
$constraints = array_merge($constraints, $devConstraints);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\count($constraints) > 1) {
|
||||||
|
return new MultiConstraint($constraints, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\count($constraints) === 1) {
|
||||||
|
return $constraints[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MatchNoneConstraint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an array of numeric intervals and branch constraints representing a given constraint
|
||||||
|
*
|
||||||
|
* if the returned numeric array is empty it means the constraint matches nothing in the numeric range (0 - +inf)
|
||||||
|
* if the returned branches array is empty it means no dev-* versions are matched
|
||||||
|
* if a constraint matches all possible dev-* versions, branches will contain Interval::anyDev()
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* @phpstan-return array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}}
|
||||||
|
*/
|
||||||
|
public static function get(ConstraintInterface $constraint)
|
||||||
|
{
|
||||||
|
$key = (string) $constraint;
|
||||||
|
|
||||||
|
if (!isset(self::$intervalsCache[$key])) {
|
||||||
|
self::$intervalsCache[$key] = self::generateIntervals($constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$intervalsCache[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param bool $stopOnFirstValidInterval
|
||||||
|
*
|
||||||
|
* @phpstan-return array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}}
|
||||||
|
*/
|
||||||
|
private static function generateIntervals(ConstraintInterface $constraint, $stopOnFirstValidInterval = false)
|
||||||
|
{
|
||||||
|
if ($constraint instanceof MatchAllConstraint) {
|
||||||
|
return array('numeric' => array(new Interval(Interval::fromZero(), Interval::untilPositiveInfinity())), 'branches' => Interval::anyDev());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($constraint instanceof MatchNoneConstraint) {
|
||||||
|
return array('numeric' => array(), 'branches' => array('names' => array(), 'exclude' => false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($constraint instanceof Constraint) {
|
||||||
|
return self::generateSingleConstraintIntervals($constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$constraint instanceof MultiConstraint) {
|
||||||
|
throw new \UnexpectedValueException('The constraint passed in should be an MatchAllConstraint, Constraint or MultiConstraint instance, got '.\get_class($constraint).'.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$constraints = $constraint->getConstraints();
|
||||||
|
|
||||||
|
$numericGroups = array();
|
||||||
|
$constraintBranches = array();
|
||||||
|
foreach ($constraints as $c) {
|
||||||
|
$res = self::get($c);
|
||||||
|
$numericGroups[] = $res['numeric'];
|
||||||
|
$constraintBranches[] = $res['branches'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($constraint->isDisjunctive()) {
|
||||||
|
$branches = Interval::noDev();
|
||||||
|
foreach ($constraintBranches as $b) {
|
||||||
|
if ($b['exclude']) {
|
||||||
|
if ($branches['exclude']) {
|
||||||
|
// disjunctive constraint, so only exclude what's excluded in all constraints
|
||||||
|
// !=a,!=b || !=b,!=c => !=b
|
||||||
|
$branches['names'] = array_intersect($branches['names'], $b['names']);
|
||||||
|
} else {
|
||||||
|
// disjunctive constraint so exclude all names which are not explicitly included in the alternative
|
||||||
|
// (==b || ==c) || !=a,!=b => !=a
|
||||||
|
$branches['exclude'] = true;
|
||||||
|
$branches['names'] = array_diff($b['names'], $branches['names']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($branches['exclude']) {
|
||||||
|
// disjunctive constraint so exclude all names which are not explicitly included in the alternative
|
||||||
|
// !=a,!=b || (==b || ==c) => !=a
|
||||||
|
$branches['names'] = array_diff($branches['names'], $b['names']);
|
||||||
|
} else {
|
||||||
|
// disjunctive constraint, so just add all the other branches
|
||||||
|
// (==a || ==b) || ==c => ==a || ==b || ==c
|
||||||
|
$branches['names'] = array_merge($branches['names'], $b['names']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$branches = Interval::anyDev();
|
||||||
|
foreach ($constraintBranches as $b) {
|
||||||
|
if ($b['exclude']) {
|
||||||
|
if ($branches['exclude']) {
|
||||||
|
// conjunctive, so just add all branch names to be excluded
|
||||||
|
// !=a && !=b => !=a,!=b
|
||||||
|
$branches['names'] = array_merge($branches['names'], $b['names']);
|
||||||
|
} else {
|
||||||
|
// conjunctive, so only keep included names which are not excluded
|
||||||
|
// (==a||==c) && !=a,!=b => ==c
|
||||||
|
$branches['names'] = array_diff($branches['names'], $b['names']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($branches['exclude']) {
|
||||||
|
// conjunctive, so only keep included names which are not excluded
|
||||||
|
// !=a,!=b && (==a||==c) => ==c
|
||||||
|
$branches['names'] = array_diff($b['names'], $branches['names']);
|
||||||
|
$branches['exclude'] = false;
|
||||||
|
} else {
|
||||||
|
// conjunctive, so only keep names that are included in both
|
||||||
|
// (==a||==b) && (==a||==c) => ==a
|
||||||
|
$branches['names'] = array_intersect($branches['names'], $b['names']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$branches['names'] = array_unique($branches['names']);
|
||||||
|
|
||||||
|
if (\count($numericGroups) === 1) {
|
||||||
|
return array('numeric' => $numericGroups[0], 'branches' => $branches);
|
||||||
|
}
|
||||||
|
|
||||||
|
$borders = array();
|
||||||
|
foreach ($numericGroups as $group) {
|
||||||
|
foreach ($group as $interval) {
|
||||||
|
$borders[] = array('version' => $interval->getStart()->getVersion(), 'operator' => $interval->getStart()->getOperator(), 'side' => 'start');
|
||||||
|
$borders[] = array('version' => $interval->getEnd()->getVersion(), 'operator' => $interval->getEnd()->getOperator(), 'side' => 'end');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$opSortOrder = self::$opSortOrder;
|
||||||
|
usort($borders, function ($a, $b) use ($opSortOrder) {
|
||||||
|
$order = version_compare($a['version'], $b['version']);
|
||||||
|
if ($order === 0) {
|
||||||
|
return $opSortOrder[$a['operator']] - $opSortOrder[$b['operator']];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $order;
|
||||||
|
});
|
||||||
|
|
||||||
|
$activeIntervals = 0;
|
||||||
|
$intervals = array();
|
||||||
|
$index = 0;
|
||||||
|
$activationThreshold = $constraint->isConjunctive() ? \count($numericGroups) : 1;
|
||||||
|
$start = null;
|
||||||
|
foreach ($borders as $border) {
|
||||||
|
if ($border['side'] === 'start') {
|
||||||
|
$activeIntervals++;
|
||||||
|
} else {
|
||||||
|
$activeIntervals--;
|
||||||
|
}
|
||||||
|
if (!$start && $activeIntervals >= $activationThreshold) {
|
||||||
|
$start = new Constraint($border['operator'], $border['version']);
|
||||||
|
} elseif ($start && $activeIntervals < $activationThreshold) {
|
||||||
|
// filter out invalid intervals like > x - <= x, or >= x - < x
|
||||||
|
if (
|
||||||
|
version_compare($start->getVersion(), $border['version'], '=')
|
||||||
|
&& (
|
||||||
|
($start->getOperator() === '>' && $border['operator'] === '<=')
|
||||||
|
|| ($start->getOperator() === '>=' && $border['operator'] === '<')
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
unset($intervals[$index]);
|
||||||
|
} else {
|
||||||
|
$intervals[$index] = new Interval($start, new Constraint($border['operator'], $border['version']));
|
||||||
|
$index++;
|
||||||
|
|
||||||
|
if ($stopOnFirstValidInterval) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$start = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array('numeric' => $intervals, 'branches' => $branches);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @phpstan-return array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}}
|
||||||
|
*/
|
||||||
|
private static function generateSingleConstraintIntervals(Constraint $constraint)
|
||||||
|
{
|
||||||
|
$op = $constraint->getOperator();
|
||||||
|
|
||||||
|
// handle branch constraints first
|
||||||
|
if (strpos($constraint->getVersion(), 'dev-') === 0) {
|
||||||
|
$intervals = array();
|
||||||
|
$branches = array('names' => array(), 'exclude' => false);
|
||||||
|
|
||||||
|
// != dev-foo means any numeric version may match, we treat >/< like != they are not really defined for branches
|
||||||
|
if ($op === '!=') {
|
||||||
|
$intervals[] = new Interval(Interval::fromZero(), Interval::untilPositiveInfinity());
|
||||||
|
$branches = array('names' => array($constraint->getVersion()), 'exclude' => true);
|
||||||
|
} elseif ($op === '==') {
|
||||||
|
$branches['names'][] = $constraint->getVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'numeric' => $intervals,
|
||||||
|
'branches' => $branches,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($op[0] === '>') { // > & >=
|
||||||
|
return array('numeric' => array(new Interval($constraint, Interval::untilPositiveInfinity())), 'branches' => Interval::noDev());
|
||||||
|
}
|
||||||
|
if ($op[0] === '<') { // < & <=
|
||||||
|
return array('numeric' => array(new Interval(Interval::fromZero(), $constraint)), 'branches' => Interval::noDev());
|
||||||
|
}
|
||||||
|
if ($op === '!=') {
|
||||||
|
// convert !=x to intervals of 0 - <x && >x - +inf + dev*
|
||||||
|
return array('numeric' => array(
|
||||||
|
new Interval(Interval::fromZero(), new Constraint('<', $constraint->getVersion())),
|
||||||
|
new Interval(new Constraint('>', $constraint->getVersion()), Interval::untilPositiveInfinity()),
|
||||||
|
), 'branches' => Interval::anyDev());
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert ==x to an interval of >=x - <=x
|
||||||
|
return array('numeric' => array(
|
||||||
|
new Interval(new Constraint('>=', $constraint->getVersion()), new Constraint('<=', $constraint->getVersion())),
|
||||||
|
), 'branches' => Interval::noDev());
|
||||||
|
}
|
||||||
|
}
|
||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/semver.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Semver;
|
||||||
|
|
||||||
|
use Composer\Semver\Constraint\Constraint;
|
||||||
|
|
||||||
|
class Semver
|
||||||
|
{
|
||||||
|
const SORT_ASC = 1;
|
||||||
|
const SORT_DESC = -1;
|
||||||
|
|
||||||
|
/** @var VersionParser */
|
||||||
|
private static $versionParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if given version satisfies given constraints.
|
||||||
|
*
|
||||||
|
* @param string $version
|
||||||
|
* @param string $constraints
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function satisfies($version, $constraints)
|
||||||
|
{
|
||||||
|
if (null === self::$versionParser) {
|
||||||
|
self::$versionParser = new VersionParser();
|
||||||
|
}
|
||||||
|
|
||||||
|
$versionParser = self::$versionParser;
|
||||||
|
$provider = new Constraint('==', $versionParser->normalize($version));
|
||||||
|
$parsedConstraints = $versionParser->parseConstraints($constraints);
|
||||||
|
|
||||||
|
return $parsedConstraints->matches($provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all versions that satisfy given constraints.
|
||||||
|
*
|
||||||
|
* @param string[] $versions
|
||||||
|
* @param string $constraints
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public static function satisfiedBy(array $versions, $constraints)
|
||||||
|
{
|
||||||
|
$versions = array_filter($versions, function ($version) use ($constraints) {
|
||||||
|
return Semver::satisfies($version, $constraints);
|
||||||
|
});
|
||||||
|
|
||||||
|
return array_values($versions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort given array of versions.
|
||||||
|
*
|
||||||
|
* @param string[] $versions
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public static function sort(array $versions)
|
||||||
|
{
|
||||||
|
return self::usort($versions, self::SORT_ASC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort given array of versions in reverse.
|
||||||
|
*
|
||||||
|
* @param string[] $versions
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public static function rsort(array $versions)
|
||||||
|
{
|
||||||
|
return self::usort($versions, self::SORT_DESC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[] $versions
|
||||||
|
* @param int $direction
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
private static function usort(array $versions, $direction)
|
||||||
|
{
|
||||||
|
if (null === self::$versionParser) {
|
||||||
|
self::$versionParser = new VersionParser();
|
||||||
|
}
|
||||||
|
|
||||||
|
$versionParser = self::$versionParser;
|
||||||
|
$normalized = array();
|
||||||
|
|
||||||
|
// Normalize outside of usort() scope for minor performance increase.
|
||||||
|
// Creates an array of arrays: [[normalized, key], ...]
|
||||||
|
foreach ($versions as $key => $version) {
|
||||||
|
$normalizedVersion = $versionParser->normalize($version);
|
||||||
|
$normalizedVersion = $versionParser->normalizeDefaultBranch($normalizedVersion);
|
||||||
|
$normalized[] = array($normalizedVersion, $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($normalized, function (array $left, array $right) use ($direction) {
|
||||||
|
if ($left[0] === $right[0]) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Comparator::lessThan($left[0], $right[0])) {
|
||||||
|
return -$direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $direction;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recreate input array, using the original indexes which are now in sorted order.
|
||||||
|
$sorted = array();
|
||||||
|
foreach ($normalized as $item) {
|
||||||
|
$sorted[] = $versions[$item[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sorted;
|
||||||
|
}
|
||||||
|
}
|
||||||
+591
@@ -0,0 +1,591 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of composer/semver.
|
||||||
|
*
|
||||||
|
* (c) Composer <https://github.com/composer>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Semver;
|
||||||
|
|
||||||
|
use Composer\Semver\Constraint\ConstraintInterface;
|
||||||
|
use Composer\Semver\Constraint\MatchAllConstraint;
|
||||||
|
use Composer\Semver\Constraint\MultiConstraint;
|
||||||
|
use Composer\Semver\Constraint\Constraint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version parser.
|
||||||
|
*
|
||||||
|
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
*/
|
||||||
|
class VersionParser
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Regex to match pre-release data (sort of).
|
||||||
|
*
|
||||||
|
* Due to backwards compatibility:
|
||||||
|
* - Instead of enforcing hyphen, an underscore, dot or nothing at all are also accepted.
|
||||||
|
* - Only stabilities as recognized by Composer are allowed to precede a numerical identifier.
|
||||||
|
* - Numerical-only pre-release identifiers are not supported, see tests.
|
||||||
|
*
|
||||||
|
* |--------------|
|
||||||
|
* [major].[minor].[patch] -[pre-release] +[build-metadata]
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private static $modifierRegex = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*+)?)?([.-]?dev)?';
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
private static $stabilitiesRegex = 'stable|RC|beta|alpha|dev';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the stability of a version.
|
||||||
|
*
|
||||||
|
* @param string $version
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* @phpstan-return 'stable'|'RC'|'beta'|'alpha'|'dev'
|
||||||
|
*/
|
||||||
|
public static function parseStability($version)
|
||||||
|
{
|
||||||
|
$version = (string) preg_replace('{#.+$}', '', (string) $version);
|
||||||
|
|
||||||
|
if (strpos($version, 'dev-') === 0 || '-dev' === substr($version, -4)) {
|
||||||
|
return 'dev';
|
||||||
|
}
|
||||||
|
|
||||||
|
preg_match('{' . self::$modifierRegex . '(?:\+.*)?$}i', strtolower($version), $match);
|
||||||
|
|
||||||
|
if (!empty($match[3])) {
|
||||||
|
return 'dev';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($match[1])) {
|
||||||
|
if ('beta' === $match[1] || 'b' === $match[1]) {
|
||||||
|
return 'beta';
|
||||||
|
}
|
||||||
|
if ('alpha' === $match[1] || 'a' === $match[1]) {
|
||||||
|
return 'alpha';
|
||||||
|
}
|
||||||
|
if ('rc' === $match[1]) {
|
||||||
|
return 'RC';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'stable';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $stability
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* @phpstan-return 'stable'|'RC'|'beta'|'alpha'|'dev'
|
||||||
|
*/
|
||||||
|
public static function normalizeStability($stability)
|
||||||
|
{
|
||||||
|
$stability = strtolower((string) $stability);
|
||||||
|
|
||||||
|
if (!in_array($stability, array('stable', 'rc', 'beta', 'alpha', 'dev'), true)) {
|
||||||
|
throw new \InvalidArgumentException('Invalid stability string "'.$stability.'", expected one of stable, RC, beta, alpha or dev');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $stability === 'rc' ? 'RC' : $stability;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a version string to be able to perform comparisons on it.
|
||||||
|
*
|
||||||
|
* @param string $version
|
||||||
|
* @param ?string $fullVersion optional complete version string to give more context
|
||||||
|
*
|
||||||
|
* @throws \UnexpectedValueException
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function normalize($version, $fullVersion = null)
|
||||||
|
{
|
||||||
|
$version = trim((string) $version);
|
||||||
|
$origVersion = $version;
|
||||||
|
if (null === $fullVersion) {
|
||||||
|
$fullVersion = $version;
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip off aliasing
|
||||||
|
if (preg_match('{^([^,\s]++) ++as ++([^,\s]++)$}', $version, $match)) {
|
||||||
|
$version = $match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip off stability flag
|
||||||
|
if (preg_match('{@(?:' . self::$stabilitiesRegex . ')$}i', $version, $match)) {
|
||||||
|
$version = substr($version, 0, strlen($version) - strlen($match[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalize master/trunk/default branches to dev-name for BC with 1.x as these used to be valid constraints
|
||||||
|
if (\in_array($version, array('master', 'trunk', 'default'), true)) {
|
||||||
|
$version = 'dev-' . $version;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if requirement is branch-like, use full name
|
||||||
|
if (stripos($version, 'dev-') === 0) {
|
||||||
|
return 'dev-' . substr($version, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip off build metadata
|
||||||
|
if (preg_match('{^([^,\s+]++)\+[^\s]++$}', $version, $match)) {
|
||||||
|
$version = $match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// match classical versioning
|
||||||
|
if (preg_match('{^v?(\d{1,5}+)(\.\d++)?(\.\d++)?(\.\d++)?' . self::$modifierRegex . '$}i', $version, $matches)) {
|
||||||
|
$version = $matches[1]
|
||||||
|
. (!empty($matches[2]) ? $matches[2] : '.0')
|
||||||
|
. (!empty($matches[3]) ? $matches[3] : '.0')
|
||||||
|
. (!empty($matches[4]) ? $matches[4] : '.0');
|
||||||
|
$index = 5;
|
||||||
|
// match date(time) based versioning
|
||||||
|
} elseif (preg_match('{^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1,3}){0,2})' . self::$modifierRegex . '$}i', $version, $matches)) {
|
||||||
|
$version = (string) preg_replace('{\D}', '.', $matches[1]);
|
||||||
|
$index = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add version modifiers if a version was matched
|
||||||
|
if (isset($index)) {
|
||||||
|
if (!empty($matches[$index])) {
|
||||||
|
if ('stable' === $matches[$index]) {
|
||||||
|
return $version;
|
||||||
|
}
|
||||||
|
$version .= '-' . $this->expandStability($matches[$index]) . (isset($matches[$index + 1]) && '' !== $matches[$index + 1] ? ltrim($matches[$index + 1], '.-') : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($matches[$index + 2])) {
|
||||||
|
$version .= '-dev';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $version;
|
||||||
|
}
|
||||||
|
|
||||||
|
// match dev branches
|
||||||
|
if (preg_match('{(.*?)[.-]?dev$}i', $version, $match)) {
|
||||||
|
try {
|
||||||
|
$normalized = $this->normalizeBranch($match[1]);
|
||||||
|
// a branch ending with -dev is only valid if it is numeric
|
||||||
|
// if it gets prefixed with dev- it means the branch name should
|
||||||
|
// have had a dev- prefix already when passed to normalize
|
||||||
|
if (strpos($normalized, 'dev-') === false) {
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$extraMessage = '';
|
||||||
|
if (preg_match('{ +as +' . preg_quote($version) . '(?:@(?:'.self::$stabilitiesRegex.'))?$}', $fullVersion)) {
|
||||||
|
$extraMessage = ' in "' . $fullVersion . '", the alias must be an exact version';
|
||||||
|
} elseif (preg_match('{^' . preg_quote($version) . '(?:@(?:'.self::$stabilitiesRegex.'))? +as +}', $fullVersion)) {
|
||||||
|
$extraMessage = ' in "' . $fullVersion . '", the alias source must be an exact version, if it is a branch name you should prefix it with dev-';
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \UnexpectedValueException('Invalid version string "' . $origVersion . '"' . $extraMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract numeric prefix from alias, if it is in numeric format, suitable for version comparison.
|
||||||
|
*
|
||||||
|
* @param string $branch Branch name (e.g. 2.1.x-dev)
|
||||||
|
*
|
||||||
|
* @return string|false Numeric prefix if present (e.g. 2.1.) or false
|
||||||
|
*/
|
||||||
|
public function parseNumericAliasPrefix($branch)
|
||||||
|
{
|
||||||
|
if (preg_match('{^(?P<version>(\d++\\.)*\d++)(?:\.x)?-dev$}i', (string) $branch, $matches)) {
|
||||||
|
return $matches['version'] . '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a branch name to be able to perform comparisons on it.
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function normalizeBranch($name)
|
||||||
|
{
|
||||||
|
$name = trim((string) $name);
|
||||||
|
|
||||||
|
if (preg_match('{^v?(\d++)(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?$}i', $name, $matches)) {
|
||||||
|
$version = '';
|
||||||
|
for ($i = 1; $i < 5; ++$i) {
|
||||||
|
$version .= isset($matches[$i]) ? str_replace(array('*', 'X'), 'x', $matches[$i]) : '.x';
|
||||||
|
}
|
||||||
|
|
||||||
|
return str_replace('x', '9999999', $version) . '-dev';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'dev-' . $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a default branch name (i.e. master on git) to 9999999-dev.
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
* @deprecated No need to use this anymore in theory, Composer 2 does not normalize any branch names to 9999999-dev anymore
|
||||||
|
*/
|
||||||
|
public function normalizeDefaultBranch($name)
|
||||||
|
{
|
||||||
|
if ($name === 'dev-master' || $name === 'dev-default' || $name === 'dev-trunk') {
|
||||||
|
return '9999999-dev';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a constraint string into MultiConstraint and/or Constraint objects.
|
||||||
|
*
|
||||||
|
* @param string $constraints
|
||||||
|
*
|
||||||
|
* @return ConstraintInterface
|
||||||
|
*/
|
||||||
|
public function parseConstraints($constraints)
|
||||||
|
{
|
||||||
|
$prettyConstraint = (string) $constraints;
|
||||||
|
|
||||||
|
$orConstraints = preg_split('{\s*\|\|?\s*}', trim((string) $constraints));
|
||||||
|
if (false === $orConstraints) {
|
||||||
|
throw new \RuntimeException('Failed to preg_split string: '.$constraints);
|
||||||
|
}
|
||||||
|
$orGroups = array();
|
||||||
|
|
||||||
|
foreach ($orConstraints as $orConstraint) {
|
||||||
|
$andConstraints = preg_split('{(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)}', $orConstraint);
|
||||||
|
if (false === $andConstraints) {
|
||||||
|
throw new \RuntimeException('Failed to preg_split string: '.$orConstraint);
|
||||||
|
}
|
||||||
|
if (\count($andConstraints) > 1) {
|
||||||
|
$constraintObjects = array();
|
||||||
|
foreach ($andConstraints as $andConstraint) {
|
||||||
|
foreach ($this->parseConstraint($andConstraint) as $parsedAndConstraint) {
|
||||||
|
$constraintObjects[] = $parsedAndConstraint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$constraintObjects = $this->parseConstraint($andConstraints[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (1 === \count($constraintObjects)) {
|
||||||
|
$constraint = $constraintObjects[0];
|
||||||
|
} else {
|
||||||
|
$constraint = new MultiConstraint($constraintObjects);
|
||||||
|
}
|
||||||
|
|
||||||
|
$orGroups[] = $constraint;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parsedConstraint = MultiConstraint::create($orGroups, false);
|
||||||
|
|
||||||
|
$parsedConstraint->setPrettyString($prettyConstraint);
|
||||||
|
|
||||||
|
return $parsedConstraint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $constraint
|
||||||
|
*
|
||||||
|
* @throws \UnexpectedValueException
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*
|
||||||
|
* @phpstan-return non-empty-array<ConstraintInterface>
|
||||||
|
*/
|
||||||
|
private function parseConstraint($constraint)
|
||||||
|
{
|
||||||
|
// strip off aliasing
|
||||||
|
if (preg_match('{^([^,\s]++) ++as ++([^,\s]++)$}', $constraint, $match)) {
|
||||||
|
$constraint = $match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip @stability flags, and keep it for later use
|
||||||
|
if (preg_match('{^([^,\s]*?)@(' . self::$stabilitiesRegex . ')$}i', $constraint, $match)) {
|
||||||
|
$constraint = '' !== $match[1] ? $match[1] : '*';
|
||||||
|
if ($match[2] !== 'stable') {
|
||||||
|
$stabilityModifier = $match[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get rid of #refs as those are used by composer only
|
||||||
|
if (preg_match('{^(dev-[^,\s@]+?|[^,\s@]+?\.x-dev)#.+$}i', $constraint, $match)) {
|
||||||
|
$constraint = $match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('{^(v)?[xX*](\.[xX*])*$}i', $constraint, $match)) {
|
||||||
|
if (!empty($match[1]) || !empty($match[2])) {
|
||||||
|
return array(new Constraint('>=', '0.0.0.0-dev'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(new MatchAllConstraint());
|
||||||
|
}
|
||||||
|
|
||||||
|
$versionRegex = 'v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.(\d++))?(?:' . self::$modifierRegex . '|\.([xX*][.-]?dev))(?:\+[^\s]+)?';
|
||||||
|
|
||||||
|
// Tilde Range
|
||||||
|
//
|
||||||
|
// Like wildcard constraints, unsuffixed tilde constraints say that they must be greater than the previous
|
||||||
|
// version, to ensure that unstable instances of the current version are allowed. However, if a stability
|
||||||
|
// suffix is added to the constraint, then a >= match on the current version is used instead.
|
||||||
|
if (preg_match('{^~>?' . $versionRegex . '$}i', $constraint, $matches)) {
|
||||||
|
if (strpos($constraint, '~>') === 0) {
|
||||||
|
throw new \UnexpectedValueException(
|
||||||
|
'Could not parse version constraint ' . $constraint . ': ' .
|
||||||
|
'Invalid operator "~>", you probably meant to use the "~" operator'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Work out which position in the version we are operating at
|
||||||
|
if (isset($matches[4]) && '' !== $matches[4] && null !== $matches[4]) {
|
||||||
|
$position = 4;
|
||||||
|
} elseif (isset($matches[3]) && '' !== $matches[3] && null !== $matches[3]) {
|
||||||
|
$position = 3;
|
||||||
|
} elseif (isset($matches[2]) && '' !== $matches[2] && null !== $matches[2]) {
|
||||||
|
$position = 2;
|
||||||
|
} else {
|
||||||
|
$position = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// when matching 2.x-dev or 3.0.x-dev we have to shift the second or third number, despite no second/third number matching above
|
||||||
|
if (!empty($matches[8])) {
|
||||||
|
$position++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the stability suffix
|
||||||
|
$stabilitySuffix = '';
|
||||||
|
if (empty($matches[5]) && empty($matches[7]) && empty($matches[8])) {
|
||||||
|
$stabilitySuffix .= '-dev';
|
||||||
|
}
|
||||||
|
|
||||||
|
$lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1));
|
||||||
|
$lowerBound = new Constraint('>=', $lowVersion);
|
||||||
|
|
||||||
|
// For upper bound, we increment the position of one more significance,
|
||||||
|
// but highPosition = 0 would be illegal
|
||||||
|
$highPosition = max(1, $position - 1);
|
||||||
|
$highVersion = $this->manipulateVersionString($matches, $highPosition, 1) . '-dev';
|
||||||
|
$upperBound = new Constraint('<', $highVersion);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
$lowerBound,
|
||||||
|
$upperBound,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caret Range
|
||||||
|
//
|
||||||
|
// Allows changes that do not modify the left-most non-zero digit in the [major, minor, patch] tuple.
|
||||||
|
// In other words, this allows patch and minor updates for versions 1.0.0 and above, patch updates for
|
||||||
|
// versions 0.X >=0.1.0, and no updates for versions 0.0.X
|
||||||
|
if (preg_match('{^\^' . $versionRegex . '($)}i', $constraint, $matches)) {
|
||||||
|
// Work out which position in the version we are operating at
|
||||||
|
if ('0' !== $matches[1] || '' === $matches[2] || null === $matches[2]) {
|
||||||
|
$position = 1;
|
||||||
|
} elseif ('0' !== $matches[2] || '' === $matches[3] || null === $matches[3]) {
|
||||||
|
$position = 2;
|
||||||
|
} else {
|
||||||
|
$position = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the stability suffix
|
||||||
|
$stabilitySuffix = '';
|
||||||
|
if (empty($matches[5]) && empty($matches[7]) && empty($matches[8])) {
|
||||||
|
$stabilitySuffix .= '-dev';
|
||||||
|
}
|
||||||
|
|
||||||
|
$lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1));
|
||||||
|
$lowerBound = new Constraint('>=', $lowVersion);
|
||||||
|
|
||||||
|
// For upper bound, we increment the position of one more significance,
|
||||||
|
// but highPosition = 0 would be illegal
|
||||||
|
$highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev';
|
||||||
|
$upperBound = new Constraint('<', $highVersion);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
$lowerBound,
|
||||||
|
$upperBound,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// X Range
|
||||||
|
//
|
||||||
|
// Any of X, x, or * may be used to "stand in" for one of the numeric values in the [major, minor, patch] tuple.
|
||||||
|
// A partial version range is treated as an X-Range, so the special character is in fact optional.
|
||||||
|
if (preg_match('{^v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.[xX*])++$}', $constraint, $matches)) {
|
||||||
|
if (isset($matches[3]) && '' !== $matches[3] && null !== $matches[3]) {
|
||||||
|
$position = 3;
|
||||||
|
} elseif (isset($matches[2]) && '' !== $matches[2] && null !== $matches[2]) {
|
||||||
|
$position = 2;
|
||||||
|
} else {
|
||||||
|
$position = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lowVersion = $this->manipulateVersionString($matches, $position) . '-dev';
|
||||||
|
$highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev';
|
||||||
|
|
||||||
|
if ($lowVersion === '0.0.0.0-dev') {
|
||||||
|
return array(new Constraint('<', $highVersion));
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
new Constraint('>=', $lowVersion),
|
||||||
|
new Constraint('<', $highVersion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hyphen Range
|
||||||
|
//
|
||||||
|
// Specifies an inclusive set. If a partial version is provided as the first version in the inclusive range,
|
||||||
|
// then the missing pieces are replaced with zeroes. If a partial version is provided as the second version in
|
||||||
|
// the inclusive range, then all versions that start with the supplied parts of the tuple are accepted, but
|
||||||
|
// nothing that would be greater than the provided tuple parts.
|
||||||
|
if (preg_match('{^(?P<from>' . $versionRegex . ') +- +(?P<to>' . $versionRegex . ')($)}i', $constraint, $matches)) {
|
||||||
|
// Calculate the stability suffix
|
||||||
|
$lowStabilitySuffix = '';
|
||||||
|
if (empty($matches[6]) && empty($matches[8]) && empty($matches[9])) {
|
||||||
|
$lowStabilitySuffix = '-dev';
|
||||||
|
}
|
||||||
|
|
||||||
|
$lowVersion = $this->normalize($matches['from']);
|
||||||
|
$lowerBound = new Constraint('>=', $lowVersion . $lowStabilitySuffix);
|
||||||
|
|
||||||
|
$empty = function ($x) {
|
||||||
|
return ($x === 0 || $x === '0') ? false : empty($x);
|
||||||
|
};
|
||||||
|
|
||||||
|
if ((!$empty($matches[12]) && !$empty($matches[13])) || !empty($matches[15]) || !empty($matches[17]) || !empty($matches[18])) {
|
||||||
|
$highVersion = $this->normalize($matches['to']);
|
||||||
|
$upperBound = new Constraint('<=', $highVersion);
|
||||||
|
} else {
|
||||||
|
$highMatch = array('', $matches[11], $matches[12], $matches[13], $matches[14]);
|
||||||
|
|
||||||
|
// validate to version
|
||||||
|
$this->normalize($matches['to']);
|
||||||
|
|
||||||
|
$highVersion = $this->manipulateVersionString($highMatch, $empty($matches[12]) ? 1 : 2, 1) . '-dev';
|
||||||
|
$upperBound = new Constraint('<', $highVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
$lowerBound,
|
||||||
|
$upperBound,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic Comparators
|
||||||
|
if (preg_match('{^(<>|!=|>=?|<=?|==?)?\s*(.*)}', $constraint, $matches)) {
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
$version = $this->normalize($matches[2]);
|
||||||
|
} catch (\UnexpectedValueException $e) {
|
||||||
|
// recover from an invalid constraint like foobar-dev which should be dev-foobar
|
||||||
|
// except if the constraint uses a known operator, in which case it must be a parse error
|
||||||
|
if (substr($matches[2], -4) === '-dev' && preg_match('{^[0-9a-zA-Z-./]+$}', $matches[2])) {
|
||||||
|
$version = $this->normalize('dev-'.substr($matches[2], 0, -4));
|
||||||
|
} else {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$op = $matches[1] ?: '=';
|
||||||
|
|
||||||
|
if ($op !== '==' && $op !== '=' && !empty($stabilityModifier) && self::parseStability($version) === 'stable') {
|
||||||
|
$version .= '-' . $stabilityModifier;
|
||||||
|
} elseif ('<' === $op || '>=' === $op) {
|
||||||
|
if (!preg_match('/-' . self::$modifierRegex . '$/', strtolower($matches[2]))) {
|
||||||
|
if (strpos($matches[2], 'dev-') !== 0) {
|
||||||
|
$version .= '-dev';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(new Constraint($matches[1] ?: '=', $version));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = 'Could not parse version constraint ' . $constraint;
|
||||||
|
if (isset($e)) {
|
||||||
|
$message .= ': ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \UnexpectedValueException($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment, decrement, or simply pad a version number.
|
||||||
|
*
|
||||||
|
* Support function for {@link parseConstraint()}
|
||||||
|
*
|
||||||
|
* @param array $matches Array with version parts in array indexes 1,2,3,4
|
||||||
|
* @param int $position 1,2,3,4 - which segment of the version to increment/decrement
|
||||||
|
* @param int $increment
|
||||||
|
* @param string $pad The string to pad version parts after $position
|
||||||
|
*
|
||||||
|
* @return string|null The new version
|
||||||
|
*
|
||||||
|
* @phpstan-param string[] $matches
|
||||||
|
*/
|
||||||
|
private function manipulateVersionString(array $matches, $position, $increment = 0, $pad = '0')
|
||||||
|
{
|
||||||
|
for ($i = 4; $i > 0; --$i) {
|
||||||
|
if ($i > $position) {
|
||||||
|
$matches[$i] = $pad;
|
||||||
|
} elseif ($i === $position && $increment) {
|
||||||
|
$matches[$i] += $increment;
|
||||||
|
// If $matches[$i] was 0, carry the decrement
|
||||||
|
if ($matches[$i] < 0) {
|
||||||
|
$matches[$i] = $pad;
|
||||||
|
--$position;
|
||||||
|
|
||||||
|
// Return null on a carry overflow
|
||||||
|
if ($i === 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $matches[1] . '.' . $matches[2] . '.' . $matches[3] . '.' . $matches[4];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand shorthand stability string to long version.
|
||||||
|
*
|
||||||
|
* @param string $stability
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function expandStability($stability)
|
||||||
|
{
|
||||||
|
$stability = strtolower($stability);
|
||||||
|
|
||||||
|
switch ($stability) {
|
||||||
|
case 'a':
|
||||||
|
return 'alpha';
|
||||||
|
case 'b':
|
||||||
|
return 'beta';
|
||||||
|
case 'p':
|
||||||
|
case 'pl':
|
||||||
|
return 'patch';
|
||||||
|
case 'rc':
|
||||||
|
return 'RC';
|
||||||
|
default:
|
||||||
|
return $stability;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [3.0.3] - 2024-07-08
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed PHP 8.4 deprecation notices (#47)
|
||||||
|
|
||||||
|
## [3.0.2] - 2022-10-27
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Added missing return types to docblocks (#44, #45)
|
||||||
|
|
||||||
|
## [3.0.1] - 2021-08-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Adds ReturnTypeWillChange to suppress PHP 8.1 warnings (#40)
|
||||||
|
|
||||||
|
## [3.0.0] - 2021-01-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added support for both `.` and `/`-delimited key paths (#24)
|
||||||
|
- Added parameter and return types to everything; enabled strict type checks (#18)
|
||||||
|
- Added new exception classes to better identify certain types of errors (#20)
|
||||||
|
- `Data` now implements `ArrayAccess` (#17)
|
||||||
|
- Added ability to merge non-associative array values (#31, #32)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- All thrown exceptions are now instances or subclasses of `DataException` (#20)
|
||||||
|
- Calling `get()` on a missing key path without providing a default will throw a `MissingPathException` instead of returning `null` (#29)
|
||||||
|
- Bumped supported PHP versions to 7.1 - 8.x (#18)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed incorrect merging of array values into string values (#32)
|
||||||
|
- Fixed `get()` method behaving as if keys with `null` values didn't exist
|
||||||
|
|
||||||
|
## [2.0.0] - 2017-12-21
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Bumped supported PHP versions to 7.0 - 7.4 (#12)
|
||||||
|
- Switched to PSR-4 autoloading
|
||||||
|
|
||||||
|
## [1.1.0] - 2017-01-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added new `has()` method to check for the existence of the given key (#4, #7)
|
||||||
|
|
||||||
|
## [1.0.1] - 2015-08-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added new optional `$default` parameter to the `get()` method (#2)
|
||||||
|
|
||||||
|
## [1.0.0] - 2012-07-17
|
||||||
|
|
||||||
|
**Initial release!**
|
||||||
|
|
||||||
|
[Unreleased]: https://github.com/dflydev/dflydev-dot-access-data/compare/v3.0.3...main
|
||||||
|
[3.0.3]: https://github.com/dflydev/dflydev-dot-access-data/compare/v3.0.2...v3.0.3
|
||||||
|
[3.0.2]: https://github.com/dflydev/dflydev-dot-access-data/compare/v3.0.1...v3.0.2
|
||||||
|
[3.0.1]: https://github.com/dflydev/dflydev-dot-access-data/compare/v3.0.0...v3.0.1
|
||||||
|
[3.0.0]: https://github.com/dflydev/dflydev-dot-access-data/compare/v2.0.0...v3.0.0
|
||||||
|
[2.0.0]: https://github.com/dflydev/dflydev-dot-access-data/compare/v1.1.0...v2.0.0
|
||||||
|
[1.1.0]: https://github.com/dflydev/dflydev-dot-access-data/compare/v1.0.1...v1.1.0
|
||||||
|
[1.0.1]: https://github.com/dflydev/dflydev-dot-access-data/compare/v1.0.0...v1.0.1
|
||||||
|
[1.0.0]: https://github.com/dflydev/dflydev-dot-access-data/releases/tag/v1.0.0
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2012 Dragonfly Development Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
+158
@@ -0,0 +1,158 @@
|
|||||||
|
Dot Access Data
|
||||||
|
===============
|
||||||
|
|
||||||
|
[](https://packagist.org/packages/dflydev/dot-access-data)
|
||||||
|
[](https://packagist.org/packages/dflydev/dot-access-data)
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://github.com/dflydev/dflydev-dot-access-data/actions?query=workflow%3ATests+branch%3Amain)
|
||||||
|
[](https://scrutinizer-ci.com/g/dflydev/dflydev-dot-access-data/code-structure/)
|
||||||
|
[](https://scrutinizer-ci.com/g/dflydev/dflydev-dot-access-data)
|
||||||
|
|
||||||
|
Given a deep data structure, access data by dot notation.
|
||||||
|
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
|
* PHP (7.1+)
|
||||||
|
|
||||||
|
> For PHP (5.3+) please refer to version `1.0`.
|
||||||
|
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
Abstract example:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Dflydev\DotAccessData\Data;
|
||||||
|
|
||||||
|
$data = new Data;
|
||||||
|
|
||||||
|
$data->set('a.b.c', 'C');
|
||||||
|
$data->set('a.b.d', 'D1');
|
||||||
|
$data->append('a.b.d', 'D2');
|
||||||
|
$data->set('a.b.e', ['E0', 'E1', 'E2']);
|
||||||
|
|
||||||
|
// C
|
||||||
|
$data->get('a.b.c');
|
||||||
|
|
||||||
|
// ['D1', 'D2']
|
||||||
|
$data->get('a.b.d');
|
||||||
|
|
||||||
|
// ['E0', 'E1', 'E2']
|
||||||
|
$data->get('a.b.e');
|
||||||
|
|
||||||
|
// true
|
||||||
|
$data->has('a.b.c');
|
||||||
|
|
||||||
|
// false
|
||||||
|
$data->has('a.b.d.j');
|
||||||
|
|
||||||
|
|
||||||
|
// 'some-default-value'
|
||||||
|
$data->get('some.path.that.does.not.exist', 'some-default-value');
|
||||||
|
|
||||||
|
// throws a MissingPathException because no default was given
|
||||||
|
$data->get('some.path.that.does.not.exist');
|
||||||
|
```
|
||||||
|
|
||||||
|
A more concrete example:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Dflydev\DotAccessData\Data;
|
||||||
|
|
||||||
|
$data = new Data([
|
||||||
|
'hosts' => [
|
||||||
|
'hewey' => [
|
||||||
|
'username' => 'hman',
|
||||||
|
'password' => 'HPASS',
|
||||||
|
'roles' => ['web'],
|
||||||
|
],
|
||||||
|
'dewey' => [
|
||||||
|
'username' => 'dman',
|
||||||
|
'password' => 'D---S',
|
||||||
|
'roles' => ['web', 'db'],
|
||||||
|
'nick' => 'dewey dman',
|
||||||
|
],
|
||||||
|
'lewey' => [
|
||||||
|
'username' => 'lman',
|
||||||
|
'password' => 'LP@$$',
|
||||||
|
'roles' => ['db'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// hman
|
||||||
|
$username = $data->get('hosts.hewey.username');
|
||||||
|
// HPASS
|
||||||
|
$password = $data->get('hosts.hewey.password');
|
||||||
|
// ['web']
|
||||||
|
$roles = $data->get('hosts.hewey.roles');
|
||||||
|
// dewey dman
|
||||||
|
$nick = $data->get('hosts.dewey.nick');
|
||||||
|
// Unknown
|
||||||
|
$nick = $data->get('hosts.lewey.nick', 'Unknown');
|
||||||
|
|
||||||
|
// DataInterface instance
|
||||||
|
$dewey = $data->getData('hosts.dewey');
|
||||||
|
// dman
|
||||||
|
$username = $dewey->get('username');
|
||||||
|
// D---S
|
||||||
|
$password = $dewey->get('password');
|
||||||
|
// ['web', 'db']
|
||||||
|
$roles = $dewey->get('roles');
|
||||||
|
|
||||||
|
// No more lewey
|
||||||
|
$data->remove('hosts.lewey');
|
||||||
|
|
||||||
|
// Add DB to hewey's roles
|
||||||
|
$data->append('hosts.hewey.roles', 'db');
|
||||||
|
|
||||||
|
$data->set('hosts.april', [
|
||||||
|
'username' => 'aman',
|
||||||
|
'password' => '@---S',
|
||||||
|
'roles' => ['web'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check if a key exists (true to this case)
|
||||||
|
$hasKey = $data->has('hosts.dewey.username');
|
||||||
|
```
|
||||||
|
|
||||||
|
`Data` may be used as an array, since it implements `ArrayAccess` interface:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Get
|
||||||
|
$data->get('name') === $data['name']; // true
|
||||||
|
|
||||||
|
$data['name'] = 'Dewey';
|
||||||
|
// is equivalent to
|
||||||
|
$data->set($name, 'Dewey');
|
||||||
|
|
||||||
|
isset($data['name']) === $data->has('name');
|
||||||
|
|
||||||
|
// Remove key
|
||||||
|
unset($data['name']);
|
||||||
|
```
|
||||||
|
|
||||||
|
`/` can also be used as a path delimiter:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$data->set('a/b/c', 'd');
|
||||||
|
echo $data->get('a/b/c'); // "d"
|
||||||
|
|
||||||
|
$data->get('a/b/c') === $data->get('a.b.c'); // true
|
||||||
|
```
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
This library is licensed under the MIT License - see the LICENSE file
|
||||||
|
for details.
|
||||||
|
|
||||||
|
|
||||||
|
Community
|
||||||
|
---------
|
||||||
|
|
||||||
|
If you have questions or want to help out, join us in the
|
||||||
|
[#dflydev](irc://irc.freenode.net/#dflydev) channel on irc.freenode.net.
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"name": "dflydev/dot-access-data",
|
||||||
|
"type": "library",
|
||||||
|
"description": "Given a deep data structure, access data by dot notation.",
|
||||||
|
"homepage": "https://github.com/dflydev/dflydev-dot-access-data",
|
||||||
|
"keywords": ["dot", "access", "data", "notation"],
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Dragonfly Development Inc.",
|
||||||
|
"email": "info@dflydev.com",
|
||||||
|
"homepage": "http://dflydev.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Beau Simensen",
|
||||||
|
"email": "beau@dflydev.com",
|
||||||
|
"homepage": "http://beausimensen.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Carlos Frutos",
|
||||||
|
"email": "carlos@kiwing.it",
|
||||||
|
"homepage": "https://github.com/cfrutos"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Colin O'Dell",
|
||||||
|
"email": "colinodell@gmail.com",
|
||||||
|
"homepage": "https://www.colinodell.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^0.12.42",
|
||||||
|
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.3",
|
||||||
|
"scrutinizer/ocular": "1.6.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.5",
|
||||||
|
"vimeo/psalm": "^4.0.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Dflydev\\DotAccessData\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Dflydev\\DotAccessData\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "3.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"phpcs": "phpcs",
|
||||||
|
"phpstan": "phpstan analyse",
|
||||||
|
"phpunit": "phpunit --no-coverage",
|
||||||
|
"psalm": "psalm",
|
||||||
|
"test": [
|
||||||
|
"@phpcs",
|
||||||
|
"@phpstan",
|
||||||
|
"@psalm",
|
||||||
|
"@phpunit"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+286
@@ -0,0 +1,286 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is a part of dflydev/dot-access-data.
|
||||||
|
*
|
||||||
|
* (c) Dragonfly Development Inc.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Dflydev\DotAccessData;
|
||||||
|
|
||||||
|
use ArrayAccess;
|
||||||
|
use Dflydev\DotAccessData\Exception\DataException;
|
||||||
|
use Dflydev\DotAccessData\Exception\InvalidPathException;
|
||||||
|
use Dflydev\DotAccessData\Exception\MissingPathException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements ArrayAccess<string, mixed>
|
||||||
|
*/
|
||||||
|
class Data implements DataInterface, ArrayAccess
|
||||||
|
{
|
||||||
|
private const DELIMITERS = ['.', '/'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal representation of data data
|
||||||
|
*
|
||||||
|
* @var array<string, mixed>
|
||||||
|
*/
|
||||||
|
protected $data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $data
|
||||||
|
*/
|
||||||
|
public function __construct(array $data = [])
|
||||||
|
{
|
||||||
|
$this->data = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function append(string $key, $value = null): void
|
||||||
|
{
|
||||||
|
$currentValue =& $this->data;
|
||||||
|
$keyPath = self::keyToPathArray($key);
|
||||||
|
|
||||||
|
$endKey = array_pop($keyPath);
|
||||||
|
foreach ($keyPath as $currentKey) {
|
||||||
|
if (! isset($currentValue[$currentKey])) {
|
||||||
|
$currentValue[$currentKey] = [];
|
||||||
|
}
|
||||||
|
$currentValue =& $currentValue[$currentKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($currentValue[$endKey])) {
|
||||||
|
$currentValue[$endKey] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($currentValue[$endKey])) {
|
||||||
|
// Promote this key to an array.
|
||||||
|
// TODO: Is this really what we want to do?
|
||||||
|
$currentValue[$endKey] = [$currentValue[$endKey]];
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentValue[$endKey][] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function set(string $key, $value = null): void
|
||||||
|
{
|
||||||
|
$currentValue =& $this->data;
|
||||||
|
$keyPath = self::keyToPathArray($key);
|
||||||
|
|
||||||
|
$endKey = array_pop($keyPath);
|
||||||
|
foreach ($keyPath as $currentKey) {
|
||||||
|
if (!isset($currentValue[$currentKey])) {
|
||||||
|
$currentValue[$currentKey] = [];
|
||||||
|
}
|
||||||
|
if (!is_array($currentValue[$currentKey])) {
|
||||||
|
throw new DataException(sprintf('Key path "%s" within "%s" cannot be indexed into (is not an array)', $currentKey, self::formatPath($key)));
|
||||||
|
}
|
||||||
|
$currentValue =& $currentValue[$currentKey];
|
||||||
|
}
|
||||||
|
$currentValue[$endKey] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function remove(string $key): void
|
||||||
|
{
|
||||||
|
$currentValue =& $this->data;
|
||||||
|
$keyPath = self::keyToPathArray($key);
|
||||||
|
|
||||||
|
$endKey = array_pop($keyPath);
|
||||||
|
foreach ($keyPath as $currentKey) {
|
||||||
|
if (!isset($currentValue[$currentKey])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$currentValue =& $currentValue[$currentKey];
|
||||||
|
}
|
||||||
|
unset($currentValue[$endKey]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @psalm-mutation-free
|
||||||
|
*/
|
||||||
|
public function get(string $key, $default = null)
|
||||||
|
{
|
||||||
|
/** @psalm-suppress ImpureFunctionCall */
|
||||||
|
$hasDefault = \func_num_args() > 1;
|
||||||
|
|
||||||
|
$currentValue = $this->data;
|
||||||
|
$keyPath = self::keyToPathArray($key);
|
||||||
|
|
||||||
|
foreach ($keyPath as $currentKey) {
|
||||||
|
if (!is_array($currentValue) || !array_key_exists($currentKey, $currentValue)) {
|
||||||
|
if ($hasDefault) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new MissingPathException($key, sprintf('No data exists at the given path: "%s"', self::formatPath($keyPath)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentValue = $currentValue[$currentKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $currentValue === null ? $default : $currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @psalm-mutation-free
|
||||||
|
*/
|
||||||
|
public function has(string $key): bool
|
||||||
|
{
|
||||||
|
$currentValue = $this->data;
|
||||||
|
|
||||||
|
foreach (self::keyToPathArray($key) as $currentKey) {
|
||||||
|
if (
|
||||||
|
!is_array($currentValue) ||
|
||||||
|
!array_key_exists($currentKey, $currentValue)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$currentValue = $currentValue[$currentKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @psalm-mutation-free
|
||||||
|
*/
|
||||||
|
public function getData(string $key): DataInterface
|
||||||
|
{
|
||||||
|
$value = $this->get($key);
|
||||||
|
if (is_array($value) && Util::isAssoc($value)) {
|
||||||
|
return new Data($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DataException(sprintf('Value at "%s" could not be represented as a DataInterface', self::formatPath($key)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function import(array $data, int $mode = self::REPLACE): void
|
||||||
|
{
|
||||||
|
$this->data = Util::mergeAssocArray($this->data, $data, $mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function importData(DataInterface $data, int $mode = self::REPLACE): void
|
||||||
|
{
|
||||||
|
$this->import($data->export(), $mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @psalm-mutation-free
|
||||||
|
*/
|
||||||
|
public function export(): array
|
||||||
|
{
|
||||||
|
return $this->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
#[\ReturnTypeWillChange]
|
||||||
|
public function offsetExists($key)
|
||||||
|
{
|
||||||
|
return $this->has($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
#[\ReturnTypeWillChange]
|
||||||
|
public function offsetGet($key)
|
||||||
|
{
|
||||||
|
return $this->get($key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @param mixed $value
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
#[\ReturnTypeWillChange]
|
||||||
|
public function offsetSet($key, $value)
|
||||||
|
{
|
||||||
|
$this->set($key, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
#[\ReturnTypeWillChange]
|
||||||
|
public function offsetUnset($key)
|
||||||
|
{
|
||||||
|
$this->remove($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $path
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*
|
||||||
|
* @psalm-return non-empty-list<string>
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
protected static function keyToPathArray(string $path): array
|
||||||
|
{
|
||||||
|
if (\strlen($path) === 0) {
|
||||||
|
throw new InvalidPathException('Path cannot be an empty string');
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = \str_replace(self::DELIMITERS, '.', $path);
|
||||||
|
|
||||||
|
return \explode('.', $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|string[] $path
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
protected static function formatPath($path): string
|
||||||
|
{
|
||||||
|
if (is_string($path)) {
|
||||||
|
$path = self::keyToPathArray($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' » ', $path);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is a part of dflydev/dot-access-data.
|
||||||
|
*
|
||||||
|
* (c) Dragonfly Development Inc.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Dflydev\DotAccessData;
|
||||||
|
|
||||||
|
use Dflydev\DotAccessData\Exception\DataException;
|
||||||
|
use Dflydev\DotAccessData\Exception\InvalidPathException;
|
||||||
|
|
||||||
|
interface DataInterface
|
||||||
|
{
|
||||||
|
public const PRESERVE = 0;
|
||||||
|
public const REPLACE = 1;
|
||||||
|
public const MERGE = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append a value to a key (assumes key refers to an array value)
|
||||||
|
*
|
||||||
|
* If the key does not yet exist it will be created.
|
||||||
|
* If the key references a non-array it's existing contents will be added into a new array before appending the new value.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @param mixed $value
|
||||||
|
*
|
||||||
|
* @throws InvalidPathException if the given key is empty
|
||||||
|
*/
|
||||||
|
public function append(string $key, $value = null): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a value for a key
|
||||||
|
*
|
||||||
|
* If the key does not yet exist it will be created.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @param mixed $value
|
||||||
|
*
|
||||||
|
* @throws InvalidPathException if the given key is empty
|
||||||
|
* @throws DataException if the given key does not target an array
|
||||||
|
*/
|
||||||
|
public function set(string $key, $value = null): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a key
|
||||||
|
*
|
||||||
|
* No exception will be thrown if the key does not exist
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
*
|
||||||
|
* @throws InvalidPathException if the given key is empty
|
||||||
|
*/
|
||||||
|
public function remove(string $key): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the raw value for a key
|
||||||
|
*
|
||||||
|
* If the key does not exist, an optional default value can be returned instead.
|
||||||
|
* If no default is provided then an exception will be thrown instead.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @param mixed $default
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*
|
||||||
|
* @throws InvalidPathException if the given key is empty
|
||||||
|
* @throws InvalidPathException if the given key does not exist and no default value was given
|
||||||
|
*
|
||||||
|
* @psalm-mutation-free
|
||||||
|
*/
|
||||||
|
public function get(string $key, $default = null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the key exists
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*
|
||||||
|
* @throws InvalidPathException if the given key is empty
|
||||||
|
*
|
||||||
|
* @psalm-mutation-free
|
||||||
|
*/
|
||||||
|
public function has(string $key): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a data instance for a key
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
*
|
||||||
|
* @return DataInterface
|
||||||
|
*
|
||||||
|
* @throws InvalidPathException if the given key is empty
|
||||||
|
* @throws DataException if the given key does not reference an array
|
||||||
|
*
|
||||||
|
* @psalm-mutation-free
|
||||||
|
*/
|
||||||
|
public function getData(string $key): DataInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import data into existing data
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $data
|
||||||
|
* @param self::PRESERVE|self::REPLACE|self::MERGE $mode
|
||||||
|
*/
|
||||||
|
public function import(array $data, int $mode = self::REPLACE): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import data from an external data into existing data
|
||||||
|
*
|
||||||
|
* @param DataInterface $data
|
||||||
|
* @param self::PRESERVE|self::REPLACE|self::MERGE $mode
|
||||||
|
*/
|
||||||
|
public function importData(DataInterface $data, int $mode = self::REPLACE): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export data as raw data
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*
|
||||||
|
* @psalm-mutation-free
|
||||||
|
*/
|
||||||
|
public function export(): array;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is a part of dflydev/dot-access-data.
|
||||||
|
*
|
||||||
|
* (c) Dragonfly Development Inc.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Dflydev\DotAccessData\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base runtime exception type thrown by this library
|
||||||
|
*/
|
||||||
|
class DataException extends \RuntimeException
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is a part of dflydev/dot-access-data.
|
||||||
|
*
|
||||||
|
* (c) Dragonfly Development Inc.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Dflydev\DotAccessData\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when trying to access an invalid path in the data array
|
||||||
|
*/
|
||||||
|
class InvalidPathException extends DataException
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is a part of dflydev/dot-access-data.
|
||||||
|
*
|
||||||
|
* (c) Dragonfly Development Inc.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Dflydev\DotAccessData\Exception;
|
||||||
|
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when trying to access a path that does not exist
|
||||||
|
*/
|
||||||
|
class MissingPathException extends DataException
|
||||||
|
{
|
||||||
|
/** @var string */
|
||||||
|
protected $path;
|
||||||
|
|
||||||
|
public function __construct(string $path, string $message = '', int $code = 0, ?Throwable $previous = null)
|
||||||
|
{
|
||||||
|
$this->path = $path;
|
||||||
|
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPath(): string
|
||||||
|
{
|
||||||
|
return $this->path;
|
||||||
|
}
|
||||||
|
}
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is a part of dflydev/dot-access-data.
|
||||||
|
*
|
||||||
|
* (c) Dragonfly Development Inc.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Dflydev\DotAccessData;
|
||||||
|
|
||||||
|
class Util
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Test if array is an associative array
|
||||||
|
*
|
||||||
|
* Note that this function will return true if an array is empty. Meaning
|
||||||
|
* empty arrays will be treated as if they are associative arrays.
|
||||||
|
*
|
||||||
|
* @param array<mixed> $arr
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function isAssoc(array $arr): bool
|
||||||
|
{
|
||||||
|
return !count($arr) || count(array_filter(array_keys($arr), 'is_string')) == count($arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge contents from one associtative array to another
|
||||||
|
*
|
||||||
|
* @param mixed $to
|
||||||
|
* @param mixed $from
|
||||||
|
* @param DataInterface::PRESERVE|DataInterface::REPLACE|DataInterface::MERGE $mode
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
public static function mergeAssocArray($to, $from, int $mode = DataInterface::REPLACE)
|
||||||
|
{
|
||||||
|
if ($mode === DataInterface::MERGE && self::isList($to) && self::isList($from)) {
|
||||||
|
return array_merge($to, $from);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($from) && is_array($to)) {
|
||||||
|
foreach ($from as $k => $v) {
|
||||||
|
if (!isset($to[$k])) {
|
||||||
|
$to[$k] = $v;
|
||||||
|
} else {
|
||||||
|
$to[$k] = self::mergeAssocArray($to[$k], $v, $mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $to;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $mode === DataInterface::PRESERVE ? $to : $from;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*
|
||||||
|
* @psalm-pure
|
||||||
|
*/
|
||||||
|
private static function isList($value): bool
|
||||||
|
{
|
||||||
|
return is_array($value) && array_values($value) === $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+19
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2006-2015 Doctrine Project
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
# Doctrine Inflector
|
||||||
|
|
||||||
|
Doctrine Inflector is a small library that can perform string manipulations
|
||||||
|
with regard to uppercase/lowercase and singular/plural forms of words.
|
||||||
|
|
||||||
|
[](https://github.com/doctrine/inflector/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A4.0.x)
|
||||||
|
[](https://codecov.io/gh/doctrine/inflector/branch/2.0.x)
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "doctrine/inflector",
|
||||||
|
"type": "library",
|
||||||
|
"description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.",
|
||||||
|
"keywords": ["php", "strings", "words", "manipulation", "inflector", "inflection", "uppercase", "lowercase", "singular", "plural"],
|
||||||
|
"homepage": "https://www.doctrine-project.org/projects/inflector.html",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"},
|
||||||
|
{"name": "Roman Borschel", "email": "roman@code-factory.org"},
|
||||||
|
{"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"},
|
||||||
|
{"name": "Jonathan Wage", "email": "jonwage@gmail.com"},
|
||||||
|
{"name": "Johannes Schmitt", "email": "schmittjoh@gmail.com"}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^7.2 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"doctrine/coding-standard": "^11.0",
|
||||||
|
"phpstan/phpstan": "^1.8",
|
||||||
|
"phpstan/phpstan-phpunit": "^1.1",
|
||||||
|
"phpstan/phpstan-strict-rules": "^1.3",
|
||||||
|
"phpunit/phpunit": "^8.5 || ^9.5",
|
||||||
|
"vimeo/psalm": "^4.25 || ^5.4"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Doctrine\\Inflector\\": "lib/Doctrine/Inflector"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Doctrine\\Tests\\Inflector\\": "tests/Doctrine/Tests/Inflector"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"dealerdirect/phpcodesniffer-composer-installer": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+226
@@ -0,0 +1,226 @@
|
|||||||
|
Introduction
|
||||||
|
============
|
||||||
|
|
||||||
|
The Doctrine Inflector has methods for inflecting text. The features include pluralization,
|
||||||
|
singularization, converting between camelCase and under_score and capitalizing
|
||||||
|
words.
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
You can install the Inflector with composer:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ composer require doctrine/inflector
|
||||||
|
|
||||||
|
Usage
|
||||||
|
=====
|
||||||
|
|
||||||
|
Using the inflector is easy, you can create a new ``Doctrine\Inflector\Inflector`` instance by using
|
||||||
|
the ``Doctrine\Inflector\InflectorFactory`` class:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
use Doctrine\Inflector\InflectorFactory;
|
||||||
|
|
||||||
|
$inflector = InflectorFactory::create()->build();
|
||||||
|
|
||||||
|
By default it will create an English inflector. If you want to use another language, just pass the language
|
||||||
|
you want to create an inflector for to the ``createForLanguage()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
use Doctrine\Inflector\InflectorFactory;
|
||||||
|
use Doctrine\Inflector\Language;
|
||||||
|
|
||||||
|
$inflector = InflectorFactory::createForLanguage(Language::SPANISH)->build();
|
||||||
|
|
||||||
|
The supported languages are as follows:
|
||||||
|
|
||||||
|
- ``Language::ENGLISH``
|
||||||
|
- ``Language::FRENCH``
|
||||||
|
- ``Language::NORWEGIAN_BOKMAL``
|
||||||
|
- ``Language::PORTUGUESE``
|
||||||
|
- ``Language::SPANISH``
|
||||||
|
- ``Language::TURKISH``
|
||||||
|
|
||||||
|
If you want to manually construct the inflector instead of using a factory, you can do so like this:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
use Doctrine\Inflector\CachedWordInflector;
|
||||||
|
use Doctrine\Inflector\RulesetInflector;
|
||||||
|
use Doctrine\Inflector\Rules\English;
|
||||||
|
|
||||||
|
$inflector = new Inflector(
|
||||||
|
new CachedWordInflector(new RulesetInflector(
|
||||||
|
English\Rules::getSingularRuleset()
|
||||||
|
)),
|
||||||
|
new CachedWordInflector(new RulesetInflector(
|
||||||
|
English\Rules::getPluralRuleset()
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
Adding Languages
|
||||||
|
----------------
|
||||||
|
|
||||||
|
If you are interested in adding support for your language, take a look at the other languages defined in the
|
||||||
|
``Doctrine\Inflector\Rules`` namespace and the tests located in ``Doctrine\Tests\Inflector\Rules``. You can copy
|
||||||
|
one of the languages and update the rules for your language.
|
||||||
|
|
||||||
|
Once you have done this, send a pull request to the ``doctrine/inflector`` repository with the additions.
|
||||||
|
|
||||||
|
Custom Setup
|
||||||
|
============
|
||||||
|
|
||||||
|
If you want to setup custom singular and plural rules, you can configure these in the factory:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
use Doctrine\Inflector\InflectorFactory;
|
||||||
|
use Doctrine\Inflector\Rules\Pattern;
|
||||||
|
use Doctrine\Inflector\Rules\Patterns;
|
||||||
|
use Doctrine\Inflector\Rules\Ruleset;
|
||||||
|
use Doctrine\Inflector\Rules\Substitution;
|
||||||
|
use Doctrine\Inflector\Rules\Substitutions;
|
||||||
|
use Doctrine\Inflector\Rules\Transformation;
|
||||||
|
use Doctrine\Inflector\Rules\Transformations;
|
||||||
|
use Doctrine\Inflector\Rules\Word;
|
||||||
|
|
||||||
|
$inflector = InflectorFactory::create()
|
||||||
|
->withSingularRules(
|
||||||
|
new Ruleset(
|
||||||
|
new Transformations(
|
||||||
|
new Transformation(new Pattern('/^(bil)er$/i'), '\1'),
|
||||||
|
new Transformation(new Pattern('/^(inflec|contribu)tors$/i'), '\1ta')
|
||||||
|
),
|
||||||
|
new Patterns(new Pattern('singulars')),
|
||||||
|
new Substitutions(new Substitution(new Word('spins'), new Word('spinor')))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
->withPluralRules(
|
||||||
|
new Ruleset(
|
||||||
|
new Transformations(
|
||||||
|
new Transformation(new Pattern('^(bil)er$'), '\1'),
|
||||||
|
new Transformation(new Pattern('^(inflec|contribu)tors$'), '\1ta')
|
||||||
|
),
|
||||||
|
new Patterns(new Pattern('noflect'), new Pattern('abtuse')),
|
||||||
|
new Substitutions(
|
||||||
|
new Substitution(new Word('amaze'), new Word('amazable')),
|
||||||
|
new Substitution(new Word('phone'), new Word('phonezes'))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
->build();
|
||||||
|
|
||||||
|
No operation inflector
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
The ``Doctrine\Inflector\NoopWordInflector`` may be used to configure an inflector that doesn't perform any operation for
|
||||||
|
pluralization and/or singularization. If will simply return the input as output.
|
||||||
|
|
||||||
|
This is an implementation of the `Null Object design pattern <https://sourcemaking.com/design_patterns/null_object>`_.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
use Doctrine\Inflector\Inflector;
|
||||||
|
use Doctrine\Inflector\NoopWordInflector;
|
||||||
|
|
||||||
|
$inflector = new Inflector(new NoopWordInflector(), new NoopWordInflector());
|
||||||
|
|
||||||
|
Tableize
|
||||||
|
========
|
||||||
|
|
||||||
|
Converts ``ModelName`` to ``model_name``:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
echo $inflector->tableize('ModelName'); // model_name
|
||||||
|
|
||||||
|
Classify
|
||||||
|
========
|
||||||
|
|
||||||
|
Converts ``model_name`` to ``ModelName``:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
echo $inflector->classify('model_name'); // ModelName
|
||||||
|
|
||||||
|
Camelize
|
||||||
|
========
|
||||||
|
|
||||||
|
This method uses `Classify`_ and then converts the first character to lowercase:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
echo $inflector->camelize('model_name'); // modelName
|
||||||
|
|
||||||
|
Capitalize
|
||||||
|
==========
|
||||||
|
|
||||||
|
Takes a string and capitalizes all of the words, like PHP's built-in
|
||||||
|
``ucwords`` function. This extends that behavior, however, by allowing the
|
||||||
|
word delimiters to be configured, rather than only separating on
|
||||||
|
whitespace.
|
||||||
|
|
||||||
|
Here is an example:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
$string = 'top-o-the-morning to all_of_you!';
|
||||||
|
|
||||||
|
echo $inflector->capitalize($string); // Top-O-The-Morning To All_of_you!
|
||||||
|
|
||||||
|
echo $inflector->capitalize($string, '-_ '); // Top-O-The-Morning To All_Of_You!
|
||||||
|
|
||||||
|
Pluralize
|
||||||
|
=========
|
||||||
|
|
||||||
|
Returns a word in plural form.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
echo $inflector->pluralize('browser'); // browsers
|
||||||
|
|
||||||
|
Singularize
|
||||||
|
===========
|
||||||
|
|
||||||
|
Returns a word in singular form.
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
echo $inflector->singularize('browsers'); // browser
|
||||||
|
|
||||||
|
Urlize
|
||||||
|
======
|
||||||
|
|
||||||
|
Generate a URL friendly string from a string of text:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
echo $inflector->urlize('My first blog post'); // my-first-blog-post
|
||||||
|
|
||||||
|
Unaccent
|
||||||
|
========
|
||||||
|
|
||||||
|
You can unaccent a string of text using the ``unaccent()`` method:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
echo $inflector->unaccent('año'); // ano
|
||||||
|
|
||||||
|
Legacy API
|
||||||
|
==========
|
||||||
|
|
||||||
|
The API present in Inflector 1.x is still available, but will be deprecated in a future release and dropped for 3.0.
|
||||||
|
Support for languages other than English is available in the 2.0 API only.
|
||||||
|
|
||||||
|
Acknowledgements
|
||||||
|
================
|
||||||
|
|
||||||
|
The language rules in this library have been adapted from several different sources, including but not limited to:
|
||||||
|
|
||||||
|
- `Ruby On Rails Inflector <http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html>`_
|
||||||
|
- `ICanBoogie Inflector <https://github.com/ICanBoogie/Inflector>`_
|
||||||
|
- `CakePHP Inflector <https://book.cakephp.org/3.0/en/core-libraries/inflector.html>`_
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Doctrine\Inflector;
|
||||||
|
|
||||||
|
class CachedWordInflector implements WordInflector
|
||||||
|
{
|
||||||
|
/** @var WordInflector */
|
||||||
|
private $wordInflector;
|
||||||
|
|
||||||
|
/** @var string[] */
|
||||||
|
private $cache = [];
|
||||||
|
|
||||||
|
public function __construct(WordInflector $wordInflector)
|
||||||
|
{
|
||||||
|
$this->wordInflector = $wordInflector;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inflect(string $word): string
|
||||||
|
{
|
||||||
|
return $this->cache[$word] ?? $this->cache[$word] = $this->wordInflector->inflect($word);
|
||||||
|
}
|
||||||
|
}
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Doctrine\Inflector;
|
||||||
|
|
||||||
|
use Doctrine\Inflector\Rules\Ruleset;
|
||||||
|
|
||||||
|
use function array_unshift;
|
||||||
|
|
||||||
|
abstract class GenericLanguageInflectorFactory implements LanguageInflectorFactory
|
||||||
|
{
|
||||||
|
/** @var Ruleset[] */
|
||||||
|
private $singularRulesets = [];
|
||||||
|
|
||||||
|
/** @var Ruleset[] */
|
||||||
|
private $pluralRulesets = [];
|
||||||
|
|
||||||
|
final public function __construct()
|
||||||
|
{
|
||||||
|
$this->singularRulesets[] = $this->getSingularRuleset();
|
||||||
|
$this->pluralRulesets[] = $this->getPluralRuleset();
|
||||||
|
}
|
||||||
|
|
||||||
|
final public function build(): Inflector
|
||||||
|
{
|
||||||
|
return new Inflector(
|
||||||
|
new CachedWordInflector(new RulesetInflector(
|
||||||
|
...$this->singularRulesets
|
||||||
|
)),
|
||||||
|
new CachedWordInflector(new RulesetInflector(
|
||||||
|
...$this->pluralRulesets
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final public function withSingularRules(?Ruleset $singularRules, bool $reset = false): LanguageInflectorFactory
|
||||||
|
{
|
||||||
|
if ($reset) {
|
||||||
|
$this->singularRulesets = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($singularRules instanceof Ruleset) {
|
||||||
|
array_unshift($this->singularRulesets, $singularRules);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
final public function withPluralRules(?Ruleset $pluralRules, bool $reset = false): LanguageInflectorFactory
|
||||||
|
{
|
||||||
|
if ($reset) {
|
||||||
|
$this->pluralRulesets = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pluralRules instanceof Ruleset) {
|
||||||
|
array_unshift($this->pluralRulesets, $pluralRules);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected function getSingularRuleset(): Ruleset;
|
||||||
|
|
||||||
|
abstract protected function getPluralRuleset(): Ruleset;
|
||||||
|
}
|
||||||
@@ -0,0 +1,507 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Doctrine\Inflector;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
use function chr;
|
||||||
|
use function function_exists;
|
||||||
|
use function lcfirst;
|
||||||
|
use function mb_strtolower;
|
||||||
|
use function ord;
|
||||||
|
use function preg_match;
|
||||||
|
use function preg_replace;
|
||||||
|
use function sprintf;
|
||||||
|
use function str_replace;
|
||||||
|
use function strlen;
|
||||||
|
use function strtolower;
|
||||||
|
use function strtr;
|
||||||
|
use function trim;
|
||||||
|
use function ucwords;
|
||||||
|
|
||||||
|
class Inflector
|
||||||
|
{
|
||||||
|
private const ACCENTED_CHARACTERS = [
|
||||||
|
'À' => 'A',
|
||||||
|
'Á' => 'A',
|
||||||
|
'Â' => 'A',
|
||||||
|
'Ã' => 'A',
|
||||||
|
'Ä' => 'Ae',
|
||||||
|
'Æ' => 'Ae',
|
||||||
|
'Å' => 'Aa',
|
||||||
|
'æ' => 'a',
|
||||||
|
'Ç' => 'C',
|
||||||
|
'È' => 'E',
|
||||||
|
'É' => 'E',
|
||||||
|
'Ê' => 'E',
|
||||||
|
'Ë' => 'E',
|
||||||
|
'Ì' => 'I',
|
||||||
|
'Í' => 'I',
|
||||||
|
'Î' => 'I',
|
||||||
|
'Ï' => 'I',
|
||||||
|
'Ñ' => 'N',
|
||||||
|
'Ò' => 'O',
|
||||||
|
'Ó' => 'O',
|
||||||
|
'Ô' => 'O',
|
||||||
|
'Õ' => 'O',
|
||||||
|
'Ö' => 'Oe',
|
||||||
|
'Ù' => 'U',
|
||||||
|
'Ú' => 'U',
|
||||||
|
'Û' => 'U',
|
||||||
|
'Ü' => 'Ue',
|
||||||
|
'Ý' => 'Y',
|
||||||
|
'ß' => 'ss',
|
||||||
|
'à' => 'a',
|
||||||
|
'á' => 'a',
|
||||||
|
'â' => 'a',
|
||||||
|
'ã' => 'a',
|
||||||
|
'ä' => 'ae',
|
||||||
|
'å' => 'aa',
|
||||||
|
'ç' => 'c',
|
||||||
|
'è' => 'e',
|
||||||
|
'é' => 'e',
|
||||||
|
'ê' => 'e',
|
||||||
|
'ë' => 'e',
|
||||||
|
'ì' => 'i',
|
||||||
|
'í' => 'i',
|
||||||
|
'î' => 'i',
|
||||||
|
'ï' => 'i',
|
||||||
|
'ñ' => 'n',
|
||||||
|
'ò' => 'o',
|
||||||
|
'ó' => 'o',
|
||||||
|
'ô' => 'o',
|
||||||
|
'õ' => 'o',
|
||||||
|
'ö' => 'oe',
|
||||||
|
'ù' => 'u',
|
||||||
|
'ú' => 'u',
|
||||||
|
'û' => 'u',
|
||||||
|
'ü' => 'ue',
|
||||||
|
'ý' => 'y',
|
||||||
|
'ÿ' => 'y',
|
||||||
|
'Ā' => 'A',
|
||||||
|
'ā' => 'a',
|
||||||
|
'Ă' => 'A',
|
||||||
|
'ă' => 'a',
|
||||||
|
'Ą' => 'A',
|
||||||
|
'ą' => 'a',
|
||||||
|
'Ć' => 'C',
|
||||||
|
'ć' => 'c',
|
||||||
|
'Ĉ' => 'C',
|
||||||
|
'ĉ' => 'c',
|
||||||
|
'Ċ' => 'C',
|
||||||
|
'ċ' => 'c',
|
||||||
|
'Č' => 'C',
|
||||||
|
'č' => 'c',
|
||||||
|
'Ď' => 'D',
|
||||||
|
'ď' => 'd',
|
||||||
|
'Đ' => 'D',
|
||||||
|
'đ' => 'd',
|
||||||
|
'Ē' => 'E',
|
||||||
|
'ē' => 'e',
|
||||||
|
'Ĕ' => 'E',
|
||||||
|
'ĕ' => 'e',
|
||||||
|
'Ė' => 'E',
|
||||||
|
'ė' => 'e',
|
||||||
|
'Ę' => 'E',
|
||||||
|
'ę' => 'e',
|
||||||
|
'Ě' => 'E',
|
||||||
|
'ě' => 'e',
|
||||||
|
'Ĝ' => 'G',
|
||||||
|
'ĝ' => 'g',
|
||||||
|
'Ğ' => 'G',
|
||||||
|
'ğ' => 'g',
|
||||||
|
'Ġ' => 'G',
|
||||||
|
'ġ' => 'g',
|
||||||
|
'Ģ' => 'G',
|
||||||
|
'ģ' => 'g',
|
||||||
|
'Ĥ' => 'H',
|
||||||
|
'ĥ' => 'h',
|
||||||
|
'Ħ' => 'H',
|
||||||
|
'ħ' => 'h',
|
||||||
|
'Ĩ' => 'I',
|
||||||
|
'ĩ' => 'i',
|
||||||
|
'Ī' => 'I',
|
||||||
|
'ī' => 'i',
|
||||||
|
'Ĭ' => 'I',
|
||||||
|
'ĭ' => 'i',
|
||||||
|
'Į' => 'I',
|
||||||
|
'į' => 'i',
|
||||||
|
'İ' => 'I',
|
||||||
|
'ı' => 'i',
|
||||||
|
'IJ' => 'IJ',
|
||||||
|
'ij' => 'ij',
|
||||||
|
'Ĵ' => 'J',
|
||||||
|
'ĵ' => 'j',
|
||||||
|
'Ķ' => 'K',
|
||||||
|
'ķ' => 'k',
|
||||||
|
'ĸ' => 'k',
|
||||||
|
'Ĺ' => 'L',
|
||||||
|
'ĺ' => 'l',
|
||||||
|
'Ļ' => 'L',
|
||||||
|
'ļ' => 'l',
|
||||||
|
'Ľ' => 'L',
|
||||||
|
'ľ' => 'l',
|
||||||
|
'Ŀ' => 'L',
|
||||||
|
'ŀ' => 'l',
|
||||||
|
'Ł' => 'L',
|
||||||
|
'ł' => 'l',
|
||||||
|
'Ń' => 'N',
|
||||||
|
'ń' => 'n',
|
||||||
|
'Ņ' => 'N',
|
||||||
|
'ņ' => 'n',
|
||||||
|
'Ň' => 'N',
|
||||||
|
'ň' => 'n',
|
||||||
|
'ʼn' => 'N',
|
||||||
|
'Ŋ' => 'n',
|
||||||
|
'ŋ' => 'N',
|
||||||
|
'Ō' => 'O',
|
||||||
|
'ō' => 'o',
|
||||||
|
'Ŏ' => 'O',
|
||||||
|
'ŏ' => 'o',
|
||||||
|
'Ő' => 'O',
|
||||||
|
'ő' => 'o',
|
||||||
|
'Œ' => 'OE',
|
||||||
|
'œ' => 'oe',
|
||||||
|
'Ø' => 'O',
|
||||||
|
'ø' => 'o',
|
||||||
|
'Ŕ' => 'R',
|
||||||
|
'ŕ' => 'r',
|
||||||
|
'Ŗ' => 'R',
|
||||||
|
'ŗ' => 'r',
|
||||||
|
'Ř' => 'R',
|
||||||
|
'ř' => 'r',
|
||||||
|
'Ś' => 'S',
|
||||||
|
'ś' => 's',
|
||||||
|
'Ŝ' => 'S',
|
||||||
|
'ŝ' => 's',
|
||||||
|
'Ş' => 'S',
|
||||||
|
'ş' => 's',
|
||||||
|
'Š' => 'S',
|
||||||
|
'š' => 's',
|
||||||
|
'Ţ' => 'T',
|
||||||
|
'ţ' => 't',
|
||||||
|
'Ť' => 'T',
|
||||||
|
'ť' => 't',
|
||||||
|
'Ŧ' => 'T',
|
||||||
|
'ŧ' => 't',
|
||||||
|
'Ũ' => 'U',
|
||||||
|
'ũ' => 'u',
|
||||||
|
'Ū' => 'U',
|
||||||
|
'ū' => 'u',
|
||||||
|
'Ŭ' => 'U',
|
||||||
|
'ŭ' => 'u',
|
||||||
|
'Ů' => 'U',
|
||||||
|
'ů' => 'u',
|
||||||
|
'Ű' => 'U',
|
||||||
|
'ű' => 'u',
|
||||||
|
'Ų' => 'U',
|
||||||
|
'ų' => 'u',
|
||||||
|
'Ŵ' => 'W',
|
||||||
|
'ŵ' => 'w',
|
||||||
|
'Ŷ' => 'Y',
|
||||||
|
'ŷ' => 'y',
|
||||||
|
'Ÿ' => 'Y',
|
||||||
|
'Ź' => 'Z',
|
||||||
|
'ź' => 'z',
|
||||||
|
'Ż' => 'Z',
|
||||||
|
'ż' => 'z',
|
||||||
|
'Ž' => 'Z',
|
||||||
|
'ž' => 'z',
|
||||||
|
'ſ' => 's',
|
||||||
|
'€' => 'E',
|
||||||
|
'£' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var WordInflector */
|
||||||
|
private $singularizer;
|
||||||
|
|
||||||
|
/** @var WordInflector */
|
||||||
|
private $pluralizer;
|
||||||
|
|
||||||
|
public function __construct(WordInflector $singularizer, WordInflector $pluralizer)
|
||||||
|
{
|
||||||
|
$this->singularizer = $singularizer;
|
||||||
|
$this->pluralizer = $pluralizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a word into the format for a Doctrine table name. Converts 'ModelName' to 'model_name'.
|
||||||
|
*/
|
||||||
|
public function tableize(string $word): string
|
||||||
|
{
|
||||||
|
$tableized = preg_replace('~(?<=\\w)([A-Z])~u', '_$1', $word);
|
||||||
|
|
||||||
|
if ($tableized === null) {
|
||||||
|
throw new RuntimeException(sprintf(
|
||||||
|
'preg_replace returned null for value "%s"',
|
||||||
|
$word
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mb_strtolower($tableized);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a word into the format for a Doctrine class name. Converts 'table_name' to 'TableName'.
|
||||||
|
*/
|
||||||
|
public function classify(string $word): string
|
||||||
|
{
|
||||||
|
return str_replace([' ', '_', '-'], '', ucwords($word, ' _-'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Camelizes a word. This uses the classify() method and turns the first character to lowercase.
|
||||||
|
*/
|
||||||
|
public function camelize(string $word): string
|
||||||
|
{
|
||||||
|
return lcfirst($this->classify($word));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uppercases words with configurable delimiters between words.
|
||||||
|
*
|
||||||
|
* Takes a string and capitalizes all of the words, like PHP's built-in
|
||||||
|
* ucwords function. This extends that behavior, however, by allowing the
|
||||||
|
* word delimiters to be configured, rather than only separating on
|
||||||
|
* whitespace.
|
||||||
|
*
|
||||||
|
* Here is an example:
|
||||||
|
* <code>
|
||||||
|
* <?php
|
||||||
|
* $string = 'top-o-the-morning to all_of_you!';
|
||||||
|
* echo $inflector->capitalize($string);
|
||||||
|
* // Top-O-The-Morning To All_of_you!
|
||||||
|
*
|
||||||
|
* echo $inflector->capitalize($string, '-_ ');
|
||||||
|
* // Top-O-The-Morning To All_Of_You!
|
||||||
|
* ?>
|
||||||
|
* </code>
|
||||||
|
*
|
||||||
|
* @param string $string The string to operate on.
|
||||||
|
* @param string $delimiters A list of word separators.
|
||||||
|
*
|
||||||
|
* @return string The string with all delimiter-separated words capitalized.
|
||||||
|
*/
|
||||||
|
public function capitalize(string $string, string $delimiters = " \n\t\r\0\x0B-"): string
|
||||||
|
{
|
||||||
|
return ucwords($string, $delimiters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given string seems like it has utf8 characters in it.
|
||||||
|
*
|
||||||
|
* @param string $string The string to check for utf8 characters in.
|
||||||
|
*/
|
||||||
|
public function seemsUtf8(string $string): bool
|
||||||
|
{
|
||||||
|
for ($i = 0; $i < strlen($string); $i++) {
|
||||||
|
if (ord($string[$i]) < 0x80) {
|
||||||
|
continue; // 0bbbbbbb
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((ord($string[$i]) & 0xE0) === 0xC0) {
|
||||||
|
$n = 1; // 110bbbbb
|
||||||
|
} elseif ((ord($string[$i]) & 0xF0) === 0xE0) {
|
||||||
|
$n = 2; // 1110bbbb
|
||||||
|
} elseif ((ord($string[$i]) & 0xF8) === 0xF0) {
|
||||||
|
$n = 3; // 11110bbb
|
||||||
|
} elseif ((ord($string[$i]) & 0xFC) === 0xF8) {
|
||||||
|
$n = 4; // 111110bb
|
||||||
|
} elseif ((ord($string[$i]) & 0xFE) === 0xFC) {
|
||||||
|
$n = 5; // 1111110b
|
||||||
|
} else {
|
||||||
|
return false; // Does not match any model
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($j = 0; $j < $n; $j++) { // n bytes matching 10bbbbbb follow ?
|
||||||
|
if (++$i === strlen($string) || ((ord($string[$i]) & 0xC0) !== 0x80)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove any illegal characters, accents, etc.
|
||||||
|
*
|
||||||
|
* @param string $string String to unaccent
|
||||||
|
*
|
||||||
|
* @return string Unaccented string
|
||||||
|
*/
|
||||||
|
public function unaccent(string $string): string
|
||||||
|
{
|
||||||
|
if (preg_match('/[\x80-\xff]/', $string) === false) {
|
||||||
|
return $string;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->seemsUtf8($string)) {
|
||||||
|
$string = strtr($string, self::ACCENTED_CHARACTERS);
|
||||||
|
} else {
|
||||||
|
$characters = [];
|
||||||
|
|
||||||
|
// Assume ISO-8859-1 if not UTF-8
|
||||||
|
$characters['in'] =
|
||||||
|
chr(128)
|
||||||
|
. chr(131)
|
||||||
|
. chr(138)
|
||||||
|
. chr(142)
|
||||||
|
. chr(154)
|
||||||
|
. chr(158)
|
||||||
|
. chr(159)
|
||||||
|
. chr(162)
|
||||||
|
. chr(165)
|
||||||
|
. chr(181)
|
||||||
|
. chr(192)
|
||||||
|
. chr(193)
|
||||||
|
. chr(194)
|
||||||
|
. chr(195)
|
||||||
|
. chr(196)
|
||||||
|
. chr(197)
|
||||||
|
. chr(199)
|
||||||
|
. chr(200)
|
||||||
|
. chr(201)
|
||||||
|
. chr(202)
|
||||||
|
. chr(203)
|
||||||
|
. chr(204)
|
||||||
|
. chr(205)
|
||||||
|
. chr(206)
|
||||||
|
. chr(207)
|
||||||
|
. chr(209)
|
||||||
|
. chr(210)
|
||||||
|
. chr(211)
|
||||||
|
. chr(212)
|
||||||
|
. chr(213)
|
||||||
|
. chr(214)
|
||||||
|
. chr(216)
|
||||||
|
. chr(217)
|
||||||
|
. chr(218)
|
||||||
|
. chr(219)
|
||||||
|
. chr(220)
|
||||||
|
. chr(221)
|
||||||
|
. chr(224)
|
||||||
|
. chr(225)
|
||||||
|
. chr(226)
|
||||||
|
. chr(227)
|
||||||
|
. chr(228)
|
||||||
|
. chr(229)
|
||||||
|
. chr(231)
|
||||||
|
. chr(232)
|
||||||
|
. chr(233)
|
||||||
|
. chr(234)
|
||||||
|
. chr(235)
|
||||||
|
. chr(236)
|
||||||
|
. chr(237)
|
||||||
|
. chr(238)
|
||||||
|
. chr(239)
|
||||||
|
. chr(241)
|
||||||
|
. chr(242)
|
||||||
|
. chr(243)
|
||||||
|
. chr(244)
|
||||||
|
. chr(245)
|
||||||
|
. chr(246)
|
||||||
|
. chr(248)
|
||||||
|
. chr(249)
|
||||||
|
. chr(250)
|
||||||
|
. chr(251)
|
||||||
|
. chr(252)
|
||||||
|
. chr(253)
|
||||||
|
. chr(255);
|
||||||
|
|
||||||
|
$characters['out'] = 'EfSZszYcYuAAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyy';
|
||||||
|
|
||||||
|
$string = strtr($string, $characters['in'], $characters['out']);
|
||||||
|
|
||||||
|
$doubleChars = [];
|
||||||
|
|
||||||
|
$doubleChars['in'] = [
|
||||||
|
chr(140),
|
||||||
|
chr(156),
|
||||||
|
chr(198),
|
||||||
|
chr(208),
|
||||||
|
chr(222),
|
||||||
|
chr(223),
|
||||||
|
chr(230),
|
||||||
|
chr(240),
|
||||||
|
chr(254),
|
||||||
|
];
|
||||||
|
|
||||||
|
$doubleChars['out'] = ['OE', 'oe', 'AE', 'DH', 'TH', 'ss', 'ae', 'dh', 'th'];
|
||||||
|
|
||||||
|
$string = str_replace($doubleChars['in'], $doubleChars['out'], $string);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert any passed string to a url friendly string.
|
||||||
|
* Converts 'My first blog post' to 'my-first-blog-post'
|
||||||
|
*
|
||||||
|
* @param string $string String to urlize.
|
||||||
|
*
|
||||||
|
* @return string Urlized string.
|
||||||
|
*/
|
||||||
|
public function urlize(string $string): string
|
||||||
|
{
|
||||||
|
// Remove all non url friendly characters with the unaccent function
|
||||||
|
$unaccented = $this->unaccent($string);
|
||||||
|
|
||||||
|
if (function_exists('mb_strtolower')) {
|
||||||
|
$lowered = mb_strtolower($unaccented);
|
||||||
|
} else {
|
||||||
|
$lowered = strtolower($unaccented);
|
||||||
|
}
|
||||||
|
|
||||||
|
$replacements = [
|
||||||
|
'/\W/' => ' ',
|
||||||
|
'/([A-Z]+)([A-Z][a-z])/' => '\1_\2',
|
||||||
|
'/([a-z\d])([A-Z])/' => '\1_\2',
|
||||||
|
'/[^A-Z^a-z^0-9^\/]+/' => '-',
|
||||||
|
];
|
||||||
|
|
||||||
|
$urlized = $lowered;
|
||||||
|
|
||||||
|
foreach ($replacements as $pattern => $replacement) {
|
||||||
|
$replaced = preg_replace($pattern, $replacement, $urlized);
|
||||||
|
|
||||||
|
if ($replaced === null) {
|
||||||
|
throw new RuntimeException(sprintf(
|
||||||
|
'preg_replace returned null for value "%s"',
|
||||||
|
$urlized
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$urlized = $replaced;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($urlized, '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a word in singular form.
|
||||||
|
*
|
||||||
|
* @param string $word The word in plural form.
|
||||||
|
*
|
||||||
|
* @return string The word in singular form.
|
||||||
|
*/
|
||||||
|
public function singularize(string $word): string
|
||||||
|
{
|
||||||
|
return $this->singularizer->inflect($word);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a word in plural form.
|
||||||
|
*
|
||||||
|
* @param string $word The word in singular form.
|
||||||
|
*
|
||||||
|
* @return string The word in plural form.
|
||||||
|
*/
|
||||||
|
public function pluralize(string $word): string
|
||||||
|
{
|
||||||
|
return $this->pluralizer->inflect($word);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Doctrine\Inflector;
|
||||||
|
|
||||||
|
use Doctrine\Inflector\Rules\English;
|
||||||
|
use Doctrine\Inflector\Rules\French;
|
||||||
|
use Doctrine\Inflector\Rules\NorwegianBokmal;
|
||||||
|
use Doctrine\Inflector\Rules\Portuguese;
|
||||||
|
use Doctrine\Inflector\Rules\Spanish;
|
||||||
|
use Doctrine\Inflector\Rules\Turkish;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
|
final class InflectorFactory
|
||||||
|
{
|
||||||
|
public static function create(): LanguageInflectorFactory
|
||||||
|
{
|
||||||
|
return self::createForLanguage(Language::ENGLISH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function createForLanguage(string $language): LanguageInflectorFactory
|
||||||
|
{
|
||||||
|
switch ($language) {
|
||||||
|
case Language::ENGLISH:
|
||||||
|
return new English\InflectorFactory();
|
||||||
|
|
||||||
|
case Language::FRENCH:
|
||||||
|
return new French\InflectorFactory();
|
||||||
|
|
||||||
|
case Language::NORWEGIAN_BOKMAL:
|
||||||
|
return new NorwegianBokmal\InflectorFactory();
|
||||||
|
|
||||||
|
case Language::PORTUGUESE:
|
||||||
|
return new Portuguese\InflectorFactory();
|
||||||
|
|
||||||
|
case Language::SPANISH:
|
||||||
|
return new Spanish\InflectorFactory();
|
||||||
|
|
||||||
|
case Language::TURKISH:
|
||||||
|
return new Turkish\InflectorFactory();
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new InvalidArgumentException(sprintf(
|
||||||
|
'Language "%s" is not supported.',
|
||||||
|
$language
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Doctrine\Inflector;
|
||||||
|
|
||||||
|
final class Language
|
||||||
|
{
|
||||||
|
public const ENGLISH = 'english';
|
||||||
|
public const FRENCH = 'french';
|
||||||
|
public const NORWEGIAN_BOKMAL = 'norwegian-bokmal';
|
||||||
|
public const PORTUGUESE = 'portuguese';
|
||||||
|
public const SPANISH = 'spanish';
|
||||||
|
public const TURKISH = 'turkish';
|
||||||
|
|
||||||
|
private function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Doctrine\Inflector;
|
||||||
|
|
||||||
|
use Doctrine\Inflector\Rules\Ruleset;
|
||||||
|
|
||||||
|
interface LanguageInflectorFactory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Applies custom rules for singularisation
|
||||||
|
*
|
||||||
|
* @param bool $reset If true, will unset default inflections for all new rules
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function withSingularRules(?Ruleset $singularRules, bool $reset = false): self;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies custom rules for pluralisation
|
||||||
|
*
|
||||||
|
* @param bool $reset If true, will unset default inflections for all new rules
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function withPluralRules(?Ruleset $pluralRules, bool $reset = false): self;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the inflector instance with all applicable rules
|
||||||
|
*/
|
||||||
|
public function build(): Inflector;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Doctrine\Inflector;
|
||||||
|
|
||||||
|
class NoopWordInflector implements WordInflector
|
||||||
|
{
|
||||||
|
public function inflect(string $word): string
|
||||||
|
{
|
||||||
|
return $word;
|
||||||
|
}
|
||||||
|
}
|
||||||
+184
@@ -0,0 +1,184 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Doctrine\Inflector\Rules\English;
|
||||||
|
|
||||||
|
use Doctrine\Inflector\Rules\Pattern;
|
||||||
|
use Doctrine\Inflector\Rules\Substitution;
|
||||||
|
use Doctrine\Inflector\Rules\Transformation;
|
||||||
|
use Doctrine\Inflector\Rules\Word;
|
||||||
|
|
||||||
|
class Inflectible
|
||||||
|
{
|
||||||
|
/** @return Transformation[] */
|
||||||
|
public static function getSingular(): iterable
|
||||||
|
{
|
||||||
|
yield new Transformation(new Pattern('(s)tatuses$'), '\1\2tatus');
|
||||||
|
yield new Transformation(new Pattern('(s)tatus$'), '\1\2tatus');
|
||||||
|
yield new Transformation(new Pattern('(c)ampus$'), '\1\2ampus');
|
||||||
|
yield new Transformation(new Pattern('^(.*)(menu)s$'), '\1\2');
|
||||||
|
yield new Transformation(new Pattern('(quiz)zes$'), '\\1');
|
||||||
|
yield new Transformation(new Pattern('(matr)ices$'), '\1ix');
|
||||||
|
yield new Transformation(new Pattern('(vert|ind)ices$'), '\1ex');
|
||||||
|
yield new Transformation(new Pattern('^(ox)en'), '\1');
|
||||||
|
yield new Transformation(new Pattern('(alias)(es)*$'), '\1');
|
||||||
|
yield new Transformation(new Pattern('(buffal|her|potat|tomat|volcan)oes$'), '\1o');
|
||||||
|
yield new Transformation(new Pattern('(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$'), '\1us');
|
||||||
|
yield new Transformation(new Pattern('([ftw]ax)es'), '\1');
|
||||||
|
yield new Transformation(new Pattern('(analys|ax|cris|test|thes)es$'), '\1is');
|
||||||
|
yield new Transformation(new Pattern('(shoe|slave)s$'), '\1');
|
||||||
|
yield new Transformation(new Pattern('(o)es$'), '\1');
|
||||||
|
yield new Transformation(new Pattern('ouses$'), 'ouse');
|
||||||
|
yield new Transformation(new Pattern('([^a])uses$'), '\1us');
|
||||||
|
yield new Transformation(new Pattern('([m|l])ice$'), '\1ouse');
|
||||||
|
yield new Transformation(new Pattern('(x|ch|ss|sh)es$'), '\1');
|
||||||
|
yield new Transformation(new Pattern('(m)ovies$'), '\1\2ovie');
|
||||||
|
yield new Transformation(new Pattern('(s)eries$'), '\1\2eries');
|
||||||
|
yield new Transformation(new Pattern('([^aeiouy]|qu)ies$'), '\1y');
|
||||||
|
yield new Transformation(new Pattern('([lr])ves$'), '\1f');
|
||||||
|
yield new Transformation(new Pattern('(tive)s$'), '\1');
|
||||||
|
yield new Transformation(new Pattern('(hive)s$'), '\1');
|
||||||
|
yield new Transformation(new Pattern('(drive)s$'), '\1');
|
||||||
|
yield new Transformation(new Pattern('(dive)s$'), '\1');
|
||||||
|
yield new Transformation(new Pattern('(olive)s$'), '\1');
|
||||||
|
yield new Transformation(new Pattern('([^fo])ves$'), '\1fe');
|
||||||
|
yield new Transformation(new Pattern('(^analy)ses$'), '\1sis');
|
||||||
|
yield new Transformation(new Pattern('(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$'), '\1\2sis');
|
||||||
|
yield new Transformation(new Pattern('(tax)a$'), '\1on');
|
||||||
|
yield new Transformation(new Pattern('(c)riteria$'), '\1riterion');
|
||||||
|
yield new Transformation(new Pattern('([ti])a(?<!regatta)$'), '\1um');
|
||||||
|
yield new Transformation(new Pattern('(p)eople$'), '\1\2erson');
|
||||||
|
yield new Transformation(new Pattern('(m)en$'), '\1an');
|
||||||
|
yield new Transformation(new Pattern('(c)hildren$'), '\1\2hild');
|
||||||
|
yield new Transformation(new Pattern('(f)eet$'), '\1oot');
|
||||||
|
yield new Transformation(new Pattern('(n)ews$'), '\1\2ews');
|
||||||
|
yield new Transformation(new Pattern('eaus$'), 'eau');
|
||||||
|
yield new Transformation(new Pattern('^tights$'), 'tights');
|
||||||
|
yield new Transformation(new Pattern('^shorts$'), 'shorts');
|
||||||
|
yield new Transformation(new Pattern('s$'), '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return Transformation[] */
|
||||||
|
public static function getPlural(): iterable
|
||||||
|
{
|
||||||
|
yield new Transformation(new Pattern('(s)tatus$'), '\1\2tatuses');
|
||||||
|
yield new Transformation(new Pattern('(quiz)$'), '\1zes');
|
||||||
|
yield new Transformation(new Pattern('^(ox)$'), '\1\2en');
|
||||||
|
yield new Transformation(new Pattern('([m|l])ouse$'), '\1ice');
|
||||||
|
yield new Transformation(new Pattern('(matr|vert|ind)(ix|ex)$'), '\1ices');
|
||||||
|
yield new Transformation(new Pattern('(x|ch|ss|sh)$'), '\1es');
|
||||||
|
yield new Transformation(new Pattern('([^aeiouy]|qu)y$'), '\1ies');
|
||||||
|
yield new Transformation(new Pattern('(hive|gulf)$'), '\1s');
|
||||||
|
yield new Transformation(new Pattern('(?:([^f])fe|([lr])f)$'), '\1\2ves');
|
||||||
|
yield new Transformation(new Pattern('sis$'), 'ses');
|
||||||
|
yield new Transformation(new Pattern('([ti])um$'), '\1a');
|
||||||
|
yield new Transformation(new Pattern('(tax)on$'), '\1a');
|
||||||
|
yield new Transformation(new Pattern('(c)riterion$'), '\1riteria');
|
||||||
|
yield new Transformation(new Pattern('(p)erson$'), '\1eople');
|
||||||
|
yield new Transformation(new Pattern('(m)an$'), '\1en');
|
||||||
|
yield new Transformation(new Pattern('(c)hild$'), '\1hildren');
|
||||||
|
yield new Transformation(new Pattern('(f)oot$'), '\1eet');
|
||||||
|
yield new Transformation(new Pattern('(buffal|her|potat|tomat|volcan)o$'), '\1\2oes');
|
||||||
|
yield new Transformation(new Pattern('(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|vir)us$'), '\1i');
|
||||||
|
yield new Transformation(new Pattern('us$'), 'uses');
|
||||||
|
yield new Transformation(new Pattern('(alias)$'), '\1es');
|
||||||
|
yield new Transformation(new Pattern('(analys|ax|cris|test|thes)is$'), '\1es');
|
||||||
|
yield new Transformation(new Pattern('s$'), 's');
|
||||||
|
yield new Transformation(new Pattern('^$'), '');
|
||||||
|
yield new Transformation(new Pattern('$'), 's');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return Substitution[] */
|
||||||
|
public static function getIrregular(): iterable
|
||||||
|
{
|
||||||
|
yield new Substitution(new Word('atlas'), new Word('atlases'));
|
||||||
|
yield new Substitution(new Word('axis'), new Word('axes'));
|
||||||
|
yield new Substitution(new Word('axe'), new Word('axes'));
|
||||||
|
yield new Substitution(new Word('beef'), new Word('beefs'));
|
||||||
|
yield new Substitution(new Word('blouse'), new Word('blouses'));
|
||||||
|
yield new Substitution(new Word('brother'), new Word('brothers'));
|
||||||
|
yield new Substitution(new Word('cafe'), new Word('cafes'));
|
||||||
|
yield new Substitution(new Word('cave'), new Word('caves'));
|
||||||
|
yield new Substitution(new Word('chateau'), new Word('chateaux'));
|
||||||
|
yield new Substitution(new Word('niveau'), new Word('niveaux'));
|
||||||
|
yield new Substitution(new Word('child'), new Word('children'));
|
||||||
|
yield new Substitution(new Word('canvas'), new Word('canvases'));
|
||||||
|
yield new Substitution(new Word('cookie'), new Word('cookies'));
|
||||||
|
yield new Substitution(new Word('brownie'), new Word('brownies'));
|
||||||
|
yield new Substitution(new Word('corpus'), new Word('corpuses'));
|
||||||
|
yield new Substitution(new Word('cow'), new Word('cows'));
|
||||||
|
yield new Substitution(new Word('criterion'), new Word('criteria'));
|
||||||
|
yield new Substitution(new Word('curriculum'), new Word('curricula'));
|
||||||
|
yield new Substitution(new Word('demo'), new Word('demos'));
|
||||||
|
yield new Substitution(new Word('domino'), new Word('dominoes'));
|
||||||
|
yield new Substitution(new Word('echo'), new Word('echoes'));
|
||||||
|
yield new Substitution(new Word('epoch'), new Word('epochs'));
|
||||||
|
yield new Substitution(new Word('foot'), new Word('feet'));
|
||||||
|
yield new Substitution(new Word('fungus'), new Word('fungi'));
|
||||||
|
yield new Substitution(new Word('ganglion'), new Word('ganglions'));
|
||||||
|
yield new Substitution(new Word('gas'), new Word('gases'));
|
||||||
|
yield new Substitution(new Word('genie'), new Word('genies'));
|
||||||
|
yield new Substitution(new Word('genus'), new Word('genera'));
|
||||||
|
yield new Substitution(new Word('goose'), new Word('geese'));
|
||||||
|
yield new Substitution(new Word('graffito'), new Word('graffiti'));
|
||||||
|
yield new Substitution(new Word('hippopotamus'), new Word('hippopotami'));
|
||||||
|
yield new Substitution(new Word('hoof'), new Word('hoofs'));
|
||||||
|
yield new Substitution(new Word('human'), new Word('humans'));
|
||||||
|
yield new Substitution(new Word('iris'), new Word('irises'));
|
||||||
|
yield new Substitution(new Word('larva'), new Word('larvae'));
|
||||||
|
yield new Substitution(new Word('leaf'), new Word('leaves'));
|
||||||
|
yield new Substitution(new Word('lens'), new Word('lenses'));
|
||||||
|
yield new Substitution(new Word('loaf'), new Word('loaves'));
|
||||||
|
yield new Substitution(new Word('man'), new Word('men'));
|
||||||
|
yield new Substitution(new Word('medium'), new Word('media'));
|
||||||
|
yield new Substitution(new Word('memorandum'), new Word('memoranda'));
|
||||||
|
yield new Substitution(new Word('money'), new Word('monies'));
|
||||||
|
yield new Substitution(new Word('mongoose'), new Word('mongooses'));
|
||||||
|
yield new Substitution(new Word('motto'), new Word('mottoes'));
|
||||||
|
yield new Substitution(new Word('move'), new Word('moves'));
|
||||||
|
yield new Substitution(new Word('mythos'), new Word('mythoi'));
|
||||||
|
yield new Substitution(new Word('niche'), new Word('niches'));
|
||||||
|
yield new Substitution(new Word('nucleus'), new Word('nuclei'));
|
||||||
|
yield new Substitution(new Word('numen'), new Word('numina'));
|
||||||
|
yield new Substitution(new Word('occiput'), new Word('occiputs'));
|
||||||
|
yield new Substitution(new Word('octopus'), new Word('octopuses'));
|
||||||
|
yield new Substitution(new Word('opus'), new Word('opuses'));
|
||||||
|
yield new Substitution(new Word('ox'), new Word('oxen'));
|
||||||
|
yield new Substitution(new Word('passerby'), new Word('passersby'));
|
||||||
|
yield new Substitution(new Word('penis'), new Word('penises'));
|
||||||
|
yield new Substitution(new Word('person'), new Word('people'));
|
||||||
|
yield new Substitution(new Word('plateau'), new Word('plateaux'));
|
||||||
|
yield new Substitution(new Word('runner-up'), new Word('runners-up'));
|
||||||
|
yield new Substitution(new Word('safe'), new Word('safes'));
|
||||||
|
yield new Substitution(new Word('sex'), new Word('sexes'));
|
||||||
|
yield new Substitution(new Word('sieve'), new Word('sieves'));
|
||||||
|
yield new Substitution(new Word('soliloquy'), new Word('soliloquies'));
|
||||||
|
yield new Substitution(new Word('son-in-law'), new Word('sons-in-law'));
|
||||||
|
yield new Substitution(new Word('syllabus'), new Word('syllabi'));
|
||||||
|
yield new Substitution(new Word('testis'), new Word('testes'));
|
||||||
|
yield new Substitution(new Word('thief'), new Word('thieves'));
|
||||||
|
yield new Substitution(new Word('tooth'), new Word('teeth'));
|
||||||
|
yield new Substitution(new Word('tornado'), new Word('tornadoes'));
|
||||||
|
yield new Substitution(new Word('trilby'), new Word('trilbys'));
|
||||||
|
yield new Substitution(new Word('turf'), new Word('turfs'));
|
||||||
|
yield new Substitution(new Word('valve'), new Word('valves'));
|
||||||
|
yield new Substitution(new Word('volcano'), new Word('volcanoes'));
|
||||||
|
yield new Substitution(new Word('abuse'), new Word('abuses'));
|
||||||
|
yield new Substitution(new Word('avalanche'), new Word('avalanches'));
|
||||||
|
yield new Substitution(new Word('cache'), new Word('caches'));
|
||||||
|
yield new Substitution(new Word('criterion'), new Word('criteria'));
|
||||||
|
yield new Substitution(new Word('curve'), new Word('curves'));
|
||||||
|
yield new Substitution(new Word('emphasis'), new Word('emphases'));
|
||||||
|
yield new Substitution(new Word('foe'), new Word('foes'));
|
||||||
|
yield new Substitution(new Word('grave'), new Word('graves'));
|
||||||
|
yield new Substitution(new Word('hoax'), new Word('hoaxes'));
|
||||||
|
yield new Substitution(new Word('medium'), new Word('media'));
|
||||||
|
yield new Substitution(new Word('neurosis'), new Word('neuroses'));
|
||||||
|
yield new Substitution(new Word('save'), new Word('saves'));
|
||||||
|
yield new Substitution(new Word('wave'), new Word('waves'));
|
||||||
|
yield new Substitution(new Word('oasis'), new Word('oases'));
|
||||||
|
yield new Substitution(new Word('valve'), new Word('valves'));
|
||||||
|
yield new Substitution(new Word('zombie'), new Word('zombies'));
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Doctrine\Inflector\Rules\English;
|
||||||
|
|
||||||
|
use Doctrine\Inflector\GenericLanguageInflectorFactory;
|
||||||
|
use Doctrine\Inflector\Rules\Ruleset;
|
||||||
|
|
||||||
|
final class InflectorFactory extends GenericLanguageInflectorFactory
|
||||||
|
{
|
||||||
|
protected function getSingularRuleset(): Ruleset
|
||||||
|
{
|
||||||
|
return Rules::getSingularRuleset();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getPluralRuleset(): Ruleset
|
||||||
|
{
|
||||||
|
return Rules::getPluralRuleset();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Doctrine\Inflector\Rules\English;
|
||||||
|
|
||||||
|
use Doctrine\Inflector\Rules\Patterns;
|
||||||
|
use Doctrine\Inflector\Rules\Ruleset;
|
||||||
|
use Doctrine\Inflector\Rules\Substitutions;
|
||||||
|
use Doctrine\Inflector\Rules\Transformations;
|
||||||
|
|
||||||
|
final class Rules
|
||||||
|
{
|
||||||
|
public static function getSingularRuleset(): Ruleset
|
||||||
|
{
|
||||||
|
return new Ruleset(
|
||||||
|
new Transformations(...Inflectible::getSingular()),
|
||||||
|
new Patterns(...Uninflected::getSingular()),
|
||||||
|
(new Substitutions(...Inflectible::getIrregular()))->getFlippedSubstitutions()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPluralRuleset(): Ruleset
|
||||||
|
{
|
||||||
|
return new Ruleset(
|
||||||
|
new Transformations(...Inflectible::getPlural()),
|
||||||
|
new Patterns(...Uninflected::getPlural()),
|
||||||
|
new Substitutions(...Inflectible::getIrregular())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+189
@@ -0,0 +1,189 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Doctrine\Inflector\Rules\English;
|
||||||
|
|
||||||
|
use Doctrine\Inflector\Rules\Pattern;
|
||||||
|
|
||||||
|
final class Uninflected
|
||||||
|
{
|
||||||
|
/** @return Pattern[] */
|
||||||
|
public static function getSingular(): iterable
|
||||||
|
{
|
||||||
|
yield from self::getDefault();
|
||||||
|
|
||||||
|
yield new Pattern('.*ss');
|
||||||
|
yield new Pattern('clothes');
|
||||||
|
yield new Pattern('data');
|
||||||
|
yield new Pattern('fascia');
|
||||||
|
yield new Pattern('fuchsia');
|
||||||
|
yield new Pattern('galleria');
|
||||||
|
yield new Pattern('mafia');
|
||||||
|
yield new Pattern('militia');
|
||||||
|
yield new Pattern('pants');
|
||||||
|
yield new Pattern('petunia');
|
||||||
|
yield new Pattern('sepia');
|
||||||
|
yield new Pattern('trivia');
|
||||||
|
yield new Pattern('utopia');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return Pattern[] */
|
||||||
|
public static function getPlural(): iterable
|
||||||
|
{
|
||||||
|
yield from self::getDefault();
|
||||||
|
|
||||||
|
yield new Pattern('people');
|
||||||
|
yield new Pattern('trivia');
|
||||||
|
yield new Pattern('\w+ware$');
|
||||||
|
yield new Pattern('media');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return Pattern[] */
|
||||||
|
private static function getDefault(): iterable
|
||||||
|
{
|
||||||
|
yield new Pattern('\w+media');
|
||||||
|
yield new Pattern('advice');
|
||||||
|
yield new Pattern('aircraft');
|
||||||
|
yield new Pattern('amoyese');
|
||||||
|
yield new Pattern('art');
|
||||||
|
yield new Pattern('audio');
|
||||||
|
yield new Pattern('baggage');
|
||||||
|
yield new Pattern('bison');
|
||||||
|
yield new Pattern('borghese');
|
||||||
|
yield new Pattern('bream');
|
||||||
|
yield new Pattern('breeches');
|
||||||
|
yield new Pattern('britches');
|
||||||
|
yield new Pattern('buffalo');
|
||||||
|
yield new Pattern('butter');
|
||||||
|
yield new Pattern('cantus');
|
||||||
|
yield new Pattern('carp');
|
||||||
|
yield new Pattern('cattle');
|
||||||
|
yield new Pattern('chassis');
|
||||||
|
yield new Pattern('clippers');
|
||||||
|
yield new Pattern('clothing');
|
||||||
|
yield new Pattern('coal');
|
||||||
|
yield new Pattern('cod');
|
||||||
|
yield new Pattern('coitus');
|
||||||
|
yield new Pattern('compensation');
|
||||||
|
yield new Pattern('congoese');
|
||||||
|
yield new Pattern('contretemps');
|
||||||
|
yield new Pattern('coreopsis');
|
||||||
|
yield new Pattern('corps');
|
||||||
|
yield new Pattern('cotton');
|
||||||
|
yield new Pattern('data');
|
||||||
|
yield new Pattern('debris');
|
||||||
|
yield new Pattern('deer');
|
||||||
|
yield new Pattern('diabetes');
|
||||||
|
yield new Pattern('djinn');
|
||||||
|
yield new Pattern('education');
|
||||||
|
yield new Pattern('eland');
|
||||||
|
yield new Pattern('elk');
|
||||||
|
yield new Pattern('emoji');
|
||||||
|
yield new Pattern('equipment');
|
||||||
|
yield new Pattern('evidence');
|
||||||
|
yield new Pattern('faroese');
|
||||||
|
yield new Pattern('feedback');
|
||||||
|
yield new Pattern('fish');
|
||||||
|
yield new Pattern('flounder');
|
||||||
|
yield new Pattern('flour');
|
||||||
|
yield new Pattern('foochowese');
|
||||||
|
yield new Pattern('food');
|
||||||
|
yield new Pattern('furniture');
|
||||||
|
yield new Pattern('gallows');
|
||||||
|
yield new Pattern('genevese');
|
||||||
|
yield new Pattern('genoese');
|
||||||
|
yield new Pattern('gilbertese');
|
||||||
|
yield new Pattern('gold');
|
||||||
|
yield new Pattern('headquarters');
|
||||||
|
yield new Pattern('herpes');
|
||||||
|
yield new Pattern('hijinks');
|
||||||
|
yield new Pattern('homework');
|
||||||
|
yield new Pattern('hottentotese');
|
||||||
|
yield new Pattern('impatience');
|
||||||
|
yield new Pattern('information');
|
||||||
|
yield new Pattern('innings');
|
||||||
|
yield new Pattern('jackanapes');
|
||||||
|
yield new Pattern('jeans');
|
||||||
|
yield new Pattern('jedi');
|
||||||
|
yield new Pattern('kin');
|
||||||
|
yield new Pattern('kiplingese');
|
||||||
|
yield new Pattern('knowledge');
|
||||||
|
yield new Pattern('kongoese');
|
||||||
|
yield new Pattern('leather');
|
||||||
|
yield new Pattern('love');
|
||||||
|
yield new Pattern('lucchese');
|
||||||
|
yield new Pattern('luggage');
|
||||||
|
yield new Pattern('mackerel');
|
||||||
|
yield new Pattern('Maltese');
|
||||||
|
yield new Pattern('management');
|
||||||
|
yield new Pattern('metadata');
|
||||||
|
yield new Pattern('mews');
|
||||||
|
yield new Pattern('money');
|
||||||
|
yield new Pattern('moose');
|
||||||
|
yield new Pattern('mumps');
|
||||||
|
yield new Pattern('music');
|
||||||
|
yield new Pattern('nankingese');
|
||||||
|
yield new Pattern('news');
|
||||||
|
yield new Pattern('nexus');
|
||||||
|
yield new Pattern('niasese');
|
||||||
|
yield new Pattern('nutrition');
|
||||||
|
yield new Pattern('offspring');
|
||||||
|
yield new Pattern('oil');
|
||||||
|
yield new Pattern('patience');
|
||||||
|
yield new Pattern('pekingese');
|
||||||
|
yield new Pattern('piedmontese');
|
||||||
|
yield new Pattern('pincers');
|
||||||
|
yield new Pattern('pistoiese');
|
||||||
|
yield new Pattern('plankton');
|
||||||
|
yield new Pattern('pliers');
|
||||||
|
yield new Pattern('pokemon');
|
||||||
|
yield new Pattern('police');
|
||||||
|
yield new Pattern('polish');
|
||||||
|
yield new Pattern('portuguese');
|
||||||
|
yield new Pattern('proceedings');
|
||||||
|
yield new Pattern('progress');
|
||||||
|
yield new Pattern('rabies');
|
||||||
|
yield new Pattern('rain');
|
||||||
|
yield new Pattern('research');
|
||||||
|
yield new Pattern('rhinoceros');
|
||||||
|
yield new Pattern('rice');
|
||||||
|
yield new Pattern('salmon');
|
||||||
|
yield new Pattern('sand');
|
||||||
|
yield new Pattern('sarawakese');
|
||||||
|
yield new Pattern('scissors');
|
||||||
|
yield new Pattern('sea[- ]bass');
|
||||||
|
yield new Pattern('series');
|
||||||
|
yield new Pattern('shavese');
|
||||||
|
yield new Pattern('shears');
|
||||||
|
yield new Pattern('sheep');
|
||||||
|
yield new Pattern('siemens');
|
||||||
|
yield new Pattern('silk');
|
||||||
|
yield new Pattern('sms');
|
||||||
|
yield new Pattern('soap');
|
||||||
|
yield new Pattern('social media');
|
||||||
|
yield new Pattern('spam');
|
||||||
|
yield new Pattern('species');
|
||||||
|
yield new Pattern('staff');
|
||||||
|
yield new Pattern('sugar');
|
||||||
|
yield new Pattern('swine');
|
||||||
|
yield new Pattern('talent');
|
||||||
|
yield new Pattern('toothpaste');
|
||||||
|
yield new Pattern('traffic');
|
||||||
|
yield new Pattern('travel');
|
||||||
|
yield new Pattern('trousers');
|
||||||
|
yield new Pattern('trout');
|
||||||
|
yield new Pattern('tuna');
|
||||||
|
yield new Pattern('us');
|
||||||
|
yield new Pattern('vermontese');
|
||||||
|
yield new Pattern('vinegar');
|
||||||
|
yield new Pattern('weather');
|
||||||
|
yield new Pattern('wenchowese');
|
||||||
|
yield new Pattern('wheat');
|
||||||
|
yield new Pattern('whiting');
|
||||||
|
yield new Pattern('wildebeest');
|
||||||
|
yield new Pattern('wood');
|
||||||
|
yield new Pattern('wool');
|
||||||
|
yield new Pattern('yengeese');
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user