glastree_on_gitea
This commit is contained in:
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 ComposerAutoloaderInit74199c0a3889e8d1256a25f93805ef56::getLoader();
|
||||
+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';
|
||||
+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';
|
||||
+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';
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../phpunit/phpunit/phpunit)
|
||||
* 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';
|
||||
$GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'] = $GLOBALS['__PHPUNIT_ISOLATION_BLACKLIST'] = array(realpath(__DIR__ . '/..'.'/phpunit/phpunit/phpunit'));
|
||||
|
||||
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 = 'phpvfscomposer://'.$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);
|
||||
}
|
||||
$data = str_replace('__DIR__', var_export(dirname($this->realpath), true), $data);
|
||||
$data = str_replace('__FILE__', var_export($this->realpath, true), $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__ . '/..'.'/phpunit/phpunit/phpunit');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/phpunit/phpunit/phpunit';
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../laravel/pint/builds/pint)
|
||||
* 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__ . '/..'.'/laravel/pint/builds/pint');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/laravel/pint/builds/pint';
|
||||
+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';
|
||||
+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
+634
@@ -0,0 +1,634 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.14.8](https://github.com/brick/math/releases/tag/0.14.8) - 2026-02-10
|
||||
|
||||
🗑️ **Deprecations**
|
||||
|
||||
- Method `BigInteger::testBit()` is deprecated, use `isBitSet()` instead
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- New method: `BigInteger::isBitSet()` (replaces `testBit()`)
|
||||
- New method: `BigNumber::toString()` (alias of magic method `__toString()`)
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- Performance optimization of `BigRational` comparison methods
|
||||
- More exceptions have been documented with `@throws` annotations
|
||||
|
||||
## [0.14.7](https://github.com/brick/math/releases/tag/0.14.7) - 2026-02-07
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `clamp()` is now available on the base `BigNumber` class
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- Improved `@throws` exception documentation
|
||||
|
||||
## [0.14.6](https://github.com/brick/math/releases/tag/0.14.6) - 2026-02-05
|
||||
|
||||
🗑️ **Deprecations**
|
||||
|
||||
- Not passing a `$scale` to `BigDecimal::dividedBy()` is deprecated; **`$scale` will be required in 0.15**
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- `BigRational::toFloat()` never returns `NAN` anymore
|
||||
|
||||
## [0.14.5](https://github.com/brick/math/releases/tag/0.14.5) - 2026-02-03
|
||||
|
||||
🗑️ **Deprecations**
|
||||
|
||||
- Not passing a rounding mode to `BigInteger::sqrt()` and `BigDecimal::sqrt()` triggers a deprecation notice: **the default rounding mode will change from `Down` to `Unnecessary` in 0.15**
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::sqrt()` and `BigDecimal::sqrt()` now support rounding
|
||||
- `abs()` and `negated()` methods are now available on the base `BigNumber` class
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- Alphabet is now checked for duplicate characters in `BigInteger::(from|to)ArbitraryBase()`
|
||||
- `BigNumber::ofNullable()` is now marked as `@pure`
|
||||
|
||||
## [0.14.4](https://github.com/brick/math/releases/tag/0.14.4) - 2026-02-02
|
||||
|
||||
🗑️ **Deprecations**
|
||||
|
||||
- Passing a negative modulus to `BigInteger::mod()` is deprecated to align with Euclidean modulo semantics; it will throw `NegativeNumberException` in 0.15
|
||||
- Method `BigDecimal::stripTrailingZeros()` is deprecated, use `strippedOfTrailingZeros()` instead
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::modPow()` now accepts negative bases
|
||||
- New method: `BigDecimal::strippedOfTrailingZeros()` (replaces `stripTrailingZeros()`)
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- `clamp()` methods are now marked as `@pure`
|
||||
|
||||
## [0.14.3](https://github.com/brick/math/releases/tag/0.14.3) - 2026-02-01
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- New method: `BigInteger::lcm()`
|
||||
- New method: `BigInteger::lcmAll()`
|
||||
- New method: `BigRational::toRepeatingDecimalString()`
|
||||
|
||||
🐛 **Bug fixes**
|
||||
|
||||
- `BigInteger::gcdAll()` / `gcdMultiple()` could return a negative result when used with a single negative number
|
||||
|
||||
## [0.14.2](https://github.com/brick/math/releases/tag/0.14.2) - 2026-01-30
|
||||
|
||||
🗑️ **Deprecations**
|
||||
|
||||
- **Passing `float` values to `of()` or arithmetic methods is deprecated** and will be removed in 0.15; cast to string explicitly to preserve the previous behaviour (#105)
|
||||
- **Accessing `RoundingMode` enum cases through upper snake case (e.g. `HALF_UP`) is deprecated**, use the pascal case version (e.g. `HalfUp`) instead
|
||||
- Method `BigInteger::gcdMultiple()` is deprecated, use `gcdAll()` instead
|
||||
- Method `BigDecimal::exactlyDividedBy()` is deprecated, use `dividedByExact()` instead
|
||||
- Method `BigDecimal::getIntegralPart()` is deprecated (will be removed in 0.15, and re-introduced as returning `BigInteger` in 0.16)
|
||||
- Method `BigDecimal::getFractionalPart()` is deprecated (will be removed in 0.15, and re-introduced as returning `BigDecimal` with a different meaning in 0.16)
|
||||
- Method `BigRational::nd()` is deprecated, use `ofFraction()` instead
|
||||
- Method `BigRational::quotient()` is deprecated, use `getIntegralPart()` instead
|
||||
- Method `BigRational::remainder()` is deprecated, use `$number->getNumerator()->remainder($number->getDenominator())` instead
|
||||
- Method `BigRational::quotientAndRemainder()` is deprecated, use `$number->getNumerator()->quotientAndRemainder($number->getDenominator())` instead
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- New method: `BigInteger::gcdAll()` (replaces `gcdMultiple()`)
|
||||
- New method: `BigRational::clamp()`
|
||||
- New method: `BigRational::ofFraction()` (replaces `nd()`)
|
||||
- New method: `BigRational::getIntegralPart()` (replaces `quotient()`)
|
||||
- New method: `BigRational::getFractionalPart()`
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- `BigInteger::modInverse()` now accepts `BigNumber|int|float|string` instead of just `BigInteger`
|
||||
- `BigInteger::gcdMultiple()` now accepts `BigNumber|int|float|string` instead of just `BigInteger`
|
||||
|
||||
🐛 **Bug fixes**
|
||||
|
||||
- `BigInteger::clamp()` and `BigDecimal::clamp()` now throw an exception on inverted bounds, instead of returning an incorrect result
|
||||
|
||||
## [0.14.1](https://github.com/brick/math/releases/tag/0.14.1) - 2025-11-24
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- New method: `BigNumber::ofNullable()` (#94 by @mrkh995)
|
||||
|
||||
✨ **Compatibility fixes**
|
||||
|
||||
- Fixed warnings on PHP 8.5 (#101 and #102 by @julien-boudry)
|
||||
|
||||
## [0.14.0](https://github.com/brick/math/releases/tag/0.14.0) - 2025-08-29
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- New methods: `BigInteger::clamp()` and `BigDecimal::clamp()` (#96 by @JesterIruka)
|
||||
|
||||
✨ **Improvements**
|
||||
|
||||
- All pure methods in `BigNumber` classes are now marked as `@pure` for better static analysis
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- Minimum PHP version is now 8.2
|
||||
- `BigNumber` classes are now `readonly`
|
||||
- `BigNumber` is now marked as sealed: it must not be extended outside of this package
|
||||
- Exception classes are now `final`
|
||||
|
||||
## [0.13.1](https://github.com/brick/math/releases/tag/0.13.1) - 2025-03-29
|
||||
|
||||
✨ **Improvements**
|
||||
|
||||
- `__toString()` methods of `BigInteger` and `BigDecimal` are now type-hinted as returning `numeric-string` instead of `string` (#90 by @vudaltsov)
|
||||
|
||||
## [0.13.0](https://github.com/brick/math/releases/tag/0.13.0) - 2025-03-03
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- `BigDecimal::ofUnscaledValue()` no longer throws an exception if the scale is negative
|
||||
- `MathException` now extends `RuntimeException` instead of `Exception`; this reverts the change introduced in version `0.11.0` (#82)
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigDecimal::ofUnscaledValue()` allows a negative scale (and converts the values to create a zero scale number)
|
||||
|
||||
## [0.12.3](https://github.com/brick/math/releases/tag/0.12.3) - 2025-02-28
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigDecimal::getPrecision()` Returns the number of significant digits in a decimal number
|
||||
|
||||
## [0.12.2](https://github.com/brick/math/releases/tag/0.12.2) - 2025-02-26
|
||||
|
||||
⚡️ **Performance improvements**
|
||||
|
||||
- Division in `NativeCalculator` is now faster for small divisors, thanks to [@Izumi-kun](https://github.com/Izumi-kun) in [#87](https://github.com/brick/math/pull/87).
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- Add missing `RoundingNecessaryException` to the `@throws` annotation of `BigNumber::of()`
|
||||
|
||||
## [0.12.1](https://github.com/brick/math/releases/tag/0.12.1) - 2023-11-29
|
||||
|
||||
⚡️ **Performance improvements**
|
||||
|
||||
- `BigNumber::of()` is now faster, thanks to [@SebastienDug](https://github.com/SebastienDug) in [#77](https://github.com/brick/math/pull/77).
|
||||
|
||||
## [0.12.0](https://github.com/brick/math/releases/tag/0.12.0) - 2023-11-26
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- Minimum PHP version is now 8.1
|
||||
- `RoundingMode` is now an `enum`; if you're type-hinting rounding modes, you need to type-hint against `RoundingMode` instead of `int` now
|
||||
- `BigNumber` classes do not implement the `Serializable` interface anymore (they use the [new custom object serialization mechanism](https://wiki.php.net/rfc/custom_object_serialization))
|
||||
- The following breaking changes only affect you if you're creating your own `BigNumber` subclasses:
|
||||
- the return type of `BigNumber::of()` is now `static`
|
||||
- `BigNumber` has a new abstract method `from()`
|
||||
- all `public` and `protected` functions of `BigNumber` are now `final`
|
||||
|
||||
## [0.11.0](https://github.com/brick/math/releases/tag/0.11.0) - 2023-01-16
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- Minimum PHP version is now 8.0
|
||||
- Methods accepting a union of types are now strongly typed<sup>*</sup>
|
||||
- `MathException` now extends `Exception` instead of `RuntimeException`
|
||||
|
||||
<sup>* You may now run into type errors if you were passing `Stringable` objects to `of()` or any of the methods
|
||||
internally calling `of()`, with `strict_types` enabled. You can fix this by casting `Stringable` objects to `string`
|
||||
first.</sup>
|
||||
|
||||
## [0.10.2](https://github.com/brick/math/releases/tag/0.10.2) - 2022-08-11
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- `BigRational::toFloat()` now simplifies the fraction before performing division (#73) thanks to @olsavmic
|
||||
|
||||
## [0.10.1](https://github.com/brick/math/releases/tag/0.10.1) - 2022-08-02
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::gcdMultiple()` returns the GCD of multiple `BigInteger` numbers
|
||||
|
||||
## [0.10.0](https://github.com/brick/math/releases/tag/0.10.0) - 2022-06-18
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- Minimum PHP version is now 7.4
|
||||
|
||||
## [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 (#60) 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 anymore
|
||||
- `toBigRational()` does not simplify the fraction anymore; 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` anymore, 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
+39
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "brick/math",
|
||||
"description": "Arbitrary-precision arithmetic library",
|
||||
"type": "library",
|
||||
"keywords": [
|
||||
"Brick",
|
||||
"Math",
|
||||
"Mathematics",
|
||||
"Arbitrary-precision",
|
||||
"Arithmetic",
|
||||
"BigInteger",
|
||||
"BigDecimal",
|
||||
"BigRational",
|
||||
"BigNumber",
|
||||
"Bignum",
|
||||
"Decimal",
|
||||
"Rational",
|
||||
"Integer"
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11.5",
|
||||
"php-coveralls/php-coveralls": "^2.2",
|
||||
"phpstan/phpstan": "2.1.22"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Brick\\Math\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Brick\\Math\\Tests\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+975
@@ -0,0 +1,975 @@
|
||||
<?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\Exception\RoundingNecessaryException;
|
||||
use Brick\Math\Internal\Calculator;
|
||||
use Brick\Math\Internal\CalculatorRegistry;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use Override;
|
||||
|
||||
use function func_num_args;
|
||||
use function in_array;
|
||||
use function intdiv;
|
||||
use function max;
|
||||
use function rtrim;
|
||||
use function sprintf;
|
||||
use function str_pad;
|
||||
use function str_repeat;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
use function trigger_error;
|
||||
|
||||
use const E_USER_DEPRECATED;
|
||||
use const STR_PAD_LEFT;
|
||||
|
||||
/**
|
||||
* An arbitrarily large decimal number.
|
||||
*
|
||||
* This class is immutable.
|
||||
*
|
||||
* The scale of the number is the number of digits after the decimal point. It is always positive or zero.
|
||||
*/
|
||||
final readonly 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.
|
||||
*/
|
||||
private string $value;
|
||||
|
||||
/**
|
||||
* The scale (number of digits after the decimal point) of this decimal number.
|
||||
*
|
||||
* This must be zero or more.
|
||||
*/
|
||||
private int $scale;
|
||||
|
||||
/**
|
||||
* Protected constructor. Use a factory method to obtain an instance.
|
||||
*
|
||||
* @param string $value The unscaled value, validated.
|
||||
* @param int $scale The scale, validated.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
protected function __construct(string $value, int $scale = 0)
|
||||
{
|
||||
$this->value = $value;
|
||||
$this->scale = $scale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BigDecimal from an unscaled value and a scale.
|
||||
*
|
||||
* Example: `(12345, 3)` will result in the BigDecimal `12.345`.
|
||||
*
|
||||
* A negative scale is normalized to zero by appending zeros to the unscaled value.
|
||||
*
|
||||
* Example: `(12345, -3)` will result in the BigDecimal `12345000`.
|
||||
*
|
||||
* @param BigNumber|int|float|string $value The unscaled value. Must be convertible to a BigInteger.
|
||||
* @param int $scale The scale of the number. If negative, the scale will be set to zero
|
||||
* and the unscaled value will be adjusted accordingly.
|
||||
*
|
||||
* @throws MathException If the value is not valid, or is not convertible to a BigInteger.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function ofUnscaledValue(BigNumber|int|float|string $value, int $scale = 0): BigDecimal
|
||||
{
|
||||
$value = BigInteger::of($value)->toString();
|
||||
|
||||
if ($scale < 0) {
|
||||
if ($value !== '0') {
|
||||
$value .= str_repeat('0', -$scale);
|
||||
}
|
||||
$scale = 0;
|
||||
}
|
||||
|
||||
return new BigDecimal($value, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a BigDecimal representing zero, with a scale of zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function zero(): BigDecimal
|
||||
{
|
||||
/** @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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function one(): BigDecimal
|
||||
{
|
||||
/** @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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function ten(): BigDecimal
|
||||
{
|
||||
/** @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.
|
||||
*
|
||||
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function plus(BigNumber|int|float|string $that): BigDecimal
|
||||
{
|
||||
$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 = CalculatorRegistry::get()->add($a, $b);
|
||||
$scale = max($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.
|
||||
*
|
||||
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function minus(BigNumber|int|float|string $that): BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->value === '0' && $that->scale <= $this->scale) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
[$a, $b] = $this->scaleValues($this, $that);
|
||||
|
||||
$value = CalculatorRegistry::get()->sub($a, $b);
|
||||
$scale = max($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.
|
||||
*
|
||||
* @throws MathException If the multiplier is not valid, or is not convertible to a BigDecimal.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function multipliedBy(BigNumber|int|float|string $that): BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->value === '1' && $that->scale === 0) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($this->value === '1' && $this->scale === 0) {
|
||||
return $that;
|
||||
}
|
||||
|
||||
$value = CalculatorRegistry::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. Must be convertible to a BigDecimal.
|
||||
* @param int|null $scale The desired scale. Omitting this parameter is deprecated; it will be required in 0.15.
|
||||
* @param RoundingMode $roundingMode An optional rounding mode, defaults to Unnecessary.
|
||||
*
|
||||
* @throws InvalidArgumentException If the scale is negative.
|
||||
* @throws MathException If the divisor is not valid, or is not convertible to a BigDecimal.
|
||||
* @throws DivisionByZeroException If the divisor is zero.
|
||||
* @throws RoundingNecessaryException If RoundingMode::Unnecessary is used and the result cannot be represented
|
||||
* exactly at the given scale.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function dividedBy(BigNumber|int|float|string $that, ?int $scale = null, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->isZero()) {
|
||||
throw DivisionByZeroException::divisionByZero();
|
||||
}
|
||||
|
||||
if ($scale === null) {
|
||||
// @phpstan-ignore-next-line
|
||||
trigger_error(
|
||||
'Not passing a $scale to BigDecimal::dividedBy() is deprecated. ' .
|
||||
'Use $a->dividedBy($b, $a->getScale(), $roundingMode) to retain current behavior.',
|
||||
E_USER_DEPRECATED,
|
||||
);
|
||||
$scale = $this->scale;
|
||||
} elseif ($scale < 0) {
|
||||
throw new InvalidArgumentException('Scale must not 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 = CalculatorRegistry::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.
|
||||
*
|
||||
* @deprecated Will be removed in 0.15. Use dividedByExact() instead.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
|
||||
*
|
||||
* @throws MathException If the divisor is not a valid number, is not convertible to a BigDecimal, is zero,
|
||||
* or the result yields an infinite number of digits.
|
||||
*/
|
||||
public function exactlyDividedBy(BigNumber|int|float|string $that): BigDecimal
|
||||
{
|
||||
trigger_error(
|
||||
'BigDecimal::exactlyDividedBy() is deprecated and will be removed in 0.15. Use dividedByExact() instead.',
|
||||
E_USER_DEPRECATED,
|
||||
);
|
||||
|
||||
return $this->dividedByExact($that);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the exact result of the division of this number by the given one.
|
||||
*
|
||||
* The scale of the result is automatically calculated to fit all the fraction digits.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
|
||||
*
|
||||
* @throws MathException If the divisor is not valid, or is not convertible to a BigDecimal.
|
||||
* @throws DivisionByZeroException If the divisor is zero.
|
||||
* @throws RoundingNecessaryException If the result yields an infinite number of digits.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function dividedByExact(BigNumber|int|float|string $that): BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->value === '0') {
|
||||
throw DivisionByZeroException::divisionByZero();
|
||||
}
|
||||
|
||||
[, $b] = $this->scaleValues($this, $that);
|
||||
|
||||
$d = rtrim($b, '0');
|
||||
$scale = strlen($b) - strlen($d);
|
||||
|
||||
$calculator = CalculatorRegistry::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)->strippedOfTrailingZeros();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this number exponentiated to the given value.
|
||||
*
|
||||
* The result has a scale of `$this->scale * $exponent`.
|
||||
*
|
||||
* @throws InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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(CalculatorRegistry::get()->pow($this->value, $exponent), $this->scale * $exponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the quotient of the division of this number by the given one.
|
||||
*
|
||||
* The quotient has a scale of `0`.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* - `7.5` quotient `3` returns `2`
|
||||
* - `7.5` quotient `-3` returns `-2`
|
||||
* - `-7.5` quotient `3` returns `-2`
|
||||
* - `-7.5` quotient `-3` returns `2`
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
|
||||
*
|
||||
* @throws MathException If the divisor is not valid, or is not convertible to a BigDecimal.
|
||||
* @throws DivisionByZeroException If the divisor is zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function quotient(BigNumber|int|float|string $that): BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->isZero()) {
|
||||
throw DivisionByZeroException::divisionByZero();
|
||||
}
|
||||
|
||||
$p = $this->valueWithMinScale($that->scale);
|
||||
$q = $that->valueWithMinScale($this->scale);
|
||||
|
||||
$quotient = CalculatorRegistry::get()->divQ($p, $q);
|
||||
|
||||
return new BigDecimal($quotient, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remainder of the division of this number by the given one.
|
||||
*
|
||||
* The remainder has a scale of `max($this->scale, $that->scale)`.
|
||||
* The remainder, when non-zero, has the same sign as the dividend.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* - `7.5` remainder `3` returns `1.5`
|
||||
* - `7.5` remainder `-3` returns `1.5`
|
||||
* - `-7.5` remainder `3` returns `-1.5`
|
||||
* - `-7.5` remainder `-3` returns `-1.5`
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
|
||||
*
|
||||
* @throws MathException If the divisor is not valid, or is not convertible to a BigDecimal.
|
||||
* @throws DivisionByZeroException If the divisor is zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function remainder(BigNumber|int|float|string $that): BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->isZero()) {
|
||||
throw DivisionByZeroException::divisionByZero();
|
||||
}
|
||||
|
||||
$p = $this->valueWithMinScale($that->scale);
|
||||
$q = $that->valueWithMinScale($this->scale);
|
||||
|
||||
$remainder = CalculatorRegistry::get()->divR($p, $q);
|
||||
|
||||
$scale = max($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)`.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* - `7.5` quotientAndRemainder `3` returns [`2`, `1.5`]
|
||||
* - `7.5` quotientAndRemainder `-3` returns [`-2`, `1.5`]
|
||||
* - `-7.5` quotientAndRemainder `3` returns [`-2`, `-1.5`]
|
||||
* - `-7.5` quotientAndRemainder `-3` returns [`2`, `-1.5`]
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
|
||||
*
|
||||
* @return array{BigDecimal, BigDecimal} An array containing the quotient and the remainder.
|
||||
*
|
||||
* @throws MathException If the divisor is not valid, or is not convertible to a BigDecimal.
|
||||
* @throws DivisionByZeroException If the divisor is zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function quotientAndRemainder(BigNumber|int|float|string $that): array
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->isZero()) {
|
||||
throw DivisionByZeroException::divisionByZero();
|
||||
}
|
||||
|
||||
$p = $this->valueWithMinScale($that->scale);
|
||||
$q = $that->valueWithMinScale($this->scale);
|
||||
|
||||
[$quotient, $remainder] = CalculatorRegistry::get()->divQR($p, $q);
|
||||
|
||||
$scale = max($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 to the given scale according to the given rounding mode.
|
||||
*
|
||||
* @param int $scale The target scale. Must be non-negative.
|
||||
* @param RoundingMode $roundingMode The rounding mode to use, defaults to Down.
|
||||
* ⚠️ WARNING: the default rounding mode was kept as Down for backward
|
||||
* compatibility, but will change to Unnecessary in version 0.15. Pass a rounding
|
||||
* mode explicitly to avoid this upcoming breaking change.
|
||||
*
|
||||
* @throws InvalidArgumentException If the scale is negative.
|
||||
* @throws NegativeNumberException If this number is negative.
|
||||
* @throws RoundingNecessaryException If RoundingMode::Unnecessary is used and the result cannot be represented
|
||||
* exactly at the given scale.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function sqrt(int $scale, RoundingMode $roundingMode = RoundingMode::Down): BigDecimal
|
||||
{
|
||||
if (func_num_args() === 1) {
|
||||
// @phpstan-ignore-next-line
|
||||
trigger_error(
|
||||
'The default rounding mode of BigDecimal::sqrt() will change from Down to Unnecessary in version 0.15. ' .
|
||||
'Pass a rounding mode explicitly to avoid this breaking change.',
|
||||
E_USER_DEPRECATED,
|
||||
);
|
||||
}
|
||||
|
||||
if ($scale < 0) {
|
||||
throw new InvalidArgumentException('Scale must not 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;
|
||||
$inputScale = $this->scale;
|
||||
|
||||
if ($inputScale % 2 !== 0) {
|
||||
$value .= '0';
|
||||
$inputScale++;
|
||||
}
|
||||
|
||||
$calculator = CalculatorRegistry::get();
|
||||
|
||||
// Keep one extra digit for rounding.
|
||||
$intermediateScale = max($scale, intdiv($inputScale, 2)) + 1;
|
||||
$value .= str_repeat('0', 2 * $intermediateScale - $inputScale);
|
||||
|
||||
$sqrt = $calculator->sqrt($value);
|
||||
$isExact = $calculator->mul($sqrt, $sqrt) === $value;
|
||||
|
||||
if (! $isExact) {
|
||||
if ($roundingMode === RoundingMode::Unnecessary) {
|
||||
throw RoundingNecessaryException::roundingNecessary();
|
||||
}
|
||||
|
||||
// Non-perfect-square sqrt is irrational, so the true value is strictly above this sqrt floor.
|
||||
// Add one at the intermediate scale to guarantee Up/Ceiling round up at the target scale.
|
||||
if (in_array($roundingMode, [RoundingMode::Up, RoundingMode::Ceiling], true)) {
|
||||
$sqrt = $calculator->add($sqrt, '1');
|
||||
}
|
||||
|
||||
// Irrational sqrt cannot land exactly on a midpoint; treat tie-to-down modes as HalfUp.
|
||||
elseif (in_array($roundingMode, [RoundingMode::HalfDown, RoundingMode::HalfEven, RoundingMode::HalfFloor], true)) {
|
||||
$roundingMode = RoundingMode::HalfUp;
|
||||
}
|
||||
}
|
||||
|
||||
return (new BigDecimal($sqrt, $intermediateScale))->toScale($scale, $roundingMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this BigDecimal with the decimal point moved to the left by the given number of places.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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 to the right by the given number of places.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @deprecated Use strippedOfTrailingZeros() instead.
|
||||
*/
|
||||
public function stripTrailingZeros(): BigDecimal
|
||||
{
|
||||
trigger_error(
|
||||
'BigDecimal::stripTrailingZeros() is deprecated, use strippedOfTrailingZeros() instead.',
|
||||
E_USER_DEPRECATED,
|
||||
);
|
||||
|
||||
return $this->strippedOfTrailingZeros();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this BigDecimal with any trailing zeros removed from the fractional part.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function strippedOfTrailingZeros(): BigDecimal
|
||||
{
|
||||
if ($this->scale === 0) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$trimmedValue = rtrim($this->value, '0');
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function negated(): static
|
||||
{
|
||||
return new BigDecimal(CalculatorRegistry::get()->neg($this->value), $this->scale);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function compareTo(BigNumber|int|float|string $that): int
|
||||
{
|
||||
$that = BigNumber::of($that);
|
||||
|
||||
if ($that instanceof BigInteger) {
|
||||
$that = $that->toBigDecimal();
|
||||
}
|
||||
|
||||
if ($that instanceof BigDecimal) {
|
||||
[$a, $b] = $this->scaleValues($this, $that);
|
||||
|
||||
return CalculatorRegistry::get()->cmp($a, $b);
|
||||
}
|
||||
|
||||
return -$that->compareTo($this);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function getSign(): int
|
||||
{
|
||||
return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
public function getUnscaledValue(): BigInteger
|
||||
{
|
||||
return self::newBigInteger($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
public function getScale(): int
|
||||
{
|
||||
return $this->scale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of significant digits in the number.
|
||||
*
|
||||
* This is the number of digits to both sides of the decimal point, stripped of leading zeros.
|
||||
* The sign has no impact on the result.
|
||||
*
|
||||
* Examples:
|
||||
* 0 => 0
|
||||
* 0.0 => 0
|
||||
* 123 => 3
|
||||
* 123.456 => 6
|
||||
* 0.00123 => 3
|
||||
* 0.0012300 => 5
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function getPrecision(): int
|
||||
{
|
||||
$value = $this->value;
|
||||
|
||||
if ($value === '0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$length = strlen($value);
|
||||
|
||||
return ($value[0] === '-') ? $length - 1 : $length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representing the integral part of this decimal number.
|
||||
*
|
||||
* Example: `-123.456` => `-123`.
|
||||
*
|
||||
* @deprecated Will be removed in 0.15 and re-introduced as returning BigInteger in 0.16.
|
||||
*/
|
||||
public function getIntegralPart(): string
|
||||
{
|
||||
trigger_error(
|
||||
'BigDecimal::getIntegralPart() is deprecated and will be removed in 0.15. It will be re-introduced as returning BigInteger in 0.16.',
|
||||
E_USER_DEPRECATED,
|
||||
);
|
||||
|
||||
if ($this->scale === 0) {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
$value = $this->getUnscaledValueWithLeadingZeros();
|
||||
|
||||
return substr($value, 0, -$this->scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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` => ''.
|
||||
*
|
||||
* @deprecated Will be removed in 0.15 and re-introduced as returning BigDecimal with a different meaning in 0.16.
|
||||
*/
|
||||
public function getFractionalPart(): string
|
||||
{
|
||||
trigger_error(
|
||||
'BigDecimal::getFractionalPart() is deprecated and will be removed in 0.15. It will be re-introduced as returning BigDecimal with a different meaning in 0.16.',
|
||||
E_USER_DEPRECATED,
|
||||
);
|
||||
|
||||
if ($this->scale === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$value = $this->getUnscaledValueWithLeadingZeros();
|
||||
|
||||
return substr($value, -$this->scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this decimal number has a non-zero fractional part.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function hasNonZeroFractionalPart(): bool
|
||||
{
|
||||
if ($this->scale === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$value = $this->getUnscaledValueWithLeadingZeros();
|
||||
|
||||
return substr($value, -$this->scale) !== str_repeat('0', $this->scale);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBigInteger(): BigInteger
|
||||
{
|
||||
$zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0);
|
||||
|
||||
return self::newBigInteger($zeroScaleDecimal->value);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBigDecimal(): BigDecimal
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBigRational(): BigRational
|
||||
{
|
||||
$numerator = self::newBigInteger($this->value);
|
||||
$denominator = self::newBigInteger('1' . str_repeat('0', $this->scale));
|
||||
|
||||
return self::newBigRational($numerator, $denominator, false);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal
|
||||
{
|
||||
if ($scale === $this->scale) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return $this->dividedBy(BigDecimal::one(), $scale, $roundingMode);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toInt(): int
|
||||
{
|
||||
return $this->toBigInteger()->toInt();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toFloat(): float
|
||||
{
|
||||
return (float) $this->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return numeric-string
|
||||
*/
|
||||
#[Override]
|
||||
public function toString(): string
|
||||
{
|
||||
if ($this->scale === 0) {
|
||||
/** @var numeric-string */
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
$value = $this->getUnscaledValueWithLeadingZeros();
|
||||
|
||||
/** @phpstan-ignore return.type */
|
||||
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
|
||||
*
|
||||
* @param array{value: string, scale: int} $data
|
||||
*
|
||||
* @throws LogicException
|
||||
*/
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
/** @phpstan-ignore isset.initializedProperty */
|
||||
if (isset($this->value)) {
|
||||
throw new LogicException('__unserialize() is an internal function, it must not be called directly.');
|
||||
}
|
||||
|
||||
/** @phpstan-ignore deadCode.unreachable */
|
||||
$this->value = $data['value'];
|
||||
$this->scale = $data['scale'];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
protected static function from(BigNumber $number): static
|
||||
{
|
||||
return $number->toBigDecimal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the internal values of the given decimal numbers on the same scale.
|
||||
*
|
||||
* @return array{string, string} The scaled integer values of $x and $y.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private function scaleValues(BigDecimal $x, BigDecimal $y): array
|
||||
{
|
||||
$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];
|
||||
}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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
+1351
File diff suppressed because it is too large
Load Diff
Vendored
+712
@@ -0,0 +1,712 @@
|
||||
<?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;
|
||||
use InvalidArgumentException;
|
||||
use JsonSerializable;
|
||||
use Override;
|
||||
use Stringable;
|
||||
|
||||
use function array_shift;
|
||||
use function assert;
|
||||
use function filter_var;
|
||||
use function is_float;
|
||||
use function is_int;
|
||||
use function is_nan;
|
||||
use function is_null;
|
||||
use function ltrim;
|
||||
use function preg_match;
|
||||
use function str_contains;
|
||||
use function str_repeat;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
use function trigger_error;
|
||||
|
||||
use const E_USER_DEPRECATED;
|
||||
use const FILTER_VALIDATE_INT;
|
||||
use const PREG_UNMATCHED_AS_NULL;
|
||||
|
||||
/**
|
||||
* Base class for arbitrary-precision numbers.
|
||||
*
|
||||
* This class is sealed: it is part of the public API but should not be subclassed in userland.
|
||||
* Protected methods may change in any version.
|
||||
*
|
||||
* @phpstan-sealed BigInteger|BigDecimal|BigRational
|
||||
*/
|
||||
abstract readonly class BigNumber implements JsonSerializable, Stringable
|
||||
{
|
||||
/**
|
||||
* The regular expression used to parse integer or decimal numbers.
|
||||
*/
|
||||
private const PARSE_REGEXP_NUMERICAL =
|
||||
'/^' .
|
||||
'(?<sign>[\-\+])?' .
|
||||
'(?<integral>[0-9]+)?' .
|
||||
'(?<point>\.)?' .
|
||||
'(?<fractional>[0-9]+)?' .
|
||||
'(?:[eE](?<exponent>[\-\+]?[0-9]+))?' .
|
||||
'$/';
|
||||
|
||||
/**
|
||||
* The regular expression used to parse rational numbers.
|
||||
*/
|
||||
private const PARSE_REGEXP_RATIONAL =
|
||||
'/^' .
|
||||
'(?<sign>[\-\+])?' .
|
||||
'(?<numerator>[0-9]+)' .
|
||||
'\/' .
|
||||
'(?<denominator>[0-9]+)' .
|
||||
'$/';
|
||||
|
||||
/**
|
||||
* Creates a BigNumber of the given value.
|
||||
*
|
||||
* When of() is called on BigNumber, the concrete return type is dependent on the given value, with the following
|
||||
* rules:
|
||||
*
|
||||
* - BigNumber instances are returned as is
|
||||
* - integer numbers are returned as BigInteger
|
||||
* - floating point numbers are converted to a string then parsed as such (deprecated, will be removed in 0.15)
|
||||
* - strings containing a `/` character are returned as BigRational
|
||||
* - strings containing a `.` character or using an exponential notation are returned as BigDecimal
|
||||
* - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger
|
||||
*
|
||||
* When of() is called on BigInteger, BigDecimal, or BigRational, the resulting number is converted to an instance
|
||||
* of the subclass when possible; otherwise a RoundingNecessaryException exception is thrown.
|
||||
*
|
||||
* @throws NumberFormatException If the format of the number is not valid.
|
||||
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
|
||||
* @throws RoundingNecessaryException If the value cannot be converted to an instance of the subclass without rounding.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public static function of(BigNumber|int|float|string $value): static
|
||||
{
|
||||
$value = self::_of($value);
|
||||
|
||||
if (static::class === BigNumber::class) {
|
||||
assert($value instanceof static);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
return static::from($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BigNumber of the given value, or returns null if the input is null.
|
||||
*
|
||||
* Behaves like of() for non-null values.
|
||||
*
|
||||
* @see BigNumber::of()
|
||||
*
|
||||
* @throws NumberFormatException If the format of the number is not valid.
|
||||
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
|
||||
* @throws RoundingNecessaryException If the value cannot be converted to an instance of the subclass without rounding.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public static function ofNullable(BigNumber|int|float|string|null $value): ?static
|
||||
{
|
||||
if (is_null($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return static::of($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum of the given values.
|
||||
*
|
||||
* If several values are equal and minimal, the first one is returned.
|
||||
* This can affect the concrete return type when calling this method on BigNumber.
|
||||
*
|
||||
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers must be convertible to an
|
||||
* instance of the class this method is called on.
|
||||
*
|
||||
* @throws InvalidArgumentException If no values are given.
|
||||
* @throws MathException If a number is not valid, or is not convertible to an instance of the class
|
||||
* this method is called on.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public static function min(BigNumber|int|float|string ...$values): static
|
||||
{
|
||||
$min = null;
|
||||
|
||||
foreach ($values as $value) {
|
||||
$value = static::of($value);
|
||||
|
||||
if ($min === null || $value->isLessThan($min)) {
|
||||
$min = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($min === null) {
|
||||
throw new InvalidArgumentException(__METHOD__ . '() expects at least one value.');
|
||||
}
|
||||
|
||||
return $min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum of the given values.
|
||||
*
|
||||
* If several values are equal and maximal, the first one is returned.
|
||||
* This can affect the concrete return type when calling this method on BigNumber.
|
||||
*
|
||||
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers must be convertible to an
|
||||
* instance of the class this method is called on.
|
||||
*
|
||||
* @throws InvalidArgumentException If no values are given.
|
||||
* @throws MathException If a number is not valid, or is not convertible to an instance of the class
|
||||
* this method is called on.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public static function max(BigNumber|int|float|string ...$values): static
|
||||
{
|
||||
$max = null;
|
||||
|
||||
foreach ($values as $value) {
|
||||
$value = static::of($value);
|
||||
|
||||
if ($max === null || $value->isGreaterThan($max)) {
|
||||
$max = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($max === null) {
|
||||
throw new InvalidArgumentException(__METHOD__ . '() expects at least one value.');
|
||||
}
|
||||
|
||||
return $max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sum of the given values.
|
||||
*
|
||||
* When called on BigNumber, sum() accepts any supported type and returns a result whose type is the widest among
|
||||
* the given values (BigInteger < BigDecimal < BigRational).
|
||||
*
|
||||
* When called on BigInteger, BigDecimal, or BigRational, sum() requires that all values can be converted to that
|
||||
* specific subclass, and returns a result of the same type.
|
||||
*
|
||||
* @param BigNumber|int|float|string ...$values The numbers to add. All the numbers must be convertible to an
|
||||
* instance of the class this method is called on.
|
||||
*
|
||||
* @throws InvalidArgumentException If no values are given.
|
||||
* @throws MathException If a number is not valid, or is not convertible to an instance of the class
|
||||
* this method is called on.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public static function sum(BigNumber|int|float|string ...$values): static
|
||||
{
|
||||
$first = array_shift($values);
|
||||
|
||||
if ($first === null) {
|
||||
throw new InvalidArgumentException(__METHOD__ . '() expects at least one value.');
|
||||
}
|
||||
|
||||
$sum = static::of($first);
|
||||
|
||||
foreach ($values as $value) {
|
||||
$sum = self::add($sum, static::of($value));
|
||||
}
|
||||
|
||||
assert($sum instanceof static);
|
||||
|
||||
return $sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is equal to the given one.
|
||||
*
|
||||
* @throws MathException If the given number is not valid.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isEqualTo(BigNumber|int|float|string $that): bool
|
||||
{
|
||||
return $this->compareTo($that) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is strictly less than the given one.
|
||||
*
|
||||
* @throws MathException If the given number is not valid.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isLessThan(BigNumber|int|float|string $that): bool
|
||||
{
|
||||
return $this->compareTo($that) < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is less than or equal to the given one.
|
||||
*
|
||||
* @throws MathException If the given number is not valid.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isLessThanOrEqualTo(BigNumber|int|float|string $that): bool
|
||||
{
|
||||
return $this->compareTo($that) <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is strictly greater than the given one.
|
||||
*
|
||||
* @throws MathException If the given number is not valid.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isGreaterThan(BigNumber|int|float|string $that): bool
|
||||
{
|
||||
return $this->compareTo($that) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is greater than or equal to the given one.
|
||||
*
|
||||
* @throws MathException If the given number is not valid.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isGreaterThanOrEqualTo(BigNumber|int|float|string $that): bool
|
||||
{
|
||||
return $this->compareTo($that) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number equals zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isZero(): bool
|
||||
{
|
||||
return $this->getSign() === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is strictly negative.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isNegative(): bool
|
||||
{
|
||||
return $this->getSign() < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is negative or zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isNegativeOrZero(): bool
|
||||
{
|
||||
return $this->getSign() <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is strictly positive.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isPositive(): bool
|
||||
{
|
||||
return $this->getSign() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is positive or zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isPositiveOrZero(): bool
|
||||
{
|
||||
return $this->getSign() >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute value of this number.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function abs(): static
|
||||
{
|
||||
return $this->isNegative() ? $this->negated() : $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the negated value of this number.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function negated(): static;
|
||||
|
||||
/**
|
||||
* Returns the sign of this number.
|
||||
*
|
||||
* Returns -1 if the number is negative, 0 if zero, 1 if positive.
|
||||
*
|
||||
* @return -1|0|1
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function getSign(): int;
|
||||
|
||||
/**
|
||||
* Compares this number to the given one.
|
||||
*
|
||||
* Returns -1 if `$this` is lower than, 0 if equal to, 1 if greater than `$that`.
|
||||
*
|
||||
* @return -1|0|1
|
||||
*
|
||||
* @throws MathException If the number is not valid.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function compareTo(BigNumber|int|float|string $that): int;
|
||||
|
||||
/**
|
||||
* Limits (clamps) this number between the given minimum and maximum values.
|
||||
*
|
||||
* If the number is lower than $min, returns $min.
|
||||
* If the number is greater than $max, returns $max.
|
||||
* Otherwise, returns this number unchanged.
|
||||
*
|
||||
* @param BigNumber|int|float|string $min The minimum. Must be convertible to an instance of the class this method is called on.
|
||||
* @param BigNumber|int|float|string $max The maximum. Must be convertible to an instance of the class this method is called on.
|
||||
*
|
||||
* @throws MathException If min/max are not convertible to an instance of the class this method is called on.
|
||||
* @throws InvalidArgumentException If min is greater than max.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function clamp(BigNumber|int|float|string $min, BigNumber|int|float|string $max): static
|
||||
{
|
||||
$min = static::of($min);
|
||||
$max = static::of($max);
|
||||
|
||||
if ($min->isGreaterThan($max)) {
|
||||
throw new InvalidArgumentException('Minimum value must be less than or equal to maximum value.');
|
||||
}
|
||||
|
||||
if ($this->isLessThan($min)) {
|
||||
return $min;
|
||||
}
|
||||
|
||||
if ($this->isGreaterThan($max)) {
|
||||
return $max;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this number to a BigInteger.
|
||||
*
|
||||
* @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function toBigInteger(): BigInteger;
|
||||
|
||||
/**
|
||||
* Converts this number to a BigDecimal.
|
||||
*
|
||||
* @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function toBigDecimal(): BigDecimal;
|
||||
|
||||
/**
|
||||
* Converts this number to a BigRational.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function toBigRational(): BigRational;
|
||||
|
||||
/**
|
||||
* Converts this number to a BigDecimal with the given scale, using rounding if necessary.
|
||||
*
|
||||
* @param int $scale The scale of the resulting `BigDecimal`. Must be non-negative.
|
||||
* @param RoundingMode $roundingMode An optional rounding mode, defaults to Unnecessary.
|
||||
*
|
||||
* @throws InvalidArgumentException If the scale is negative.
|
||||
* @throws RoundingNecessaryException If RoundingMode::Unnecessary is used, and this number cannot be converted to
|
||||
* the given scale without rounding.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal;
|
||||
|
||||
/**
|
||||
* Returns the exact value of this number as a native integer.
|
||||
*
|
||||
* If this number cannot be converted to a native integer without losing precision, an exception is thrown.
|
||||
* Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit.
|
||||
*
|
||||
* @throws MathException If this number cannot be exactly converted to a native integer.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function toInt(): int;
|
||||
|
||||
/**
|
||||
* Returns an approximation of this number as a floating-point value.
|
||||
*
|
||||
* Note that this method can discard information as the precision of a floating-point value
|
||||
* is inherently limited.
|
||||
*
|
||||
* If the number is greater than the largest representable floating point number, positive infinity is returned.
|
||||
* If the number is less than the smallest representable floating point number, negative infinity is returned.
|
||||
* This method never returns NaN.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function toFloat(): float;
|
||||
|
||||
/**
|
||||
* Returns a string representation of this number.
|
||||
*
|
||||
* The output of this method can be parsed by the `of()` factory method; this will yield an object equal to this
|
||||
* one, but possibly of a different type if instantiated through `BigNumber::of()`.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function toString(): string;
|
||||
|
||||
#[Override]
|
||||
final public function jsonSerialize(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
final public function __toString(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridden by subclasses to convert a BigNumber to an instance of the subclass.
|
||||
*
|
||||
* @throws RoundingNecessaryException If the value cannot be converted.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract protected static function from(BigNumber $number): static;
|
||||
|
||||
/**
|
||||
* Proxy method to access BigInteger's protected constructor from sibling classes.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final protected function newBigInteger(string $value): BigInteger
|
||||
{
|
||||
return new BigInteger($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy method to access BigDecimal's protected constructor from sibling classes.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final protected function newBigDecimal(string $value, int $scale = 0): BigDecimal
|
||||
{
|
||||
return new BigDecimal($value, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy method to access BigRational's protected constructor from sibling classes.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final protected function newBigRational(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator): BigRational
|
||||
{
|
||||
return new BigRational($numerator, $denominator, $checkDenominator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NumberFormatException If the format of the number is not valid.
|
||||
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private static function _of(BigNumber|int|float|string $value): BigNumber
|
||||
{
|
||||
if ($value instanceof BigNumber) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (is_int($value)) {
|
||||
return new BigInteger((string) $value);
|
||||
}
|
||||
|
||||
if (is_float($value)) {
|
||||
// @phpstan-ignore-next-line
|
||||
trigger_error(
|
||||
'Passing floats to BigNumber::of() and arithmetic methods is deprecated and will be removed in 0.15. ' .
|
||||
'Cast the float to string explicitly to preserve the previous behaviour.',
|
||||
E_USER_DEPRECATED,
|
||||
);
|
||||
|
||||
if (is_nan($value)) {
|
||||
$value = 'NAN';
|
||||
} else {
|
||||
$value = (string) $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (str_contains($value, '/')) {
|
||||
// Rational number
|
||||
if (preg_match(self::PARSE_REGEXP_RATIONAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
|
||||
throw NumberFormatException::invalidFormat($value);
|
||||
}
|
||||
|
||||
$sign = $matches['sign'];
|
||||
$numerator = $matches['numerator'];
|
||||
$denominator = $matches['denominator'];
|
||||
|
||||
$numerator = self::cleanUp($sign, $numerator);
|
||||
$denominator = self::cleanUp(null, $denominator);
|
||||
|
||||
if ($denominator === '0') {
|
||||
throw DivisionByZeroException::denominatorMustNotBeZero();
|
||||
}
|
||||
|
||||
return new BigRational(
|
||||
new BigInteger($numerator),
|
||||
new BigInteger($denominator),
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
// Integer or decimal number
|
||||
if (preg_match(self::PARSE_REGEXP_NUMERICAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
|
||||
throw NumberFormatException::invalidFormat($value);
|
||||
}
|
||||
|
||||
$sign = $matches['sign'];
|
||||
$point = $matches['point'];
|
||||
$integral = $matches['integral'];
|
||||
$fractional = $matches['fractional'];
|
||||
$exponent = $matches['exponent'];
|
||||
|
||||
if ($integral === null && $fractional === null) {
|
||||
throw NumberFormatException::invalidFormat($value);
|
||||
}
|
||||
|
||||
if ($integral === null) {
|
||||
$integral = '0';
|
||||
}
|
||||
|
||||
if ($point !== null || $exponent !== null) {
|
||||
$fractional ??= '';
|
||||
|
||||
if ($exponent !== null) {
|
||||
if ($exponent[0] === '-') {
|
||||
$exponent = ltrim(substr($exponent, 1), '0') ?: '0';
|
||||
$exponent = filter_var($exponent, FILTER_VALIDATE_INT);
|
||||
if ($exponent !== false) {
|
||||
$exponent = -$exponent;
|
||||
}
|
||||
} else {
|
||||
if ($exponent[0] === '+') {
|
||||
$exponent = substr($exponent, 1);
|
||||
}
|
||||
$exponent = ltrim($exponent, '0') ?: '0';
|
||||
$exponent = filter_var($exponent, FILTER_VALIDATE_INT);
|
||||
}
|
||||
} else {
|
||||
$exponent = 0;
|
||||
}
|
||||
|
||||
if ($exponent === false) {
|
||||
throw new NumberFormatException('Exponent too large.');
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes optional leading zeros and applies sign.
|
||||
*
|
||||
* @param string|null $sign The sign, '+' or '-', optional. Null is allowed for convenience and treated as '+'.
|
||||
* @param string $number The number, validated as a string of digits.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private static function cleanUp(string|null $sign, string $number): string
|
||||
{
|
||||
$number = ltrim($number, '0');
|
||||
|
||||
if ($number === '') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
return $sign === '-' ? '-' . $number : $number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException.
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
return $a->plus($b);
|
||||
}
|
||||
}
|
||||
+606
@@ -0,0 +1,606 @@
|
||||
<?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;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use Override;
|
||||
|
||||
use function is_finite;
|
||||
use function max;
|
||||
use function min;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
use function trigger_error;
|
||||
|
||||
use const E_USER_DEPRECATED;
|
||||
|
||||
/**
|
||||
* An arbitrarily large rational number.
|
||||
*
|
||||
* This class is immutable.
|
||||
*
|
||||
* Fractions are automatically simplified to lowest terms. For example, `2/4` becomes `1/2`.
|
||||
* The denominator is always strictly positive; the sign is carried by the numerator.
|
||||
*/
|
||||
final readonly class BigRational extends BigNumber
|
||||
{
|
||||
/**
|
||||
* The numerator.
|
||||
*/
|
||||
private BigInteger $numerator;
|
||||
|
||||
/**
|
||||
* The denominator. Always strictly positive.
|
||||
*/
|
||||
private BigInteger $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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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 out of a numerator and a denominator.
|
||||
*
|
||||
* If the denominator is negative, the signs of both the numerator and the denominator
|
||||
* will be inverted to ensure that the denominator is always positive.
|
||||
*
|
||||
* @deprecated Use ofFraction() instead.
|
||||
*
|
||||
* @param BigNumber|int|float|string $numerator The numerator. Must be convertible to a BigInteger.
|
||||
* @param BigNumber|int|float|string $denominator The denominator. Must be convertible to a BigInteger.
|
||||
*
|
||||
* @throws NumberFormatException If an argument does not represent a valid number.
|
||||
* @throws RoundingNecessaryException If an argument represents a non-integer number.
|
||||
* @throws DivisionByZeroException If the denominator is zero.
|
||||
*/
|
||||
public static function nd(
|
||||
BigNumber|int|float|string $numerator,
|
||||
BigNumber|int|float|string $denominator,
|
||||
): BigRational {
|
||||
trigger_error(
|
||||
'The BigRational::nd() method is deprecated, use BigRational::ofFraction() instead.',
|
||||
E_USER_DEPRECATED,
|
||||
);
|
||||
|
||||
return self::ofFraction($numerator, $denominator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @throws MathException If an argument is not valid, or is not convertible to a BigInteger.
|
||||
* @throws DivisionByZeroException If the denominator is zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function ofFraction(
|
||||
BigNumber|int|float|string $numerator,
|
||||
BigNumber|int|float|string $denominator,
|
||||
): BigRational {
|
||||
$numerator = BigInteger::of($numerator);
|
||||
$denominator = BigInteger::of($denominator);
|
||||
|
||||
return new BigRational($numerator, $denominator, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a BigRational representing zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function zero(): BigRational
|
||||
{
|
||||
/** @var BigRational|null $zero */
|
||||
static $zero;
|
||||
|
||||
if ($zero === null) {
|
||||
$zero = new BigRational(BigInteger::zero(), BigInteger::one(), false);
|
||||
}
|
||||
|
||||
return $zero;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a BigRational representing one.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function one(): BigRational
|
||||
{
|
||||
/** @var BigRational|null $one */
|
||||
static $one;
|
||||
|
||||
if ($one === null) {
|
||||
$one = new BigRational(BigInteger::one(), BigInteger::one(), false);
|
||||
}
|
||||
|
||||
return $one;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a BigRational representing ten.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function ten(): BigRational
|
||||
{
|
||||
/** @var BigRational|null $ten */
|
||||
static $ten;
|
||||
|
||||
if ($ten === null) {
|
||||
$ten = new BigRational(BigInteger::ten(), BigInteger::one(), false);
|
||||
}
|
||||
|
||||
return $ten;
|
||||
}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
public function getNumerator(): BigInteger
|
||||
{
|
||||
return $this->numerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
public function getDenominator(): BigInteger
|
||||
{
|
||||
return $this->denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the quotient of the division of the numerator by the denominator.
|
||||
*
|
||||
* @deprecated Will be removed in 0.15. Use getIntegralPart() instead.
|
||||
*/
|
||||
public function quotient(): BigInteger
|
||||
{
|
||||
trigger_error(
|
||||
'BigRational::quotient() is deprecated and will be removed in 0.15. Use getIntegralPart() instead.',
|
||||
E_USER_DEPRECATED,
|
||||
);
|
||||
|
||||
return $this->numerator->quotient($this->denominator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remainder of the division of the numerator by the denominator.
|
||||
*
|
||||
* @deprecated Will be removed in 0.15. Use `$number->getNumerator()->remainder($number->getDenominator())` instead.
|
||||
*/
|
||||
public function remainder(): BigInteger
|
||||
{
|
||||
trigger_error(
|
||||
'BigRational::remainder() is deprecated and will be removed in 0.15. Use `$number->getNumerator()->remainder($number->getDenominator())` instead.',
|
||||
E_USER_DEPRECATED,
|
||||
);
|
||||
|
||||
return $this->numerator->remainder($this->denominator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the quotient and remainder of the division of the numerator by the denominator.
|
||||
*
|
||||
* @deprecated Will be removed in 0.15. Use `$number->getNumerator()->quotientAndRemainder($number->getDenominator())` instead.
|
||||
*
|
||||
* @return array{BigInteger, BigInteger}
|
||||
*/
|
||||
public function quotientAndRemainder(): array
|
||||
{
|
||||
trigger_error(
|
||||
'BigRational::quotientAndRemainder() is deprecated and will be removed in 0.15. Use `$number->getNumerator()->quotientAndRemainder($number->getDenominator())` instead.',
|
||||
E_USER_DEPRECATED,
|
||||
);
|
||||
|
||||
return $this->numerator->quotientAndRemainder($this->denominator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the integral part of this rational number.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* - `7/3` returns `2` (since 7/3 = 2 + 1/3)
|
||||
* - `-7/3` returns `-2` (since -7/3 = -2 + (-1/3))
|
||||
*
|
||||
* The following identity holds: `$r->isEqualTo($r->getFractionalPart()->plus($r->getIntegralPart()))`.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function getIntegralPart(): BigInteger
|
||||
{
|
||||
return $this->numerator->quotient($this->denominator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fractional part of this rational number.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* - `7/3` returns `1/3` (since 7/3 = 2 + 1/3)
|
||||
* - `-7/3` returns `-1/3` (since -7/3 = -2 + (-1/3))
|
||||
*
|
||||
* The following identity holds: `$r->isEqualTo($r->getFractionalPart()->plus($r->getIntegralPart()))`.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function getFractionalPart(): BigRational
|
||||
{
|
||||
return new BigRational($this->numerator->remainder($this->denominator), $this->denominator, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sum of this number and the given one.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The number to add.
|
||||
*
|
||||
* @throws MathException If the number is not valid.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function plus(BigNumber|int|float|string $that): BigRational
|
||||
{
|
||||
$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.
|
||||
*
|
||||
* @throws MathException If the number is not valid.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function minus(BigNumber|int|float|string $that): BigRational
|
||||
{
|
||||
$that = BigRational::of($that);
|
||||
|
||||
$numerator = $this->numerator->multipliedBy($that->denominator);
|
||||
$numerator = $numerator->minus($that->numerator->multipliedBy($this->denominator));
|
||||
$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.
|
||||
*
|
||||
* @throws MathException If the multiplier is not valid.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function multipliedBy(BigNumber|int|float|string $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.
|
||||
*
|
||||
* @throws MathException If the divisor is not valid.
|
||||
* @throws DivisionByZeroException If the divisor is zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function dividedBy(BigNumber|int|float|string $that): BigRational
|
||||
{
|
||||
$that = BigRational::of($that);
|
||||
|
||||
if ($that->isZero()) {
|
||||
throw DivisionByZeroException::divisionByZero();
|
||||
}
|
||||
|
||||
$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.
|
||||
*
|
||||
* @throws InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function power(int $exponent): BigRational
|
||||
{
|
||||
if ($exponent === 0) {
|
||||
return BigRational::one();
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
* @throws DivisionByZeroException If the numerator is zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function reciprocal(): BigRational
|
||||
{
|
||||
return new BigRational($this->denominator, $this->numerator, true);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function negated(): static
|
||||
{
|
||||
return new BigRational($this->numerator->negated(), $this->denominator, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the simplified value of this BigRational.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function simplified(): BigRational
|
||||
{
|
||||
$gcd = $this->numerator->gcd($this->denominator);
|
||||
|
||||
$numerator = $this->numerator->quotient($gcd);
|
||||
$denominator = $this->denominator->quotient($gcd);
|
||||
|
||||
return new BigRational($numerator, $denominator, false);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function compareTo(BigNumber|int|float|string $that): int
|
||||
{
|
||||
$that = BigRational::of($that);
|
||||
|
||||
if ($this->denominator->isEqualTo($that->denominator)) {
|
||||
return $this->numerator->compareTo($that->numerator);
|
||||
}
|
||||
|
||||
return $this->numerator
|
||||
->multipliedBy($that->denominator)
|
||||
->compareTo($that->numerator->multipliedBy($this->denominator));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function getSign(): int
|
||||
{
|
||||
return $this->numerator->getSign();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBigInteger(): BigInteger
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBigDecimal(): BigDecimal
|
||||
{
|
||||
return $this->numerator->toBigDecimal()->dividedByExact($this->denominator);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBigRational(): BigRational
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal
|
||||
{
|
||||
return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toInt(): int
|
||||
{
|
||||
return $this->toBigInteger()->toInt();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toFloat(): float
|
||||
{
|
||||
$simplified = $this->simplified();
|
||||
$numeratorFloat = $simplified->numerator->toFloat();
|
||||
$denominatorFloat = $simplified->denominator->toFloat();
|
||||
|
||||
if (is_finite($numeratorFloat) && is_finite($denominatorFloat)) {
|
||||
return $numeratorFloat / $denominatorFloat;
|
||||
}
|
||||
|
||||
// At least one side overflows to INF; use a decimal approximation instead.
|
||||
// We need ~17 significant digits for double precision (we use 20 for some margin). Since $scale controls
|
||||
// decimal places (not significant digits), we subtract the estimated order of magnitude so that large results
|
||||
// use fewer decimal places and small results use more (to look past leading zeros). Clamped to [0, 350] as
|
||||
// doubles range from e-324 to e308 (350 ≈ 324 + 20 significant digits + margin).
|
||||
$magnitude = strlen($simplified->numerator->abs()->toString()) - strlen($simplified->denominator->toString());
|
||||
$scale = min(350, max(0, 20 - $magnitude));
|
||||
|
||||
return $simplified->numerator
|
||||
->toBigDecimal()
|
||||
->dividedBy($simplified->denominator, $scale, RoundingMode::HalfEven)
|
||||
->toFloat();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toString(): string
|
||||
{
|
||||
$numerator = $this->numerator->toString();
|
||||
$denominator = $this->denominator->toString();
|
||||
|
||||
if ($denominator === '1') {
|
||||
return $numerator;
|
||||
}
|
||||
|
||||
return $numerator . '/' . $denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the decimal representation of this rational number, with repeating decimals in parentheses.
|
||||
*
|
||||
* WARNING: This method is unbounded.
|
||||
* The length of the repeating decimal period can be as large as `denominator - 1`.
|
||||
* For fractions with large denominators, this method can use excessive memory and CPU time.
|
||||
* For example, `1/100019` has a repeating period of 100,018 digits.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* - `10/3` returns `3.(3)`
|
||||
* - `171/70` returns `2.4(428571)`
|
||||
* - `1/2` returns `0.5`
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function toRepeatingDecimalString(): string
|
||||
{
|
||||
if ($this->numerator->isZero()) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
$sign = $this->numerator->isNegative() ? '-' : '';
|
||||
$numerator = $this->numerator->abs();
|
||||
$denominator = $this->denominator;
|
||||
|
||||
$integral = $numerator->quotient($denominator);
|
||||
$remainder = $numerator->remainder($denominator);
|
||||
|
||||
$integralString = $integral->toString();
|
||||
|
||||
if ($remainder->isZero()) {
|
||||
return $sign . $integralString;
|
||||
}
|
||||
|
||||
$digits = '';
|
||||
$remainderPositions = [];
|
||||
$index = 0;
|
||||
|
||||
while (! $remainder->isZero()) {
|
||||
$remainderString = $remainder->toString();
|
||||
|
||||
if (isset($remainderPositions[$remainderString])) {
|
||||
$repeatIndex = $remainderPositions[$remainderString];
|
||||
$nonRepeating = substr($digits, 0, $repeatIndex);
|
||||
$repeating = substr($digits, $repeatIndex);
|
||||
|
||||
return $sign . $integralString . '.' . $nonRepeating . '(' . $repeating . ')';
|
||||
}
|
||||
|
||||
$remainderPositions[$remainderString] = $index;
|
||||
$remainder = $remainder->multipliedBy(10);
|
||||
|
||||
$digits .= $remainder->quotient($denominator)->toString();
|
||||
$remainder = $remainder->remainder($denominator);
|
||||
$index++;
|
||||
}
|
||||
|
||||
return $sign . $integralString . '.' . $digits;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is required for serializing the object and SHOULD NOT be accessed directly.
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @param array{numerator: BigInteger, denominator: BigInteger} $data
|
||||
*
|
||||
* @throws LogicException
|
||||
*/
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
/** @phpstan-ignore isset.initializedProperty */
|
||||
if (isset($this->numerator)) {
|
||||
throw new LogicException('__unserialize() is an internal function, it must not be called directly.');
|
||||
}
|
||||
|
||||
/** @phpstan-ignore deadCode.unreachable */
|
||||
$this->numerator = $data['numerator'];
|
||||
$this->denominator = $data['denominator'];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
protected static function from(BigNumber $number): static
|
||||
{
|
||||
return $number->toBigRational();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when a division by zero occurs.
|
||||
*/
|
||||
final class DivisionByZeroException extends MathException
|
||||
{
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
public static function divisionByZero(): DivisionByZeroException
|
||||
{
|
||||
return new self('Division by zero.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
public static function modulusMustNotBeZero(): DivisionByZeroException
|
||||
{
|
||||
return new self('The modulus must not be zero.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
public static function denominatorMustNotBeZero(): DivisionByZeroException
|
||||
{
|
||||
return new self('The denominator of a rational number cannot be zero.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use Brick\Math\BigInteger;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
use const PHP_INT_MAX;
|
||||
use const PHP_INT_MIN;
|
||||
|
||||
/**
|
||||
* Exception thrown when an integer overflow occurs.
|
||||
*/
|
||||
final class IntegerOverflowException extends MathException
|
||||
{
|
||||
/**
|
||||
* @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, $value->toString(), PHP_INT_MIN, PHP_INT_MAX));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Base class for all math exceptions.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
final class NegativeNumberException extends MathException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use function dechex;
|
||||
use function ord;
|
||||
use function sprintf;
|
||||
use function strtoupper;
|
||||
|
||||
/**
|
||||
* Exception thrown when attempting to create a number from a string with an invalid format.
|
||||
*/
|
||||
final class NumberFormatException extends MathException
|
||||
{
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
public static function invalidFormat(string $value): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'The given value "%s" does not represent a valid number.',
|
||||
$value,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $char The failing character.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function charNotInAlphabet(string $char): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Character %s is not valid in the given alphabet.',
|
||||
self::charToString($char),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
private static function charToString(string $char): string
|
||||
{
|
||||
$ord = ord($char);
|
||||
|
||||
if ($ord < 32 || $ord > 126) {
|
||||
$char = strtoupper(dechex($ord));
|
||||
|
||||
if ($ord < 16) {
|
||||
$char = '0' . $char;
|
||||
}
|
||||
|
||||
return '0x' . $char;
|
||||
}
|
||||
|
||||
return '"' . $char . '"';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when a number cannot be represented at the requested scale without rounding.
|
||||
*/
|
||||
final class RoundingNecessaryException extends MathException
|
||||
{
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
public static function roundingNecessary(): RoundingNecessaryException
|
||||
{
|
||||
return new self('Rounding is necessary to represent the result of the operation at this scale.');
|
||||
}
|
||||
}
|
||||
+704
@@ -0,0 +1,704 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal;
|
||||
|
||||
use Brick\Math\Exception\RoundingNecessaryException;
|
||||
use Brick\Math\RoundingMode;
|
||||
|
||||
use function chr;
|
||||
use function ltrim;
|
||||
use function ord;
|
||||
use function str_repeat;
|
||||
use function strlen;
|
||||
use function strpos;
|
||||
use function strrev;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* Performs basic operations on arbitrary size integers.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
abstract readonly class Calculator
|
||||
{
|
||||
/**
|
||||
* The maximum exponent value allowed for the pow() method.
|
||||
*/
|
||||
public const MAX_POWER = 1_000_000;
|
||||
|
||||
/**
|
||||
* The alphabet for converting from and to base 2 to 36, lowercase.
|
||||
*/
|
||||
public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
/**
|
||||
* Returns the absolute value of a number.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function abs(string $n): string
|
||||
{
|
||||
return ($n[0] === '-') ? substr($n, 1) : $n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Negates a number.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function neg(string $n): string
|
||||
{
|
||||
if ($n === '0') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
if ($n[0] === '-') {
|
||||
return substr($n, 1);
|
||||
}
|
||||
|
||||
return '-' . $n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two numbers.
|
||||
*
|
||||
* Returns -1 if the first number is less than, 0 if equal to, 1 if greater than the second number.
|
||||
*
|
||||
* @return -1|0|1
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function cmp(string $a, string $b): int
|
||||
{
|
||||
[$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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function add(string $a, string $b): string;
|
||||
|
||||
/**
|
||||
* Subtracts two numbers.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function sub(string $a, string $b): string;
|
||||
|
||||
/**
|
||||
* Multiplies two numbers.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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 array{string, string} An array containing the quotient and remainder.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function pow(string $a, int $e): string;
|
||||
|
||||
/**
|
||||
* @param string $b The modulus; must not be zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function mod(string $a, string $b): string
|
||||
{
|
||||
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 $m The modulus; must not be negative or zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
[$g, $x] = $this->gcdExtended($modVal, $m);
|
||||
|
||||
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.
|
||||
* @param string $exp The exponent; must be positive or zero.
|
||||
* @param string $mod The modulus; must be strictly positive.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function modPow(string $base, string $exp, string $mod): string;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return string The GCD, always positive, or zero if both arguments are zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the least common multiple of the two numbers.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for LCM calculations.
|
||||
*
|
||||
* @return string The LCM, always positive, or zero if at least one argument is zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function lcm(string $a, string $b): string
|
||||
{
|
||||
if ($a === '0' || $b === '0') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
return $this->divQ($this->abs($this->mul($a, $b)), $this->gcd($a, $b));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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 RoundingMode $roundingMode The rounding mode.
|
||||
*
|
||||
* @throws RoundingNecessaryException If RoundingMode::Unnecessary is provided but rounding is necessary.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function divRound(string $a, string $b, RoundingMode $roundingMode): string
|
||||
{
|
||||
[$quotient, $remainder] = $this->divQR($a, $b);
|
||||
|
||||
$hasDiscardedFraction = ($remainder !== '0');
|
||||
$isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-');
|
||||
|
||||
$discardedFractionSign = function () use ($remainder, $b): int {
|
||||
$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::HalfUp:
|
||||
$increment = $discardedFractionSign() >= 0;
|
||||
|
||||
break;
|
||||
|
||||
case RoundingMode::HalfDown:
|
||||
$increment = $discardedFractionSign() > 0;
|
||||
|
||||
break;
|
||||
|
||||
case RoundingMode::HalfCeiling:
|
||||
$increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0;
|
||||
|
||||
break;
|
||||
|
||||
case RoundingMode::HalfFloor:
|
||||
$increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
|
||||
|
||||
break;
|
||||
|
||||
case RoundingMode::HalfEven:
|
||||
$lastDigit = (int) $quotient[-1];
|
||||
$lastDigitIsEven = ($lastDigit % 2 === 0);
|
||||
$increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function xor(string $a, string $b): string
|
||||
{
|
||||
return $this->bitwise('xor', $a, $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the sign & digits of the operands.
|
||||
*
|
||||
* @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final protected function init(string $a, string $b): array
|
||||
{
|
||||
return [
|
||||
$aNeg = ($a[0] === '-'),
|
||||
$bNeg = ($b[0] === '-'),
|
||||
|
||||
$aNeg ? substr($a, 1) : $a,
|
||||
$bNeg ? substr($b, 1) : $b,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{string, string, string} GCD, X, Y
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private function gcdExtended(string $a, string $b): array
|
||||
{
|
||||
if ($a === '0') {
|
||||
return [$b, '0', '1'];
|
||||
}
|
||||
|
||||
[$gcd, $x1, $y1] = $this->gcdExtended($this->mod($b, $a), $a);
|
||||
|
||||
$x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1));
|
||||
$y = $x1;
|
||||
|
||||
return [$gcd, $x, $y];
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a bitwise operation on a decimal number.
|
||||
*
|
||||
* @param 'and'|'or'|'xor' $operator The operator to use.
|
||||
* @param string $a The left operand.
|
||||
* @param string $b The right operand.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private function bitwise(string $operator, string $a, string $b): string
|
||||
{
|
||||
[$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);
|
||||
}
|
||||
|
||||
$value = match ($operator) {
|
||||
'and' => $aBin & $bBin,
|
||||
'or' => $aBin | $bBin,
|
||||
'xor' => $aBin ^ $bBin,
|
||||
};
|
||||
|
||||
$negative = match ($operator) {
|
||||
'and' => $aNeg and $bNeg,
|
||||
'or' => $aNeg or $bNeg,
|
||||
'xor' => $aNeg xor $bNeg,
|
||||
};
|
||||
|
||||
if ($negative) {
|
||||
$value = $this->twosComplement($value);
|
||||
}
|
||||
|
||||
$result = $this->toDecimal($value);
|
||||
|
||||
return $negative ? $this->neg($result) : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $number A positive, binary number.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal\Calculator;
|
||||
|
||||
use Brick\Math\Internal\Calculator;
|
||||
use Override;
|
||||
|
||||
use function bcadd;
|
||||
use function bcdiv;
|
||||
use function bcmod;
|
||||
use function bcmul;
|
||||
use function bcpow;
|
||||
use function bcpowmod;
|
||||
use function bcsqrt;
|
||||
use function bcsub;
|
||||
|
||||
/**
|
||||
* Calculator implementation built around the bcmath library.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class BcMathCalculator extends Calculator
|
||||
{
|
||||
#[Override]
|
||||
public function add(string $a, string $b): string
|
||||
{
|
||||
return bcadd($a, $b, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function sub(string $a, string $b): string
|
||||
{
|
||||
return bcsub($a, $b, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function mul(string $a, string $b): string
|
||||
{
|
||||
return bcmul($a, $b, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divQ(string $a, string $b): string
|
||||
{
|
||||
return bcdiv($a, $b, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divR(string $a, string $b): string
|
||||
{
|
||||
return bcmod($a, $b, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divQR(string $a, string $b): array
|
||||
{
|
||||
$q = bcdiv($a, $b, 0);
|
||||
$r = bcmod($a, $b, 0);
|
||||
|
||||
return [$q, $r];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function pow(string $a, int $e): string
|
||||
{
|
||||
return bcpow($a, (string) $e, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function modPow(string $base, string $exp, string $mod): string
|
||||
{
|
||||
// normalize to Euclidean representative so modPow() stays consistent with mod()
|
||||
$base = $this->mod($base, $mod);
|
||||
|
||||
return bcpowmod($base, $exp, $mod, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function sqrt(string $n): string
|
||||
{
|
||||
return bcsqrt($n, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal\Calculator;
|
||||
|
||||
use Brick\Math\Internal\Calculator;
|
||||
use GMP;
|
||||
use Override;
|
||||
|
||||
use function gmp_add;
|
||||
use function gmp_and;
|
||||
use function gmp_div_q;
|
||||
use function gmp_div_qr;
|
||||
use function gmp_div_r;
|
||||
use function gmp_gcd;
|
||||
use function gmp_init;
|
||||
use function gmp_invert;
|
||||
use function gmp_lcm;
|
||||
use function gmp_mul;
|
||||
use function gmp_or;
|
||||
use function gmp_pow;
|
||||
use function gmp_powm;
|
||||
use function gmp_sqrt;
|
||||
use function gmp_strval;
|
||||
use function gmp_sub;
|
||||
use function gmp_xor;
|
||||
|
||||
/**
|
||||
* Calculator implementation built around the GMP library.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class GmpCalculator extends Calculator
|
||||
{
|
||||
#[Override]
|
||||
public function add(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_add($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function sub(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_sub($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function mul(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_mul($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divQ(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_div_q($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divR(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_div_r($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divQR(string $a, string $b): array
|
||||
{
|
||||
[$q, $r] = gmp_div_qr($a, $b);
|
||||
|
||||
/**
|
||||
* @var GMP $q
|
||||
* @var GMP $r
|
||||
*/
|
||||
return [
|
||||
gmp_strval($q),
|
||||
gmp_strval($r),
|
||||
];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function pow(string $a, int $e): string
|
||||
{
|
||||
return gmp_strval(gmp_pow($a, $e));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function modInverse(string $x, string $m): ?string
|
||||
{
|
||||
$result = gmp_invert($x, $m);
|
||||
|
||||
if ($result === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return gmp_strval($result);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function modPow(string $base, string $exp, string $mod): string
|
||||
{
|
||||
return gmp_strval(gmp_powm($base, $exp, $mod));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function gcd(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_gcd($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function lcm(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_lcm($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function fromBase(string $number, int $base): string
|
||||
{
|
||||
return gmp_strval(gmp_init($number, $base));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBase(string $number, int $base): string
|
||||
{
|
||||
return gmp_strval($number, $base);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function and(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_and($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function or(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_or($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function xor(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_xor($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function sqrt(string $n): string
|
||||
{
|
||||
return gmp_strval(gmp_sqrt($n));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,616 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal\Calculator;
|
||||
|
||||
use Brick\Math\Internal\Calculator;
|
||||
use Override;
|
||||
|
||||
use function assert;
|
||||
use function in_array;
|
||||
use function intdiv;
|
||||
use function is_int;
|
||||
use function ltrim;
|
||||
use function str_pad;
|
||||
use function str_repeat;
|
||||
use function strcmp;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
|
||||
use const PHP_INT_SIZE;
|
||||
use const STR_PAD_LEFT;
|
||||
|
||||
/**
|
||||
* Calculator implementation using only native PHP code.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly 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.
|
||||
*
|
||||
* In 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)
|
||||
*/
|
||||
private int $maxDigits;
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->maxDigits = match (PHP_INT_SIZE) {
|
||||
4 => 9,
|
||||
8 => 18,
|
||||
};
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function add(string $a, string $b): string
|
||||
{
|
||||
/**
|
||||
* @var numeric-string $a
|
||||
* @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;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function sub(string $a, string $b): string
|
||||
{
|
||||
return $this->add($a, $this->neg($b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function mul(string $a, string $b): string
|
||||
{
|
||||
/**
|
||||
* @var numeric-string $a
|
||||
* @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;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divQ(string $a, string $b): string
|
||||
{
|
||||
return $this->divQR($a, $b)[0];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divR(string $a, string $b): string
|
||||
{
|
||||
return $this->divQR($a, $b)[1];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
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'];
|
||||
}
|
||||
|
||||
/** @var numeric-string $a */
|
||||
$na = $a * 1; // cast to number
|
||||
|
||||
if (is_int($na)) {
|
||||
/** @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.
|
||||
$q = intdiv($na, $nb);
|
||||
$r = $na % $nb;
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
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);
|
||||
|
||||
$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/.
|
||||
*/
|
||||
#[Override]
|
||||
public function modPow(string $base, string $exp, string $mod): string
|
||||
{
|
||||
// normalize to Euclidean representative so modPow() stays consistent with mod()
|
||||
$base = $this->mod($base, $mod);
|
||||
|
||||
// special case: the algorithm below fails with power 0 mod 1 (returns 1 instead of 0)
|
||||
if ($exp === '0' && $mod === '1') {
|
||||
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.
|
||||
*/
|
||||
#[Override]
|
||||
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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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;
|
||||
$i = 0;
|
||||
}
|
||||
|
||||
/** @var numeric-string $blockA */
|
||||
$blockA = substr($a, $i, $blockLength);
|
||||
|
||||
/** @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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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;
|
||||
$i = 0;
|
||||
}
|
||||
|
||||
/** @var numeric-string $blockA */
|
||||
$blockA = substr($a, $i, $blockLength);
|
||||
|
||||
/** @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.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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;
|
||||
$i = 0;
|
||||
}
|
||||
|
||||
$blockA = (int) substr($a, $i, $blockALength);
|
||||
|
||||
$line = '';
|
||||
$carry = 0;
|
||||
|
||||
for ($j = $y - $maxDigits; ; $j -= $maxDigits) {
|
||||
$blockBLength = $maxDigits;
|
||||
|
||||
if ($j < 0) {
|
||||
$blockBLength += $j;
|
||||
$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.
|
||||
*
|
||||
* @return string[] The quotient and remainder.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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
|
||||
|
||||
/** @var numeric-string $b */
|
||||
$nb = $b * 1; // cast to number
|
||||
// performance optimization in cases where the remainder will never cause int overflow
|
||||
if (is_int(($nb - 1) * 10 + 9)) {
|
||||
$r = (int) substr($a, 0, $z - 1);
|
||||
|
||||
for ($i = $z - 1; $i < $x; $i++) {
|
||||
$n = $r * 10 + (int) $a[$i];
|
||||
/** @var int $nb */
|
||||
$q .= intdiv($n, $nb);
|
||||
$r = $n % $nb;
|
||||
}
|
||||
|
||||
return [ltrim($q, '0') ?: '0', (string) $r];
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
* @return -1|0|1
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @return array{string, string, int}
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
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];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal;
|
||||
|
||||
use function extension_loaded;
|
||||
|
||||
/**
|
||||
* Stores the current Calculator instance used by BigNumber classes.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class CalculatorRegistry
|
||||
{
|
||||
/**
|
||||
* The Calculator instance in use.
|
||||
*/
|
||||
private static ?Calculator $instance = null;
|
||||
|
||||
/**
|
||||
* Sets the Calculator instance to use.
|
||||
*
|
||||
* An instance is typically set only in unit tests: autodetect is usually the best option.
|
||||
*
|
||||
* @param Calculator|null $calculator The calculator instance, or null to revert to autodetect.
|
||||
*/
|
||||
final public static function set(?Calculator $calculator): void
|
||||
{
|
||||
self::$instance = $calculator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Calculator instance to use.
|
||||
*
|
||||
* If none has been explicitly set, the fastest available implementation will be returned.
|
||||
*
|
||||
* Note: even though this method is not technically pure, it is considered pure when used in a normal context, when
|
||||
* only relying on autodetect.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public static function get(): Calculator
|
||||
{
|
||||
/** @phpstan-ignore impure.staticPropertyAccess */
|
||||
if (self::$instance === null) {
|
||||
/** @phpstan-ignore impure.propertyAssign */
|
||||
self::$instance = self::detect();
|
||||
}
|
||||
|
||||
/** @phpstan-ignore impure.staticPropertyAccess */
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fastest available Calculator implementation.
|
||||
*
|
||||
* @pure
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private static function detect(): Calculator
|
||||
{
|
||||
if (extension_loaded('gmp')) {
|
||||
return new Calculator\GmpCalculator();
|
||||
}
|
||||
|
||||
if (extension_loaded('bcmath')) {
|
||||
return new Calculator\BcMathCalculator();
|
||||
}
|
||||
|
||||
return new Calculator\NativeCalculator();
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math;
|
||||
|
||||
/**
|
||||
* Specifies rounding behavior by defining how discarded digits affect the returned result when an exact value cannot
|
||||
* be represented at the requested scale.
|
||||
*/
|
||||
enum RoundingMode
|
||||
{
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
case Unnecessary;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
case Up;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
case Down;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
case Ceiling;
|
||||
|
||||
/**
|
||||
* Rounds towards negative infinity.
|
||||
*
|
||||
* If the result is positive, behaves as for Down; if negative, behaves as for Up.
|
||||
* Note that this rounding mode never increases the calculated value.
|
||||
*/
|
||||
case Floor;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
case HalfUp;
|
||||
|
||||
/**
|
||||
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round down.
|
||||
*
|
||||
* Behaves as for Up if the discarded fraction is > 0.5; otherwise, behaves as for Down.
|
||||
*/
|
||||
case HalfDown;
|
||||
|
||||
/**
|
||||
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards positive infinity.
|
||||
*
|
||||
* If the result is positive, behaves as for HalfUp; if negative, behaves as for HalfDown.
|
||||
*/
|
||||
case HalfCeiling;
|
||||
|
||||
/**
|
||||
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards negative infinity.
|
||||
*
|
||||
* If the result is positive, behaves as for HalfDown; if negative, behaves as for HalfUp.
|
||||
*/
|
||||
case HalfFloor;
|
||||
|
||||
/**
|
||||
* Rounds towards the "nearest neighbor" unless both neighbors are equidistant, in which case rounds towards the even neighbor.
|
||||
*
|
||||
* Behaves as for HalfUp if the digit to the left of the discarded fraction is odd;
|
||||
* behaves as for HalfDown if it's even.
|
||||
*
|
||||
* Note that this is the rounding mode that statistically minimizes
|
||||
* cumulative error when applied repeatedly over a sequence of calculations.
|
||||
* It is sometimes known as "Banker's rounding", and is chiefly used in the USA.
|
||||
*/
|
||||
case HalfEven;
|
||||
|
||||
/**
|
||||
* @deprecated Use RoundingMode::Unnecessary instead.
|
||||
*/
|
||||
public const UNNECESSARY = self::Unnecessary;
|
||||
|
||||
/**
|
||||
* @deprecated Use RoundingMode::Up instead.
|
||||
*/
|
||||
public const UP = self::Up;
|
||||
|
||||
/**
|
||||
* @deprecated Use RoundingMode::Down instead.
|
||||
*/
|
||||
public const DOWN = self::Down;
|
||||
|
||||
/**
|
||||
* @deprecated Use RoundingMode::Ceiling instead.
|
||||
*/
|
||||
public const CEILING = self::Ceiling;
|
||||
|
||||
/**
|
||||
* @deprecated Use RoundingMode::Floor instead.
|
||||
*/
|
||||
public const FLOOR = self::Floor;
|
||||
|
||||
/**
|
||||
* @deprecated Use RoundingMode::HalfUp instead.
|
||||
*/
|
||||
public const HALF_UP = self::HalfUp;
|
||||
|
||||
/**
|
||||
* @deprecated Use RoundingMode::HalfDown instead.
|
||||
*/
|
||||
public const HALF_DOWN = self::HalfDown;
|
||||
|
||||
/**
|
||||
* @deprecated Use RoundingMode::HalfCeiling instead.
|
||||
*/
|
||||
public const HALF_CEILING = self::HalfCeiling;
|
||||
|
||||
/**
|
||||
* @deprecated Use RoundingMode::HalfFloor instead.
|
||||
*/
|
||||
public const HALF_FLOOR = self::HalfFloor;
|
||||
|
||||
/**
|
||||
* @deprecated Use RoundingMode::HalfEven instead.
|
||||
*/
|
||||
public const HALF_EVEN = self::HalfEven;
|
||||
}
|
||||
+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": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/dbal": "^4.0.0",
|
||||
"nesbot/carbon": "^2.71.0 || ^3.0.0",
|
||||
"phpunit/phpunit": "^10.3"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/dbal": "<4.0.0 || >=5.0.0"
|
||||
},
|
||||
"license": "MIT",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Carbon\\Doctrine\\": "src/Carbon/Doctrine/"
|
||||
}
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "KyleKatarn",
|
||||
"email": "kylekatarnls@gmail.com"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
|
||||
interface CarbonDoctrineType
|
||||
{
|
||||
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform);
|
||||
|
||||
public function convertToPHPValue(mixed $value, AbstractPlatform $platform);
|
||||
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform);
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
class CarbonImmutableType extends DateTimeImmutableType implements CarbonDoctrineType
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
class CarbonType extends DateTimeType implements CarbonDoctrineType
|
||||
{
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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\Exception\InvalidType;
|
||||
use Doctrine\DBAL\Types\Exception\ValueNotConvertible;
|
||||
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)
|
||||
*/
|
||||
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 InvalidType::new(
|
||||
$value,
|
||||
static::class,
|
||||
['null', 'DateTime', 'Carbon']
|
||||
);
|
||||
}
|
||||
|
||||
private function doConvertToPHPValue(mixed $value)
|
||||
{
|
||||
$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 ValueNotConvertible::new(
|
||||
$value,
|
||||
static::class,
|
||||
'Y-m-d H:i:s.u or any format supported by '.$class.'::parse()',
|
||||
$error
|
||||
);
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Types\VarDateTimeImmutableType;
|
||||
|
||||
class DateTimeImmutableType extends VarDateTimeImmutableType implements CarbonDoctrineType
|
||||
{
|
||||
/** @use CarbonTypeConverter<CarbonImmutable> */
|
||||
use CarbonTypeConverter;
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?CarbonImmutable
|
||||
{
|
||||
return $this->doConvertToPHPValue($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<CarbonImmutable>
|
||||
*/
|
||||
protected function getCarbonClassName(): string
|
||||
{
|
||||
return CarbonImmutable::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use DateTime;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Types\VarDateTimeType;
|
||||
|
||||
class DateTimeType extends VarDateTimeType implements CarbonDoctrineType
|
||||
{
|
||||
/** @use CarbonTypeConverter<Carbon> */
|
||||
use CarbonTypeConverter;
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?Carbon
|
||||
{
|
||||
return $this->doConvertToPHPValue($value);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
+380
@@ -0,0 +1,380 @@
|
||||
<?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|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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||
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 (strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
|
||||
self::$installed = $required;
|
||||
$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
+19
@@ -0,0 +1,19 @@
|
||||
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.
|
||||
+7109
File diff suppressed because it is too large
Load Diff
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
// autoload_files.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
|
||||
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
|
||||
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
|
||||
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
|
||||
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
|
||||
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
|
||||
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
|
||||
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
|
||||
'def43f6c87e4f8dfd0c9e1b1bab14fe8' => $vendorDir . '/symfony/polyfill-iconv/bootstrap.php',
|
||||
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
|
||||
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
|
||||
'2203a247e6fda86070a5e4e07aed533a' => $vendorDir . '/symfony/clock/Resources/now.php',
|
||||
'606a39d89246991a373564698c2d8383' => $vendorDir . '/symfony/polyfill-php85/bootstrap.php',
|
||||
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
|
||||
'35a6ad97d21e794e7e22a17d806652e4' => $vendorDir . '/nunomaduro/termwind/src/Functions.php',
|
||||
'b33e3d135e5d9e47d845c576147bda89' => $vendorDir . '/php-di/php-di/src/functions.php',
|
||||
'09f6b20656683369174dd6fa83b7e5fb' => $vendorDir . '/symfony/polyfill-uuid/bootstrap.php',
|
||||
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
|
||||
'23dd7ece5822da3d0100ef3deb0ef55f' => $vendorDir . '/laravel/agent-detector/src/functions.php',
|
||||
'47e1160838b5e5a10346ac4084b58c23' => $vendorDir . '/laravel/prompts/src/helpers.php',
|
||||
'6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
|
||||
'801c31d8ed748cfa537fa45402288c95' => $vendorDir . '/psy/psysh/src/functions.php',
|
||||
'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php',
|
||||
'9d2b9fc6db0f153a0a149fefb182415e' => $vendorDir . '/symfony/polyfill-php84/bootstrap.php',
|
||||
'2c2415ec15363ede1bff13a287462ba1' => $vendorDir . '/symfony/polyfill-php86/bootstrap.php',
|
||||
'476ca15b8d69b04665cd879be9cb4c68' => $vendorDir . '/laravel/framework/src/Illuminate/Collections/functions.php',
|
||||
'265b4faa2b3a9766332744949e83bf97' => $vendorDir . '/laravel/framework/src/Illuminate/Collections/helpers.php',
|
||||
'c7a3c339e7e14b60e06a2d7fcce9476b' => $vendorDir . '/laravel/framework/src/Illuminate/Events/functions.php',
|
||||
'f57d353b41eb2e234b26064d63d8c5dd' => $vendorDir . '/laravel/framework/src/Illuminate/Filesystem/functions.php',
|
||||
'f0906e6318348a765ffb6eb24e0d0938' => $vendorDir . '/laravel/framework/src/Illuminate/Foundation/helpers.php',
|
||||
'7f7ac2ddea9cc3fb4b2cc201d63dbc10' => $vendorDir . '/laravel/framework/src/Illuminate/Log/functions.php',
|
||||
'91892b814db86b8442ad76273bb7aec5' => $vendorDir . '/laravel/framework/src/Illuminate/Reflection/helpers.php',
|
||||
'493c6aea52f6009bab023b26c21a386a' => $vendorDir . '/laravel/framework/src/Illuminate/Support/functions.php',
|
||||
'58571171fd5812e6e447dce228f52f4d' => $vendorDir . '/laravel/framework/src/Illuminate/Support/helpers.php',
|
||||
'309cd39bb536ff667b25a3a76938cb83' => $vendorDir . '/laravel/pao/src/Autoload.php',
|
||||
'c72349b1fe8d0deeedd3a52e8aa814d8' => $vendorDir . '/mockery/mockery/library/helpers.php',
|
||||
'ce9671a430e4846b44e1c68c7611f9f5' => $vendorDir . '/mockery/mockery/library/Mockery.php',
|
||||
'a1cfe24d14977df6878b9bf804af2d1c' => $vendorDir . '/nunomaduro/collision/src/Adapters/Phpunit/Autoload.php',
|
||||
'ec07570ca5a812141189b1fa81503674' => $vendorDir . '/phpunit/phpunit/src/Framework/Assert/Functions.php',
|
||||
'377b22b161c09ed6e5152de788ca020a' => $vendorDir . '/spatie/laravel-permission/src/helpers.php',
|
||||
);
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
// autoload_namespaces.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
);
|
||||
Vendored
+104
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
// autoload_psr4.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'voku\\' => array($vendorDir . '/voku/portable-ascii/src/voku'),
|
||||
'ZBateson\\StreamDecorators\\' => array($vendorDir . '/zbateson/stream-decorators/src'),
|
||||
'ZBateson\\MbWrapper\\' => array($vendorDir . '/zbateson/mb-wrapper/src'),
|
||||
'ZBateson\\MailMimeParser\\' => array($vendorDir . '/zbateson/mail-mime-parser/src'),
|
||||
'Whoops\\' => array($vendorDir . '/filp/whoops/src/Whoops'),
|
||||
'Webklex\\PHPIMAP\\' => array($vendorDir . '/webklex/php-imap/src'),
|
||||
'TijsVerkoyen\\CssToInlineStyles\\' => array($vendorDir . '/tijsverkoyen/css-to-inline-styles/src'),
|
||||
'Tests\\' => array($baseDir . '/tests'),
|
||||
'Termwind\\' => array($vendorDir . '/nunomaduro/termwind/src'),
|
||||
'Symfony\\Polyfill\\Uuid\\' => array($vendorDir . '/symfony/polyfill-uuid'),
|
||||
'Symfony\\Polyfill\\Php86\\' => array($vendorDir . '/symfony/polyfill-php86'),
|
||||
'Symfony\\Polyfill\\Php85\\' => array($vendorDir . '/symfony/polyfill-php85'),
|
||||
'Symfony\\Polyfill\\Php84\\' => array($vendorDir . '/symfony/polyfill-php84'),
|
||||
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
|
||||
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
|
||||
'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'),
|
||||
'Symfony\\Polyfill\\Intl\\Idn\\' => array($vendorDir . '/symfony/polyfill-intl-idn'),
|
||||
'Symfony\\Polyfill\\Intl\\Grapheme\\' => array($vendorDir . '/symfony/polyfill-intl-grapheme'),
|
||||
'Symfony\\Polyfill\\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\\Uid\\' => array($vendorDir . '/symfony/uid'),
|
||||
'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\\Mailer\\' => array($vendorDir . '/symfony/mailer'),
|
||||
'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'),
|
||||
'Symfony\\Component\\Clock\\' => array($vendorDir . '/symfony/clock'),
|
||||
'Spatie\\Permission\\' => array($vendorDir . '/spatie/laravel-permission/src'),
|
||||
'Spatie\\LaravelPackageTools\\' => array($vendorDir . '/spatie/laravel-package-tools/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/src'),
|
||||
'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'),
|
||||
'NunoMaduro\\Collision\\' => array($vendorDir . '/nunomaduro/collision/src'),
|
||||
'Nette\\' => array($vendorDir . '/nette/schema/src', $vendorDir . '/nette/utils/src'),
|
||||
'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
|
||||
'Mockery\\' => array($vendorDir . '/mockery/mockery/library/Mockery'),
|
||||
'League\\Uri\\' => array($vendorDir . '/league/uri', $vendorDir . '/league/uri-interfaces'),
|
||||
'League\\MimeTypeDetection\\' => array($vendorDir . '/league/mime-type-detection/src'),
|
||||
'League\\Flysystem\\Local\\' => array($vendorDir . '/league/flysystem-local'),
|
||||
'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\\Prompts\\' => array($vendorDir . '/laravel/prompts/src'),
|
||||
'Laravel\\Pao\\' => array($vendorDir . '/laravel/pao/src'),
|
||||
'Laravel\\Pail\\' => array($vendorDir . '/laravel/pail/src'),
|
||||
'Laravel\\AgentDetector\\' => array($vendorDir . '/laravel/agent-detector/src'),
|
||||
'Invoker\\' => array($vendorDir . '/php-di/invoker/src'),
|
||||
'Illuminate\\Support\\' => array($vendorDir . '/laravel/framework/src/Illuminate/Macroable', $vendorDir . '/laravel/framework/src/Illuminate/Collections', $vendorDir . '/laravel/framework/src/Illuminate/Conditionable', $vendorDir . '/laravel/framework/src/Illuminate/Reflection'),
|
||||
'Illuminate\\' => array($vendorDir . '/laravel/framework/src/Illuminate'),
|
||||
'GuzzleHttp\\UriTemplate\\' => array($vendorDir . '/guzzlehttp/uri-template/src'),
|
||||
'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/php-cors/src'),
|
||||
'Faker\\' => array($vendorDir . '/fakerphp/faker/src/Faker'),
|
||||
'Egulias\\EmailValidator\\' => array($vendorDir . '/egulias/email-validator/src'),
|
||||
'Dotenv\\' => array($vendorDir . '/vlucas/phpdotenv/src'),
|
||||
'Doctrine\\Inflector\\' => array($vendorDir . '/doctrine/inflector/src'),
|
||||
'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/src'),
|
||||
'DirectoryTree\\ImapEngine\\Laravel\\' => array($vendorDir . '/directorytree/imapengine-laravel/src'),
|
||||
'DirectoryTree\\ImapEngine\\' => array($vendorDir . '/directorytree/imapengine/src'),
|
||||
'Dflydev\\DotAccessData\\' => array($vendorDir . '/dflydev/dot-access-data/src'),
|
||||
'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'),
|
||||
'Database\\Seeders\\' => array($baseDir . '/database/seeders', $vendorDir . '/laravel/pint/database/seeders'),
|
||||
'Database\\Factories\\' => array($baseDir . '/database/factories', $vendorDir . '/laravel/pint/database/factories'),
|
||||
'DI\\' => array($vendorDir . '/php-di/php-di/src'),
|
||||
'Cron\\' => array($vendorDir . '/dragonmantank/cron-expression/src/Cron'),
|
||||
'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'),
|
||||
'App\\' => array($baseDir . '/app', $vendorDir . '/laravel/pint/app'),
|
||||
);
|
||||
Vendored
+50
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
// autoload_real.php @generated by Composer
|
||||
|
||||
class ComposerAutoloaderInit74199c0a3889e8d1256a25f93805ef56
|
||||
{
|
||||
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('ComposerAutoloaderInit74199c0a3889e8d1256a25f93805ef56', 'loadClassLoader'), true, true);
|
||||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
|
||||
spl_autoload_unregister(array('ComposerAutoloaderInit74199c0a3889e8d1256a25f93805ef56', 'loadClassLoader'));
|
||||
|
||||
require __DIR__ . '/autoload_static.php';
|
||||
call_user_func(\Composer\Autoload\ComposerStaticInit74199c0a3889e8d1256a25f93805ef56::getInitializer($loader));
|
||||
|
||||
$loader->register(true);
|
||||
|
||||
$filesToLoad = \Composer\Autoload\ComposerStaticInit74199c0a3889e8d1256a25f93805ef56::$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
+7708
File diff suppressed because it is too large
Load Diff
Vendored
+9415
File diff suppressed because it is too large
Load Diff
Vendored
+1418
File diff suppressed because it is too large
Load Diff
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
// platform_check.php @generated by Composer
|
||||
|
||||
$issues = array();
|
||||
|
||||
if (!(PHP_VERSION_ID >= 80400)) {
|
||||
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.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;
|
||||
}
|
||||
}
|
||||
trigger_error(
|
||||
'Composer detected issues in your platform: ' . implode(' ', $issues),
|
||||
E_USER_ERROR
|
||||
);
|
||||
}
|
||||
+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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to `ImapEngine-Laravel` will be documented in this file.
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Steve Bauman <steven_bauman@outlook.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.
|
||||
@@ -0,0 +1,18 @@
|
||||
<p align="center">
|
||||
<img src="https://github.com/directorytree/imapengine/blob/master/art/logo.svg" width="300">
|
||||
</p>
|
||||
|
||||
<p align="center">Working with IMAP doesn't need to be hard.</p>
|
||||
|
||||
<p align="center">ImapEngine provides a simple API for managing mailboxes -- without the PHP extension.</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/directorytree/imapengine-laravel/actions"><img src="https://img.shields.io/github/actions/workflow/status/directorytree/imapengine-laravel/run-tests.yml?branch=master&style=flat-square"></a>
|
||||
<a href="https://packagist.org/packages/directorytree/imapengine-laravel"><img src="https://img.shields.io/packagist/dt/directorytree/imapengine-laravel.svg?style=flat-square"></a>
|
||||
<a href="https://packagist.org/packages/directorytree/imapengine-laravel"><img src="https://img.shields.io/packagist/v/directorytree/imapengine-laravel.svg?style=flat-square"></a>
|
||||
<a href="https://packagist.org/packages/directorytree/imapengine-laravel"><img src="https://img.shields.io/github/license/directorytree/imapengine-laravel?style=flat-square"/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://imapengine.com/docs/laravel/introduction">View Documentation</a>
|
||||
</p>
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "directorytree/imapengine-laravel",
|
||||
"description": "This is my package imapengine-laravel",
|
||||
"keywords": [
|
||||
"Steve Bauman",
|
||||
"laravel",
|
||||
"imap"
|
||||
],
|
||||
"homepage": "https://github.com/directorytree/imapengine-laravel",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Steve Bauman",
|
||||
"email": "steven_bauman@outlook.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"directorytree/imapengine": "^1.19.0",
|
||||
"illuminate/contracts": "^10.0|^11.0|^12.0|^13.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "^1.14",
|
||||
"nunomaduro/collision": "^7.10|^8.1",
|
||||
"orchestra/testbench": "^8.22|^9.0|^10.0|^11.0",
|
||||
"pestphp/pest": "^2.0|^3.0|^4.0",
|
||||
"pestphp/pest-plugin-arch": "^2.0|^3.0|^4.0",
|
||||
"pestphp/pest-plugin-laravel": "^2.0|^3.0|^4.0",
|
||||
"spatie/laravel-ray": "^1.35"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"DirectoryTree\\ImapEngine\\Laravel\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"DirectoryTree\\ImapEngine\\Laravel\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": "@composer run prepare",
|
||||
"prepare": "@php vendor/bin/testbench package:discover --ansi",
|
||||
"analyse": "vendor/bin/phpstan analyse",
|
||||
"test": "vendor/bin/pest",
|
||||
"test-coverage": "vendor/bin/pest --coverage",
|
||||
"format": "vendor/bin/pint"
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"phpstan/extension-installer": true
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"DirectoryTree\\ImapEngine\\Laravel\\ImapServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mailboxes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define all of the IMAP mailboxes your application connects to.
|
||||
| Each mailbox contains its connection settings such as host, port, and
|
||||
| credentials. You are free to add as many mailboxes as needed.
|
||||
|
|
||||
*/
|
||||
|
||||
'mailboxes' => [
|
||||
'default' => [
|
||||
'port' => env('IMAP_PORT', 993),
|
||||
'host' => env('IMAP_HOST'),
|
||||
'timeout' => env('IMAP_TIMEOUT', 30),
|
||||
'debug' => env('IMAP_DEBUG', false),
|
||||
'username' => env('IMAP_USERNAME'),
|
||||
'password' => env('IMAP_PASSWORD'),
|
||||
'encryption' => env('IMAP_ENCRYPTION', 'ssl'),
|
||||
'validate_cert' => env('IMAP_VALIDATE_CERT', true),
|
||||
'authentication' => env('IMAP_AUTHENTICATION', 'plain'),
|
||||
'proxy' => [
|
||||
'socket' => env('IMAP_PROXY_SOCKET'),
|
||||
'username' => env('IMAP_PROXY_USERNAME'),
|
||||
'password' => env('IMAP_PROXY_PASSWORD'),
|
||||
'request_fulluri' => env('IMAP_PROXY_REQUEST_FULLURI', false),
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Laravel\Commands;
|
||||
|
||||
use DirectoryTree\ImapEngine\MessageQueryInterface;
|
||||
|
||||
class ConfigureIdleQuery
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected array $with = []
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Configure the query.
|
||||
*/
|
||||
public function __invoke(MessageQueryInterface $query): MessageQueryInterface
|
||||
{
|
||||
if (in_array('flags', $this->with)) {
|
||||
$query->withFlags();
|
||||
}
|
||||
|
||||
if (in_array('body', $this->with)) {
|
||||
$query->withBody();
|
||||
}
|
||||
|
||||
if (in_array('headers', $this->with)) {
|
||||
$query->withHeaders();
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Laravel\Commands;
|
||||
|
||||
use Carbon\CarbonInterface;
|
||||
use DirectoryTree\ImapEngine\Laravel\Events\MessageReceived;
|
||||
use DirectoryTree\ImapEngine\MessageInterface;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
||||
class HandleMessageReceived
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected WatchMailbox $command,
|
||||
protected int &$attempts = 0,
|
||||
protected ?CarbonInterface &$lastReceivedAt = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Handle the message received event.
|
||||
*/
|
||||
public function __invoke(MessageInterface $message): void
|
||||
{
|
||||
$this->command->info(
|
||||
"Message received: [{$message->uid()}]"
|
||||
);
|
||||
|
||||
$this->attempts = 0;
|
||||
|
||||
$this->lastReceivedAt = Date::now();
|
||||
|
||||
Event::dispatch(new MessageReceived($message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Laravel\Commands;
|
||||
|
||||
use DirectoryTree\ImapEngine\FolderInterface;
|
||||
use DirectoryTree\ImapEngine\Laravel\Events\MailboxWatchAttemptsExceeded;
|
||||
use DirectoryTree\ImapEngine\Laravel\Facades\Imap;
|
||||
use DirectoryTree\ImapEngine\Laravel\Support\LoopInterface;
|
||||
use DirectoryTree\ImapEngine\MailboxInterface;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Console\Exception\InvalidOptionException;
|
||||
|
||||
class WatchMailbox extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'imap:watch {mailbox} {folder?} {--method=idle} {--with=} {--timeout=30} {--attempts=5} {--debug=false}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Watch a mailbox for new messages.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(LoopInterface $loop): void
|
||||
{
|
||||
if (! in_array($method = $this->option('method'), ['idle', 'poll'])) {
|
||||
throw new InvalidOptionException("Invalid method [{$method}]. Valid options are [idle, poll].");
|
||||
}
|
||||
|
||||
$mailbox = Imap::mailbox($name = $this->argument('mailbox'));
|
||||
|
||||
$with = explode(',', $this->option('with'));
|
||||
|
||||
$this->info("Watching mailbox [$name]...");
|
||||
|
||||
$attempts = 0;
|
||||
|
||||
$lastReceivedAt = null;
|
||||
|
||||
$loop->run(function () use ($mailbox, $name, $with, &$attempts, &$lastReceivedAt) {
|
||||
try {
|
||||
$folder = $this->folder($mailbox);
|
||||
|
||||
match ($this->option('method')) {
|
||||
'idle' => $folder->idle(
|
||||
new HandleMessageReceived($this, $attempts, $lastReceivedAt),
|
||||
new ConfigureIdleQuery($with),
|
||||
$this->option('timeout'),
|
||||
),
|
||||
'poll' => $folder->poll(
|
||||
new HandleMessageReceived($this, $attempts, $lastReceivedAt),
|
||||
new ConfigureIdleQuery($with),
|
||||
$this->option('timeout'),
|
||||
),
|
||||
};
|
||||
} catch (Exception $e) {
|
||||
if ($this->isMessageMissing($e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->isDisconnection($e)) {
|
||||
sleep(2);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($attempts >= $this->option('attempts')) {
|
||||
$this->info("Exception: {$e->getMessage()}");
|
||||
|
||||
Event::dispatch(
|
||||
new MailboxWatchAttemptsExceeded($name, $attempts, $e, $lastReceivedAt)
|
||||
);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$attempts++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mailbox folder to idle.
|
||||
*/
|
||||
protected function folder(MailboxInterface $mailbox): FolderInterface
|
||||
{
|
||||
return ($folder = $this->argument('folder'))
|
||||
? $mailbox->folders()->findOrFail($folder)
|
||||
: $mailbox->inbox();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the exception is due to a message missing error.
|
||||
*/
|
||||
protected function isMessageMissing(Exception $e): bool
|
||||
{
|
||||
return Str::contains($e->getMessage(), [
|
||||
'no longer exist',
|
||||
], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the exception is caused by a disconnection.
|
||||
*/
|
||||
protected function isDisconnection(Exception $e): bool
|
||||
{
|
||||
return Str::contains($e->getMessage(), [
|
||||
'connection reset by peer',
|
||||
'temporary system problem',
|
||||
'failed to fetch content',
|
||||
'connection failed',
|
||||
'empty response',
|
||||
'not connected',
|
||||
'no response',
|
||||
'broken pipe',
|
||||
'unavailable',
|
||||
], true);
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Laravel\Events;
|
||||
|
||||
use Carbon\CarbonInterface;
|
||||
use Exception;
|
||||
|
||||
class MailboxWatchAttemptsExceeded
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $mailbox,
|
||||
public int $attempts,
|
||||
public Exception $exception,
|
||||
public ?CarbonInterface $lastReceivedAt = null,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Laravel\Events;
|
||||
|
||||
use DirectoryTree\ImapEngine\MessageInterface;
|
||||
|
||||
class MessageReceived
|
||||
{
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public MessageInterface $message
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Laravel\Facades;
|
||||
|
||||
use DirectoryTree\ImapEngine\Laravel\ImapManager;
|
||||
use DirectoryTree\ImapEngine\Testing\FakeMailbox;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
/**
|
||||
* @method static \DirectoryTree\ImapEngine\MailboxInterface mailbox(string $name)
|
||||
* @method static \DirectoryTree\ImapEngine\MailboxInterface swap(string $name, \DirectoryTree\ImapEngine\MailboxInterface $mailbox)
|
||||
*/
|
||||
class Imap extends Facade
|
||||
{
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
*/
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return ImapManager::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake the given mailbox.
|
||||
*/
|
||||
public static function fake(
|
||||
string $mailbox,
|
||||
array $config = [],
|
||||
array $folders = [],
|
||||
array $capabilities = []
|
||||
): FakeMailbox {
|
||||
/** @var \DirectoryTree\ImapEngine\Laravel\ImapManager $manager */
|
||||
$manager = static::getFacadeRoot();
|
||||
|
||||
$fake = new FakeMailbox($config, $folders, $capabilities);
|
||||
|
||||
$manager->swap($mailbox, $fake);
|
||||
|
||||
return $fake;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Laravel;
|
||||
|
||||
use DirectoryTree\ImapEngine\Mailbox;
|
||||
use DirectoryTree\ImapEngine\MailboxInterface;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class ImapManager
|
||||
{
|
||||
/**
|
||||
* The IMAP configuration.
|
||||
*/
|
||||
protected array $config = [];
|
||||
|
||||
/**
|
||||
* The mailbox instances.
|
||||
*/
|
||||
protected array $mailboxes = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(array $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a mailbox instance.
|
||||
*/
|
||||
public function mailbox(string $name): MailboxInterface
|
||||
{
|
||||
if (isset($this->mailboxes[$name])) {
|
||||
return $this->mailboxes[$name];
|
||||
}
|
||||
|
||||
if (! array_key_exists($name, $this->config['mailboxes'] ?? [])) {
|
||||
throw new InvalidArgumentException(
|
||||
"Mailbox [{$name}] is not defined. Please check your IMAP configuration."
|
||||
);
|
||||
}
|
||||
|
||||
return $this->mailboxes[$name] = $this->build($this->config['mailboxes'][$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a mailbox instance.
|
||||
*/
|
||||
public function register(string $name, array $config): static
|
||||
{
|
||||
$this->mailboxes[$name] = $this->build($config);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an on-demand mailbox instance.
|
||||
*/
|
||||
public function build(array $config): MailboxInterface
|
||||
{
|
||||
return new Mailbox($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a mailbox from the in-memory cache.
|
||||
*/
|
||||
public function forget(string $name): static
|
||||
{
|
||||
unset($this->mailboxes[$name]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap out a mailbox instance with a new one.
|
||||
*/
|
||||
public function swap(string $name, MailboxInterface $mailbox): void
|
||||
{
|
||||
$this->mailboxes[$name] = $mailbox;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Laravel;
|
||||
|
||||
use DirectoryTree\ImapEngine\Laravel\Support\Loop;
|
||||
use DirectoryTree\ImapEngine\Laravel\Support\LoopInterface;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class ImapServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(ImapManager::class, function () {
|
||||
return new ImapManager(config('imap', []));
|
||||
});
|
||||
|
||||
$this->app->bind(LoopInterface::class, Loop::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([
|
||||
Commands\WatchMailbox::class,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->publishes([
|
||||
__DIR__.'/../config/imap.php' => config_path('imap.php'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Laravel\Support;
|
||||
|
||||
class Loop implements LoopInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function run(callable $tick): void
|
||||
{
|
||||
while (true) {
|
||||
$tick();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Laravel\Support;
|
||||
|
||||
class LoopFake implements LoopInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function run(callable $tick): void
|
||||
{
|
||||
$tick();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Laravel\Support;
|
||||
|
||||
interface LoopInterface
|
||||
{
|
||||
/**
|
||||
* Execute the loop.
|
||||
*/
|
||||
public function run(callable $tick): void;
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "directorytree/imapengine",
|
||||
"type": "library",
|
||||
"description": "A fully-featured IMAP library -- without the PHP extension",
|
||||
"keywords": [
|
||||
"imap",
|
||||
"mail",
|
||||
"engine"
|
||||
],
|
||||
"homepage": "https://github.com/directorytree/imapengine",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Steve Bauman",
|
||||
"email": "steven_bauman@outlook.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"symfony/mime": ">=6.0",
|
||||
"nesbot/carbon": ">=2.0",
|
||||
"illuminate/collections": ">=9.0",
|
||||
"zbateson/mail-mime-parser": "^3.0",
|
||||
"egulias/email-validator": "^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"spatie/ray": "^1.0",
|
||||
"pestphp/pest": "^2.0|^3.0|^4.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"DirectoryTree\\ImapEngine\\": "src"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use DirectoryTree\ImapEngine\Support\Str;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use JsonSerializable;
|
||||
|
||||
class Address implements Arrayable, JsonSerializable
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $email,
|
||||
protected string $name,
|
||||
) {
|
||||
$this->name = Str::decodeMimeHeader($this->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the address's email.
|
||||
*/
|
||||
public function email(): string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the address's name.
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the address.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'email' => $this->email,
|
||||
'name' => $this->name,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON representation of the address.
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use GuzzleHttp\Psr7\Utils;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use JsonSerializable;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Symfony\Component\Mime\MimeTypes;
|
||||
use ZBateson\MailMimeParser\Message\IMessagePart;
|
||||
|
||||
class Attachment implements Arrayable, JsonSerializable
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected ?string $filename,
|
||||
protected ?string $contentId,
|
||||
protected string $contentType,
|
||||
protected ?string $contentDisposition,
|
||||
protected StreamInterface $contentStream,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get attachments from a parsed message.
|
||||
*
|
||||
* @return Attachment[]
|
||||
*/
|
||||
public static function parsed(MessageInterface $message): array
|
||||
{
|
||||
$attachments = [];
|
||||
|
||||
foreach ($message->parse()->getAllAttachmentParts() as $part) {
|
||||
if (static::isForwardedMessage($part)) {
|
||||
$attachments = array_merge($attachments, (new FileMessage($part->getContent()))->attachments());
|
||||
} else {
|
||||
$attachments[] = new Attachment(
|
||||
$part->getFilename(),
|
||||
$part->getContentId(),
|
||||
$part->getContentType(),
|
||||
$part->getContentDisposition(),
|
||||
$part->getBinaryContentStream() ?? Utils::streamFor(''),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attachments from a message's body structure using lazy streams.
|
||||
*
|
||||
* @return Attachment[]
|
||||
*/
|
||||
public static function lazy(Message $message): array
|
||||
{
|
||||
$attachments = [];
|
||||
|
||||
foreach ($message->bodyStructure(fetch: true)?->attachments() ?? [] as $part) {
|
||||
$attachments[] = new Attachment(
|
||||
$part->filename(),
|
||||
$part->id(),
|
||||
$part->contentType(),
|
||||
$part->disposition()?->type()?->value,
|
||||
new Support\LazyBodyPartStream($message, $part),
|
||||
);
|
||||
}
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the attachment should be treated as an embedded forwarded message.
|
||||
*/
|
||||
protected static function isForwardedMessage(IMessagePart $part): bool
|
||||
{
|
||||
return empty($part->getFilename())
|
||||
&& strtolower((string) $part->getContentType()) === 'message/rfc822'
|
||||
&& strtolower((string) $part->getContentDisposition()) !== 'attachment';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachment's filename.
|
||||
*/
|
||||
public function filename(): ?string
|
||||
{
|
||||
return $this->filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachment's content ID.
|
||||
*/
|
||||
public function contentId(): ?string
|
||||
{
|
||||
return $this->contentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachment's content type.
|
||||
*/
|
||||
public function contentType(): string
|
||||
{
|
||||
return $this->contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachment's content disposition.
|
||||
*/
|
||||
public function contentDisposition(): string
|
||||
{
|
||||
return $this->contentDisposition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachment's contents.
|
||||
*/
|
||||
public function contents(): string
|
||||
{
|
||||
return $this->contentStream->getContents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachment's content stream.
|
||||
*/
|
||||
public function contentStream(): StreamInterface
|
||||
{
|
||||
return $this->contentStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the attachment to a file.
|
||||
*/
|
||||
public function save(string $path): false|int
|
||||
{
|
||||
return file_put_contents($path, $this->contents());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachment's extension.
|
||||
*/
|
||||
public function extension(): ?string
|
||||
{
|
||||
if ($ext = pathinfo($this->filename ?? '', PATHINFO_EXTENSION)) {
|
||||
return $ext;
|
||||
}
|
||||
|
||||
if ($ext = (MimeTypes::getDefault()->getExtensions($this->contentType)[0] ?? null)) {
|
||||
return $ext;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the attachment.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'filename' => $this->filename,
|
||||
'content_type' => $this->contentType,
|
||||
'contents' => $this->contents(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON representation of the attachment.
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use Countable;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\ListData;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Nil;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use IteratorAggregate;
|
||||
use JsonSerializable;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* @implements IteratorAggregate<int, BodyStructurePart|BodyStructureCollection>
|
||||
*/
|
||||
class BodyStructureCollection implements Arrayable, Countable, IteratorAggregate, JsonSerializable
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array<BodyStructurePart|BodyStructureCollection> $parts
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $subtype = 'mixed',
|
||||
protected array $parameters = [],
|
||||
protected array $parts = [],
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Parse a multipart BODYSTRUCTURE ListData into a BodyStructureCollection.
|
||||
*/
|
||||
public static function fromListData(ListData $data, ?string $partNumber = null): static
|
||||
{
|
||||
$tokens = $data->tokens();
|
||||
|
||||
$parts = [];
|
||||
$childIndex = 1;
|
||||
$subtypeIndex = null;
|
||||
|
||||
foreach ($tokens as $index => $token) {
|
||||
if ($token instanceof Token && ! $token instanceof Nil) {
|
||||
$subtypeIndex = $index;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (! $token instanceof ListData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$childPartNumber = $partNumber ? "{$partNumber}.{$childIndex}" : (string) $childIndex;
|
||||
|
||||
$parts[] = static::isMultipart($token)
|
||||
? static::fromListData($token, $childPartNumber)
|
||||
: BodyStructurePart::fromListData($token, $childPartNumber);
|
||||
|
||||
$childIndex++;
|
||||
}
|
||||
|
||||
$parameters = [];
|
||||
|
||||
if ($subtypeIndex) {
|
||||
foreach (array_slice($tokens, $subtypeIndex + 1) as $token) {
|
||||
if ($token instanceof ListData && ! static::isDispositionList($token)) {
|
||||
$parameters = $token->toKeyValuePairs();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new static(
|
||||
$subtypeIndex ? strtolower($tokens[$subtypeIndex]->value) : 'mixed',
|
||||
$parameters,
|
||||
$parts
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a ListData represents a multipart structure.
|
||||
*/
|
||||
protected static function isMultipart(ListData $data): bool
|
||||
{
|
||||
return head($data->tokens()) instanceof ListData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a ListData represents a disposition (INLINE or ATTACHMENT).
|
||||
*/
|
||||
protected static function isDispositionList(ListData $data): bool
|
||||
{
|
||||
$tokens = $data->tokens();
|
||||
|
||||
if (count($tokens) < 2 || ! isset($tokens[0]) || ! $tokens[0] instanceof Token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array(strtoupper($tokens[0]->value), ['INLINE', 'ATTACHMENT']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the multipart subtype (mixed, alternative, related, etc.).
|
||||
*/
|
||||
public function subtype(): string
|
||||
{
|
||||
return $this->subtype;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content type.
|
||||
*/
|
||||
public function contentType(): string
|
||||
{
|
||||
return "multipart/{$this->subtype}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameters (e.g., boundary).
|
||||
*/
|
||||
public function parameters(): array
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the boundary parameter.
|
||||
*/
|
||||
public function boundary(): ?string
|
||||
{
|
||||
return $this->parameters['boundary'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the direct child parts.
|
||||
*
|
||||
* @return array<BodyStructurePart|BodyStructureCollection>
|
||||
*/
|
||||
public function parts(): array
|
||||
{
|
||||
return $this->parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all parts flattened (including nested parts).
|
||||
*
|
||||
* @return BodyStructurePart[]
|
||||
*/
|
||||
public function flatten(): array
|
||||
{
|
||||
$flattened = [];
|
||||
|
||||
foreach ($this->parts as $part) {
|
||||
if ($part instanceof self) {
|
||||
$flattened = array_merge($flattened, $part->flatten());
|
||||
} else {
|
||||
$flattened[] = $part;
|
||||
}
|
||||
}
|
||||
|
||||
return $flattened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a part by its part number.
|
||||
*/
|
||||
public function find(string $partNumber): BodyStructurePart|BodyStructureCollection|null
|
||||
{
|
||||
foreach ($this->parts as $part) {
|
||||
if ($part instanceof self) {
|
||||
if ($found = $part->find($partNumber)) {
|
||||
return $found;
|
||||
}
|
||||
} elseif ($part->partNumber() === $partNumber) {
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text/plain part if available.
|
||||
*/
|
||||
public function text(): ?BodyStructurePart
|
||||
{
|
||||
foreach ($this->flatten() as $part) {
|
||||
if ($part->isText()) {
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text/html part if available.
|
||||
*/
|
||||
public function html(): ?BodyStructurePart
|
||||
{
|
||||
foreach ($this->flatten() as $part) {
|
||||
if ($part->isHtml()) {
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all attachment parts.
|
||||
*
|
||||
* @return BodyStructurePart[]
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
$this->flatten(),
|
||||
fn (BodyStructurePart $part) => $part->isAttachment()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the collection has attachments.
|
||||
*/
|
||||
public function hasAttachments(): bool
|
||||
{
|
||||
return count($this->attachments()) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of attachments.
|
||||
*/
|
||||
public function attachmentCount(): int
|
||||
{
|
||||
return count($this->attachments());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of parts.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an iterator for the parts.
|
||||
*/
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
yield from $this->parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'subtype' => $this->subtype,
|
||||
'parameters' => $this->parameters,
|
||||
'content_type' => $this->contentType(),
|
||||
'parts' => array_map(fn (Arrayable $part) => $part->toArray(), $this->parts),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON representation.
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\ListData;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Nil;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use JsonSerializable;
|
||||
|
||||
class BodyStructurePart implements Arrayable, JsonSerializable
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $partNumber,
|
||||
protected string $type,
|
||||
protected string $subtype,
|
||||
protected array $parameters = [],
|
||||
protected ?string $id = null,
|
||||
protected ?string $description = null,
|
||||
protected ?string $encoding = null,
|
||||
protected ?int $size = null,
|
||||
protected ?int $lines = null,
|
||||
protected ?ContentDisposition $disposition = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Parse a single part BODYSTRUCTURE ListData into a BodyStructurePart.
|
||||
*/
|
||||
public static function fromListData(ListData $data, string $partNumber = '1'): static
|
||||
{
|
||||
return static::parse($data->tokens(), $partNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single (non-multipart) part.
|
||||
*
|
||||
* @param array<Token|ListData> $tokens
|
||||
*/
|
||||
protected static function parse(array $tokens, string $partNumber): static
|
||||
{
|
||||
return new static(
|
||||
partNumber: $partNumber,
|
||||
type: strtolower(static::tokenValueAt($tokens, 0) ?? 'text'),
|
||||
subtype: strtolower(static::tokenValueAt($tokens, 1) ?? 'plain'),
|
||||
parameters: isset($tokens[2]) && $tokens[2] instanceof ListData ? $tokens[2]->toKeyValuePairs() : [],
|
||||
id: static::tokenValueAt($tokens, 3),
|
||||
description: static::tokenValueAt($tokens, 4),
|
||||
encoding: static::tokenValueAt($tokens, 5),
|
||||
size: static::tokenIntValueAt($tokens, 6),
|
||||
lines: static::tokenIntValueAt($tokens, 7),
|
||||
disposition: ContentDisposition::parse($tokens),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely read a scalar token value from the parsed body structure.
|
||||
*
|
||||
* @param array<Token|ListData> $tokens
|
||||
*/
|
||||
protected static function tokenValueAt(array $tokens, int $index): ?string
|
||||
{
|
||||
$token = $tokens[$index] ?? null;
|
||||
|
||||
if (! $token instanceof Token || $token instanceof Nil) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $token->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely read an integer token value from the parsed body structure.
|
||||
*
|
||||
* @param array<Token|ListData> $tokens
|
||||
*/
|
||||
protected static function tokenIntValueAt(array $tokens, int $index): ?int
|
||||
{
|
||||
$value = static::tokenValueAt($tokens, $index);
|
||||
|
||||
return $value === null ? null : (int) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the part number (e.g., "1", "1.2", "2.1.3").
|
||||
*/
|
||||
public function partNumber(): string
|
||||
{
|
||||
return $this->partNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MIME type (e.g., "text", "image", "multipart").
|
||||
*/
|
||||
public function type(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MIME subtype (e.g., "plain", "html", "jpeg", "mixed").
|
||||
*/
|
||||
public function subtype(): string
|
||||
{
|
||||
return $this->subtype;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full content type (e.g., "text/plain", "multipart/alternative").
|
||||
*/
|
||||
public function contentType(): string
|
||||
{
|
||||
return "{$this->type}/{$this->subtype}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameters (e.g., charset, boundary).
|
||||
*/
|
||||
public function parameters(): array
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific parameter value.
|
||||
*/
|
||||
public function parameter(string $name): ?string
|
||||
{
|
||||
return $this->parameters[strtolower($name)] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content ID.
|
||||
*/
|
||||
public function id(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content description.
|
||||
*/
|
||||
public function description(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content transfer encoding.
|
||||
*/
|
||||
public function encoding(): ?string
|
||||
{
|
||||
return $this->encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size in bytes.
|
||||
*/
|
||||
public function size(): ?int
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of lines (for text parts).
|
||||
*/
|
||||
public function lines(): ?int
|
||||
{
|
||||
return $this->lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content disposition.
|
||||
*/
|
||||
public function disposition(): ?ContentDisposition
|
||||
{
|
||||
return $this->disposition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filename from disposition parameters.
|
||||
*/
|
||||
public function filename(): ?string
|
||||
{
|
||||
return $this->disposition?->filename() ?? $this->parameters['name'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the charset from parameters.
|
||||
*/
|
||||
public function charset(): ?string
|
||||
{
|
||||
return $this->parameters['charset'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this is a text part.
|
||||
*/
|
||||
public function isText(): bool
|
||||
{
|
||||
return $this->type === 'text' && $this->subtype === 'plain';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this is an HTML part.
|
||||
*/
|
||||
public function isHtml(): bool
|
||||
{
|
||||
return $this->type === 'text' && $this->subtype === 'html';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this is an attachment.
|
||||
*/
|
||||
public function isAttachment(): bool
|
||||
{
|
||||
if ($this->disposition?->isAttachment()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Inline parts are not attachments.
|
||||
if ($this->disposition?->isInline()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Consider non-text/html parts with filenames as attachments.
|
||||
if ($this->filename() && ! $this->isText() && ! $this->isHtml()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this is an inline part.
|
||||
*/
|
||||
public function isInline(): bool
|
||||
{
|
||||
return $this->disposition?->isInline() ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'type' => $this->type,
|
||||
'size' => $this->size,
|
||||
'lines' => $this->lines,
|
||||
'subtype' => $this->subtype,
|
||||
'encoding' => $this->encoding,
|
||||
'parameters' => $this->parameters,
|
||||
'part_number' => $this->partNumber,
|
||||
'description' => $this->description,
|
||||
'content_type' => $this->contentType(),
|
||||
'disposition' => $this->disposition?->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON representation.
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Collections;
|
||||
|
||||
use DirectoryTree\ImapEngine\FolderInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @template-extends Collection<array-key, FolderInterface>
|
||||
*/
|
||||
class FolderCollection extends Collection {}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Collections;
|
||||
|
||||
use DirectoryTree\ImapEngine\Message;
|
||||
use DirectoryTree\ImapEngine\MessageInterface;
|
||||
|
||||
/**
|
||||
* @template-extends PaginatedCollection<array-key, MessageInterface|Message>
|
||||
*/
|
||||
class MessageCollection extends PaginatedCollection
|
||||
{
|
||||
/**
|
||||
* Find a message by its UID.
|
||||
*/
|
||||
public function find(int $uid): ?MessageInterface
|
||||
{
|
||||
return $this->first(
|
||||
fn (MessageInterface $message) => $message->uid() === $uid
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a message by its UID or throw an exception.
|
||||
*/
|
||||
public function findOrFail(int $uid): MessageInterface
|
||||
{
|
||||
return $this->firstOrFail(
|
||||
fn (MessageInterface $message) => $message->uid() === $uid
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Collections;
|
||||
|
||||
use DirectoryTree\ImapEngine\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @template TKey of array-key
|
||||
* @template TValue
|
||||
*
|
||||
* @template-extends Collection<TKey, TValue>
|
||||
*/
|
||||
class PaginatedCollection extends Collection
|
||||
{
|
||||
/**
|
||||
* The total number of items.
|
||||
*/
|
||||
protected int $total = 0;
|
||||
|
||||
/**
|
||||
* Paginate the current collection.
|
||||
*
|
||||
* @return LengthAwarePaginator<TKey, TValue>
|
||||
*/
|
||||
public function paginate(int $perPage = 15, ?int $page = null, string $pageName = 'page', bool $prepaginated = false): LengthAwarePaginator
|
||||
{
|
||||
$total = $this->total ?: $this->count();
|
||||
|
||||
$results = ! $prepaginated && $total ? $this->forPage($page, $perPage) : $this;
|
||||
|
||||
return $this->paginator($results, $total, $perPage, $page, $pageName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new length-aware paginator instance.
|
||||
*
|
||||
* @return LengthAwarePaginator<TKey, TValue>
|
||||
*/
|
||||
protected function paginator(Collection $items, int $total, int $perPage, ?int $currentPage, string $pageName): LengthAwarePaginator
|
||||
{
|
||||
return new LengthAwarePaginator($items, $total, $perPage, $currentPage, $pageName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set the total amount.
|
||||
*/
|
||||
public function total(?int $total = null): ?int
|
||||
{
|
||||
if (is_null($total)) {
|
||||
return $this->total;
|
||||
}
|
||||
|
||||
return $this->total = $total;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Collections;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\ContinuationResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\TaggedResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @template TKey of array-key
|
||||
*
|
||||
* @template-covariant TValue
|
||||
*
|
||||
* @extends Collection<array-key, TValue>
|
||||
*/
|
||||
class ResponseCollection extends Collection
|
||||
{
|
||||
/**
|
||||
* Filter the collection to only tagged responses.
|
||||
*
|
||||
* @return self<array-key, TaggedResponse>
|
||||
*/
|
||||
public function tagged(): self
|
||||
{
|
||||
return $this->whereInstanceOf(TaggedResponse::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the collection to only untagged responses.
|
||||
*
|
||||
* @return self<array-key, UntaggedResponse>
|
||||
*/
|
||||
public function untagged(): self
|
||||
{
|
||||
return $this->whereInstanceOf(UntaggedResponse::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the collection to only continuation responses.
|
||||
*
|
||||
* @return self<array-key, ContinuationResponse>
|
||||
*/
|
||||
public function continuation(): self
|
||||
{
|
||||
return $this->whereInstanceOf(ContinuationResponse::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
trait ComparesFolders
|
||||
{
|
||||
/**
|
||||
* Determine if two folders are the same.
|
||||
*/
|
||||
protected function isSameFolder(FolderInterface $a, FolderInterface $b): bool
|
||||
{
|
||||
return $a->path() === $b->path()
|
||||
&& $a->mailbox()->config('host') === $b->mailbox()->config('host')
|
||||
&& $a->mailbox()->config('username') === $b->mailbox()->config('username');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection;
|
||||
|
||||
use DirectoryTree\ImapEngine\Collections\ResponseCollection;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\TaggedResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse;
|
||||
use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier;
|
||||
use DirectoryTree\ImapEngine\Enums\ImapSortKey;
|
||||
use Generator;
|
||||
|
||||
interface ConnectionInterface
|
||||
{
|
||||
/**
|
||||
* Open a new connection.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-state-and-flow-diagram
|
||||
*/
|
||||
public function connect(string $host, ?int $port = null, array $options = []): void;
|
||||
|
||||
/**
|
||||
* Close the current connection.
|
||||
*/
|
||||
public function disconnect(): void;
|
||||
|
||||
/**
|
||||
* Determine if the current session is connected.
|
||||
*/
|
||||
public function connected(): bool;
|
||||
|
||||
/**
|
||||
* Send a "LOGIN" command.
|
||||
*
|
||||
* Login to a new session.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-login-command
|
||||
*/
|
||||
public function login(string $user, string $password): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "LOGOUT" command.
|
||||
*
|
||||
* Logout of the current server session.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-logout-command
|
||||
*/
|
||||
public function logout(): void;
|
||||
|
||||
/**
|
||||
* Send an "AUTHENTICATE" command.
|
||||
*
|
||||
* Authenticate the current session.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-authenticate-command
|
||||
*/
|
||||
public function authenticate(string $user, string $token): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "STARTTLS" command.
|
||||
*
|
||||
* Upgrade the current plaintext connection to a secure TLS-encrypted connection.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-starttls-command
|
||||
*/
|
||||
public function startTls(): void;
|
||||
|
||||
/**
|
||||
* Send an "IDLE" command.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-idle-command
|
||||
*/
|
||||
public function idle(int $timeout): Generator;
|
||||
|
||||
/**
|
||||
* Send a "DONE" command.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.3.13
|
||||
*/
|
||||
public function done(): void;
|
||||
|
||||
/**
|
||||
* Send a "NOOP" command.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-noop-command
|
||||
*/
|
||||
public function noop(): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "EXPUNGE" command.
|
||||
*
|
||||
* Apply session saved changes to the server.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-expunge-command
|
||||
*/
|
||||
public function expunge(): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "CAPABILITY" command.
|
||||
*
|
||||
* Get the mailbox's available capabilities.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-capability-command
|
||||
*/
|
||||
public function capability(): UntaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "SEARCH" command.
|
||||
*
|
||||
* Execute a search request.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-search-command
|
||||
*/
|
||||
public function search(array $params): UntaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "SORT" command.
|
||||
*
|
||||
* Execute a sort request using RFC 5256.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc5256
|
||||
*/
|
||||
public function sort(ImapSortKey $key, string $direction, array $params): UntaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "FETCH" command.
|
||||
*
|
||||
* Exchange identification information.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc2971.
|
||||
*/
|
||||
public function id(?array $ids = null): UntaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "FETCH UID" command.
|
||||
*
|
||||
* Fetch message UIDs using the given message numbers.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-uid-command
|
||||
*/
|
||||
public function uid(int|array $ids, ImapFetchIdentifier $identifier): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "FETCH BODY[TEXT]" command.
|
||||
*
|
||||
* Fetch message text contents.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.4.5-9.9
|
||||
*/
|
||||
public function bodyText(int|array $ids, bool $peek = true): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "FETCH BODY[HEADER]" command.
|
||||
*
|
||||
* Fetch message headers.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.4.5-9.9
|
||||
*/
|
||||
public function bodyHeader(int|array $ids, bool $peek = true): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "FETCH BODYSTRUCTURE" command.
|
||||
*
|
||||
* Fetch message body structure.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.4.5-9.9
|
||||
*/
|
||||
public function bodyStructure(int|array $ids): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "FETCH BODY[i]" command.
|
||||
*
|
||||
* Fetch a specific part of the message BODY, such as BODY[1], BODY[1.2], etc.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.4.5-9.9
|
||||
*/
|
||||
public function bodyPart(string $partIndex, int|array $ids, bool $peek = false): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "FETCH FLAGS" command.
|
||||
*
|
||||
* Fetch a message flags.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.4.5-9.17
|
||||
*/
|
||||
public function flags(int|array $ids): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "FETCH" command.
|
||||
*
|
||||
* Fetch one or more items for one or more messages.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-fetch-command
|
||||
*/
|
||||
public function fetch(array|string $items, array|int $from, mixed $to = null, ImapFetchIdentifier $identifier = ImapFetchIdentifier::Uid): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "RFC822.SIZE" command.
|
||||
*
|
||||
* Fetch message sizes for one or more messages.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.4.5-9.21
|
||||
*/
|
||||
public function size(int|array $ids): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send an IMAP command.
|
||||
*/
|
||||
public function send(string $name, array $tokens = [], ?string &$tag = null): void;
|
||||
|
||||
/**
|
||||
* Send a "SELECT" command.
|
||||
*
|
||||
* Select the specified folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-select-command
|
||||
*/
|
||||
public function select(string $folder): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "EXAMINE" command.
|
||||
*
|
||||
* Examine a given folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-examine-command
|
||||
*/
|
||||
public function examine(string $folder): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "LIST" command.
|
||||
*
|
||||
* Get a list of available folders.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-list-command
|
||||
*/
|
||||
public function list(string $reference = '', string $folder = '*'): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "STATUS" command.
|
||||
*
|
||||
* Get the status of a given folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-status-command
|
||||
*/
|
||||
public function status(string $folder, array $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): UntaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "STORE" command.
|
||||
*
|
||||
* Set message flags.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-store-command
|
||||
*/
|
||||
public function store(array|string $flags, array|int $from, ?int $to = null, ?string $mode = null, bool $silent = true, ?string $item = null): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "APPEND" command.
|
||||
*
|
||||
* Append a new message to given folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-append-command
|
||||
*/
|
||||
public function append(string $folder, string $message, ?array $flags = null): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "UID COPY" command.
|
||||
*
|
||||
* Copy message set from current folder to other folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-copy-command
|
||||
*/
|
||||
public function copy(string $folder, array|int $from, ?int $to = null): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "UID MOVE" command.
|
||||
*
|
||||
* Move a message set from current folder to another folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-move-command
|
||||
*/
|
||||
public function move(string $folder, array|int $from, ?int $to = null): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "CREATE" command.
|
||||
*
|
||||
* Create a new folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-create-command
|
||||
*/
|
||||
public function create(string $folder): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "DELETE" command.
|
||||
*
|
||||
* Delete a folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-delete-command
|
||||
*/
|
||||
public function delete(string $folder): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "RENAME" command.
|
||||
*
|
||||
* Rename an existing folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-rename-command
|
||||
*/
|
||||
public function rename(string $oldPath, string $newPath): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "SUBSCRIBE" command.
|
||||
*
|
||||
* Subscribe to a folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-subscribe-command
|
||||
*/
|
||||
public function subscribe(string $folder): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "UNSUBSCRIBE" command.
|
||||
*
|
||||
* Unsubscribe from a folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-unsubscribe-command
|
||||
*/
|
||||
public function unsubscribe(string $folder): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "GETQUOTA" command.
|
||||
*
|
||||
* Retrieve quota information about a specific quota root.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9208#name-getquota
|
||||
*/
|
||||
public function quota(string $root): UntaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "GETQUOTAROOT" command.
|
||||
*
|
||||
* Retrieve quota root information about a mailbox.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9208#name-getquotaroot
|
||||
*/
|
||||
public function quotaRoot(string $mailbox): ResponseCollection;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection;
|
||||
|
||||
use Stringable;
|
||||
|
||||
class ImapCommand implements Stringable
|
||||
{
|
||||
/**
|
||||
* The compiled command lines.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected ?array $compiled = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $tag,
|
||||
protected string $command,
|
||||
protected array $tokens = [],
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the IMAP tag.
|
||||
*/
|
||||
public function tag(): string
|
||||
{
|
||||
return $this->tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the IMAP command.
|
||||
*/
|
||||
public function command(): string
|
||||
{
|
||||
return $this->command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the IMAP tokens.
|
||||
*/
|
||||
public function tokens(): array
|
||||
{
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile the command into lines for transmission.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function compile(): array
|
||||
{
|
||||
if (is_array($this->compiled)) {
|
||||
return $this->compiled;
|
||||
}
|
||||
|
||||
$lines = [];
|
||||
|
||||
$line = trim("{$this->tag} {$this->command}");
|
||||
|
||||
foreach ($this->tokens as $token) {
|
||||
if (is_array($token)) {
|
||||
// For tokens provided as arrays, the first element is a placeholder
|
||||
// (for example, "{20}") that signals a literal value will follow.
|
||||
// The second element holds the actual literal content.
|
||||
[$placeholder, $literal] = $token;
|
||||
|
||||
$lines[] = "{$line} {$placeholder}";
|
||||
|
||||
$line = $literal;
|
||||
} else {
|
||||
$line .= " {$token}";
|
||||
}
|
||||
}
|
||||
|
||||
$lines[] = $line;
|
||||
|
||||
return $this->compiled = $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a redacted version of the command for safe exposure.
|
||||
*/
|
||||
public function redacted(): ImapCommand
|
||||
{
|
||||
return new static($this->tag, $this->command, array_map(
|
||||
function (mixed $token) {
|
||||
return is_array($token)
|
||||
? array_map(fn () => '[redacted]', $token)
|
||||
: '[redacted]';
|
||||
}, $this->tokens)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the command as a string.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return implode("\r\n", $this->compile());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,815 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection;
|
||||
|
||||
use DirectoryTree\ImapEngine\Collections\ResponseCollection;
|
||||
use DirectoryTree\ImapEngine\Connection\Loggers\LoggerInterface;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\ContinuationResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\Data;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\ListData;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Response;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\TaggedResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Streams\FakeStream;
|
||||
use DirectoryTree\ImapEngine\Connection\Streams\StreamInterface;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier;
|
||||
use DirectoryTree\ImapEngine\Enums\ImapSortKey;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapCommandException;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapConnectionClosedException;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapConnectionFailedException;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapConnectionTimedOutException;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapResponseException;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapStreamException;
|
||||
use DirectoryTree\ImapEngine\Support\Str;
|
||||
use Exception;
|
||||
use Generator;
|
||||
use LogicException;
|
||||
use Throwable;
|
||||
|
||||
class ImapConnection implements ConnectionInterface
|
||||
{
|
||||
/**
|
||||
* Sequence number used to generate unique command tags.
|
||||
*/
|
||||
protected int $sequence = 0;
|
||||
|
||||
/**
|
||||
* The result instance.
|
||||
*/
|
||||
protected ?Result $result = null;
|
||||
|
||||
/**
|
||||
* The parser instance.
|
||||
*/
|
||||
protected ?ImapParser $parser = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected StreamInterface $stream,
|
||||
protected ?LoggerInterface $logger = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a new connection with a fake stream.
|
||||
*/
|
||||
public static function fake(array $responses = []): static
|
||||
{
|
||||
$stream = new FakeStream;
|
||||
|
||||
$stream->open();
|
||||
|
||||
$stream->feed($responses);
|
||||
|
||||
return new static($stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tear down the connection.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if (! $this->connected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
@$this->logout();
|
||||
} catch (Exception $e) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function connect(string $host, ?int $port = null, array $options = []): void
|
||||
{
|
||||
$transport = strtolower($options['encryption'] ?? '') ?: 'tcp';
|
||||
|
||||
if (in_array($transport, ['ssl', 'tls'])) {
|
||||
$port ??= 993;
|
||||
} else {
|
||||
$port ??= 143;
|
||||
}
|
||||
|
||||
$this->setParser(
|
||||
$this->newParser($this->stream)
|
||||
);
|
||||
|
||||
$this->stream->open(
|
||||
$transport === 'starttls' ? 'tcp' : $transport,
|
||||
$host,
|
||||
$port,
|
||||
$options['timeout'] ?? 30,
|
||||
$this->getDefaultSocketOptions(
|
||||
$transport,
|
||||
$options['proxy'] ?? [],
|
||||
$options['validate_cert'] ?? true
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertNextResponse(
|
||||
fn (Response $response) => $response instanceof UntaggedResponse,
|
||||
fn (UntaggedResponse $response) => $response->type()->is('OK'),
|
||||
fn () => new ImapConnectionFailedException("Connection to $host:$port failed")
|
||||
);
|
||||
|
||||
if ($transport === 'starttls') {
|
||||
$this->startTls();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default socket options for the given transport.
|
||||
*
|
||||
* @param 'ssl'|'tls'|'starttls'|'tcp' $transport
|
||||
*/
|
||||
protected function getDefaultSocketOptions(string $transport, array $proxy = [], bool $validateCert = true): array
|
||||
{
|
||||
$options = [];
|
||||
|
||||
$key = match ($transport) {
|
||||
'ssl', 'tls' => 'ssl',
|
||||
'starttls', 'tcp' => 'tcp',
|
||||
};
|
||||
|
||||
if (in_array($transport, ['ssl', 'tls'])) {
|
||||
$options[$key] = [
|
||||
'verify_peer' => $validateCert,
|
||||
'verify_peer_name' => $validateCert,
|
||||
];
|
||||
}
|
||||
|
||||
if (! isset($proxy['socket'])) {
|
||||
return $options;
|
||||
}
|
||||
|
||||
$options[$key]['proxy'] = $proxy['socket'];
|
||||
$options[$key]['request_fulluri'] = $proxy['request_fulluri'] ?? false;
|
||||
|
||||
if (isset($proxy['username'])) {
|
||||
$auth = base64_encode($proxy['username'].':'.$proxy['password']);
|
||||
|
||||
$options[$key]['header'] = ["Proxy-Authorization: Basic $auth"];
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function disconnect(): void
|
||||
{
|
||||
$this->stream->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function connected(): bool
|
||||
{
|
||||
return $this->stream->opened();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function login(string $user, string $password): TaggedResponse
|
||||
{
|
||||
$this->send('LOGIN', Str::literal([$user, $password]), $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag, fn (TaggedResponse $response) => (
|
||||
ImapCommandException::make($this->result->command()->redacted(), $response)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function logout(): void
|
||||
{
|
||||
$this->send('LOGOUT', tag: $tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function authenticate(string $user, string $token): TaggedResponse
|
||||
{
|
||||
$this->send('AUTHENTICATE', ['XOAUTH2', Str::credentials($user, $token)], $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag, fn (TaggedResponse $response) => (
|
||||
ImapCommandException::make($this->result->command()->redacted(), $response)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function startTls(): void
|
||||
{
|
||||
$this->send('STARTTLS', tag: $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
$this->stream->setSocketSetCrypto(true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function select(string $folder = 'INBOX'): ResponseCollection
|
||||
{
|
||||
return $this->examineOrSelect('SELECT', $folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function examine(string $folder = 'INBOX'): ResponseCollection
|
||||
{
|
||||
return $this->examineOrSelect('EXAMINE', $folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Examine and select have the same response.
|
||||
*/
|
||||
protected function examineOrSelect(string $command = 'EXAMINE', string $folder = 'INBOX'): ResponseCollection
|
||||
{
|
||||
$this->send($command, [Str::literal($folder)], $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function status(string $folder = 'INBOX', array $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): UntaggedResponse
|
||||
{
|
||||
$this->send('STATUS', [
|
||||
Str::literal($folder),
|
||||
Str::list($arguments),
|
||||
], $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged()->firstWhere(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('STATUS')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function create(string $folder): ResponseCollection
|
||||
{
|
||||
$this->send('CREATE', [Str::literal($folder)], $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged()->filter(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('LIST')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function delete(string $folder): TaggedResponse
|
||||
{
|
||||
$this->send('DELETE', [Str::literal($folder)], tag: $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function rename(string $oldPath, string $newPath): TaggedResponse
|
||||
{
|
||||
$this->send('RENAME', Str::literal([$oldPath, $newPath]), tag: $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function subscribe(string $folder): TaggedResponse
|
||||
{
|
||||
$this->send('SUBSCRIBE', [Str::literal($folder)], tag: $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function unsubscribe(string $folder): TaggedResponse
|
||||
{
|
||||
$this->send('UNSUBSCRIBE', [Str::literal($folder)], tag: $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function quota(string $root): UntaggedResponse
|
||||
{
|
||||
$this->send('GETQUOTA', [Str::literal($root)], tag: $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged()->firstOrFail(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('QUOTA')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function quotaRoot(string $mailbox): ResponseCollection
|
||||
{
|
||||
$this->send('GETQUOTAROOT', [Str::literal($mailbox)], tag: $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged()->filter(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('QUOTA')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function list(string $reference = '', string $folder = '*'): ResponseCollection
|
||||
{
|
||||
$this->send('LIST', Str::literal([$reference, $folder]), $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged()->filter(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('LIST')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function append(string $folder, string $message, ?array $flags = null): TaggedResponse
|
||||
{
|
||||
$tokens = [];
|
||||
|
||||
$tokens[] = Str::literal($folder);
|
||||
|
||||
if ($flags) {
|
||||
$tokens[] = Str::list($flags);
|
||||
}
|
||||
|
||||
$tokens[] = Str::literal($message);
|
||||
|
||||
$this->send('APPEND', $tokens, tag: $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function copy(string $folder, array|int $from, ?int $to = null): TaggedResponse
|
||||
{
|
||||
$this->send('UID COPY', [
|
||||
Str::set($from, $to),
|
||||
Str::literal($folder),
|
||||
], $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function move(string $folder, array|int $from, ?int $to = null): TaggedResponse
|
||||
{
|
||||
$this->send('UID MOVE', [
|
||||
Str::set($from, $to),
|
||||
Str::literal($folder),
|
||||
], $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function store(array|string $flags, array|int $from, ?int $to = null, ?string $mode = null, bool $silent = true, ?string $item = null): ResponseCollection
|
||||
{
|
||||
$set = Str::set($from, $to);
|
||||
|
||||
$flags = Str::list((array) $flags);
|
||||
|
||||
$item = ($mode == '-' ? '-' : '+').(is_null($item) ? 'FLAGS' : $item).($silent ? '.SILENT' : '');
|
||||
|
||||
$this->send('UID STORE', [$set, $item, $flags], tag: $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $silent ? new ResponseCollection : $this->result->responses()->untagged()->filter(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('FETCH')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function uid(int|array $ids, ImapFetchIdentifier $identifier): ResponseCollection
|
||||
{
|
||||
return $this->fetch(['UID'], (array) $ids, null, $identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function bodyText(int|array $ids, bool $peek = true): ResponseCollection
|
||||
{
|
||||
return $this->fetch([$peek ? 'BODY.PEEK[TEXT]' : 'BODY[TEXT]'], (array) $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function bodyHeader(int|array $ids, bool $peek = true): ResponseCollection
|
||||
{
|
||||
return $this->fetch([$peek ? 'BODY.PEEK[HEADER]' : 'BODY[HEADER]'], (array) $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the BODYSTRUCTURE for the given message(s).
|
||||
*/
|
||||
public function bodyStructure(int|array $ids): ResponseCollection
|
||||
{
|
||||
return $this->fetch(['BODYSTRUCTURE'], (array) $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a specific part of the message BODY, such as BODY[1], BODY[1.2], etc.
|
||||
*/
|
||||
public function bodyPart(string $partIndex, int|array $ids, bool $peek = false): ResponseCollection
|
||||
{
|
||||
$part = $peek ? "BODY.PEEK[$partIndex]" : "BODY[$partIndex]";
|
||||
|
||||
return $this->fetch([$part], (array) $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function flags(int|array $ids): ResponseCollection
|
||||
{
|
||||
return $this->fetch(['FLAGS'], (array) $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function size(int|array $ids): ResponseCollection
|
||||
{
|
||||
return $this->fetch(['RFC822.SIZE'], (array) $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function search(array $params): UntaggedResponse
|
||||
{
|
||||
$this->send('UID SEARCH', $params, tag: $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged()->firstOrFail(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('SEARCH')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function sort(ImapSortKey $key, string $direction, array $params): UntaggedResponse
|
||||
{
|
||||
$sortCriteria = $direction === 'desc' ? "REVERSE {$key->value}" : $key->value;
|
||||
|
||||
$this->send('UID SORT', ["({$sortCriteria})", 'UTF-8', ...$params], tag: $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged()->firstOrFail(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('SORT')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function capability(): UntaggedResponse
|
||||
{
|
||||
$this->send('CAPABILITY', tag: $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged()->firstOrFail(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('CAPABILITY')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function id(?array $ids = null): UntaggedResponse
|
||||
{
|
||||
$token = 'NIL';
|
||||
|
||||
if (is_array($ids) && ! empty($ids)) {
|
||||
$token = '(';
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$token .= '"'.Str::escape($id).'" ';
|
||||
}
|
||||
|
||||
$token = rtrim($token).')';
|
||||
}
|
||||
|
||||
$this->send('ID', [$token], tag: $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged()->firstOrFail(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('ID')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function expunge(): ResponseCollection
|
||||
{
|
||||
$this->send('EXPUNGE', tag: $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function noop(): TaggedResponse
|
||||
{
|
||||
$this->send('NOOP', tag: $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function idle(int $timeout): Generator
|
||||
{
|
||||
$this->stream->setTimeout($timeout);
|
||||
|
||||
$this->send('IDLE', tag: $tag);
|
||||
|
||||
$this->assertNextResponse(
|
||||
fn (Response $response) => $response instanceof ContinuationResponse,
|
||||
fn (ContinuationResponse $response) => true,
|
||||
fn (ContinuationResponse $response) => ImapCommandException::make(new ImapCommand('', 'IDLE'), $response),
|
||||
);
|
||||
|
||||
while ($response = $this->nextReply()) {
|
||||
yield $response;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function done(): void
|
||||
{
|
||||
$this->write('DONE');
|
||||
|
||||
// After issuing a "DONE" command, the server must eventually respond with a
|
||||
// tagged response to indicate that the IDLE command has been successfully
|
||||
// terminated and the server is ready to accept further commands.
|
||||
$this->assertNextResponse(
|
||||
fn (Response $response) => $response instanceof TaggedResponse,
|
||||
fn (TaggedResponse $response) => $response->successful(),
|
||||
fn (TaggedResponse $response) => ImapCommandException::make(new ImapCommand('', 'DONE'), $response),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an IMAP command.
|
||||
*
|
||||
* @param-out string $tag
|
||||
*/
|
||||
public function send(string $name, array $tokens = [], ?string &$tag = null): void
|
||||
{
|
||||
if (! $tag) {
|
||||
$this->sequence++;
|
||||
$tag = 'TAG'.$this->sequence;
|
||||
}
|
||||
|
||||
$command = new ImapCommand($tag, $name, $tokens);
|
||||
|
||||
// After every command, we'll overwrite any previous result
|
||||
// with the new command and its responses, so that we can
|
||||
// easily access the commands responses for assertion.
|
||||
$this->setResult(new Result($command));
|
||||
|
||||
foreach ($command->compile() as $line) {
|
||||
$this->write($line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write data to the connected stream.
|
||||
*/
|
||||
protected function write(string $data): void
|
||||
{
|
||||
if ($this->stream->fwrite($data."\r\n") === false) {
|
||||
throw new ImapStreamException('Failed to write data to stream');
|
||||
}
|
||||
|
||||
$this->logger?->sent($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch one or more items for one or more messages.
|
||||
*/
|
||||
public function fetch(array|string $items, array|int $from, mixed $to = null, ImapFetchIdentifier $identifier = ImapFetchIdentifier::Uid): ResponseCollection
|
||||
{
|
||||
$prefix = ($identifier === ImapFetchIdentifier::Uid) ? 'UID' : '';
|
||||
|
||||
$this->send(trim($prefix.' FETCH'), [
|
||||
Str::set($from, $to),
|
||||
Str::list((array) $items),
|
||||
], $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
// Some IMAP servers can send unsolicited untagged responses along with fetch
|
||||
// requests. We'll need to filter these out so that we can return only the
|
||||
// responses that are relevant to the fetch command. For example:
|
||||
// >> TAG123 FETCH (UID 456 BODY[TEXT])
|
||||
// << * 123 FETCH (UID 456 BODY[TEXT] {14}\nHello, World!)
|
||||
// << * 123 FETCH (FLAGS (\Seen)) <-- Unsolicited response
|
||||
return $this->result->responses()->untagged()->filter(function (UntaggedResponse $response) use ($items, $identifier) {
|
||||
// Skip over any untagged responses that are not FETCH responses.
|
||||
// The third token should always be the list of data items.
|
||||
if (! ($data = $response->tokenAt(3)) instanceof ListData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return match ($identifier) {
|
||||
// If we're fetching UIDs, we can check if a UID token is contained in the list.
|
||||
ImapFetchIdentifier::Uid => $data->contains('UID'),
|
||||
|
||||
// If we're fetching message numbers, we can check if the requested items are all contained in the list.
|
||||
ImapFetchIdentifier::MessageNumber => $data->contains($items),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current result instance.
|
||||
*/
|
||||
protected function setResult(Result $result): void
|
||||
{
|
||||
$this->result = $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current parser instance.
|
||||
*/
|
||||
protected function setParser(ImapParser $parser): void
|
||||
{
|
||||
$this->parser = $parser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new parser instance.
|
||||
*/
|
||||
protected function newParser(StreamInterface $stream): ImapParser
|
||||
{
|
||||
return new ImapParser($this->newTokenizer($stream));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new tokenizer instance.
|
||||
*/
|
||||
protected function newTokenizer(StreamInterface $stream): ImapTokenizer
|
||||
{
|
||||
return new ImapTokenizer($stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the next response is a successful tagged response.
|
||||
*/
|
||||
protected function assertTaggedResponse(string $tag, ?callable $exception = null): TaggedResponse
|
||||
{
|
||||
/** @var TaggedResponse $response */
|
||||
$response = $this->assertNextResponse(
|
||||
fn (Response $response) => (
|
||||
$response instanceof TaggedResponse && $response->tag()->is($tag)
|
||||
),
|
||||
fn (TaggedResponse $response) => (
|
||||
$response->successful()
|
||||
),
|
||||
$exception ?? fn (TaggedResponse $response) => (
|
||||
ImapCommandException::make($this->result->command(), $response)
|
||||
),
|
||||
);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the next response matches the given filter and assertion.
|
||||
*
|
||||
* @template T of Response
|
||||
*
|
||||
* @param callable(Response): bool $filter
|
||||
* @param callable(T): bool $assertion
|
||||
* @param callable(T): Throwable $exception
|
||||
* @return T
|
||||
*
|
||||
* @throws ImapResponseException
|
||||
*/
|
||||
protected function assertNextResponse(callable $filter, callable $assertion, callable $exception): Response
|
||||
{
|
||||
while ($response = $this->nextResponse($filter)) {
|
||||
if ($assertion($response)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
throw $exception($response);
|
||||
}
|
||||
|
||||
throw new ImapResponseException('No matching response found');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next response matching the given filter.
|
||||
*
|
||||
* @template T of Response
|
||||
*
|
||||
* @param callable(T): bool $filter
|
||||
* @return T|null
|
||||
*/
|
||||
protected function nextResponse(callable $filter): ?Response
|
||||
{
|
||||
if (! $this->parser) {
|
||||
throw new LogicException('No parser instance set');
|
||||
}
|
||||
|
||||
while ($response = $this->nextReply()) {
|
||||
if (! $response instanceof Response) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->result?->addResponse($response);
|
||||
|
||||
if ($filter($response)) {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the next reply from the stream.
|
||||
*/
|
||||
protected function nextReply(): Data|Token|Response|null
|
||||
{
|
||||
if (! $reply = $this->parser->next()) {
|
||||
$meta = $this->stream->meta();
|
||||
|
||||
throw match (true) {
|
||||
$meta['timed_out'] ?? false => new ImapConnectionTimedOutException('Stream timed out, no response'),
|
||||
$meta['eof'] ?? false => new ImapConnectionClosedException('Server closed the connection (EOF)'),
|
||||
default => new ImapConnectionFailedException('Unknown stream error. Metadata: '.json_encode($meta)),
|
||||
};
|
||||
}
|
||||
|
||||
$this->logger?->received($reply);
|
||||
|
||||
return $reply;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\ContinuationResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\Data;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\ListData;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\ResponseCodeData;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Response;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\TaggedResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Atom;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Crlf;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\ListClose;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\ListOpen;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Number;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\ResponseCodeClose;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\ResponseCodeOpen;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapParserException;
|
||||
|
||||
class ImapParser
|
||||
{
|
||||
/**
|
||||
* The current token being parsed.
|
||||
*
|
||||
* Expected to be an associative array with keys like "type" and "value".
|
||||
*/
|
||||
protected ?Token $currentToken = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected ImapTokenizer $tokenizer
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the next response from the tokenizer.
|
||||
*/
|
||||
public function next(): Data|Token|Response|null
|
||||
{
|
||||
// Attempt to load the first token.
|
||||
if (! $this->currentToken) {
|
||||
$this->advance();
|
||||
}
|
||||
|
||||
// No token was found, return null.
|
||||
if (! $this->currentToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the token indicates the beginning of a list, parse it.
|
||||
if ($this->currentToken instanceof ListOpen) {
|
||||
return $this->parseList();
|
||||
}
|
||||
|
||||
// If the token is an Atom or Number, check its value for special markers.
|
||||
if ($this->currentToken instanceof Atom || $this->currentToken instanceof Number) {
|
||||
// '*' marks an untagged response.
|
||||
if ($this->currentToken->value === '*') {
|
||||
return $this->parseUntaggedResponse();
|
||||
}
|
||||
|
||||
// '+' marks a continuation response.
|
||||
if ($this->currentToken->value === '+') {
|
||||
return $this->parseContinuationResponse();
|
||||
}
|
||||
|
||||
// If it's an ATOM and not '*' or '+', it's likely a tagged response.
|
||||
return $this->parseTaggedResponse();
|
||||
}
|
||||
|
||||
return $this->parseElement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an untagged response.
|
||||
*
|
||||
* An untagged response begins with the '*' token. It may contain
|
||||
* multiple elements, including lists and response codes.
|
||||
*/
|
||||
protected function parseUntaggedResponse(): UntaggedResponse
|
||||
{
|
||||
// Capture the initial '*' token.
|
||||
$elements[] = clone $this->currentToken;
|
||||
|
||||
$this->advance();
|
||||
|
||||
// Collect all tokens until the end-of-response marker.
|
||||
while ($this->currentToken && ! $this->currentToken instanceof Crlf) {
|
||||
$elements[] = $this->parseElement();
|
||||
}
|
||||
|
||||
// If the end-of-response marker (CRLF) is present, consume it.
|
||||
if ($this->currentToken && $this->currentToken instanceof Crlf) {
|
||||
$this->currentToken = null;
|
||||
} else {
|
||||
throw new ImapParserException('Unterminated untagged response');
|
||||
}
|
||||
|
||||
return new UntaggedResponse($elements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a continuation response.
|
||||
*
|
||||
* A continuation response starts with a '+' token, indicating
|
||||
* that the server expects additional data from the client.
|
||||
*/
|
||||
protected function parseContinuationResponse(): ContinuationResponse
|
||||
{
|
||||
// Capture the initial '+' token.
|
||||
$elements[] = clone $this->currentToken;
|
||||
|
||||
$this->advance();
|
||||
|
||||
// Collect all tokens until the CRLF marker.
|
||||
while ($this->currentToken && ! $this->currentToken instanceof Crlf) {
|
||||
$elements[] = $this->parseElement();
|
||||
}
|
||||
|
||||
// Consume the CRLF marker if present.
|
||||
if ($this->currentToken && $this->currentToken instanceof Crlf) {
|
||||
$this->currentToken = null;
|
||||
} else {
|
||||
throw new ImapParserException('Unterminated continuation response');
|
||||
}
|
||||
|
||||
return new ContinuationResponse($elements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a tagged response.
|
||||
*
|
||||
* A tagged response begins with a tag (which is not '*' or '+')
|
||||
* and is followed by a status and optional data.
|
||||
*/
|
||||
protected function parseTaggedResponse(): TaggedResponse
|
||||
{
|
||||
// Capture the initial TAG token.
|
||||
$tokens[] = clone $this->currentToken;
|
||||
|
||||
$this->advance();
|
||||
|
||||
// Collect tokens until the end-of-response marker is reached.
|
||||
while ($this->currentToken && ! $this->currentToken instanceof Crlf) {
|
||||
$tokens[] = $this->parseElement();
|
||||
}
|
||||
|
||||
// Consume the CRLF marker if present.
|
||||
if ($this->currentToken && $this->currentToken instanceof Crlf) {
|
||||
$this->currentToken = null;
|
||||
} else {
|
||||
throw new ImapParserException('Unterminated tagged response');
|
||||
}
|
||||
|
||||
return new TaggedResponse($tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a bracket group of elements delimited by '[' and ']'.
|
||||
*
|
||||
* Bracket groups are used to represent response codes.
|
||||
*/
|
||||
protected function parseBracketGroup(): ResponseCodeData
|
||||
{
|
||||
// Consume the opening '[' token.
|
||||
$this->advance();
|
||||
|
||||
$elements = [];
|
||||
|
||||
while (
|
||||
$this->currentToken
|
||||
&& ! $this->currentToken instanceof ResponseCodeClose
|
||||
) {
|
||||
// Skip CRLF tokens that may appear inside bracket groups.
|
||||
if ($this->currentToken instanceof Crlf) {
|
||||
$this->advance();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$elements[] = $this->parseElement();
|
||||
}
|
||||
|
||||
if ($this->currentToken === null) {
|
||||
throw new ImapParserException('Unterminated bracket group in response');
|
||||
}
|
||||
|
||||
// Consume the closing ']' token.
|
||||
$this->advance();
|
||||
|
||||
return new ResponseCodeData($elements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a list of elements delimited by '(' and ')'.
|
||||
*
|
||||
* Lists are handled recursively, as a list may contain nested lists.
|
||||
*/
|
||||
protected function parseList(): ListData
|
||||
{
|
||||
// Consume the opening '(' token.
|
||||
$this->advance();
|
||||
|
||||
$elements = [];
|
||||
|
||||
// Continue to parse elements until we find the corresponding ')'.
|
||||
while (
|
||||
$this->currentToken
|
||||
&& ! $this->currentToken instanceof ListClose
|
||||
) {
|
||||
// Skip CRLF tokens that appear inside lists (after literals).
|
||||
if ($this->currentToken instanceof Crlf) {
|
||||
$this->advance();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$elements[] = $this->parseElement();
|
||||
}
|
||||
|
||||
// If we reached the end without finding a closing ')', throw an exception.
|
||||
if ($this->currentToken === null) {
|
||||
throw new ImapParserException('Unterminated list in response');
|
||||
}
|
||||
|
||||
// Consume the closing ')' token.
|
||||
$this->advance();
|
||||
|
||||
return new ListData($elements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a single element, which might be a list or a simple token.
|
||||
*/
|
||||
protected function parseElement(): Data|Token|null
|
||||
{
|
||||
// If there is no current token, return null.
|
||||
if ($this->currentToken === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the token indicates the start of a list, parse it as a list.
|
||||
if ($this->currentToken instanceof ListOpen) {
|
||||
return $this->parseList();
|
||||
}
|
||||
|
||||
// If the token indicates the start of a group, parse it as a group.
|
||||
if ($this->currentToken instanceof ResponseCodeOpen) {
|
||||
return $this->parseBracketGroup();
|
||||
}
|
||||
|
||||
// Otherwise, capture the current token.
|
||||
$token = clone $this->currentToken;
|
||||
|
||||
$this->advance();
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance to the next token from the tokenizer.
|
||||
*/
|
||||
protected function advance(): void
|
||||
{
|
||||
$this->currentToken = $this->tokenizer->nextToken();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection;
|
||||
|
||||
use BackedEnum;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonInterface;
|
||||
use DateTimeInterface;
|
||||
use DirectoryTree\ImapEngine\Enums\ImapSearchKey;
|
||||
use DirectoryTree\ImapEngine\Support\Str;
|
||||
|
||||
class ImapQueryBuilder
|
||||
{
|
||||
/**
|
||||
* The where conditions for the query.
|
||||
*/
|
||||
protected array $wheres = [];
|
||||
|
||||
/**
|
||||
* The date format to use for date based queries.
|
||||
*/
|
||||
protected string $dateFormat = 'd-M-Y';
|
||||
|
||||
/**
|
||||
* Add a where "ALL" clause to the query.
|
||||
*/
|
||||
public function all(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::All);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "NEW" clause to the query.
|
||||
*/
|
||||
public function new(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::New);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "OLD" clause to the query.
|
||||
*/
|
||||
public function old(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Old);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "SEEN" clause to the query.
|
||||
*/
|
||||
public function seen(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Seen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "DRAFT" clause to the query.
|
||||
*/
|
||||
public function draft(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Draft);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "RECENT" clause to the query.
|
||||
*/
|
||||
public function recent(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Recent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "UNSEEN" clause to the query.
|
||||
*/
|
||||
public function unseen(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Unseen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "FLAGGED" clause to the query.
|
||||
*/
|
||||
public function flagged(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Flagged);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "DELETED" clause to the query.
|
||||
*/
|
||||
public function deleted(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Deleted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "ANSWERED" clause to the query.
|
||||
*/
|
||||
public function answered(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Answered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "UNDELETED" clause to the query.
|
||||
*/
|
||||
public function undeleted(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Undeleted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "UNFLAGGED" clause to the query.
|
||||
*/
|
||||
public function unflagged(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Unflagged);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "UNANSWERED" clause to the query.
|
||||
*/
|
||||
public function unanswered(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Unanswered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "FROM" clause to the query.
|
||||
*/
|
||||
public function from(string $email): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::From, $email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "TO" clause to the query.
|
||||
*/
|
||||
public function to(string $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::To, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "CC" clause to the query.
|
||||
*/
|
||||
public function cc(string $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Cc, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "BCC" clause to the query.
|
||||
*/
|
||||
public function bcc(string $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Bcc, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "BODY" clause to the query.
|
||||
*/
|
||||
public function body(string $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Body, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "KEYWORD" clause to the query.
|
||||
*/
|
||||
public function keyword(string $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Keyword, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "UNKEYWORD" clause to the query.
|
||||
*/
|
||||
public function unkeyword(string $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Unkeyword, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "ON" clause to the query.
|
||||
*/
|
||||
public function on(mixed $date): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::On, new RawQueryValue(
|
||||
$this->parseDate($date)->format($this->dateFormat)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "SINCE" clause to the query.
|
||||
*/
|
||||
public function since(mixed $date): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Since, new RawQueryValue(
|
||||
$this->parseDate($date)->format($this->dateFormat)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "BEFORE" clause to the query.
|
||||
*/
|
||||
public function before(mixed $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Before, new RawQueryValue(
|
||||
$this->parseDate($value)->format($this->dateFormat)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "SENTON" clause to the query.
|
||||
*/
|
||||
public function sentOn(mixed $date): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::SentOn, new RawQueryValue(
|
||||
$this->parseDate($date)->format($this->dateFormat)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "SENTSINCE" clause to the query.
|
||||
*/
|
||||
public function sentSince(mixed $date): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::SentSince, new RawQueryValue(
|
||||
$this->parseDate($date)->format($this->dateFormat)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "SENTBEFORE" clause to the query.
|
||||
*/
|
||||
public function sentBefore(mixed $date): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::SentBefore, new RawQueryValue(
|
||||
$this->parseDate($date)->format($this->dateFormat)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "SUBJECT" clause to the query.
|
||||
*/
|
||||
public function subject(string $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Subject, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "TEXT" clause to the query.
|
||||
*/
|
||||
public function text(string $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Text, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "HEADER" clause to the query.
|
||||
*/
|
||||
public function header(string $header, string $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Header->value." $header", $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "UID" clause to the query.
|
||||
*/
|
||||
public function uid(int|string|array $from, int|float|null $to = null): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Uid, new RawQueryValue(Str::set($from, $to)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "LARGER" clause to the query.
|
||||
*/
|
||||
public function larger(int $bytes): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Larger, new RawQueryValue($bytes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "SMALLER" clause to the query.
|
||||
*/
|
||||
public function smaller(int $bytes): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Smaller, new RawQueryValue($bytes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a "where" condition.
|
||||
*/
|
||||
public function where(mixed $column, mixed $value = null): static
|
||||
{
|
||||
if (is_callable($column)) {
|
||||
$this->addNestedCondition('AND', $column);
|
||||
} else {
|
||||
$this->addBasicCondition('AND', $column, $value);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an "or where" condition.
|
||||
*/
|
||||
public function orWhere(mixed $column, mixed $value = null): static
|
||||
{
|
||||
if (is_callable($column)) {
|
||||
$this->addNestedCondition('OR', $column);
|
||||
} else {
|
||||
$this->addBasicCondition('OR', $column, $value);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a "where not" condition.
|
||||
*/
|
||||
public function whereNot(mixed $column, mixed $value = null): static
|
||||
{
|
||||
$this->addBasicCondition('AND', $column, $value, true);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the query has any where conditions.
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->wheres);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the instance into an IMAP-compatible query string.
|
||||
*/
|
||||
public function toImap(): string
|
||||
{
|
||||
return $this->compileWheres($this->wheres);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new query instance (like Eloquent's newQuery).
|
||||
*/
|
||||
protected function newQuery(): static
|
||||
{
|
||||
return new static;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a basic condition to the query.
|
||||
*/
|
||||
protected function addBasicCondition(string $boolean, mixed $column, mixed $value, bool $not = false): void
|
||||
{
|
||||
$value = $this->prepareWhereValue($value);
|
||||
|
||||
$column = Str::enum($column);
|
||||
|
||||
$this->wheres[] = [
|
||||
'type' => 'basic',
|
||||
'not' => $not,
|
||||
'key' => $column,
|
||||
'value' => $value,
|
||||
'boolean' => $boolean,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the where value, escaping it as needed.
|
||||
*/
|
||||
protected function prepareWhereValue(mixed $value): RawQueryValue|string|null
|
||||
{
|
||||
if (is_null($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof RawQueryValue) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($value instanceof BackedEnum) {
|
||||
$value = $value->value;
|
||||
}
|
||||
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
$value = Carbon::instance($value);
|
||||
}
|
||||
|
||||
if ($value instanceof CarbonInterface) {
|
||||
$value = $value->format($this->dateFormat);
|
||||
}
|
||||
|
||||
return Str::escape($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a nested condition group to the query.
|
||||
*/
|
||||
protected function addNestedCondition(string $boolean, callable $callback): void
|
||||
{
|
||||
$nested = $this->newQuery();
|
||||
|
||||
$callback($nested);
|
||||
|
||||
$this->wheres[] = [
|
||||
'type' => 'nested',
|
||||
'query' => $nested,
|
||||
'boolean' => $boolean,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to parse a date string into a Carbon instance.
|
||||
*/
|
||||
protected function parseDate(mixed $date): CarbonInterface
|
||||
{
|
||||
if ($date instanceof CarbonInterface) {
|
||||
return $date;
|
||||
}
|
||||
|
||||
return Carbon::parse($date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a single expression node from a basic or nested where.
|
||||
*
|
||||
* @param array{type: 'basic'|'nested', boolean: 'AND'|'OR', query: ImapQueryBuilder} $where
|
||||
*/
|
||||
protected function makeExpressionNode(array $where): array
|
||||
{
|
||||
return match ($where['type']) {
|
||||
'basic' => [
|
||||
'expr' => $this->compileBasic($where),
|
||||
'boolean' => $where['boolean'],
|
||||
],
|
||||
|
||||
'nested' => [
|
||||
'expr' => $where['query']->toImap(),
|
||||
'boolean' => $where['boolean'],
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the existing expression with the next expression, respecting the boolean operator.
|
||||
*
|
||||
* @param 'AND'|'OR' $boolean
|
||||
*/
|
||||
protected function mergeExpressions(string $existing, string $next, string $boolean): string
|
||||
{
|
||||
return match ($boolean) {
|
||||
// AND is implicit – just append.
|
||||
'AND' => $existing.' '.$next,
|
||||
|
||||
// IMAP's OR is binary; nest accordingly.
|
||||
'OR' => 'OR ('.$existing.') ('.$next.')',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively compile the wheres array into an IMAP-compatible string.
|
||||
*/
|
||||
protected function compileWheres(array $wheres): string
|
||||
{
|
||||
if (empty($wheres)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Convert each "where" into a node for later merging.
|
||||
$exprNodes = array_map(fn (array $where) => (
|
||||
$this->makeExpressionNode($where)
|
||||
), $wheres);
|
||||
|
||||
// Start with the first expression.
|
||||
$combined = array_shift($exprNodes)['expr'];
|
||||
|
||||
// Merge the rest of the expressions.
|
||||
foreach ($exprNodes as $node) {
|
||||
$combined = $this->mergeExpressions(
|
||||
$combined, $node['expr'], $node['boolean']
|
||||
);
|
||||
}
|
||||
|
||||
return trim($combined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a basic where condition into an IMAP-compatible string.
|
||||
*/
|
||||
protected function compileBasic(array $where): string
|
||||
{
|
||||
$part = strtoupper($where['key']);
|
||||
|
||||
if ($where['value'] instanceof RawQueryValue) {
|
||||
$part .= ' '.$where['value']->value;
|
||||
} elseif ($where['value']) {
|
||||
$part .= ' "'.Str::toImapUtf7($where['value']).'"';
|
||||
}
|
||||
|
||||
if ($where['not']) {
|
||||
$part = 'NOT '.$part;
|
||||
}
|
||||
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,511 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Streams\StreamInterface;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Atom;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Crlf;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\EmailAddress;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\ListClose;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\ListOpen;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Literal;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Nil;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Number;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\QuotedString;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\ResponseCodeClose;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\ResponseCodeOpen;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapParserException;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapStreamException;
|
||||
|
||||
class ImapTokenizer
|
||||
{
|
||||
/**
|
||||
* The current position in the buffer.
|
||||
*/
|
||||
protected int $position = 0;
|
||||
|
||||
/**
|
||||
* The buffer of characters read from the stream.
|
||||
*/
|
||||
protected string $buffer = '';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected StreamInterface $stream
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns the next token from the stream.
|
||||
*/
|
||||
public function nextToken(): ?Token
|
||||
{
|
||||
$this->skipWhitespace();
|
||||
|
||||
$this->ensureBuffer(1);
|
||||
|
||||
$char = $this->currentChar();
|
||||
|
||||
if ($char === null || $char === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for line feed.
|
||||
if ($char === "\n") {
|
||||
// With a valid IMAP response, we should never reach this point,
|
||||
// but in case we receive a malformed response, we will flush
|
||||
// the buffer and return null to prevent an infinite loop.
|
||||
$this->flushBuffer();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for carriage return. (\r\n)
|
||||
if ($char === "\r") {
|
||||
$this->advance(); // Consume CR
|
||||
|
||||
$this->ensureBuffer(1);
|
||||
|
||||
if ($this->currentChar() !== "\n") {
|
||||
throw new ImapParserException('Expected LF after CR');
|
||||
}
|
||||
|
||||
$this->advance(); // Consume LF (\n)
|
||||
|
||||
return new Crlf("\r\n");
|
||||
}
|
||||
|
||||
// Check for parameter list opening.
|
||||
if ($char === '(') {
|
||||
$this->advance();
|
||||
|
||||
return new ListOpen('(');
|
||||
}
|
||||
|
||||
// Check for a parameter list closing.
|
||||
if ($char === ')') {
|
||||
$this->advance();
|
||||
|
||||
return new ListClose(')');
|
||||
}
|
||||
|
||||
// Check for a response group open.
|
||||
if ($char === '[') {
|
||||
$this->advance();
|
||||
|
||||
return new ResponseCodeOpen('[');
|
||||
}
|
||||
|
||||
// Check for response group close.
|
||||
if ($char === ']') {
|
||||
$this->advance();
|
||||
|
||||
return new ResponseCodeClose(']');
|
||||
}
|
||||
|
||||
// Check for angle bracket open (email addresses).
|
||||
if ($char === '<') {
|
||||
$this->advance();
|
||||
|
||||
return $this->readEmailAddress();
|
||||
}
|
||||
|
||||
// Check for quoted string.
|
||||
if ($char === '"') {
|
||||
return $this->readQuotedString();
|
||||
}
|
||||
|
||||
// Check for literal block open.
|
||||
if ($char === '{') {
|
||||
return $this->readLiteral();
|
||||
}
|
||||
|
||||
// Otherwise, parse a number or atom.
|
||||
return $this->readNumberOrAtom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips whitespace characters (spaces and tabs only, preserving CRLF).
|
||||
*/
|
||||
protected function skipWhitespace(): void
|
||||
{
|
||||
while (true) {
|
||||
$this->ensureBuffer(1);
|
||||
|
||||
$char = $this->currentChar();
|
||||
|
||||
// Break on EOF.
|
||||
if ($char === null || $char === '') {
|
||||
break;
|
||||
}
|
||||
|
||||
// Break on CRLF.
|
||||
if ($char === "\r" || $char === "\n") {
|
||||
break;
|
||||
}
|
||||
|
||||
// Break on non-whitespace.
|
||||
if ($char !== ' ' && $char !== "\t") {
|
||||
break;
|
||||
}
|
||||
|
||||
$this->advance();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a quoted string token.
|
||||
*
|
||||
* Quoted strings are enclosed in double quotes and may contain escaped characters.
|
||||
*/
|
||||
protected function readQuotedString(): QuotedString
|
||||
{
|
||||
// Skip the opening quote.
|
||||
$this->advance();
|
||||
|
||||
$value = '';
|
||||
|
||||
while (true) {
|
||||
$this->ensureBuffer(1);
|
||||
|
||||
$char = $this->currentChar();
|
||||
|
||||
if ($char === null) {
|
||||
throw new ImapParserException(sprintf(
|
||||
'Unterminated quoted string at buffer offset %d. Buffer: "%s"',
|
||||
$this->position,
|
||||
substr($this->buffer, max(0, $this->position - 10), 20)
|
||||
));
|
||||
}
|
||||
|
||||
if ($char === '\\') {
|
||||
$this->advance(); // Skip the backslash.
|
||||
|
||||
$this->ensureBuffer(1);
|
||||
|
||||
$escapedChar = $this->currentChar();
|
||||
|
||||
if ($escapedChar === null) {
|
||||
throw new ImapParserException('Unterminated escape sequence in quoted string');
|
||||
}
|
||||
|
||||
$value .= $escapedChar;
|
||||
|
||||
$this->advance();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($char === '"') {
|
||||
$this->advance(); // Skip the closing quote.
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$value .= $char;
|
||||
|
||||
$this->advance();
|
||||
}
|
||||
|
||||
return new QuotedString($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a literal token.
|
||||
*
|
||||
* Literal blocks in IMAP have the form {<length>}\r\n<data>.
|
||||
*/
|
||||
protected function readLiteral(): Literal
|
||||
{
|
||||
// Skip the opening '{'.
|
||||
$this->advance();
|
||||
|
||||
// This will contain the size of the literal block in a sequence of digits.
|
||||
// {<size>}\r\n<data>
|
||||
$numStr = '';
|
||||
|
||||
while (true) {
|
||||
$this->ensureBuffer(1);
|
||||
|
||||
$char = $this->currentChar();
|
||||
|
||||
if ($char === null) {
|
||||
throw new ImapParserException('Unterminated literal specifier');
|
||||
}
|
||||
|
||||
if ($char === '}') {
|
||||
$this->advance(); // Skip the '}'.
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$numStr .= $char;
|
||||
|
||||
$this->advance();
|
||||
}
|
||||
|
||||
// Expect carriage return after the literal specifier.
|
||||
$this->ensureBuffer(2);
|
||||
|
||||
// Get the carriage return.
|
||||
$crlf = substr($this->buffer, $this->position, 2);
|
||||
|
||||
if ($crlf !== "\r\n") {
|
||||
throw new ImapParserException('Expected CRLF after literal specifier');
|
||||
}
|
||||
|
||||
// Skip the CRLF.
|
||||
$this->advance(2);
|
||||
|
||||
$length = (int) $numStr;
|
||||
|
||||
// Use any data that is already in our buffer.
|
||||
$available = strlen($this->buffer) - $this->position;
|
||||
|
||||
if ($available >= $length) {
|
||||
$literal = substr($this->buffer, $this->position, $length);
|
||||
|
||||
$this->advance($length);
|
||||
} else {
|
||||
// Consume whatever is available without flushing the whole buffer.
|
||||
$literal = substr($this->buffer, $this->position);
|
||||
|
||||
$consumed = strlen($literal);
|
||||
|
||||
// Advance the pointer by the number of bytes we took.
|
||||
$this->advance($consumed);
|
||||
|
||||
// Calculate how many bytes are still needed.
|
||||
$remaining = $length - $consumed;
|
||||
|
||||
// Read the missing bytes from the stream.
|
||||
$data = $this->stream->read($remaining);
|
||||
|
||||
if ($data === false || strlen($data) !== $remaining) {
|
||||
throw new ImapStreamException('Unexpected end of stream while trying to fill the buffer');
|
||||
}
|
||||
|
||||
$literal .= $data;
|
||||
}
|
||||
|
||||
// Verify that the literal length matches the expected length.
|
||||
if (strlen($literal) !== $length) {
|
||||
throw new ImapParserException(sprintf(
|
||||
'Literal length mismatch: expected %d, got %d',
|
||||
$length,
|
||||
strlen($literal)
|
||||
));
|
||||
}
|
||||
|
||||
return new Literal($literal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a number or atom token.
|
||||
*/
|
||||
protected function readNumberOrAtom(): Token
|
||||
{
|
||||
$position = $this->position;
|
||||
|
||||
// First char must be a digit to even consider a number.
|
||||
if (! ctype_digit($this->buffer[$position] ?? '')) {
|
||||
return $this->readAtom();
|
||||
}
|
||||
|
||||
// Walk forward to find the end of the digit run.
|
||||
while (ctype_digit($this->buffer[$position] ?? '')) {
|
||||
$position++;
|
||||
|
||||
$this->ensureBuffer($position - $this->position + 1);
|
||||
}
|
||||
|
||||
$next = $this->buffer[$position] ?? null;
|
||||
|
||||
// If next is EOF or a delimiter, it's a Number.
|
||||
if ($next === null || $this->isDelimiter($next)) {
|
||||
return $this->readNumber();
|
||||
}
|
||||
|
||||
// Otherwise it's an Atom.
|
||||
return $this->readAtom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a number token.
|
||||
*
|
||||
* A number consists of one or more digit characters and represents a numeric value.
|
||||
*/
|
||||
protected function readNumber(): Number
|
||||
{
|
||||
$start = $this->position;
|
||||
|
||||
while (true) {
|
||||
$this->ensureBuffer(1);
|
||||
|
||||
$char = $this->currentChar();
|
||||
|
||||
if ($char === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (! ctype_digit($char)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$this->advance();
|
||||
}
|
||||
|
||||
return new Number(substr($this->buffer, $start, $this->position - $start));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an atom token.
|
||||
*
|
||||
* ATOMs are sequences of printable ASCII characters that do not contain delimiters.
|
||||
*/
|
||||
protected function readAtom(): Atom
|
||||
{
|
||||
$value = '';
|
||||
|
||||
while (true) {
|
||||
$this->ensureBuffer(1);
|
||||
|
||||
$char = $this->currentChar();
|
||||
|
||||
if ($char === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (! $this->isValidAtomCharacter($char)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$value .= $char;
|
||||
|
||||
$this->advance();
|
||||
}
|
||||
|
||||
if (strcasecmp($value, 'NIL') === 0) {
|
||||
return new Nil($value);
|
||||
}
|
||||
|
||||
return new Atom($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an email address token enclosed in angle brackets.
|
||||
*
|
||||
* Email addresses are enclosed in angle brackets ("<" and ">").
|
||||
*
|
||||
* For example "<johndoe@email.com>"
|
||||
*/
|
||||
protected function readEmailAddress(): ?EmailAddress
|
||||
{
|
||||
$value = '';
|
||||
|
||||
while (true) {
|
||||
$this->ensureBuffer(1);
|
||||
|
||||
$char = $this->currentChar();
|
||||
|
||||
if ($char === null) {
|
||||
throw new ImapParserException('Unterminated email address, expected ">"');
|
||||
}
|
||||
|
||||
if ($char === '>') {
|
||||
$this->advance(); // Skip the closing '>'.
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$value .= $char;
|
||||
|
||||
$this->advance();
|
||||
}
|
||||
|
||||
return new EmailAddress($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that at least the given length in characters are available in the buffer.
|
||||
*/
|
||||
protected function ensureBuffer(int $length): void
|
||||
{
|
||||
// If we have enough data in the buffer, return early.
|
||||
while ((strlen($this->buffer) - $this->position) < $length) {
|
||||
$data = $this->stream->fgets();
|
||||
|
||||
if ($data === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->buffer .= $data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current character in the buffer.
|
||||
*/
|
||||
protected function currentChar(): ?string
|
||||
{
|
||||
return $this->buffer[$this->position] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the internal pointer by $n characters.
|
||||
*/
|
||||
protected function advance(int $n = 1): void
|
||||
{
|
||||
$this->position += $n;
|
||||
|
||||
// If we have consumed the entire buffer, reset it.
|
||||
if ($this->position >= strlen($this->buffer)) {
|
||||
$this->flushBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the buffer and reset the position.
|
||||
*/
|
||||
protected function flushBuffer(): void
|
||||
{
|
||||
$this->buffer = '';
|
||||
$this->position = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given character is a valid atom character.
|
||||
*/
|
||||
protected function isValidAtomCharacter(string $char): bool
|
||||
{
|
||||
// Get the ASCII code.
|
||||
$code = ord($char);
|
||||
|
||||
// Allow only printable ASCII (32-126).
|
||||
if ($code < 32 || $code > 126) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Delimiters are not allowed inside ATOMs.
|
||||
if ($this->isDelimiter($char)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given character is a delimiter for tokenizing responses.
|
||||
*/
|
||||
protected function isDelimiter(string $char): bool
|
||||
{
|
||||
// This delimiter list includes additional characters (such as square
|
||||
// brackets, curly braces, and angle brackets) to ensure that tokens
|
||||
// like the response code group brackets are split out. This is fine
|
||||
// for tokenizing responses, even though it’s more restrictive
|
||||
// than the IMAP atom definition in RFC 3501 (section 9).
|
||||
return in_array($char, [' ', '(', ')', '[', ']', '{', '}', '<', '>'], true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Loggers;
|
||||
|
||||
class EchoLogger extends Logger
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function write(string $message): void
|
||||
{
|
||||
echo $message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Loggers;
|
||||
|
||||
class FileLogger extends Logger
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $path
|
||||
) {}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function write(string $message): void
|
||||
{
|
||||
file_put_contents($this->path, $message, FILE_APPEND);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Loggers;
|
||||
|
||||
abstract class Logger implements LoggerInterface
|
||||
{
|
||||
/**
|
||||
* Write a message to the log.
|
||||
*/
|
||||
abstract protected function write(string $message): void;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function sent(string $message): void
|
||||
{
|
||||
$this->write(sprintf('%s: >> %s', $this->date(), $message).PHP_EOL);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function received(string $message): void
|
||||
{
|
||||
$this->write(sprintf('%s: << %s', $this->date(), $message).PHP_EOL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current date and time.
|
||||
*/
|
||||
protected function date(): string
|
||||
{
|
||||
return date('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Loggers;
|
||||
|
||||
interface LoggerInterface
|
||||
{
|
||||
/**
|
||||
* Log when a message is sent.
|
||||
*/
|
||||
public function sent(string $message): void;
|
||||
|
||||
/**
|
||||
* Log when a message is received.
|
||||
*/
|
||||
public function received(string $message): void;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Loggers;
|
||||
|
||||
class RayLogger extends Logger
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function write(string $message): void
|
||||
{
|
||||
ray($message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection;
|
||||
|
||||
use Stringable;
|
||||
|
||||
class RawQueryValue
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly Stringable|string $value
|
||||
) {}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Responses;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
|
||||
class ContinuationResponse extends Response
|
||||
{
|
||||
/**
|
||||
* Get the data tokens.
|
||||
*
|
||||
* @return Token[]
|
||||
*/
|
||||
public function data(): array
|
||||
{
|
||||
return array_slice($this->tokens, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Responses\Data;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\HasTokens;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
use Stringable;
|
||||
|
||||
abstract class Data implements Stringable
|
||||
{
|
||||
use HasTokens;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected array $tokens
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the tokens.
|
||||
*
|
||||
* @return Token[]|Data[]
|
||||
*/
|
||||
public function tokens(): array
|
||||
{
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first token.
|
||||
*/
|
||||
public function first(): Token|Data|null
|
||||
{
|
||||
return $this->tokens[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last token.
|
||||
*/
|
||||
public function last(): Token|Data|null
|
||||
{
|
||||
return $this->tokens[count($this->tokens) - 1] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the data contains a specific value.
|
||||
*/
|
||||
public function contains(array|string $needles): bool
|
||||
{
|
||||
$haystack = $this->values();
|
||||
|
||||
foreach ((array) $needles as $needle) {
|
||||
if (! in_array($needle, $haystack)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the token's values.
|
||||
*/
|
||||
public function values(): array
|
||||
{
|
||||
return array_map(function (Token|Data $token) {
|
||||
return $token instanceof Data
|
||||
? $token->values()
|
||||
: $token->value;
|
||||
}, $this->tokens);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Responses\Data;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
|
||||
class ListData extends Data
|
||||
{
|
||||
/**
|
||||
* Find the immediate successor token of the given field in the list.
|
||||
*/
|
||||
public function lookup(string $field): Data|Token|null
|
||||
{
|
||||
foreach ($this->tokens as $index => $token) {
|
||||
if ((string) $token === $field) {
|
||||
return $this->tokenAt(++$index);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert alternating key/value tokens to an associative array.
|
||||
*/
|
||||
public function toKeyValuePairs(): array
|
||||
{
|
||||
$pairs = [];
|
||||
|
||||
for ($i = 0; $i < count($this->tokens) - 1; $i += 2) {
|
||||
$key = strtolower($this->tokens[$i]->value);
|
||||
|
||||
$pairs[$key] = $this->tokens[$i + 1]->value;
|
||||
}
|
||||
|
||||
return $pairs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list as a string.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf('(%s)', implode(
|
||||
' ', array_map('strval', $this->tokens)
|
||||
));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user