Composer Version Change

This commit is contained in:
Enoch 2024-02-05 00:00:23 +08:00
parent 20678a6a0c
commit 6f751ead83
789 changed files with 135417 additions and 18510 deletions

View File

@ -24,7 +24,7 @@ class IndexController
public function domain(Request $request) public function domain(Request $request)
{ {
View::assign('userinfo', $request->session()->get('userinfo')); View::assign('userinfo', $request->session()->get('userinfo'));
return view('index'); return view('domain');
} }
public function json(Request $request) public function json(Request $request)
{ {

112
app/view/domain.html Normal file
View File

@ -0,0 +1,112 @@
<?php
$page_title='域名列表';
include('common_head.html');?>
<link href="vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
<!-- Begin Page Content -->
<div class="container-fluid">
<!-- Page Heading -->
<h1 class="h3 mb-4 text-gray-800">域名列表</h1>
<a href="#" class="btn btn-primary btn-icon-split ">
<span class="icon text-white-50">
<i class="fas fa-plus"></i>
</span>
<span class="text">添加新域名</span>
</a>
<div class="my-2"></div>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">域名列表</h6>
</div>
<div class="card-body">
如遇需更改账号,请先以新账号添加用户,再删除旧账号
<div><code id="notice"></code></div>
<div class="table-responsive">
<table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>头像</th>
<th>姓名</th>
<th>邮箱</th>
<th>账号</th>
<th>密码</th>
<th>操作</th>
</tr>
</thead>
<tfoot>
<tr>
<th>头像</th>
<th>姓名</th>
<th>邮箱</th>
<th>账号</th>
<th>密码</th>
<th>操作</th>
</tr>
</tfoot>
<tbody>
<tr>
<td>新用户</td>
<td>管理员</td>
<td>admin@admin.com</td>
<td>admin</td>
<td>Start date</td>
<td>Salary</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- /.container-fluid -->
</div>
<!-- End of Main Content -->
<?php include('common_foot.html');?>
<!-- Page level plugins -->
<script src="vendor/datatables/jquery.dataTables.min.js"></script>
<script src="vendor/datatables/dataTables.bootstrap4.min.js"></script>
<!-- Page level custom scripts -->
<script src="js/demo/datatables-demo.js"></script>
<script>
function change($account){
var name = $("#"+$account+'name').val();
var password = $("#"+$account+"password").val();
var email = $("#"+$account+"email").val();
if(name=="" || email==""){
$('#notice').html('数据不完整');
return false;
}
$.ajax({
type: "POST",
url: '/api/user/change',
data: {'name':name,'password':password,'email':email,'account':$account},
async: false,
dataType: 'json',
cache: false,
beforeSubmit: function () {
$('#notice').html('正在操作');
},
success: function (data) {
if (data.code == 200) {
$('#notice').html('操作成功,即将刷新页面');
$(location).attr('href','user');
} else {
$('#notice').html(data.msg);
}
},
clearForm: false,
resetForm: false
});
}
</script>
</body>
</html>

View File

@ -32,11 +32,15 @@
"geoip2/geoip2": "~2.0", "geoip2/geoip2": "~2.0",
"illuminate/redis": "^9.45", "illuminate/redis": "^9.45",
"symfony/cache": "^6.0", "symfony/cache": "^6.0",
"illuminate/events": "^9.45", "illuminate/events": "^9.52",
"yzh52521/webman-throttle": "^1.0", "yzh52521/webman-throttle": "^1.0",
"workerman/validation": "^3.0", "workerman/validation": "^3.0",
"yzh52521/easyhttp": "^1.0", "yzh52521/easyhttp": "^1.0",
"laysense/dns": "^0.1.0" "laysense/dns": "^0.1.0",
"illuminate/database": "^9.52",
"illuminate/pagination": "^9.52",
"symfony/var-dumper": "^6.0",
"robmorgan/phinx": "^0.14.0"
}, },
"suggest": { "suggest": {
"ext-event": "For better performance. " "ext-event": "For better performance. "
@ -63,8 +67,5 @@
"pre-package-uninstall": [ "pre-package-uninstall": [
"support\\Plugin::uninstall" "support\\Plugin::uninstall"
] ]
},
"require-dev": {
"webman-tech/debugbar": "^2.1"
} }
} }

1366
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +0,0 @@
<?php
$enable = config('app.debug', false) && class_exists('WebmanTech\Debugbar\WebmanDebugBar');
return [
'enable' => $enable,
/**
* @see \WebmanTech\Debugbar\WebmanDebugBar::$config
*/
'debugbar' => [
'enable' => $enable,
],
];

View File

@ -1,9 +0,0 @@
<?php
use WebmanTech\Debugbar\Bootstrap\LaravelQuery;
use WebmanTech\Debugbar\Bootstrap\LaravelRedisExec;
return [
LaravelQuery::class,
LaravelRedisExec::class,
];

View File

@ -1,9 +0,0 @@
<?php
use WebmanTech\Debugbar\Middleware\DebugBarMiddleware;
return [
'' => [
DebugBarMiddleware::class,
],
];

View File

@ -1,5 +0,0 @@
<?php
use WebmanTech\Debugbar\DebugBar;
DebugBar::instance()->registerRoute();

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class MyNewMigration extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
}
}

21
phinx.php Normal file
View File

@ -0,0 +1,21 @@
<?php
return [
"paths" => [
"migrations" => "database/migrations",
"seeds" => "database/seeds"
],
"environments" => [
"default_migration_table" => "phinxlog",
"default_database" => "dev",
"default_environment" => "dev",
"dev" => [
"adapter" => "DB_CONNECTION",
"host" => "DB_HOST",
"name" => "DB_DATABASE",
"user" => "DB_USERNAME",
"pass" => "DB_PASSWORD",
"port" => "DB_PORT",
"charset" => "utf8"
]
]
];

120
vendor/bin/phinx vendored Executable file
View File

@ -0,0 +1,120 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../robmorgan/phinx/bin/phinx)
* 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'))
) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/robmorgan/phinx/bin/phinx');
exit(0);
}
}
include __DIR__ . '/..'.'/robmorgan/phinx/bin/phinx';

5
vendor/bin/phinx.bat vendored Executable file
View File

@ -0,0 +1,5 @@
@ECHO OFF
setlocal DISABLEDELAYEDEXPANSION
SET BIN_TARGET=%~dp0/phinx
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
php "%BIN_TARGET%" %*

445
vendor/brick/math/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,445 @@
# Changelog
All notable changes to this project will be documented in this file.
## [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.

20
vendor/brick/math/LICENSE vendored Normal file
View File

@ -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.

34
vendor/brick/math/composer.json vendored Normal file
View File

@ -0,0 +1,34 @@
{
"name": "brick/math",
"description": "Arbitrary-precision arithmetic library",
"type": "library",
"keywords": [
"Brick",
"Math",
"Arbitrary-precision",
"Arithmetic",
"BigInteger",
"BigDecimal",
"BigRational",
"Bignum"
],
"license": "MIT",
"require": {
"php": "^8.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0",
"php-coveralls/php-coveralls": "^2.2",
"vimeo/psalm": "5.0.0"
},
"autoload": {
"psr-4": {
"Brick\\Math\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Brick\\Math\\Tests\\": "tests/"
}
}
}

786
vendor/brick/math/src/BigDecimal.php vendored Normal file
View File

@ -0,0 +1,786 @@
<?php
declare(strict_types=1);
namespace Brick\Math;
use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NegativeNumberException;
use Brick\Math\Internal\Calculator;
/**
* Immutable, arbitrary-precision signed decimal numbers.
*
* @psalm-immutable
*/
final class BigDecimal extends BigNumber
{
/**
* The unscaled value of this decimal number.
*
* This is a string of digits with an optional leading minus sign.
* No leading zero must be present.
* No leading minus sign must be present if the value is 0.
*/
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.
*/
protected function __construct(string $value, int $scale = 0)
{
$this->value = $value;
$this->scale = $scale;
}
/**
* Creates a BigDecimal of the given value.
*
* @throws MathException If the value cannot be converted to a BigDecimal.
*
* @psalm-pure
*/
public static function of(BigNumber|int|float|string $value) : BigDecimal
{
return parent::of($value)->toBigDecimal();
}
/**
* Creates a BigDecimal from an unscaled value and a scale.
*
* Example: `(12345, 3)` will result in the BigDecimal `12.345`.
*
* @param BigNumber|int|float|string $value The unscaled value. Must be convertible to a BigInteger.
* @param int $scale The scale of the number, positive or zero.
*
* @throws \InvalidArgumentException If the scale is negative.
*
* @psalm-pure
*/
public static function ofUnscaledValue(BigNumber|int|float|string $value, int $scale = 0) : BigDecimal
{
if ($scale < 0) {
throw new \InvalidArgumentException('The scale cannot be negative.');
}
return new BigDecimal((string) BigInteger::of($value), $scale);
}
/**
* Returns a BigDecimal representing zero, with a scale of zero.
*
* @psalm-pure
*/
public static function zero() : BigDecimal
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigDecimal|null $zero
*/
static $zero;
if ($zero === null) {
$zero = new BigDecimal('0');
}
return $zero;
}
/**
* Returns a BigDecimal representing one, with a scale of zero.
*
* @psalm-pure
*/
public static function one() : BigDecimal
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigDecimal|null $one
*/
static $one;
if ($one === null) {
$one = new BigDecimal('1');
}
return $one;
}
/**
* Returns a BigDecimal representing ten, with a scale of zero.
*
* @psalm-pure
*/
public static function ten() : BigDecimal
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigDecimal|null $ten
*/
static $ten;
if ($ten === null) {
$ten = new BigDecimal('10');
}
return $ten;
}
/**
* Returns the sum of this number and the given one.
*
* The result has a scale of `max($this->scale, $that->scale)`.
*
* @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigDecimal.
*
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
*/
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 = Calculator::get()->add($a, $b);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
return new BigDecimal($value, $scale);
}
/**
* Returns the difference of this number and the given one.
*
* The result has a scale of `max($this->scale, $that->scale)`.
*
* @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigDecimal.
*
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
*/
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 = Calculator::get()->sub($a, $b);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
return new BigDecimal($value, $scale);
}
/**
* Returns the product of this number and the given one.
*
* The result has a scale of `$this->scale + $that->scale`.
*
* @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigDecimal.
*
* @throws MathException If the multiplier is not a valid number, or is not convertible to a BigDecimal.
*/
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 = Calculator::get()->mul($this->value, $that->value);
$scale = $this->scale + $that->scale;
return new BigDecimal($value, $scale);
}
/**
* Returns the result of the division of this number by the given one, at the given scale.
*
* @param BigNumber|int|float|string $that The divisor.
* @param int|null $scale The desired scale, or null to use the scale of this number.
* @param int $roundingMode An optional rounding mode.
*
* @throws \InvalidArgumentException If the scale or rounding mode is invalid.
* @throws MathException If the number is invalid, is zero, or rounding was necessary.
*/
public function dividedBy(BigNumber|int|float|string $that, ?int $scale = null, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
if ($scale === null) {
$scale = $this->scale;
} elseif ($scale < 0) {
throw new \InvalidArgumentException('Scale cannot be negative.');
}
if ($that->value === '1' && $that->scale === 0 && $scale === $this->scale) {
return $this;
}
$p = $this->valueWithMinScale($that->scale + $scale);
$q = $that->valueWithMinScale($this->scale - $scale);
$result = Calculator::get()->divRound($p, $q, $roundingMode);
return new BigDecimal($result, $scale);
}
/**
* Returns the exact result of the division of this number by the given one.
*
* The scale of the result is automatically calculated to fit all the fraction digits.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @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
{
$that = BigDecimal::of($that);
if ($that->value === '0') {
throw DivisionByZeroException::divisionByZero();
}
[, $b] = $this->scaleValues($this, $that);
$d = \rtrim($b, '0');
$scale = \strlen($b) - \strlen($d);
$calculator = Calculator::get();
foreach ([5, 2] as $prime) {
for (;;) {
$lastDigit = (int) $d[-1];
if ($lastDigit % $prime !== 0) {
break;
}
$d = $calculator->divQ($d, (string) $prime);
$scale++;
}
}
return $this->dividedBy($that, $scale)->stripTrailingZeros();
}
/**
* Returns this number exponentiated to the given value.
*
* The result has a scale of `$this->scale * $exponent`.
*
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
*/
public function power(int $exponent) : BigDecimal
{
if ($exponent === 0) {
return BigDecimal::one();
}
if ($exponent === 1) {
return $this;
}
if ($exponent < 0 || $exponent > Calculator::MAX_POWER) {
throw new \InvalidArgumentException(\sprintf(
'The exponent %d is not in the range 0 to %d.',
$exponent,
Calculator::MAX_POWER
));
}
return new BigDecimal(Calculator::get()->pow($this->value, $exponent), $this->scale * $exponent);
}
/**
* Returns the quotient of the division of this number by this given one.
*
* The quotient has a scale of `0`.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @throws MathException If the divisor is not a valid decimal number, or is zero.
*/
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 = Calculator::get()->divQ($p, $q);
return new BigDecimal($quotient, 0);
}
/**
* Returns the remainder of the division of this number by this given one.
*
* The remainder has a scale of `max($this->scale, $that->scale)`.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @throws MathException If the divisor is not a valid decimal number, or is zero.
*/
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 = Calculator::get()->divR($p, $q);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
return new BigDecimal($remainder, $scale);
}
/**
* Returns the quotient and remainder of the division of this number by the given one.
*
* The quotient has a scale of `0`, and the remainder has a scale of `max($this->scale, $that->scale)`.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @return BigDecimal[] An array containing the quotient and the remainder.
*
* @throws MathException If the divisor is not a valid decimal number, or is zero.
*/
public function quotientAndRemainder(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] = Calculator::get()->divQR($p, $q);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
$quotient = new BigDecimal($quotient, 0);
$remainder = new BigDecimal($remainder, $scale);
return [$quotient, $remainder];
}
/**
* Returns the square root of this number, rounded down to the given number of decimals.
*
* @throws \InvalidArgumentException If the scale is negative.
* @throws NegativeNumberException If this number is negative.
*/
public function sqrt(int $scale) : BigDecimal
{
if ($scale < 0) {
throw new \InvalidArgumentException('Scale cannot be negative.');
}
if ($this->value === '0') {
return new BigDecimal('0', $scale);
}
if ($this->value[0] === '-') {
throw new NegativeNumberException('Cannot calculate the square root of a negative number.');
}
$value = $this->value;
$addDigits = 2 * $scale - $this->scale;
if ($addDigits > 0) {
// add zeros
$value .= \str_repeat('0', $addDigits);
} elseif ($addDigits < 0) {
// trim digits
if (-$addDigits >= \strlen($this->value)) {
// requesting a scale too low, will always yield a zero result
return new BigDecimal('0', $scale);
}
$value = \substr($value, 0, $addDigits);
}
$value = Calculator::get()->sqrt($value);
return new BigDecimal($value, $scale);
}
/**
* Returns a copy of this BigDecimal with the decimal point moved $n places to the left.
*/
public function withPointMovedLeft(int $n) : BigDecimal
{
if ($n === 0) {
return $this;
}
if ($n < 0) {
return $this->withPointMovedRight(-$n);
}
return new BigDecimal($this->value, $this->scale + $n);
}
/**
* Returns a copy of this BigDecimal with the decimal point moved $n places to the right.
*/
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.
*/
public function stripTrailingZeros() : BigDecimal
{
if ($this->scale === 0) {
return $this;
}
$trimmedValue = \rtrim($this->value, '0');
if ($trimmedValue === '') {
return BigDecimal::zero();
}
$trimmableZeros = \strlen($this->value) - \strlen($trimmedValue);
if ($trimmableZeros === 0) {
return $this;
}
if ($trimmableZeros > $this->scale) {
$trimmableZeros = $this->scale;
}
$value = \substr($this->value, 0, -$trimmableZeros);
$scale = $this->scale - $trimmableZeros;
return new BigDecimal($value, $scale);
}
/**
* Returns the absolute value of this number.
*/
public function abs() : BigDecimal
{
return $this->isNegative() ? $this->negated() : $this;
}
/**
* Returns the negated value of this number.
*/
public function negated() : BigDecimal
{
return new BigDecimal(Calculator::get()->neg($this->value), $this->scale);
}
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 Calculator::get()->cmp($a, $b);
}
return - $that->compareTo($this);
}
public function getSign() : int
{
return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1);
}
public function getUnscaledValue() : BigInteger
{
return self::newBigInteger($this->value);
}
public function getScale() : int
{
return $this->scale;
}
/**
* Returns a string representing the integral part of this decimal number.
*
* Example: `-123.456` => `-123`.
*/
public function getIntegralPart() : string
{
if ($this->scale === 0) {
return $this->value;
}
$value = $this->getUnscaledValueWithLeadingZeros();
return \substr($value, 0, -$this->scale);
}
/**
* Returns a string representing the fractional part of this decimal number.
*
* If the scale is zero, an empty string is returned.
*
* Examples: `-123.456` => '456', `123` => ''.
*/
public function getFractionalPart() : string
{
if ($this->scale === 0) {
return '';
}
$value = $this->getUnscaledValueWithLeadingZeros();
return \substr($value, -$this->scale);
}
/**
* Returns whether this decimal number has a non-zero fractional part.
*/
public function hasNonZeroFractionalPart() : bool
{
return $this->getFractionalPart() !== \str_repeat('0', $this->scale);
}
public function toBigInteger() : BigInteger
{
$zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0);
return self::newBigInteger($zeroScaleDecimal->value);
}
public function toBigDecimal() : BigDecimal
{
return $this;
}
public function toBigRational() : BigRational
{
$numerator = self::newBigInteger($this->value);
$denominator = self::newBigInteger('1' . \str_repeat('0', $this->scale));
return self::newBigRational($numerator, $denominator, false);
}
public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
{
if ($scale === $this->scale) {
return $this;
}
return $this->dividedBy(BigDecimal::one(), $scale, $roundingMode);
}
public function toInt() : int
{
return $this->toBigInteger()->toInt();
}
public function toFloat() : float
{
return (float) (string) $this;
}
public function __toString() : string
{
if ($this->scale === 0) {
return $this->value;
}
$value = $this->getUnscaledValueWithLeadingZeros();
return \substr($value, 0, -$this->scale) . '.' . \substr($value, -$this->scale);
}
/**
* This method is required for serializing the object and SHOULD NOT be accessed directly.
*
* @internal
*
* @return array{value: string, scale: int}
*/
public function __serialize(): array
{
return ['value' => $this->value, 'scale' => $this->scale];
}
/**
* This method is only here to allow unserializing the object and cannot be accessed directly.
*
* @internal
* @psalm-suppress RedundantPropertyInitializationCheck
*
* @param array{value: string, scale: int} $data
*
* @throws \LogicException
*/
public function __unserialize(array $data): void
{
if (isset($this->value)) {
throw new \LogicException('__unserialize() is an internal function, it must not be called directly.');
}
$this->value = $data['value'];
$this->scale = $data['scale'];
}
/**
* This method is required by interface Serializable and SHOULD NOT be accessed directly.
*
* @internal
*/
public function serialize() : string
{
return $this->value . ':' . $this->scale;
}
/**
* This method is only here to implement interface Serializable and cannot be accessed directly.
*
* @internal
* @psalm-suppress RedundantPropertyInitializationCheck
*
* @throws \LogicException
*/
public function unserialize($value) : void
{
if (isset($this->value)) {
throw new \LogicException('unserialize() is an internal function, it must not be called directly.');
}
[$value, $scale] = \explode(':', $value);
$this->value = $value;
$this->scale = (int) $scale;
}
/**
* Puts the internal values of the given decimal numbers on the same scale.
*
* @return array{string, string} The scaled integer values of $x and $y.
*/
private function scaleValues(BigDecimal $x, BigDecimal $y) : array
{
$a = $x->value;
$b = $y->value;
if ($b !== '0' && $x->scale > $y->scale) {
$b .= \str_repeat('0', $x->scale - $y->scale);
} elseif ($a !== '0' && $x->scale < $y->scale) {
$a .= \str_repeat('0', $y->scale - $x->scale);
}
return [$a, $b];
}
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.
*/
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;
}
}

1079
vendor/brick/math/src/BigInteger.php vendored Normal file

File diff suppressed because it is too large Load Diff

512
vendor/brick/math/src/BigNumber.php vendored Normal file
View File

@ -0,0 +1,512 @@
<?php
declare(strict_types=1);
namespace Brick\Math;
use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Exception\RoundingNecessaryException;
/**
* Common interface for arbitrary-precision rational numbers.
*
* @psalm-immutable
*/
abstract class BigNumber implements \Serializable, \JsonSerializable
{
/**
* The regular expression used to parse integer, decimal and rational numbers.
*/
private const PARSE_REGEXP =
'/^' .
'(?<sign>[\-\+])?' .
'(?:' .
'(?:' .
'(?<integral>[0-9]+)?' .
'(?<point>\.)?' .
'(?<fractional>[0-9]+)?' .
'(?:[eE](?<exponent>[\-\+]?[0-9]+))?' .
')|(?:' .
'(?<numerator>[0-9]+)' .
'\/?' .
'(?<denominator>[0-9]+)' .
')' .
')' .
'$/';
/**
* Creates a BigNumber of the given value.
*
* The concrete return type is dependent on the given value, with the following rules:
*
* - BigNumber instances are returned as is
* - integer numbers are returned as BigInteger
* - floating point numbers are converted to a string then parsed as such
* - strings containing a `/` character are returned as BigRational
* - strings containing a `.` character or using an exponential notation are returned as BigDecimal
* - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger
*
* @throws NumberFormatException If the format of the number is not valid.
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
*
* @psalm-pure
*/
public static function of(BigNumber|int|float|string $value) : BigNumber
{
if ($value instanceof BigNumber) {
return $value;
}
if (\is_int($value)) {
return new BigInteger((string) $value);
}
$value = \is_float($value) ? self::floatToString($value) : $value;
$throw = static function() use ($value) : void {
throw new NumberFormatException(\sprintf(
'The given value "%s" does not represent a valid number.',
$value
));
};
if (\preg_match(self::PARSE_REGEXP, $value, $matches) !== 1) {
$throw();
}
$getMatch = static fn(string $value): ?string => (($matches[$value] ?? '') !== '') ? $matches[$value] : null;
$sign = $getMatch('sign');
$numerator = $getMatch('numerator');
$denominator = $getMatch('denominator');
if ($numerator !== null) {
assert($denominator !== null);
if ($sign !== null) {
$numerator = $sign . $numerator;
}
$numerator = self::cleanUp($numerator);
$denominator = self::cleanUp($denominator);
if ($denominator === '0') {
throw DivisionByZeroException::denominatorMustNotBeZero();
}
return new BigRational(
new BigInteger($numerator),
new BigInteger($denominator),
false
);
}
$point = $getMatch('point');
$integral = $getMatch('integral');
$fractional = $getMatch('fractional');
$exponent = $getMatch('exponent');
if ($integral === null && $fractional === null) {
$throw();
}
if ($integral === null) {
$integral = '0';
}
if ($point !== null || $exponent !== null) {
$fractional = ($fractional ?? '');
$exponent = ($exponent !== null) ? (int) $exponent : 0;
if ($exponent === PHP_INT_MIN || $exponent === PHP_INT_MAX) {
throw new NumberFormatException('Exponent too large.');
}
$unscaledValue = self::cleanUp(($sign ?? ''). $integral . $fractional);
$scale = \strlen($fractional) - $exponent;
if ($scale < 0) {
if ($unscaledValue !== '0') {
$unscaledValue .= \str_repeat('0', - $scale);
}
$scale = 0;
}
return new BigDecimal($unscaledValue, $scale);
}
$integral = self::cleanUp(($sign ?? '') . $integral);
return new BigInteger($integral);
}
/**
* Safely converts float to string, avoiding locale-dependent issues.
*
* @see https://github.com/brick/math/pull/20
*
* @psalm-pure
* @psalm-suppress ImpureFunctionCall
*/
private static function floatToString(float $float) : string
{
$currentLocale = \setlocale(LC_NUMERIC, '0');
\setlocale(LC_NUMERIC, 'C');
$result = (string) $float;
\setlocale(LC_NUMERIC, $currentLocale);
return $result;
}
/**
* Proxy method to access BigInteger's protected constructor from sibling classes.
*
* @internal
* @psalm-pure
*/
protected function newBigInteger(string $value) : BigInteger
{
return new BigInteger($value);
}
/**
* Proxy method to access BigDecimal's protected constructor from sibling classes.
*
* @internal
* @psalm-pure
*/
protected function newBigDecimal(string $value, int $scale = 0) : BigDecimal
{
return new BigDecimal($value, $scale);
}
/**
* Proxy method to access BigRational's protected constructor from sibling classes.
*
* @internal
* @psalm-pure
*/
protected function newBigRational(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator) : BigRational
{
return new BigRational($numerator, $denominator, $checkDenominator);
}
/**
* Returns the minimum of the given values.
*
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
* to an instance of the class this method is called on.
*
* @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid.
*
* @psalm-suppress LessSpecificReturnStatement
* @psalm-suppress MoreSpecificReturnType
* @psalm-pure
*/
public static function min(BigNumber|int|float|string ...$values) : static
{
$min = null;
foreach ($values as $value) {
$value = static::of($value);
if ($min === null || $value->isLessThan($min)) {
$min = $value;
}
}
if ($min === null) {
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
}
return $min;
}
/**
* Returns the maximum of the given values.
*
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
* to an instance of the class this method is called on.
*
* @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid.
*
* @psalm-suppress LessSpecificReturnStatement
* @psalm-suppress MoreSpecificReturnType
* @psalm-pure
*/
public static function max(BigNumber|int|float|string ...$values) : static
{
$max = null;
foreach ($values as $value) {
$value = static::of($value);
if ($max === null || $value->isGreaterThan($max)) {
$max = $value;
}
}
if ($max === null) {
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
}
return $max;
}
/**
* Returns the sum of the given values.
*
* @param BigNumber|int|float|string ...$values The numbers to add. All the numbers need to be convertible
* to an instance of the class this method is called on.
*
* @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid.
*
* @psalm-pure
*/
public static function sum(BigNumber|int|float|string ...$values) : static
{
/** @var static|null $sum */
$sum = null;
foreach ($values as $value) {
$value = static::of($value);
$sum = $sum === null ? $value : self::add($sum, $value);
}
if ($sum === null) {
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
}
return $sum;
}
/**
* Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException.
*
* @todo This could be better resolved by creating an abstract protected method in BigNumber, and leaving to
* concrete classes the responsibility to perform the addition themselves or delegate it to the given number,
* depending on their ability to perform the operation. This will also require a version bump because we're
* potentially breaking custom BigNumber implementations (if any...)
*
* @psalm-pure
*/
private static function add(BigNumber $a, BigNumber $b) : BigNumber
{
if ($a instanceof BigRational) {
return $a->plus($b);
}
if ($b instanceof BigRational) {
return $b->plus($a);
}
if ($a instanceof BigDecimal) {
return $a->plus($b);
}
if ($b instanceof BigDecimal) {
return $b->plus($a);
}
/** @var BigInteger $a */
return $a->plus($b);
}
/**
* Removes optional leading zeros and + sign from the given number.
*
* @param string $number The number, validated as a non-empty string of digits with optional leading sign.
*
* @psalm-pure
*/
private static function cleanUp(string $number) : string
{
$firstChar = $number[0];
if ($firstChar === '+' || $firstChar === '-') {
$number = \substr($number, 1);
}
$number = \ltrim($number, '0');
if ($number === '') {
return '0';
}
if ($firstChar === '-') {
return '-' . $number;
}
return $number;
}
/**
* Checks if this number is equal to the given one.
*/
public function isEqualTo(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) === 0;
}
/**
* Checks if this number is strictly lower than the given one.
*/
public function isLessThan(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) < 0;
}
/**
* Checks if this number is lower than or equal to the given one.
*/
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.
*/
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.
*/
public function isGreaterThanOrEqualTo(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) >= 0;
}
/**
* Checks if this number equals zero.
*/
public function isZero() : bool
{
return $this->getSign() === 0;
}
/**
* Checks if this number is strictly negative.
*/
public function isNegative() : bool
{
return $this->getSign() < 0;
}
/**
* Checks if this number is negative or zero.
*/
public function isNegativeOrZero() : bool
{
return $this->getSign() <= 0;
}
/**
* Checks if this number is strictly positive.
*/
public function isPositive() : bool
{
return $this->getSign() > 0;
}
/**
* Checks if this number is positive or zero.
*/
public function isPositiveOrZero() : bool
{
return $this->getSign() >= 0;
}
/**
* Returns the sign of this number.
*
* @return int -1 if the number is negative, 0 if zero, 1 if positive.
*/
abstract public function getSign() : int;
/**
* Compares this number to the given one.
*
* @return int [-1,0,1] If `$this` is lower than, equal to, or greater than `$that`.
*
* @throws MathException If the number is not valid.
*/
abstract public function compareTo(BigNumber|int|float|string $that) : int;
/**
* Converts this number to a BigInteger.
*
* @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding.
*/
abstract public function toBigInteger() : BigInteger;
/**
* Converts this number to a BigDecimal.
*
* @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding.
*/
abstract public function toBigDecimal() : BigDecimal;
/**
* Converts this number to a BigRational.
*/
abstract public function toBigRational() : BigRational;
/**
* Converts this number to a BigDecimal with the given scale, using rounding if necessary.
*
* @param int $scale The scale of the resulting `BigDecimal`.
* @param int $roundingMode A `RoundingMode` constant.
*
* @throws RoundingNecessaryException If this number cannot be converted to the given scale without rounding.
* This only applies when RoundingMode::UNNECESSARY is used.
*/
abstract public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal;
/**
* Returns the exact value of this number as a native integer.
*
* If this number cannot be converted to a native integer without losing precision, an exception is thrown.
* Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit.
*
* @throws MathException If this number cannot be exactly converted to a native integer.
*/
abstract public function toInt() : int;
/**
* Returns an approximation of this number as a floating-point value.
*
* Note that this method can discard information as the precision of a floating-point value
* is inherently limited.
*
* If the number is greater than the largest representable floating point number, positive infinity is returned.
* If the number is less than the smallest representable floating point number, negative infinity is returned.
*/
abstract public function toFloat() : float;
/**
* Returns a string representation of this number.
*
* The output of this method can be parsed by the `of()` factory method;
* this will yield an object equal to this one, without any information loss.
*/
abstract public function __toString() : string;
public function jsonSerialize() : string
{
return $this->__toString();
}
}

445
vendor/brick/math/src/BigRational.php vendored Normal file
View File

@ -0,0 +1,445 @@
<?php
declare(strict_types=1);
namespace Brick\Math;
use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Exception\RoundingNecessaryException;
/**
* An arbitrarily large rational number.
*
* This class is immutable.
*
* @psalm-immutable
*/
final class BigRational extends BigNumber
{
/**
* The numerator.
*/
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.
*/
protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator)
{
if ($checkDenominator) {
if ($denominator->isZero()) {
throw DivisionByZeroException::denominatorMustNotBeZero();
}
if ($denominator->isNegative()) {
$numerator = $numerator->negated();
$denominator = $denominator->negated();
}
}
$this->numerator = $numerator;
$this->denominator = $denominator;
}
/**
* Creates a BigRational of the given value.
*
* @throws MathException If the value cannot be converted to a BigRational.
*
* @psalm-pure
*/
public static function of(BigNumber|int|float|string $value) : BigRational
{
return parent::of($value)->toBigRational();
}
/**
* Creates a BigRational out of a numerator and a denominator.
*
* If the denominator is negative, the signs of both the numerator and the denominator
* will be inverted to ensure that the denominator is always positive.
*
* @param BigNumber|int|float|string $numerator The numerator. Must be convertible to a BigInteger.
* @param BigNumber|int|float|string $denominator The denominator. Must be convertible to a BigInteger.
*
* @throws NumberFormatException If an argument does not represent a valid number.
* @throws RoundingNecessaryException If an argument represents a non-integer number.
* @throws DivisionByZeroException If the denominator is zero.
*
* @psalm-pure
*/
public static function nd(
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.
*
* @psalm-pure
*/
public static function zero() : BigRational
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigRational|null $zero
*/
static $zero;
if ($zero === null) {
$zero = new BigRational(BigInteger::zero(), BigInteger::one(), false);
}
return $zero;
}
/**
* Returns a BigRational representing one.
*
* @psalm-pure
*/
public static function one() : BigRational
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigRational|null $one
*/
static $one;
if ($one === null) {
$one = new BigRational(BigInteger::one(), BigInteger::one(), false);
}
return $one;
}
/**
* Returns a BigRational representing ten.
*
* @psalm-pure
*/
public static function ten() : BigRational
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigRational|null $ten
*/
static $ten;
if ($ten === null) {
$ten = new BigRational(BigInteger::ten(), BigInteger::one(), false);
}
return $ten;
}
public function getNumerator() : BigInteger
{
return $this->numerator;
}
public function getDenominator() : BigInteger
{
return $this->denominator;
}
/**
* Returns the quotient of the division of the numerator by the denominator.
*/
public function quotient() : BigInteger
{
return $this->numerator->quotient($this->denominator);
}
/**
* Returns the remainder of the division of the numerator by the denominator.
*/
public function remainder() : BigInteger
{
return $this->numerator->remainder($this->denominator);
}
/**
* Returns the quotient and remainder of the division of the numerator by the denominator.
*
* @return BigInteger[]
*/
public function quotientAndRemainder() : array
{
return $this->numerator->quotientAndRemainder($this->denominator);
}
/**
* Returns the sum of this number and the given one.
*
* @param BigNumber|int|float|string $that The number to add.
*
* @throws MathException If the number is not valid.
*/
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.
*/
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 a valid number.
*/
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 a valid number, or is zero.
*/
public function dividedBy(BigNumber|int|float|string $that) : BigRational
{
$that = BigRational::of($that);
$numerator = $this->numerator->multipliedBy($that->denominator);
$denominator = $this->denominator->multipliedBy($that->numerator);
return new BigRational($numerator, $denominator, true);
}
/**
* Returns this number exponentiated to the given value.
*
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
*/
public function power(int $exponent) : BigRational
{
if ($exponent === 0) {
$one = BigInteger::one();
return new BigRational($one, $one, false);
}
if ($exponent === 1) {
return $this;
}
return new BigRational(
$this->numerator->power($exponent),
$this->denominator->power($exponent),
false
);
}
/**
* Returns the reciprocal of this BigRational.
*
* The reciprocal has the numerator and denominator swapped.
*
* @throws DivisionByZeroException If the numerator is zero.
*/
public function reciprocal() : BigRational
{
return new BigRational($this->denominator, $this->numerator, true);
}
/**
* Returns the absolute value of this BigRational.
*/
public function abs() : BigRational
{
return new BigRational($this->numerator->abs(), $this->denominator, false);
}
/**
* Returns the negated value of this BigRational.
*/
public function negated() : BigRational
{
return new BigRational($this->numerator->negated(), $this->denominator, false);
}
/**
* Returns the simplified value of this BigRational.
*/
public function simplified() : BigRational
{
$gcd = $this->numerator->gcd($this->denominator);
$numerator = $this->numerator->quotient($gcd);
$denominator = $this->denominator->quotient($gcd);
return new BigRational($numerator, $denominator, false);
}
public function compareTo(BigNumber|int|float|string $that) : int
{
return $this->minus($that)->getSign();
}
public function getSign() : int
{
return $this->numerator->getSign();
}
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;
}
public function toBigDecimal() : BigDecimal
{
return $this->numerator->toBigDecimal()->exactlyDividedBy($this->denominator);
}
public function toBigRational() : BigRational
{
return $this;
}
public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
{
return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode);
}
public function toInt() : int
{
return $this->toBigInteger()->toInt();
}
public function toFloat() : float
{
$simplified = $this->simplified();
return $simplified->numerator->toFloat() / $simplified->denominator->toFloat();
}
public function __toString() : string
{
$numerator = (string) $this->numerator;
$denominator = (string) $this->denominator;
if ($denominator === '1') {
return $numerator;
}
return $this->numerator . '/' . $this->denominator;
}
/**
* This method is required for serializing the object and SHOULD NOT be accessed directly.
*
* @internal
*
* @return array{numerator: BigInteger, denominator: BigInteger}
*/
public function __serialize(): array
{
return ['numerator' => $this->numerator, 'denominator' => $this->denominator];
}
/**
* This method is only here to allow unserializing the object and cannot be accessed directly.
*
* @internal
* @psalm-suppress RedundantPropertyInitializationCheck
*
* @param array{numerator: BigInteger, denominator: BigInteger} $data
*
* @throws \LogicException
*/
public function __unserialize(array $data): void
{
if (isset($this->numerator)) {
throw new \LogicException('__unserialize() is an internal function, it must not be called directly.');
}
$this->numerator = $data['numerator'];
$this->denominator = $data['denominator'];
}
/**
* This method is required by interface Serializable and SHOULD NOT be accessed directly.
*
* @internal
*/
public function serialize() : string
{
return $this->numerator . '/' . $this->denominator;
}
/**
* This method is only here to implement interface Serializable and cannot be accessed directly.
*
* @internal
* @psalm-suppress RedundantPropertyInitializationCheck
*
* @throws \LogicException
*/
public function unserialize($value) : void
{
if (isset($this->numerator)) {
throw new \LogicException('unserialize() is an internal function, it must not be called directly.');
}
[$numerator, $denominator] = \explode('/', $value);
$this->numerator = BigInteger::of($numerator);
$this->denominator = BigInteger::of($denominator);
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Exception thrown when a division by zero occurs.
*/
class DivisionByZeroException extends MathException
{
/**
* @psalm-pure
*/
public static function divisionByZero() : DivisionByZeroException
{
return new self('Division by zero.');
}
/**
* @psalm-pure
*/
public static function modulusMustNotBeZero() : DivisionByZeroException
{
return new self('The modulus must not be zero.');
}
/**
* @psalm-pure
*/
public static function denominatorMustNotBeZero() : DivisionByZeroException
{
return new self('The denominator of a rational number cannot be zero.');
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
use Brick\Math\BigInteger;
/**
* Exception thrown when an integer overflow occurs.
*/
class IntegerOverflowException extends MathException
{
/**
* @psalm-pure
*/
public static function toIntOverflow(BigInteger $value) : IntegerOverflowException
{
$message = '%s is out of range %d to %d and cannot be represented as an integer.';
return new self(\sprintf($message, (string) $value, PHP_INT_MIN, PHP_INT_MAX));
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Base class for all math exceptions.
*/
class MathException extends \Exception
{
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Exception thrown when attempting to perform an unsupported operation, such as a square root, on a negative number.
*/
class NegativeNumberException extends MathException
{
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Exception thrown when attempting to create a number from a string with an invalid format.
*/
class NumberFormatException extends MathException
{
/**
* @param string $char The failing character.
*
* @psalm-pure
*/
public static function charNotInAlphabet(string $char) : self
{
$ord = \ord($char);
if ($ord < 32 || $ord > 126) {
$char = \strtoupper(\dechex($ord));
if ($ord < 10) {
$char = '0' . $char;
}
} else {
$char = '"' . $char . '"';
}
return new self(sprintf('Char %s is not a valid character in the given alphabet.', $char));
}
}

View File

@ -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.
*/
class RoundingNecessaryException extends MathException
{
/**
* @psalm-pure
*/
public static function roundingNecessary() : RoundingNecessaryException
{
return new self('Rounding is necessary to represent the result of the operation at this scale.');
}
}

View File

@ -0,0 +1,676 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal;
use Brick\Math\Exception\RoundingNecessaryException;
use Brick\Math\RoundingMode;
/**
* Performs basic operations on arbitrary size integers.
*
* Unless otherwise specified, all parameters must be validated as non-empty strings of digits,
* without leading zero, and with an optional leading minus sign if the number is not zero.
*
* Any other parameter format will lead to undefined behaviour.
* All methods must return strings respecting this format, unless specified otherwise.
*
* @internal
*
* @psalm-immutable
*/
abstract class Calculator
{
/**
* The maximum exponent value allowed for the pow() method.
*/
public const MAX_POWER = 1000000;
/**
* The alphabet for converting from and to base 2 to 36, lowercase.
*/
public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
/**
* The Calculator instance in use.
*/
private static ?Calculator $instance = null;
/**
* Sets the Calculator instance to use.
*
* An instance is typically set only in unit tests: the autodetect is usually the best option.
*
* @param Calculator|null $calculator The calculator instance, or NULL to revert to autodetect.
*/
final public static function set(?Calculator $calculator) : void
{
self::$instance = $calculator;
}
/**
* Returns the Calculator instance to use.
*
* If none has been explicitly set, the fastest available implementation will be returned.
*
* @psalm-pure
* @psalm-suppress ImpureStaticProperty
*/
final public static function get() : Calculator
{
if (self::$instance === null) {
/** @psalm-suppress ImpureMethodCall */
self::$instance = self::detect();
}
return self::$instance;
}
/**
* Returns the fastest available Calculator implementation.
*
* @codeCoverageIgnore
*/
private static function detect() : Calculator
{
if (\extension_loaded('gmp')) {
return new Calculator\GmpCalculator();
}
if (\extension_loaded('bcmath')) {
return new Calculator\BcMathCalculator();
}
return new Calculator\NativeCalculator();
}
/**
* Extracts the sign & digits of the operands.
*
* @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits.
*/
final protected function init(string $a, string $b) : array
{
return [
$aNeg = ($a[0] === '-'),
$bNeg = ($b[0] === '-'),
$aNeg ? \substr($a, 1) : $a,
$bNeg ? \substr($b, 1) : $b,
];
}
/**
* Returns the absolute value of a number.
*/
final public function abs(string $n) : string
{
return ($n[0] === '-') ? \substr($n, 1) : $n;
}
/**
* Negates a number.
*/
final public function neg(string $n) : string
{
if ($n === '0') {
return '0';
}
if ($n[0] === '-') {
return \substr($n, 1);
}
return '-' . $n;
}
/**
* Compares two numbers.
*
* @return int [-1, 0, 1] If the first number is less than, equal to, or greater than the second number.
*/
final public function cmp(string $a, string $b) : int
{
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
if ($aNeg && ! $bNeg) {
return -1;
}
if ($bNeg && ! $aNeg) {
return 1;
}
$aLen = \strlen($aDig);
$bLen = \strlen($bDig);
if ($aLen < $bLen) {
$result = -1;
} elseif ($aLen > $bLen) {
$result = 1;
} else {
$result = $aDig <=> $bDig;
}
return $aNeg ? -$result : $result;
}
/**
* Adds two numbers.
*/
abstract public function add(string $a, string $b) : string;
/**
* Subtracts two numbers.
*/
abstract public function sub(string $a, string $b) : string;
/**
* Multiplies two numbers.
*/
abstract public function mul(string $a, string $b) : string;
/**
* Returns the quotient of the division of two numbers.
*
* @param string $a The dividend.
* @param string $b The divisor, must not be zero.
*
* @return string The quotient.
*/
abstract public function divQ(string $a, string $b) : string;
/**
* Returns the remainder of the division of two numbers.
*
* @param string $a The dividend.
* @param string $b The divisor, must not be zero.
*
* @return string The remainder.
*/
abstract public function divR(string $a, string $b) : string;
/**
* Returns the quotient and remainder of the division of two numbers.
*
* @param string $a The dividend.
* @param string $b The divisor, must not be zero.
*
* @return array{string, string} An array containing the quotient and remainder.
*/
abstract public function divQR(string $a, string $b) : array;
/**
* Exponentiates a number.
*
* @param string $a The base number.
* @param int $e The exponent, validated as an integer between 0 and MAX_POWER.
*
* @return string The power.
*/
abstract public function pow(string $a, int $e) : string;
/**
* @param string $b The modulus; must not be zero.
*/
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.
*/
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; must be positive or zero.
* @param string $exp The exponent; must be positive or zero.
* @param string $mod The modulus; must be strictly positive.
*/
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.
*/
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));
}
/**
* @return array{string, string, string} GCD, X, Y
*/
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];
}
/**
* Returns the square root of the given number, rounded down.
*
* The result is the largest x such that n.
* The input MUST NOT be negative.
*/
abstract public function sqrt(string $n) : string;
/**
* Converts a number from an arbitrary base.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for base conversion.
*
* @param string $number The number, positive or zero, non-empty, case-insensitively validated for the given base.
* @param int $base The base of the number, validated from 2 to 36.
*
* @return string The converted number, following the Calculator conventions.
*/
public function fromBase(string $number, int $base) : string
{
return $this->fromArbitraryBase(\strtolower($number), self::ALPHABET, $base);
}
/**
* Converts a number to an arbitrary base.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for base conversion.
*
* @param string $number The number to convert, following the Calculator conventions.
* @param int $base The base to convert to, validated from 2 to 36.
*
* @return string The converted number, lowercase.
*/
public function toBase(string $number, int $base) : string
{
$negative = ($number[0] === '-');
if ($negative) {
$number = \substr($number, 1);
}
$number = $this->toArbitraryBase($number, self::ALPHABET, $base);
if ($negative) {
return '-' . $number;
}
return $number;
}
/**
* Converts a non-negative number in an arbitrary base using a custom alphabet, to base 10.
*
* @param string $number The number to convert, validated as a non-empty string,
* containing only chars in the given alphabet/base.
* @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
* @param int $base The base of the number, validated from 2 to alphabet length.
*
* @return string The number in base 10, following the Calculator conventions.
*/
final public function fromArbitraryBase(string $number, string $alphabet, int $base) : string
{
// remove leading "zeros"
$number = \ltrim($number, $alphabet[0]);
if ($number === '') {
return '0';
}
// optimize for "one"
if ($number === $alphabet[1]) {
return '1';
}
$result = '0';
$power = '1';
$base = (string) $base;
for ($i = \strlen($number) - 1; $i >= 0; $i--) {
$index = \strpos($alphabet, $number[$i]);
if ($index !== 0) {
$result = $this->add($result, ($index === 1)
? $power
: $this->mul($power, (string) $index)
);
}
if ($i !== 0) {
$power = $this->mul($power, $base);
}
}
return $result;
}
/**
* Converts a non-negative number to an arbitrary base using a custom alphabet.
*
* @param string $number The number to convert, positive or zero, following the Calculator conventions.
* @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
* @param int $base The base to convert to, validated from 2 to alphabet length.
*
* @return string The converted number in the given alphabet.
*/
final public function toArbitraryBase(string $number, string $alphabet, int $base) : string
{
if ($number === '0') {
return $alphabet[0];
}
$base = (string) $base;
$result = '';
while ($number !== '0') {
[$number, $remainder] = $this->divQR($number, $base);
$remainder = (int) $remainder;
$result .= $alphabet[$remainder];
}
return \strrev($result);
}
/**
* Performs a rounded division.
*
* Rounding is performed when the remainder of the division is not zero.
*
* @param string $a The dividend.
* @param string $b The divisor, must not be zero.
* @param int $roundingMode The rounding mode.
*
* @throws \InvalidArgumentException If the rounding mode is invalid.
* @throws RoundingNecessaryException If RoundingMode::UNNECESSARY is provided but rounding is necessary.
*
* @psalm-suppress ImpureFunctionCall
*/
final public function divRound(string $a, string $b, int $roundingMode) : string
{
[$quotient, $remainder] = $this->divQR($a, $b);
$hasDiscardedFraction = ($remainder !== '0');
$isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-');
$discardedFractionSign = function() use ($remainder, $b) : int {
$r = $this->abs($this->mul($remainder, '2'));
$b = $this->abs($b);
return $this->cmp($r, $b);
};
$increment = false;
switch ($roundingMode) {
case RoundingMode::UNNECESSARY:
if ($hasDiscardedFraction) {
throw RoundingNecessaryException::roundingNecessary();
}
break;
case RoundingMode::UP:
$increment = $hasDiscardedFraction;
break;
case RoundingMode::DOWN:
break;
case RoundingMode::CEILING:
$increment = $hasDiscardedFraction && $isPositiveOrZero;
break;
case RoundingMode::FLOOR:
$increment = $hasDiscardedFraction && ! $isPositiveOrZero;
break;
case RoundingMode::HALF_UP:
$increment = $discardedFractionSign() >= 0;
break;
case RoundingMode::HALF_DOWN:
$increment = $discardedFractionSign() > 0;
break;
case RoundingMode::HALF_CEILING:
$increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0;
break;
case RoundingMode::HALF_FLOOR:
$increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
break;
case RoundingMode::HALF_EVEN:
$lastDigit = (int) $quotient[-1];
$lastDigitIsEven = ($lastDigit % 2 === 0);
$increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
break;
default:
throw new \InvalidArgumentException('Invalid rounding mode.');
}
if ($increment) {
return $this->add($quotient, $isPositiveOrZero ? '1' : '-1');
}
return $quotient;
}
/**
* Calculates bitwise AND of two numbers.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for bitwise operations.
*/
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.
*/
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.
*/
public function xor(string $a, string $b) : string
{
return $this->bitwise('xor', $a, $b);
}
/**
* 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.
*/
private function bitwise(string $operator, string $a, string $b) : string
{
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
$aBin = $this->toBinary($aDig);
$bBin = $this->toBinary($bDig);
$aLen = \strlen($aBin);
$bLen = \strlen($bBin);
if ($aLen > $bLen) {
$bBin = \str_repeat("\x00", $aLen - $bLen) . $bBin;
} elseif ($bLen > $aLen) {
$aBin = \str_repeat("\x00", $bLen - $aLen) . $aBin;
}
if ($aNeg) {
$aBin = $this->twosComplement($aBin);
}
if ($bNeg) {
$bBin = $this->twosComplement($bBin);
}
switch ($operator) {
case 'and':
$value = $aBin & $bBin;
$negative = ($aNeg and $bNeg);
break;
case 'or':
$value = $aBin | $bBin;
$negative = ($aNeg or $bNeg);
break;
case 'xor':
$value = $aBin ^ $bBin;
$negative = ($aNeg xor $bNeg);
break;
// @codeCoverageIgnoreStart
default:
throw new \InvalidArgumentException('Invalid bitwise operator.');
// @codeCoverageIgnoreEnd
}
if ($negative) {
$value = $this->twosComplement($value);
}
$result = $this->toDecimal($value);
return $negative ? $this->neg($result) : $result;
}
/**
* @param string $number A positive, binary number.
*/
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.
*/
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.
*/
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;
}
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator;
/**
* Calculator implementation built around the bcmath library.
*
* @internal
*
* @psalm-immutable
*/
class BcMathCalculator extends Calculator
{
public function add(string $a, string $b) : string
{
return \bcadd($a, $b, 0);
}
public function sub(string $a, string $b) : string
{
return \bcsub($a, $b, 0);
}
public function mul(string $a, string $b) : string
{
return \bcmul($a, $b, 0);
}
public function divQ(string $a, string $b) : string
{
return \bcdiv($a, $b, 0);
}
/**
* @psalm-suppress InvalidNullableReturnType
* @psalm-suppress NullableReturnStatement
*/
public function divR(string $a, string $b) : string
{
return \bcmod($a, $b, 0);
}
public function divQR(string $a, string $b) : array
{
$q = \bcdiv($a, $b, 0);
$r = \bcmod($a, $b, 0);
assert($r !== null);
return [$q, $r];
}
public function pow(string $a, int $e) : string
{
return \bcpow($a, (string) $e, 0);
}
public function modPow(string $base, string $exp, string $mod) : string
{
return \bcpowmod($base, $exp, $mod, 0);
}
/**
* @psalm-suppress InvalidNullableReturnType
* @psalm-suppress NullableReturnStatement
*/
public function sqrt(string $n) : string
{
return \bcsqrt($n, 0);
}
}

View File

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator;
/**
* Calculator implementation built around the GMP library.
*
* @internal
*
* @psalm-immutable
*/
class GmpCalculator extends Calculator
{
public function add(string $a, string $b) : string
{
return \gmp_strval(\gmp_add($a, $b));
}
public function sub(string $a, string $b) : string
{
return \gmp_strval(\gmp_sub($a, $b));
}
public function mul(string $a, string $b) : string
{
return \gmp_strval(\gmp_mul($a, $b));
}
public function divQ(string $a, string $b) : string
{
return \gmp_strval(\gmp_div_q($a, $b));
}
public function divR(string $a, string $b) : string
{
return \gmp_strval(\gmp_div_r($a, $b));
}
public function divQR(string $a, string $b) : array
{
[$q, $r] = \gmp_div_qr($a, $b);
return [
\gmp_strval($q),
\gmp_strval($r)
];
}
public function pow(string $a, int $e) : string
{
return \gmp_strval(\gmp_pow($a, $e));
}
public function modInverse(string $x, string $m) : ?string
{
$result = \gmp_invert($x, $m);
if ($result === false) {
return null;
}
return \gmp_strval($result);
}
public function modPow(string $base, string $exp, string $mod) : string
{
return \gmp_strval(\gmp_powm($base, $exp, $mod));
}
public function gcd(string $a, string $b) : string
{
return \gmp_strval(\gmp_gcd($a, $b));
}
public function fromBase(string $number, int $base) : string
{
return \gmp_strval(\gmp_init($number, $base));
}
public function toBase(string $number, int $base) : string
{
return \gmp_strval($number, $base);
}
public function and(string $a, string $b) : string
{
return \gmp_strval(\gmp_and($a, $b));
}
public function or(string $a, string $b) : string
{
return \gmp_strval(\gmp_or($a, $b));
}
public function xor(string $a, string $b) : string
{
return \gmp_strval(\gmp_xor($a, $b));
}
public function sqrt(string $n) : string
{
return \gmp_strval(\gmp_sqrt($n));
}
}

View File

@ -0,0 +1,581 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator;
/**
* Calculator implementation using only native PHP code.
*
* @internal
*
* @psalm-immutable
*/
class NativeCalculator extends Calculator
{
/**
* The max number of digits the platform can natively add, subtract, multiply or divide without overflow.
* For multiplication, this represents the max sum of the lengths of both operands.
*
* 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;
/**
* @codeCoverageIgnore
*/
public function __construct()
{
switch (PHP_INT_SIZE) {
case 4:
$this->maxDigits = 9;
break;
case 8:
$this->maxDigits = 18;
break;
default:
throw new \RuntimeException('The platform is not 32-bit or 64-bit as expected.');
}
}
public function add(string $a, string $b) : string
{
/**
* @psalm-var numeric-string $a
* @psalm-var numeric-string $b
*/
$result = $a + $b;
if (is_int($result)) {
return (string) $result;
}
if ($a === '0') {
return $b;
}
if ($b === '0') {
return $a;
}
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
$result = $aNeg === $bNeg ? $this->doAdd($aDig, $bDig) : $this->doSub($aDig, $bDig);
if ($aNeg) {
$result = $this->neg($result);
}
return $result;
}
public function sub(string $a, string $b) : string
{
return $this->add($a, $this->neg($b));
}
public function mul(string $a, string $b) : string
{
/**
* @psalm-var numeric-string $a
* @psalm-var numeric-string $b
*/
$result = $a * $b;
if (is_int($result)) {
return (string) $result;
}
if ($a === '0' || $b === '0') {
return '0';
}
if ($a === '1') {
return $b;
}
if ($b === '1') {
return $a;
}
if ($a === '-1') {
return $this->neg($b);
}
if ($b === '-1') {
return $this->neg($a);
}
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
$result = $this->doMul($aDig, $bDig);
if ($aNeg !== $bNeg) {
$result = $this->neg($result);
}
return $result;
}
public function divQ(string $a, string $b) : string
{
return $this->divQR($a, $b)[0];
}
public function divR(string $a, string $b): string
{
return $this->divQR($a, $b)[1];
}
public function divQR(string $a, string $b) : array
{
if ($a === '0') {
return ['0', '0'];
}
if ($a === $b) {
return ['1', '0'];
}
if ($b === '1') {
return [$a, '0'];
}
if ($b === '-1') {
return [$this->neg($a), '0'];
}
/** @psalm-var numeric-string $a */
$na = $a * 1; // cast to number
if (is_int($na)) {
/** @psalm-var numeric-string $b */
$nb = $b * 1;
if (is_int($nb)) {
// the only division that may overflow is PHP_INT_MIN / -1,
// which cannot happen here as we've already handled a divisor of -1 above.
$r = $na % $nb;
$q = ($na - $r) / $nb;
assert(is_int($q));
return [
(string) $q,
(string) $r
];
}
}
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
[$q, $r] = $this->doDiv($aDig, $bDig);
if ($aNeg !== $bNeg) {
$q = $this->neg($q);
}
if ($aNeg) {
$r = $this->neg($r);
}
return [$q, $r];
}
public function pow(string $a, int $e) : string
{
if ($e === 0) {
return '1';
}
if ($e === 1) {
return $a;
}
$odd = $e % 2;
$e -= $odd;
$aa = $this->mul($a, $a);
/** @psalm-suppress PossiblyInvalidArgument We're sure that $e / 2 is an int now */
$result = $this->pow($aa, $e / 2);
if ($odd === 1) {
$result = $this->mul($result, $a);
}
return $result;
}
/**
* Algorithm from: https://www.geeksforgeeks.org/modular-exponentiation-power-in-modular-arithmetic/
*/
public function modPow(string $base, string $exp, string $mod) : string
{
// special case: the algorithm below fails with 0 power 0 mod 1 (returns 1 instead of 0)
if ($base === '0' && $exp === '0' && $mod === '1') {
return '0';
}
// special case: the algorithm below fails with power 0 mod 1 (returns 1 instead of 0)
if ($exp === '0' && $mod === '1') {
return '0';
}
$x = $base;
$res = '1';
// numbers are positive, so we can use remainder instead of modulo
$x = $this->divR($x, $mod);
while ($exp !== '0') {
if (in_array($exp[-1], ['1', '3', '5', '7', '9'])) { // odd
$res = $this->divR($this->mul($res, $x), $mod);
}
$exp = $this->divQ($exp, '2');
$x = $this->divR($this->mul($x, $x), $mod);
}
return $res;
}
/**
* Adapted from https://cp-algorithms.com/num_methods/roots_newton.html
*/
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.
*/
private function doAdd(string $a, string $b) : string
{
[$a, $b, $length] = $this->pad($a, $b);
$carry = 0;
$result = '';
for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) {
$blockLength = $this->maxDigits;
if ($i < 0) {
$blockLength += $i;
/** @psalm-suppress LoopInvalidation */
$i = 0;
}
/** @psalm-var numeric-string $blockA */
$blockA = \substr($a, $i, $blockLength);
/** @psalm-var numeric-string $blockB */
$blockB = \substr($b, $i, $blockLength);
$sum = (string) ($blockA + $blockB + $carry);
$sumLength = \strlen($sum);
if ($sumLength > $blockLength) {
$sum = \substr($sum, 1);
$carry = 1;
} else {
if ($sumLength < $blockLength) {
$sum = \str_repeat('0', $blockLength - $sumLength) . $sum;
}
$carry = 0;
}
$result = $sum . $result;
if ($i === 0) {
break;
}
}
if ($carry === 1) {
$result = '1' . $result;
}
return $result;
}
/**
* Performs the subtraction of two non-signed large integers.
*/
private function doSub(string $a, string $b) : string
{
if ($a === $b) {
return '0';
}
// Ensure that we always subtract to a positive result: biggest minus smallest.
$cmp = $this->doCmp($a, $b);
$invert = ($cmp === -1);
if ($invert) {
$c = $a;
$a = $b;
$b = $c;
}
[$a, $b, $length] = $this->pad($a, $b);
$carry = 0;
$result = '';
$complement = 10 ** $this->maxDigits;
for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) {
$blockLength = $this->maxDigits;
if ($i < 0) {
$blockLength += $i;
/** @psalm-suppress LoopInvalidation */
$i = 0;
}
/** @psalm-var numeric-string $blockA */
$blockA = \substr($a, $i, $blockLength);
/** @psalm-var numeric-string $blockB */
$blockB = \substr($b, $i, $blockLength);
$sum = $blockA - $blockB - $carry;
if ($sum < 0) {
$sum += $complement;
$carry = 1;
} else {
$carry = 0;
}
$sum = (string) $sum;
$sumLength = \strlen($sum);
if ($sumLength < $blockLength) {
$sum = \str_repeat('0', $blockLength - $sumLength) . $sum;
}
$result = $sum . $result;
if ($i === 0) {
break;
}
}
// Carry cannot be 1 when the loop ends, as a > b
assert($carry === 0);
$result = \ltrim($result, '0');
if ($invert) {
$result = $this->neg($result);
}
return $result;
}
/**
* Performs the multiplication of two non-signed large integers.
*/
private function doMul(string $a, string $b) : string
{
$x = \strlen($a);
$y = \strlen($b);
$maxDigits = \intdiv($this->maxDigits, 2);
$complement = 10 ** $maxDigits;
$result = '0';
for ($i = $x - $maxDigits;; $i -= $maxDigits) {
$blockALength = $maxDigits;
if ($i < 0) {
$blockALength += $i;
/** @psalm-suppress LoopInvalidation */
$i = 0;
}
$blockA = (int) \substr($a, $i, $blockALength);
$line = '';
$carry = 0;
for ($j = $y - $maxDigits;; $j -= $maxDigits) {
$blockBLength = $maxDigits;
if ($j < 0) {
$blockBLength += $j;
/** @psalm-suppress LoopInvalidation */
$j = 0;
}
$blockB = (int) \substr($b, $j, $blockBLength);
$mul = $blockA * $blockB + $carry;
$value = $mul % $complement;
$carry = ($mul - $value) / $complement;
$value = (string) $value;
$value = \str_pad($value, $maxDigits, '0', STR_PAD_LEFT);
$line = $value . $line;
if ($j === 0) {
break;
}
}
if ($carry !== 0) {
$line = $carry . $line;
}
$line = \ltrim($line, '0');
if ($line !== '') {
$line .= \str_repeat('0', $x - $blockALength - $i);
$result = $this->add($result, $line);
}
if ($i === 0) {
break;
}
}
return $result;
}
/**
* Performs the division of two non-signed large integers.
*
* @return string[] The quotient and remainder.
*/
private function doDiv(string $a, string $b) : array
{
$cmp = $this->doCmp($a, $b);
if ($cmp === -1) {
return ['0', $a];
}
$x = \strlen($a);
$y = \strlen($b);
// we now know that a >= b && x >= y
$q = '0'; // quotient
$r = $a; // remainder
$z = $y; // focus length, always $y or $y+1
for (;;) {
$focus = \substr($a, 0, $z);
$cmp = $this->doCmp($focus, $b);
if ($cmp === -1) {
if ($z === $x) { // remainder < dividend
break;
}
$z++;
}
$zeros = \str_repeat('0', $x - $z);
$q = $this->add($q, '1' . $zeros);
$a = $this->sub($a, $b . $zeros);
$r = $a;
if ($r === '0') { // remainder == 0
break;
}
$x = \strlen($a);
if ($x < $y) { // remainder < dividend
break;
}
$z = $y;
}
return [$q, $r];
}
/**
* Compares two non-signed large numbers.
*
* @return int [-1, 0, 1]
*/
private function doCmp(string $a, string $b) : int
{
$x = \strlen($a);
$y = \strlen($b);
$cmp = $x <=> $y;
if ($cmp !== 0) {
return $cmp;
}
return \strcmp($a, $b) <=> 0; // enforce [-1, 0, 1]
}
/**
* Pads the left of one of the given numbers with zeros if necessary to make both numbers the same length.
*
* The numbers must only consist of digits, without leading minus sign.
*
* @return array{string, string, int}
*/
private function pad(string $a, string $b) : array
{
$x = \strlen($a);
$y = \strlen($b);
if ($x > $y) {
$b = \str_repeat('0', $x - $y) . $b;
return [$a, $b, $x];
}
if ($x < $y) {
$a = \str_repeat('0', $y - $x) . $a;
return [$a, $b, $y];
}
return [$a, $b, $x];
}
}

107
vendor/brick/math/src/RoundingMode.php vendored Normal file
View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Brick\Math;
/**
* Specifies a rounding behavior for numerical operations capable of discarding precision.
*
* Each rounding mode indicates how the least significant returned digit of a rounded result
* is to be calculated. If fewer digits are returned than the digits needed to represent the
* exact numerical result, the discarded digits will be referred to as the discarded fraction
* regardless the digits' contribution to the value of the number. In other words, considered
* as a numerical value, the discarded fraction could have an absolute value greater than one.
*/
final class RoundingMode
{
/**
* Private constructor. This class is not instantiable.
*
* @codeCoverageIgnore
*/
private function __construct()
{
}
/**
* Asserts that the requested operation has an exact result, hence no rounding is necessary.
*
* If this rounding mode is specified on an operation that yields a result that
* cannot be represented at the requested scale, a RoundingNecessaryException is thrown.
*/
public const UNNECESSARY = 0;
/**
* Rounds away from zero.
*
* Always increments the digit prior to a nonzero discarded fraction.
* Note that this rounding mode never decreases the magnitude of the calculated value.
*/
public const UP = 1;
/**
* Rounds towards zero.
*
* Never increments the digit prior to a discarded fraction (i.e., truncates).
* Note that this rounding mode never increases the magnitude of the calculated value.
*/
public const DOWN = 2;
/**
* Rounds towards positive infinity.
*
* If the result is positive, behaves as for UP; if negative, behaves as for DOWN.
* Note that this rounding mode never decreases the calculated value.
*/
public const CEILING = 3;
/**
* Rounds towards negative infinity.
*
* If the result is positive, behave as for DOWN; if negative, behave as for UP.
* Note that this rounding mode never increases the calculated value.
*/
public const FLOOR = 4;
/**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round up.
*
* Behaves as for UP if the discarded fraction is >= 0.5; otherwise, behaves as for DOWN.
* Note that this is the rounding mode commonly taught at school.
*/
public const HALF_UP = 5;
/**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round down.
*
* Behaves as for UP if the discarded fraction is > 0.5; otherwise, behaves as for DOWN.
*/
public const HALF_DOWN = 6;
/**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards positive infinity.
*
* If the result is positive, behaves as for HALF_UP; if negative, behaves as for HALF_DOWN.
*/
public const HALF_CEILING = 7;
/**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards negative infinity.
*
* If the result is positive, behaves as for HALF_DOWN; if negative, behaves as for HALF_UP.
*/
public const HALF_FLOOR = 8;
/**
* Rounds towards the "nearest neighbor" unless both neighbors are equidistant, in which case rounds towards the even neighbor.
*
* Behaves as for HALF_UP if the digit to the left of the discarded fraction is odd;
* behaves as for HALF_DOWN if it's even.
*
* Note that this is the rounding mode that statistically minimizes
* cumulative error when applied repeatedly over a sequence of calculations.
* It is sometimes known as "Banker's rounding", and is chiefly used in the USA.
*/
public const HALF_EVEN = 9;
}

269
vendor/cakephp/core/App.php vendored Normal file
View File

@ -0,0 +1,269 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 1.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
/**
* App is responsible for resource location, and path management.
*
* ### Adding paths
*
* Additional paths for Templates and Plugins are configured with Configure now. See config/app.php for an
* example. The `App.paths.plugins` and `App.paths.templates` variables are used to configure paths for plugins
* and templates respectively. All class based resources should be mapped using your application's autoloader.
*
* ### Inspecting loaded paths
*
* You can inspect the currently loaded paths using `App::classPath('Controller')` for example to see loaded
* controller paths.
*
* It is also possible to inspect paths for plugin classes, for instance, to get
* the path to a plugin's helpers you would call `App::classPath('View/Helper', 'MyPlugin')`
*
* ### Locating plugins
*
* Plugins can be located with App as well. Using Plugin::path('DebugKit') for example, will
* give you the full path to the DebugKit plugin.
*
* @link https://book.cakephp.org/4/en/core-libraries/app.html
*/
class App
{
/**
* Return the class name namespaced. This method checks if the class is defined on the
* application/plugin, otherwise try to load from the CakePHP core
*
* @param string $class Class name
* @param string $type Type of class
* @param string $suffix Class name suffix
* @return string|null Namespaced class name, null if the class is not found.
* @psalm-return class-string|null
*/
public static function className(string $class, string $type = '', string $suffix = ''): ?string
{
if (strpos($class, '\\') !== false) {
return class_exists($class) ? $class : null;
}
[$plugin, $name] = pluginSplit($class);
$fullname = '\\' . str_replace('/', '\\', $type . '\\' . $name) . $suffix;
$base = $plugin ?: Configure::read('App.namespace');
if ($base !== null) {
$base = str_replace('/', '\\', rtrim($base, '\\'));
if (static::_classExistsInBase($fullname, $base)) {
/** @var class-string */
return $base . $fullname;
}
}
if ($plugin || !static::_classExistsInBase($fullname, 'Cake')) {
return null;
}
/** @var class-string */
return 'Cake' . $fullname;
}
/**
* Returns the plugin split name of a class
*
* Examples:
*
* ```
* App::shortName(
* 'SomeVendor\SomePlugin\Controller\Component\TestComponent',
* 'Controller/Component',
* 'Component'
* )
* ```
*
* Returns: SomeVendor/SomePlugin.Test
*
* ```
* App::shortName(
* 'SomeVendor\SomePlugin\Controller\Component\Subfolder\TestComponent',
* 'Controller/Component',
* 'Component'
* )
* ```
*
* Returns: SomeVendor/SomePlugin.Subfolder/Test
*
* ```
* App::shortName(
* 'Cake\Controller\Component\AuthComponent',
* 'Controller/Component',
* 'Component'
* )
* ```
*
* Returns: Auth
*
* @param string $class Class name
* @param string $type Type of class
* @param string $suffix Class name suffix
* @return string Plugin split name of class
*/
public static function shortName(string $class, string $type, string $suffix = ''): string
{
$class = str_replace('\\', '/', $class);
$type = '/' . $type . '/';
$pos = strrpos($class, $type);
if ($pos === false) {
return $class;
}
$pluginName = (string)substr($class, 0, $pos);
$name = (string)substr($class, $pos + strlen($type));
if ($suffix) {
$name = (string)substr($name, 0, -strlen($suffix));
}
$nonPluginNamespaces = [
'Cake',
str_replace('\\', '/', (string)Configure::read('App.namespace')),
];
if (in_array($pluginName, $nonPluginNamespaces, true)) {
return $name;
}
return $pluginName . '.' . $name;
}
/**
* _classExistsInBase
*
* Test isolation wrapper
*
* @param string $name Class name.
* @param string $namespace Namespace.
* @return bool
*/
protected static function _classExistsInBase(string $name, string $namespace): bool
{
return class_exists($namespace . $name);
}
/**
* Used to read information stored path.
*
* The 1st character of $type argument should be lower cased and will return the
* value of `App.paths.$type` config.
*
* Default types:
* - plugins
* - templates
* - locales
*
* Example:
*
* ```
* App::path('plugins');
* ```
*
* Will return the value of `App.paths.plugins` config.
*
* Deprecated: 4.0 App::path() is deprecated for class path (inside src/ directory).
* Use \Cake\Core\App::classPath() instead or directly the method on \Cake\Core\Plugin class.
*
* @param string $type Type of path
* @param string|null $plugin Plugin name
* @return array<string>
* @link https://book.cakephp.org/4/en/core-libraries/app.html#finding-paths-to-namespaces
*/
public static function path(string $type, ?string $plugin = null): array
{
if ($plugin === null && $type[0] === strtolower($type[0])) {
return (array)Configure::read('App.paths.' . $type);
}
if ($type === 'templates') {
/** @psalm-suppress PossiblyNullArgument */
return [Plugin::templatePath($plugin)];
}
if ($type === 'locales') {
/** @psalm-suppress PossiblyNullArgument */
return [Plugin::path($plugin) . 'resources' . DIRECTORY_SEPARATOR . 'locales' . DIRECTORY_SEPARATOR];
}
deprecationWarning(
'App::path() is deprecated for class path.'
. ' Use \Cake\Core\App::classPath() or \Cake\Core\Plugin::classPath() instead.'
);
return static::classPath($type, $plugin);
}
/**
* Gets the path to a class type in the application or a plugin.
*
* Example:
*
* ```
* App::classPath('Model/Table');
* ```
*
* Will return the path for tables - e.g. `src/Model/Table/`.
*
* ```
* App::classPath('Model/Table', 'My/Plugin');
* ```
*
* Will return the plugin based path for those.
*
* @param string $type Package type.
* @param string|null $plugin Plugin name.
* @return array<string>
*/
public static function classPath(string $type, ?string $plugin = null): array
{
if ($plugin !== null) {
return [
Plugin::classPath($plugin) . $type . DIRECTORY_SEPARATOR,
];
}
return [APP . $type . DIRECTORY_SEPARATOR];
}
/**
* Returns the full path to a package inside the CakePHP core
*
* Usage:
*
* ```
* App::core('Cache/Engine');
* ```
*
* Will return the full path to the cache engines package.
*
* @param string $type Package type.
* @return array<string> Full path to package
*/
public static function core(string $type): array
{
if ($type === 'templates') {
return [CORE_PATH . 'templates' . DIRECTORY_SEPARATOR];
}
return [CAKE . str_replace('/', DIRECTORY_SEPARATOR, $type) . DIRECTORY_SEPARATOR];
}
}

305
vendor/cakephp/core/BasePlugin.php vendored Normal file
View File

@ -0,0 +1,305 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Console\CommandCollection;
use Cake\Http\MiddlewareQueue;
use Cake\Routing\RouteBuilder;
use Closure;
use InvalidArgumentException;
use ReflectionClass;
/**
* Base Plugin Class
*
* Every plugin should extend from this class or implement the interfaces and
* include a plugin class in its src root folder.
*/
class BasePlugin implements PluginInterface
{
/**
* Do bootstrapping or not
*
* @var bool
*/
protected $bootstrapEnabled = true;
/**
* Console middleware
*
* @var bool
*/
protected $consoleEnabled = true;
/**
* Enable middleware
*
* @var bool
*/
protected $middlewareEnabled = true;
/**
* Register container services
*
* @var bool
*/
protected $servicesEnabled = true;
/**
* Load routes or not
*
* @var bool
*/
protected $routesEnabled = true;
/**
* The path to this plugin.
*
* @var string
*/
protected $path;
/**
* The class path for this plugin.
*
* @var string
*/
protected $classPath;
/**
* The config path for this plugin.
*
* @var string
*/
protected $configPath;
/**
* The templates path for this plugin.
*
* @var string
*/
protected $templatePath;
/**
* The name of this plugin
*
* @var string
*/
protected $name;
/**
* Constructor
*
* @param array<string, mixed> $options Options
*/
public function __construct(array $options = [])
{
foreach (static::VALID_HOOKS as $key) {
if (isset($options[$key])) {
$this->{"{$key}Enabled"} = (bool)$options[$key];
}
}
foreach (['name', 'path', 'classPath', 'configPath', 'templatePath'] as $path) {
if (isset($options[$path])) {
$this->{$path} = $options[$path];
}
}
$this->initialize();
}
/**
* Initialization hook called from constructor.
*
* @return void
*/
public function initialize(): void
{
}
/**
* @inheritDoc
*/
public function getName(): string
{
if ($this->name) {
return $this->name;
}
$parts = explode('\\', static::class);
array_pop($parts);
$this->name = implode('/', $parts);
return $this->name;
}
/**
* @inheritDoc
*/
public function getPath(): string
{
if ($this->path) {
return $this->path;
}
$reflection = new ReflectionClass($this);
$path = dirname($reflection->getFileName());
// Trim off src
if (substr($path, -3) === 'src') {
$path = substr($path, 0, -3);
}
$this->path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
return $this->path;
}
/**
* @inheritDoc
*/
public function getConfigPath(): string
{
if ($this->configPath) {
return $this->configPath;
}
$path = $this->getPath();
return $path . 'config' . DIRECTORY_SEPARATOR;
}
/**
* @inheritDoc
*/
public function getClassPath(): string
{
if ($this->classPath) {
return $this->classPath;
}
$path = $this->getPath();
return $path . 'src' . DIRECTORY_SEPARATOR;
}
/**
* @inheritDoc
*/
public function getTemplatePath(): string
{
if ($this->templatePath) {
return $this->templatePath;
}
$path = $this->getPath();
return $this->templatePath = $path . 'templates' . DIRECTORY_SEPARATOR;
}
/**
* @inheritDoc
*/
public function enable(string $hook)
{
$this->checkHook($hook);
$this->{"{$hook}Enabled"} = true;
return $this;
}
/**
* @inheritDoc
*/
public function disable(string $hook)
{
$this->checkHook($hook);
$this->{"{$hook}Enabled"} = false;
return $this;
}
/**
* @inheritDoc
*/
public function isEnabled(string $hook): bool
{
$this->checkHook($hook);
return $this->{"{$hook}Enabled"} === true;
}
/**
* Check if a hook name is valid
*
* @param string $hook The hook name to check
* @throws \InvalidArgumentException on invalid hooks
* @return void
*/
protected function checkHook(string $hook): void
{
if (!in_array($hook, static::VALID_HOOKS, true)) {
throw new InvalidArgumentException(
"`$hook` is not a valid hook name. Must be one of " . implode(', ', static::VALID_HOOKS)
);
}
}
/**
* @inheritDoc
*/
public function routes(RouteBuilder $routes): void
{
$path = $this->getConfigPath() . 'routes.php';
if (is_file($path)) {
$return = require $path;
if ($return instanceof Closure) {
$return($routes);
}
}
}
/**
* @inheritDoc
*/
public function bootstrap(PluginApplicationInterface $app): void
{
$bootstrap = $this->getConfigPath() . 'bootstrap.php';
if (is_file($bootstrap)) {
require $bootstrap;
}
}
/**
* @inheritDoc
*/
public function console(CommandCollection $commands): CommandCollection
{
return $commands->addMany($commands->discoverPlugin($this->getName()));
}
/**
* @inheritDoc
*/
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
return $middlewareQueue;
}
/**
* Register container services for this plugin.
*
* @param \Cake\Core\ContainerInterface $container The container to add services to.
* @return void
*/
public function services(ContainerInterface $container): void
{
}
}

139
vendor/cakephp/core/ClassLoader.php vendored Normal file
View File

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
/**
* ClassLoader
*
* @deprecated 4.0.3 Use composer to generate autoload files instead.
*/
class ClassLoader
{
/**
* An associative array where the key is a namespace prefix and the value
* is an array of base directories for classes in that namespace.
*
* @var array<string, array>
*/
protected $_prefixes = [];
/**
* Register loader with SPL autoloader stack.
*
* @return void
*/
public function register(): void
{
/** @var callable $callable */
$callable = [$this, 'loadClass'];
spl_autoload_register($callable);
}
/**
* Adds a base directory for a namespace prefix.
*
* @param string $prefix The namespace prefix.
* @param string $baseDir A base directory for class files in the
* namespace.
* @param bool $prepend If true, prepend the base directory to the stack
* instead of appending it; this causes it to be searched first rather
* than last.
* @return void
*/
public function addNamespace(string $prefix, string $baseDir, bool $prepend = false): void
{
$prefix = trim($prefix, '\\') . '\\';
$baseDir = rtrim($baseDir, '/') . DIRECTORY_SEPARATOR;
$baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . '/';
$this->_prefixes[$prefix] = $this->_prefixes[$prefix] ?? [];
if ($prepend) {
array_unshift($this->_prefixes[$prefix], $baseDir);
} else {
$this->_prefixes[$prefix][] = $baseDir;
}
}
/**
* Loads the class file for a given class name.
*
* @param string $class The fully-qualified class name.
* @return string|false The mapped file name on success, or boolean false on
* failure.
*/
public function loadClass(string $class)
{
$prefix = $class;
while (($pos = strrpos($prefix, '\\')) !== false) {
$prefix = substr($class, 0, $pos + 1);
$relativeClass = substr($class, $pos + 1);
$mappedFile = $this->_loadMappedFile($prefix, $relativeClass);
if ($mappedFile) {
return $mappedFile;
}
$prefix = rtrim($prefix, '\\');
}
return false;
}
/**
* Load the mapped file for a namespace prefix and relative class.
*
* @param string $prefix The namespace prefix.
* @param string $relativeClass The relative class name.
* @return string|false Boolean false if no mapped file can be loaded, or the
* name of the mapped file that was loaded.
*/
protected function _loadMappedFile(string $prefix, string $relativeClass)
{
if (!isset($this->_prefixes[$prefix])) {
return false;
}
foreach ($this->_prefixes[$prefix] as $baseDir) {
$file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php';
if ($this->_requireFile($file)) {
return $file;
}
}
return false;
}
/**
* If a file exists, require it from the file system.
*
* @param string $file The file to require.
* @return bool True if the file exists, false if not.
*/
protected function _requireFile(string $file): bool
{
if (file_exists($file)) {
require $file;
return true;
}
return false;
}
}

498
vendor/cakephp/core/Configure.php vendored Normal file
View File

@ -0,0 +1,498 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 1.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Cache\Cache;
use Cake\Core\Configure\ConfigEngineInterface;
use Cake\Core\Configure\Engine\PhpConfig;
use Cake\Core\Exception\CakeException;
use Cake\Utility\Hash;
use RuntimeException;
/**
* Configuration class. Used for managing runtime configuration information.
*
* Provides features for reading and writing to the runtime configuration, as well
* as methods for loading additional configuration files or storing runtime configuration
* for future use.
*
* @link https://book.cakephp.org/4/en/development/configuration.html
*/
class Configure
{
/**
* Array of values currently stored in Configure.
*
* @var array<string, mixed>
*/
protected static $_values = [
'debug' => false,
];
/**
* Configured engine classes, used to load config files from resources
*
* @see \Cake\Core\Configure::load()
* @var array<\Cake\Core\Configure\ConfigEngineInterface>
*/
protected static $_engines = [];
/**
* Flag to track whether ini_set exists.
*
* @var bool|null
*/
protected static $_hasIniSet;
/**
* Used to store a dynamic variable in Configure.
*
* Usage:
* ```
* Configure::write('One.key1', 'value of the Configure::One[key1]');
* Configure::write(['One.key1' => 'value of the Configure::One[key1]']);
* Configure::write('One', [
* 'key1' => 'value of the Configure::One[key1]',
* 'key2' => 'value of the Configure::One[key2]'
* ]);
*
* Configure::write([
* 'One.key1' => 'value of the Configure::One[key1]',
* 'One.key2' => 'value of the Configure::One[key2]'
* ]);
* ```
*
* @param array<string, mixed>|string $config The key to write, can be a dot notation value.
* Alternatively can be an array containing key(s) and value(s).
* @param mixed $value Value to set for the given key.
* @return void
* @link https://book.cakephp.org/4/en/development/configuration.html#writing-configuration-data
*/
public static function write($config, $value = null): void
{
if (!is_array($config)) {
$config = [$config => $value];
}
foreach ($config as $name => $valueToInsert) {
static::$_values = Hash::insert(static::$_values, $name, $valueToInsert);
}
if (isset($config['debug'])) {
if (static::$_hasIniSet === null) {
static::$_hasIniSet = function_exists('ini_set');
}
if (static::$_hasIniSet) {
ini_set('display_errors', $config['debug'] ? '1' : '0');
}
}
}
/**
* Used to read information stored in Configure. It's not
* possible to store `null` values in Configure.
*
* Usage:
* ```
* Configure::read('Name'); will return all values for Name
* Configure::read('Name.key'); will return only the value of Configure::Name[key]
* ```
*
* @param string|null $var Variable to obtain. Use '.' to access array elements.
* @param mixed $default The return value when the configure does not exist
* @return mixed Value stored in configure, or null.
* @link https://book.cakephp.org/4/en/development/configuration.html#reading-configuration-data
*/
public static function read(?string $var = null, $default = null)
{
if ($var === null) {
return static::$_values;
}
return Hash::get(static::$_values, $var, $default);
}
/**
* Returns true if given variable is set in Configure.
*
* @param string $var Variable name to check for
* @return bool True if variable is there
*/
public static function check(string $var): bool
{
if (empty($var)) {
return false;
}
return static::read($var) !== null;
}
/**
* Used to get information stored in Configure. It's not
* possible to store `null` values in Configure.
*
* Acts as a wrapper around Configure::read() and Configure::check().
* The configure key/value pair fetched via this method is expected to exist.
* In case it does not an exception will be thrown.
*
* Usage:
* ```
* Configure::readOrFail('Name'); will return all values for Name
* Configure::readOrFail('Name.key'); will return only the value of Configure::Name[key]
* ```
*
* @param string $var Variable to obtain. Use '.' to access array elements.
* @return mixed Value stored in configure.
* @throws \RuntimeException if the requested configuration is not set.
* @link https://book.cakephp.org/4/en/development/configuration.html#reading-configuration-data
*/
public static function readOrFail(string $var)
{
if (!static::check($var)) {
throw new RuntimeException(sprintf('Expected configuration key "%s" not found.', $var));
}
return static::read($var);
}
/**
* Used to delete a variable from Configure.
*
* Usage:
* ```
* Configure::delete('Name'); will delete the entire Configure::Name
* Configure::delete('Name.key'); will delete only the Configure::Name[key]
* ```
*
* @param string $var the var to be deleted
* @return void
* @link https://book.cakephp.org/4/en/development/configuration.html#deleting-configuration-data
*/
public static function delete(string $var): void
{
static::$_values = Hash::remove(static::$_values, $var);
}
/**
* Used to consume information stored in Configure. It's not
* possible to store `null` values in Configure.
*
* Acts as a wrapper around Configure::consume() and Configure::check().
* The configure key/value pair consumed via this method is expected to exist.
* In case it does not an exception will be thrown.
*
* @param string $var Variable to consume. Use '.' to access array elements.
* @return mixed Value stored in configure.
* @throws \RuntimeException if the requested configuration is not set.
* @since 3.6.0
*/
public static function consumeOrFail(string $var)
{
if (!static::check($var)) {
throw new RuntimeException(sprintf('Expected configuration key "%s" not found.', $var));
}
return static::consume($var);
}
/**
* Used to read and delete a variable from Configure.
*
* This is primarily used during bootstrapping to move configuration data
* out of configure into the various other classes in CakePHP.
*
* @param string $var The key to read and remove.
* @return array|string|null
*/
public static function consume(string $var)
{
if (strpos($var, '.') === false) {
if (!isset(static::$_values[$var])) {
return null;
}
$value = static::$_values[$var];
unset(static::$_values[$var]);
return $value;
}
$value = Hash::get(static::$_values, $var);
static::delete($var);
return $value;
}
/**
* Add a new engine to Configure. Engines allow you to read configuration
* files in various formats/storage locations. CakePHP comes with two built-in engines
* PhpConfig and IniConfig. You can also implement your own engine classes in your application.
*
* To add a new engine to Configure:
*
* ```
* Configure::config('ini', new IniConfig());
* ```
*
* @param string $name The name of the engine being configured. This alias is used later to
* read values from a specific engine.
* @param \Cake\Core\Configure\ConfigEngineInterface $engine The engine to append.
* @return void
*/
public static function config(string $name, ConfigEngineInterface $engine): void
{
static::$_engines[$name] = $engine;
}
/**
* Returns true if the Engine objects is configured.
*
* @param string $name Engine name.
* @return bool
*/
public static function isConfigured(string $name): bool
{
return isset(static::$_engines[$name]);
}
/**
* Gets the names of the configured Engine objects.
*
* @return array<string>
*/
public static function configured(): array
{
$engines = array_keys(static::$_engines);
return array_map(function ($key) {
return (string)$key;
}, $engines);
}
/**
* Remove a configured engine. This will unset the engine
* and make any future attempts to use it cause an Exception.
*
* @param string $name Name of the engine to drop.
* @return bool Success
*/
public static function drop(string $name): bool
{
if (!isset(static::$_engines[$name])) {
return false;
}
unset(static::$_engines[$name]);
return true;
}
/**
* Loads stored configuration information from a resource. You can add
* config file resource engines with `Configure::config()`.
*
* Loaded configuration information will be merged with the current
* runtime configuration. You can load configuration files from plugins
* by preceding the filename with the plugin name.
*
* `Configure::load('Users.user', 'default')`
*
* Would load the 'user' config file using the default config engine. You can load
* app config files by giving the name of the resource you want loaded.
*
* ```
* Configure::load('setup', 'default');
* ```
*
* If using `default` config and no engine has been configured for it yet,
* one will be automatically created using PhpConfig
*
* @param string $key name of configuration resource to load.
* @param string $config Name of the configured engine to use to read the resource identified by $key.
* @param bool $merge if config files should be merged instead of simply overridden
* @return bool True if load successful.
* @throws \Cake\Core\Exception\CakeException if the $config engine is not found
* @link https://book.cakephp.org/4/en/development/configuration.html#reading-and-writing-configuration-files
*/
public static function load(string $key, string $config = 'default', bool $merge = true): bool
{
$engine = static::_getEngine($config);
if (!$engine) {
throw new CakeException(
sprintf(
'Config %s engine not found when attempting to load %s.',
$config,
$key
)
);
}
$values = $engine->read($key);
if ($merge) {
$values = Hash::merge(static::$_values, $values);
}
static::write($values);
return true;
}
/**
* Dump data currently in Configure into $key. The serialization format
* is decided by the config engine attached as $config. For example, if the
* 'default' adapter is a PhpConfig, the generated file will be a PHP
* configuration file loadable by the PhpConfig.
*
* ### Usage
*
* Given that the 'default' engine is an instance of PhpConfig.
* Save all data in Configure to the file `my_config.php`:
*
* ```
* Configure::dump('my_config', 'default');
* ```
*
* Save only the error handling configuration:
*
* ```
* Configure::dump('error', 'default', ['Error', 'Exception'];
* ```
*
* @param string $key The identifier to create in the config adapter.
* This could be a filename or a cache key depending on the adapter being used.
* @param string $config The name of the configured adapter to dump data with.
* @param array<string> $keys The name of the top-level keys you want to dump.
* This allows you save only some data stored in Configure.
* @return bool Success
* @throws \Cake\Core\Exception\CakeException if the adapter does not implement a `dump` method.
*/
public static function dump(string $key, string $config = 'default', array $keys = []): bool
{
$engine = static::_getEngine($config);
if (!$engine) {
throw new CakeException(sprintf('There is no "%s" config engine.', $config));
}
$values = static::$_values;
if (!empty($keys)) {
$values = array_intersect_key($values, array_flip($keys));
}
return $engine->dump($key, $values);
}
/**
* Get the configured engine. Internally used by `Configure::load()` and `Configure::dump()`
* Will create new PhpConfig for default if not configured yet.
*
* @param string $config The name of the configured adapter
* @return \Cake\Core\Configure\ConfigEngineInterface|null Engine instance or null
*/
protected static function _getEngine(string $config): ?ConfigEngineInterface
{
if (!isset(static::$_engines[$config])) {
if ($config !== 'default') {
return null;
}
static::config($config, new PhpConfig());
}
return static::$_engines[$config];
}
/**
* Used to determine the current version of CakePHP.
*
* Usage
* ```
* Configure::version();
* ```
*
* @return string Current version of CakePHP
*/
public static function version(): string
{
$version = static::read('Cake.version');
if ($version !== null) {
return $version;
}
$path = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'config/config.php';
if (is_file($path)) {
$config = require $path;
static::write($config);
return static::read('Cake.version');
}
return 'unknown';
}
/**
* Used to write runtime configuration into Cache. Stored runtime configuration can be
* restored using `Configure::restore()`. These methods can be used to enable configuration managers
* frontends, or other GUI type interfaces for configuration.
*
* @param string $name The storage name for the saved configuration.
* @param string $cacheConfig The cache configuration to save into. Defaults to 'default'
* @param array|null $data Either an array of data to store, or leave empty to store all values.
* @return bool Success
* @throws \RuntimeException
*/
public static function store(string $name, string $cacheConfig = 'default', ?array $data = null): bool
{
if ($data === null) {
$data = static::$_values;
}
if (!class_exists(Cache::class)) {
throw new RuntimeException('You must install cakephp/cache to use Configure::store()');
}
return Cache::write($name, $data, $cacheConfig);
}
/**
* Restores configuration data stored in the Cache into configure. Restored
* values will overwrite existing ones.
*
* @param string $name Name of the stored config file to load.
* @param string $cacheConfig Name of the Cache configuration to read from.
* @return bool Success.
* @throws \RuntimeException
*/
public static function restore(string $name, string $cacheConfig = 'default'): bool
{
if (!class_exists(Cache::class)) {
throw new RuntimeException('You must install cakephp/cache to use Configure::restore()');
}
$values = Cache::read($name, $cacheConfig);
if ($values) {
static::write($values);
return true;
}
return false;
}
/**
* Clear all values stored in Configure.
*
* @return void
*/
public static function clear(): void
{
static::$_values = [];
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 1.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Configure;
/**
* An interface for creating objects compatible with Configure::load()
*/
interface ConfigEngineInterface
{
/**
* Read a configuration file/storage key
*
* This method is used for reading configuration information from sources.
* These sources can either be static resources like files, or dynamic ones like
* a database, or other datasource.
*
* @param string $key Key to read.
* @return array An array of data to merge into the runtime configuration
*/
public function read(string $key): array;
/**
* Dumps the configure data into the storage key/file of the given `$key`.
*
* @param string $key The identifier to write to.
* @param array $data The data to dump.
* @return bool True on success or false on failure.
*/
public function dump(string $key, array $data): bool;
}

View File

@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 2.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Configure\Engine;
use Cake\Core\Configure\ConfigEngineInterface;
use Cake\Core\Configure\FileConfigTrait;
use Cake\Utility\Hash;
/**
* Ini file configuration engine.
*
* Since IniConfig uses parse_ini_file underneath, you should be aware that this
* class shares the same behavior, especially with regards to boolean and null values.
*
* In addition to the native `parse_ini_file` features, IniConfig also allows you
* to create nested array structures through usage of `.` delimited names. This allows
* you to create nested arrays structures in an ini config file. For example:
*
* `db.password = secret` would turn into `['db' => ['password' => 'secret']]`
*
* You can nest properties as deeply as needed using `.`'s. In addition to using `.` you
* can use standard ini section notation to create nested structures:
*
* ```
* [section]
* key = value
* ```
*
* Once loaded into Configure, the above would be accessed using:
*
* `Configure::read('section.key');
*
* You can also use `.` separated values in section names to create more deeply
* nested structures.
*
* IniConfig also manipulates how the special ini values of
* 'yes', 'no', 'on', 'off', 'null' are handled. These values will be
* converted to their boolean equivalents.
*
* @see https://secure.php.net/parse_ini_file
*/
class IniConfig implements ConfigEngineInterface
{
use FileConfigTrait;
/**
* File extension.
*
* @var string
*/
protected $_extension = '.ini';
/**
* The section to read, if null all sections will be read.
*
* @var string|null
*/
protected $_section;
/**
* Build and construct a new ini file parser. The parser can be used to read
* ini files that are on the filesystem.
*
* @param string|null $path Path to load ini config files from. Defaults to CONFIG.
* @param string|null $section Only get one section, leave null to parse and fetch
* all sections in the ini file.
*/
public function __construct(?string $path = null, ?string $section = null)
{
if ($path === null) {
$path = CONFIG;
}
$this->_path = $path;
$this->_section = $section;
}
/**
* Read an ini file and return the results as an array.
*
* @param string $key The identifier to read from. If the key has a . it will be treated
* as a plugin prefix. The chosen file must be on the engine's path.
* @return array Parsed configuration values.
* @throws \Cake\Core\Exception\CakeException when files don't exist.
* Or when files contain '..' as this could lead to abusive reads.
*/
public function read(string $key): array
{
$file = $this->_getFilePath($key, true);
$contents = parse_ini_file($file, true);
if ($this->_section && isset($contents[$this->_section])) {
$values = $this->_parseNestedValues($contents[$this->_section]);
} else {
$values = [];
foreach ($contents as $section => $attribs) {
if (is_array($attribs)) {
$values[$section] = $this->_parseNestedValues($attribs);
} else {
$parse = $this->_parseNestedValues([$attribs]);
$values[$section] = array_shift($parse);
}
}
}
return $values;
}
/**
* parses nested values out of keys.
*
* @param array $values Values to be exploded.
* @return array Array of values exploded
*/
protected function _parseNestedValues(array $values): array
{
foreach ($values as $key => $value) {
if ($value === '1') {
$value = true;
}
if ($value === '') {
$value = false;
}
unset($values[$key]);
if (strpos((string)$key, '.') !== false) {
$values = Hash::insert($values, $key, $value);
} else {
$values[$key] = $value;
}
}
return $values;
}
/**
* Dumps the state of Configure data into an ini formatted string.
*
* @param string $key The identifier to write to. If the key has a . it will be treated
* as a plugin prefix.
* @param array $data The data to convert to ini file.
* @return bool Success.
*/
public function dump(string $key, array $data): bool
{
$result = [];
foreach ($data as $k => $value) {
$isSection = false;
/** @psalm-suppress InvalidArrayAccess */
if ($k[0] !== '[') {
$result[] = "[$k]";
$isSection = true;
}
if (is_array($value)) {
$kValues = Hash::flatten($value, '.');
foreach ($kValues as $k2 => $v) {
$result[] = "$k2 = " . $this->_value($v);
}
}
if ($isSection) {
$result[] = '';
}
}
$contents = trim(implode("\n", $result));
$filename = $this->_getFilePath($key);
return file_put_contents($filename, $contents) > 0;
}
/**
* Converts a value into the ini equivalent
*
* @param mixed $value Value to export.
* @return string String value for ini file.
*/
protected function _value($value): string
{
if ($value === null) {
return 'null';
}
if ($value === true) {
return 'true';
}
if ($value === false) {
return 'false';
}
return (string)$value;
}
}

View File

@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Configure\Engine;
use Cake\Core\Configure\ConfigEngineInterface;
use Cake\Core\Configure\FileConfigTrait;
use Cake\Core\Exception\CakeException;
/**
* JSON engine allows Configure to load configuration values from
* files containing JSON strings.
*
* An example JSON file would look like::
*
* ```
* {
* "debug": false,
* "App": {
* "namespace": "MyApp"
* },
* "Security": {
* "salt": "its-secret"
* }
* }
* ```
*/
class JsonConfig implements ConfigEngineInterface
{
use FileConfigTrait;
/**
* File extension.
*
* @var string
*/
protected $_extension = '.json';
/**
* Constructor for JSON Config file reading.
*
* @param string|null $path The path to read config files from. Defaults to CONFIG.
*/
public function __construct(?string $path = null)
{
if ($path === null) {
$path = CONFIG;
}
$this->_path = $path;
}
/**
* Read a config file and return its contents.
*
* Files with `.` in the name will be treated as values in plugins. Instead of
* reading from the initialized path, plugin keys will be located using Plugin::path().
*
* @param string $key The identifier to read from. If the key has a . it will be treated
* as a plugin prefix.
* @return array Parsed configuration values.
* @throws \Cake\Core\Exception\CakeException When files don't exist or when
* files contain '..' (as this could lead to abusive reads) or when there
* is an error parsing the JSON string.
*/
public function read(string $key): array
{
$file = $this->_getFilePath($key, true);
$values = json_decode(file_get_contents($file), true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new CakeException(sprintf(
'Error parsing JSON string fetched from config file "%s.json": %s',
$key,
json_last_error_msg()
));
}
if (!is_array($values)) {
throw new CakeException(sprintf(
'Decoding JSON config file "%s.json" did not return an array',
$key
));
}
return $values;
}
/**
* Converts the provided $data into a JSON string that can be used saved
* into a file and loaded later.
*
* @param string $key The identifier to write to. If the key has a . it will
* be treated as a plugin prefix.
* @param array $data Data to dump.
* @return bool Success
*/
public function dump(string $key, array $data): bool
{
$filename = $this->_getFilePath($key);
return file_put_contents($filename, json_encode($data, JSON_PRETTY_PRINT)) > 0;
}
}

View File

@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 2.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Configure\Engine;
use Cake\Core\Configure\ConfigEngineInterface;
use Cake\Core\Configure\FileConfigTrait;
use Cake\Core\Exception\CakeException;
/**
* PHP engine allows Configure to load configuration values from
* files containing simple PHP arrays.
*
* Files compatible with PhpConfig should return an array that
* contains all the configuration data contained in the file.
*
* An example configuration file would look like::
*
* ```
* <?php
* return [
* 'debug' => false,
* 'Security' => [
* 'salt' => 'its-secret'
* ],
* 'App' => [
* 'namespace' => 'App'
* ]
* ];
* ```
*
* @see \Cake\Core\Configure::load() for how to load custom configuration files.
*/
class PhpConfig implements ConfigEngineInterface
{
use FileConfigTrait;
/**
* File extension.
*
* @var string
*/
protected $_extension = '.php';
/**
* Constructor for PHP Config file reading.
*
* @param string|null $path The path to read config files from. Defaults to CONFIG.
*/
public function __construct(?string $path = null)
{
if ($path === null) {
$path = CONFIG;
}
$this->_path = $path;
}
/**
* Read a config file and return its contents.
*
* Files with `.` in the name will be treated as values in plugins. Instead of
* reading from the initialized path, plugin keys will be located using Plugin::path().
*
* @param string $key The identifier to read from. If the key has a . it will be treated
* as a plugin prefix.
* @return array Parsed configuration values.
* @throws \Cake\Core\Exception\CakeException when files don't exist or they don't contain `$config`.
* Or when files contain '..' as this could lead to abusive reads.
*/
public function read(string $key): array
{
$file = $this->_getFilePath($key, true);
$config = null;
$return = include $file;
if (is_array($return)) {
return $return;
}
throw new CakeException(sprintf('Config file "%s" did not return an array', $key . '.php'));
}
/**
* Converts the provided $data into a string of PHP code that can
* be used saved into a file and loaded later.
*
* @param string $key The identifier to write to. If the key has a . it will be treated
* as a plugin prefix.
* @param array $data Data to dump.
* @return bool Success
*/
public function dump(string $key, array $data): bool
{
$contents = '<?php' . "\n" . 'return ' . var_export($data, true) . ';';
$filename = $this->_getFilePath($key);
return file_put_contents($filename, $contents) > 0;
}
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Configure;
use Cake\Core\Exception\CakeException;
use Cake\Core\Plugin;
use function Cake\Core\pluginSplit;
/**
* Trait providing utility methods for file based config engines.
*/
trait FileConfigTrait
{
/**
* The path this engine finds files on.
*
* @var string
*/
protected $_path = '';
/**
* Get file path
*
* @param string $key The identifier to write to. If the key has a . it will be treated
* as a plugin prefix.
* @param bool $checkExists Whether to check if file exists. Defaults to false.
* @return string Full file path
* @throws \Cake\Core\Exception\CakeException When files don't exist or when
* files contain '..' as this could lead to abusive reads.
*/
protected function _getFilePath(string $key, bool $checkExists = false): string
{
if (strpos($key, '..') !== false) {
throw new CakeException('Cannot load/dump configuration files with ../ in them.');
}
[$plugin, $key] = pluginSplit($key);
if ($plugin) {
$file = Plugin::configPath($plugin) . $key;
} else {
$file = $this->_path . $key;
}
$file .= $this->_extension;
if (!$checkExists || is_file($file)) {
return $file;
}
$realPath = realpath($file);
if ($realPath !== false && is_file($realPath)) {
return $realPath;
}
throw new CakeException(sprintf('Could not load configuration file: %s', $file));
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.5.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Console\CommandCollection;
/**
* An interface defining the methods that the
* console runner depend on.
*/
interface ConsoleApplicationInterface
{
/**
* Load all the application configuration and bootstrap logic.
*
* Override this method to add additional bootstrap logic for your application.
*
* @return void
*/
public function bootstrap(): void;
/**
* Define the console commands for an application.
*
* @param \Cake\Console\CommandCollection $commands The CommandCollection to add commands into.
* @return \Cake\Console\CommandCollection The updated collection.
*/
public function console(CommandCollection $commands): CommandCollection;
}

28
vendor/cakephp/core/Container.php vendored Normal file
View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use League\Container\Container as LeagueContainer;
/**
* Dependency Injection container
*
* Based on the container out of League\Container
*/
class Container extends LeagueContainer implements ContainerInterface
{
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
/**
* Interface for applications that configure and use a dependency injection container.
*/
interface ContainerApplicationInterface
{
/**
* Register services to the container
*
* Registered services can have instances fetched out of the container
* using `get()`. Dependencies and parameters will be resolved based
* on service definitions.
*
* @param \Cake\Core\ContainerInterface $container The container to add services to
* @return void
*/
public function services(ContainerInterface $container): void;
/**
* Create a new container and register services.
*
* This will `register()` services provided by both the application
* and any plugins if the application has plugin support.
*
* @return \Cake\Core\ContainerInterface A populated container
*/
public function getContainer(): ContainerInterface;
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use League\Container\DefinitionContainerInterface;
/**
* Interface for the Dependency Injection Container in CakePHP applications
*
* This interface extends the PSR-11 container interface and adds
* methods to add services and service providers to the container.
*
* The methods defined in this interface use the conventions provided
* by league/container as that is the library that CakePHP uses.
*/
interface ContainerInterface extends DefinitionContainerInterface
{
}

156
vendor/cakephp/core/ConventionsTrait.php vendored Normal file
View File

@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Utility\Inflector;
/**
* Provides methods that allow other classes access to conventions based inflections.
*/
trait ConventionsTrait
{
/**
* Creates a fixture name
*
* @param string $name Model class name
* @return string Singular model key
*/
protected function _fixtureName(string $name): string
{
return Inflector::camelize($name);
}
/**
* Creates the proper entity name (singular) for the specified name
*
* @param string $name Name
* @return string Camelized and plural model name
*/
protected function _entityName(string $name): string
{
return Inflector::singularize(Inflector::camelize($name));
}
/**
* Creates the proper underscored model key for associations
*
* If the input contains a dot, assume that the right side is the real table name.
*
* @param string $name Model class name
* @return string Singular model key
*/
protected function _modelKey(string $name): string
{
[, $name] = pluginSplit($name);
return Inflector::underscore(Inflector::singularize($name)) . '_id';
}
/**
* Creates the proper model name from a foreign key
*
* @param string $key Foreign key
* @return string Model name
*/
protected function _modelNameFromKey(string $key): string
{
$key = str_replace('_id', '', $key);
return Inflector::camelize(Inflector::pluralize($key));
}
/**
* Creates the singular name for use in views.
*
* @param string $name Name to use
* @return string Variable name
*/
protected function _singularName(string $name): string
{
return Inflector::variable(Inflector::singularize($name));
}
/**
* Creates the plural variable name for views
*
* @param string $name Name to use
* @return string Plural name for views
*/
protected function _variableName(string $name): string
{
return Inflector::variable($name);
}
/**
* Creates the singular human name used in views
*
* @param string $name Controller name
* @return string Singular human name
*/
protected function _singularHumanName(string $name): string
{
return Inflector::humanize(Inflector::underscore(Inflector::singularize($name)));
}
/**
* Creates a camelized version of $name
*
* @param string $name name
* @return string Camelized name
*/
protected function _camelize(string $name): string
{
return Inflector::camelize($name);
}
/**
* Creates the plural human name used in views
*
* @param string $name Controller name
* @return string Plural human name
*/
protected function _pluralHumanName(string $name): string
{
return Inflector::humanize(Inflector::underscore($name));
}
/**
* Find the correct path for a plugin. Scans $pluginPaths for the plugin you want.
*
* @param string $pluginName Name of the plugin you want ie. DebugKit
* @return string path path to the correct plugin.
*/
protected function _pluginPath(string $pluginName): string
{
if (Plugin::isLoaded($pluginName)) {
return Plugin::path($pluginName);
}
return current(App::path('plugins')) . $pluginName . DIRECTORY_SEPARATOR;
}
/**
* Return plugin's namespace
*
* @param string $pluginName Plugin name
* @return string Plugin's namespace
*/
protected function _pluginNamespace(string $pluginName): string
{
return str_replace('/', '\\', $pluginName);
}
}

View File

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Exception;
use RuntimeException;
use Throwable;
use function Cake\Core\deprecationWarning;
/**
* Base class that all CakePHP Exceptions extend.
*
* @method int getCode() Gets the Exception code.
*/
class CakeException extends RuntimeException
{
/**
* Array of attributes that are passed in from the constructor, and
* made available in the view when a development error is displayed.
*
* @var array
*/
protected $_attributes = [];
/**
* Template string that has attributes sprintf()'ed into it.
*
* @var string
*/
protected $_messageTemplate = '';
/**
* Array of headers to be passed to {@link \Cake\Http\Response::withHeader()}
*
* @var array|null
*/
protected $_responseHeaders;
/**
* Default exception code
*
* @var int
*/
protected $_defaultCode = 0;
/**
* Constructor.
*
* Allows you to create exceptions that are treated as framework errors and disabled
* when debug mode is off.
*
* @param array|string $message Either the string of the error message, or an array of attributes
* that are made available in the view, and sprintf()'d into Exception::$_messageTemplate
* @param int|null $code The error code
* @param \Throwable|null $previous the previous exception.
*/
public function __construct($message = '', ?int $code = null, ?Throwable $previous = null)
{
if (is_array($message)) {
$this->_attributes = $message;
$message = vsprintf($this->_messageTemplate, $message);
}
parent::__construct($message, $code ?? $this->_defaultCode, $previous);
}
/**
* Get the passed in attributes
*
* @return array
*/
public function getAttributes(): array
{
return $this->_attributes;
}
/**
* Get/set the response header to be used
*
* See also {@link \Cake\Http\Response::withHeader()}
*
* @param array|string|null $header A single header string or an associative
* array of "header name" => "header value"
* @param string|null $value The header value.
* @return array|null
* @deprecated 4.2.0 Use `HttpException::setHeaders()` instead. Response headers
* should be set for HttpException only.
*/
public function responseHeader($header = null, $value = null): ?array
{
if ($header === null) {
return $this->_responseHeaders;
}
deprecationWarning(
'Setting HTTP response headers from Exception directly is deprecated. ' .
'If your exceptions extend Exception, they must now extend HttpException. ' .
'You should only set HTTP headers on HttpException instances via the `setHeaders()` method.'
);
if (is_array($header)) {
return $this->_responseHeaders = $header;
}
return $this->_responseHeaders = [$header => $value];
}
}
// phpcs:disable
class_alias(
'Cake\Core\Exception\CakeException',
'Cake\Core\Exception\Exception'
);
// phpcs:enable

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
use function Cake\Core\deprecationWarning;
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
deprecationWarning(
'Since 4.2.0: Cake\Core\Exception\Exception is deprecated.' .
'Use Cake\Core\Exception\CakeException instead.'
);
class_exists('Cake\Core\Exception\CakeException');

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Exception;
/**
* Exception raised when a plugin could not be found
*/
class MissingPluginException extends CakeException
{
/**
* @inheritDoc
*/
protected $_messageTemplate = 'Plugin %s could not be found.';
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.5.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Http\MiddlewareQueue;
use Psr\Http\Server\RequestHandlerInterface;
/**
* An interface defining the methods that the
* http server depend on.
*/
interface HttpApplicationInterface extends RequestHandlerInterface
{
/**
* Load all the application configuration and bootstrap logic.
*
* Override this method to add additional bootstrap logic for your application.
*
* @return void
*/
public function bootstrap(): void;
/**
* Define the HTTP middleware layers for an application.
*
* @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to set in your App Class
* @return \Cake\Http\MiddlewareQueue
*/
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue;
}

View File

@ -0,0 +1,312 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Core\Exception\CakeException;
use Cake\Utility\Hash;
use InvalidArgumentException;
/**
* A trait for reading and writing instance config
*
* Implementing objects are expected to declare a `$_defaultConfig` property.
*/
trait InstanceConfigTrait
{
/**
* Runtime config
*
* @var array<string, mixed>
*/
protected $_config = [];
/**
* Whether the config property has already been configured with defaults
*
* @var bool
*/
protected $_configInitialized = false;
/**
* Sets the config.
*
* ### Usage
*
* Setting a specific value:
*
* ```
* $this->setConfig('key', $value);
* ```
*
* Setting a nested value:
*
* ```
* $this->setConfig('some.nested.key', $value);
* ```
*
* Updating multiple config settings at the same time:
*
* ```
* $this->setConfig(['one' => 'value', 'another' => 'value']);
* ```
*
* @param array<string, mixed>|string $key The key to set, or a complete array of configs.
* @param mixed|null $value The value to set.
* @param bool $merge Whether to recursively merge or overwrite existing config, defaults to true.
* @return $this
* @throws \Cake\Core\Exception\CakeException When trying to set a key that is invalid.
*/
public function setConfig($key, $value = null, $merge = true)
{
if (!$this->_configInitialized) {
$this->_config = $this->_defaultConfig;
$this->_configInitialized = true;
}
$this->_configWrite($key, $value, $merge);
return $this;
}
/**
* Returns the config.
*
* ### Usage
*
* Reading the whole config:
*
* ```
* $this->getConfig();
* ```
*
* Reading a specific value:
*
* ```
* $this->getConfig('key');
* ```
*
* Reading a nested value:
*
* ```
* $this->getConfig('some.nested.key');
* ```
*
* Reading with default value:
*
* ```
* $this->getConfig('some-key', 'default-value');
* ```
*
* @param string|null $key The key to get or null for the whole config.
* @param mixed $default The return value when the key does not exist.
* @return mixed Configuration data at the named key or null if the key does not exist.
*/
public function getConfig(?string $key = null, $default = null)
{
if (!$this->_configInitialized) {
$this->_config = $this->_defaultConfig;
$this->_configInitialized = true;
}
$return = $this->_configRead($key);
return $return ?? $default;
}
/**
* Returns the config for this specific key.
*
* The config value for this key must exist, it can never be null.
*
* @param string $key The key to get.
* @return mixed Configuration data at the named key
* @throws \InvalidArgumentException
*/
public function getConfigOrFail(string $key)
{
$config = $this->getConfig($key);
if ($config === null) {
throw new InvalidArgumentException(sprintf('Expected configuration `%s` not found.', $key));
}
return $config;
}
/**
* Merge provided config with existing config. Unlike `config()` which does
* a recursive merge for nested keys, this method does a simple merge.
*
* Setting a specific value:
*
* ```
* $this->configShallow('key', $value);
* ```
*
* Setting a nested value:
*
* ```
* $this->configShallow('some.nested.key', $value);
* ```
*
* Updating multiple config settings at the same time:
*
* ```
* $this->configShallow(['one' => 'value', 'another' => 'value']);
* ```
*
* @param array<string, mixed>|string $key The key to set, or a complete array of configs.
* @param mixed|null $value The value to set.
* @return $this
*/
public function configShallow($key, $value = null)
{
if (!$this->_configInitialized) {
$this->_config = $this->_defaultConfig;
$this->_configInitialized = true;
}
$this->_configWrite($key, $value, 'shallow');
return $this;
}
/**
* Reads a config key.
*
* @param string|null $key Key to read.
* @return mixed
*/
protected function _configRead(?string $key)
{
if ($key === null) {
return $this->_config;
}
if (strpos($key, '.') === false) {
return $this->_config[$key] ?? null;
}
$return = $this->_config;
foreach (explode('.', $key) as $k) {
if (!is_array($return) || !isset($return[$k])) {
$return = null;
break;
}
$return = $return[$k];
}
return $return;
}
/**
* Writes a config key.
*
* @param array<string, mixed>|string $key Key to write to.
* @param mixed $value Value to write.
* @param string|bool $merge True to merge recursively, 'shallow' for simple merge,
* false to overwrite, defaults to false.
* @return void
* @throws \Cake\Core\Exception\CakeException if attempting to clobber existing config
*/
protected function _configWrite($key, $value, $merge = false): void
{
if (is_string($key) && $value === null) {
$this->_configDelete($key);
return;
}
if ($merge) {
$update = is_array($key) ? $key : [$key => $value];
if ($merge === 'shallow') {
$this->_config = array_merge($this->_config, Hash::expand($update));
} else {
$this->_config = Hash::merge($this->_config, Hash::expand($update));
}
return;
}
if (is_array($key)) {
foreach ($key as $k => $val) {
$this->_configWrite($k, $val);
}
return;
}
if (strpos($key, '.') === false) {
$this->_config[$key] = $value;
return;
}
$update = &$this->_config;
$stack = explode('.', $key);
foreach ($stack as $k) {
if (!is_array($update)) {
throw new CakeException(sprintf('Cannot set %s value', $key));
}
$update[$k] = $update[$k] ?? [];
$update = &$update[$k];
}
$update = $value;
}
/**
* Deletes a single config key.
*
* @param string $key Key to delete.
* @return void
* @throws \Cake\Core\Exception\CakeException if attempting to clobber existing config
*/
protected function _configDelete(string $key): void
{
if (strpos($key, '.') === false) {
unset($this->_config[$key]);
return;
}
$update = &$this->_config;
$stack = explode('.', $key);
$length = count($stack);
foreach ($stack as $i => $k) {
if (!is_array($update)) {
throw new CakeException(sprintf('Cannot unset %s value', $key));
}
if (!isset($update[$k])) {
break;
}
if ($i === $length - 1) {
unset($update[$k]);
break;
}
$update = &$update[$k];
}
}
}

22
vendor/cakephp/core/LICENSE.txt vendored Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org)
Copyright (c) 2005-2020, Cake Software Foundation, Inc. (https://cakefoundation.org)
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.

416
vendor/cakephp/core/ObjectRegistry.php vendored Normal file
View File

@ -0,0 +1,416 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use ArrayIterator;
use Cake\Event\EventDispatcherInterface;
use Cake\Event\EventListenerInterface;
use Countable;
use IteratorAggregate;
use RuntimeException;
use Traversable;
/**
* Acts as a registry/factory for objects.
*
* Provides registry & factory functionality for object types. Used
* as a super class for various composition based re-use features in CakePHP.
*
* Each subclass needs to implement the various abstract methods to complete
* the template method load().
*
* The ObjectRegistry is EventManager aware, but each extending class will need to use
* \Cake\Event\EventDispatcherTrait to attach and detach on set and bind
*
* @see \Cake\Controller\ComponentRegistry
* @see \Cake\View\HelperRegistry
* @see \Cake\Console\TaskRegistry
* @template TObject of object
* @template-implements \IteratorAggregate<string, TObject>
*/
abstract class ObjectRegistry implements Countable, IteratorAggregate
{
/**
* Map of loaded objects.
*
* @var array<object>
* @psalm-var array<array-key, TObject>
*/
protected $_loaded = [];
/**
* Loads/constructs an object instance.
*
* Will return the instance in the registry if it already exists.
* If a subclass provides event support, you can use `$config['enabled'] = false`
* to exclude constructed objects from being registered for events.
*
* Using {@link \Cake\Controller\Component::$components} as an example. You can alias
* an object by setting the 'className' key, i.e.,
*
* ```
* protected $components = [
* 'Email' => [
* 'className' => 'App\Controller\Component\AliasedEmailComponent'
* ];
* ];
* ```
*
* All calls to the `Email` component would use `AliasedEmail` instead.
*
* @param string $name The name/class of the object to load.
* @param array<string, mixed> $config Additional settings to use when loading the object.
* @return mixed
* @psalm-return TObject
* @throws \Exception If the class cannot be found.
*/
public function load(string $name, array $config = [])
{
if (isset($config['className'])) {
$objName = $name;
$name = $config['className'];
} else {
[, $objName] = pluginSplit($name);
}
$loaded = isset($this->_loaded[$objName]);
if ($loaded && !empty($config)) {
$this->_checkDuplicate($objName, $config);
}
if ($loaded) {
return $this->_loaded[$objName];
}
$className = $name;
if (is_string($name)) {
$className = $this->_resolveClassName($name);
if ($className === null) {
[$plugin, $name] = pluginSplit($name);
$this->_throwMissingClassError($name, $plugin);
}
}
/**
* @psalm-var TObject $instance
* @psalm-suppress PossiblyNullArgument
**/
$instance = $this->_create($className, $objName, $config);
$this->_loaded[$objName] = $instance;
return $instance;
}
/**
* Check for duplicate object loading.
*
* If a duplicate is being loaded and has different configuration, that is
* bad and an exception will be raised.
*
* An exception is raised, as replacing the object will not update any
* references other objects may have. Additionally, simply updating the runtime
* configuration is not a good option as we may be missing important constructor
* logic dependent on the configuration.
*
* @param string $name The name of the alias in the registry.
* @param array<string, mixed> $config The config data for the new instance.
* @return void
* @throws \RuntimeException When a duplicate is found.
*/
protected function _checkDuplicate(string $name, array $config): void
{
$existing = $this->_loaded[$name];
$msg = sprintf('The "%s" alias has already been loaded.', $name);
$hasConfig = method_exists($existing, 'getConfig');
if (!$hasConfig) {
throw new RuntimeException($msg);
}
if (empty($config)) {
return;
}
$existingConfig = $existing->getConfig();
unset($config['enabled'], $existingConfig['enabled']);
$failure = null;
foreach ($config as $key => $value) {
if (!array_key_exists($key, $existingConfig)) {
$failure = " The `{$key}` was not defined in the previous configuration data.";
break;
}
if (isset($existingConfig[$key]) && $existingConfig[$key] !== $value) {
$failure = sprintf(
' The `%s` key has a value of `%s` but previously had a value of `%s`',
$key,
json_encode($value),
json_encode($existingConfig[$key])
);
break;
}
}
if ($failure) {
throw new RuntimeException($msg . $failure);
}
}
/**
* Should resolve the classname for a given object type.
*
* @param string $class The class to resolve.
* @return string|null The resolved name or null for failure.
* @psalm-return class-string|null
*/
abstract protected function _resolveClassName(string $class): ?string;
/**
* Throw an exception when the requested object name is missing.
*
* @param string $class The class that is missing.
* @param string|null $plugin The plugin $class is missing from.
* @return void
* @throws \Exception
*/
abstract protected function _throwMissingClassError(string $class, ?string $plugin): void;
/**
* Create an instance of a given classname.
*
* This method should construct and do any other initialization logic
* required.
*
* @param object|string $class The class to build.
* @param string $alias The alias of the object.
* @param array<string, mixed> $config The Configuration settings for construction
* @return object
* @psalm-param TObject|string $class
* @psalm-return TObject
*/
abstract protected function _create($class, string $alias, array $config);
/**
* Get the list of loaded objects.
*
* @return array<string> List of object names.
*/
public function loaded(): array
{
return array_keys($this->_loaded);
}
/**
* Check whether a given object is loaded.
*
* @param string $name The object name to check for.
* @return bool True is object is loaded else false.
*/
public function has(string $name): bool
{
return isset($this->_loaded[$name]);
}
/**
* Get loaded object instance.
*
* @param string $name Name of object.
* @return object Object instance.
* @throws \RuntimeException If not loaded or found.
* @psalm-return TObject
*/
public function get(string $name)
{
if (!isset($this->_loaded[$name])) {
throw new RuntimeException(sprintf('Unknown object "%s"', $name));
}
return $this->_loaded[$name];
}
/**
* Provide public read access to the loaded objects
*
* @param string $name Name of property to read
* @return object|null
* @psalm-return TObject|null
*/
public function __get(string $name)
{
return $this->_loaded[$name] ?? null;
}
/**
* Provide isset access to _loaded
*
* @param string $name Name of object being checked.
* @return bool
*/
public function __isset(string $name): bool
{
return $this->has($name);
}
/**
* Sets an object.
*
* @param string $name Name of a property to set.
* @param object $object Object to set.
* @psalm-param TObject $object
* @return void
*/
public function __set(string $name, $object): void
{
$this->set($name, $object);
}
/**
* Unsets an object.
*
* @param string $name Name of a property to unset.
* @return void
*/
public function __unset(string $name): void
{
$this->unload($name);
}
/**
* Normalizes an object array, creates an array that makes lazy loading
* easier
*
* @param array $objects Array of child objects to normalize.
* @return array<string, array> Array of normalized objects.
*/
public function normalizeArray(array $objects): array
{
$normal = [];
foreach ($objects as $i => $objectName) {
$config = [];
if (!is_int($i)) {
$config = (array)$objectName;
$objectName = $i;
}
[, $name] = pluginSplit($objectName);
if (isset($config['class'])) {
$normal[$name] = $config + ['config' => []];
} else {
$normal[$name] = ['class' => $objectName, 'config' => $config];
}
}
return $normal;
}
/**
* Clear loaded instances in the registry.
*
* If the registry subclass has an event manager, the objects will be detached from events as well.
*
* @return $this
*/
public function reset()
{
foreach (array_keys($this->_loaded) as $name) {
$this->unload((string)$name);
}
return $this;
}
/**
* Set an object directly into the registry by name.
*
* If this collection implements events, the passed object will
* be attached into the event manager
*
* @param string $name The name of the object to set in the registry.
* @param object $object instance to store in the registry
* @return $this
* @psalm-param TObject $object
*/
public function set(string $name, object $object)
{
[, $objName] = pluginSplit($name);
// Just call unload if the object was loaded before
if (array_key_exists($name, $this->_loaded)) {
$this->unload($name);
}
if ($this instanceof EventDispatcherInterface && $object instanceof EventListenerInterface) {
$this->getEventManager()->on($object);
}
$this->_loaded[$objName] = $object;
return $this;
}
/**
* Remove an object from the registry.
*
* If this registry has an event manager, the object will be detached from any events as well.
*
* @param string $name The name of the object to remove from the registry.
* @return $this
*/
public function unload(string $name)
{
if (empty($this->_loaded[$name])) {
[$plugin, $name] = pluginSplit($name);
$this->_throwMissingClassError($name, $plugin);
}
$object = $this->_loaded[$name];
if ($this instanceof EventDispatcherInterface && $object instanceof EventListenerInterface) {
$this->getEventManager()->off($object);
}
unset($this->_loaded[$name]);
return $this;
}
/**
* Returns an array iterator.
*
* @return \Traversable
* @psalm-return \Traversable<string, TObject>
*/
public function getIterator(): Traversable
{
return new ArrayIterator($this->_loaded);
}
/**
* Returns the number of loaded objects.
*
* @return int
*/
public function count(): int
{
return count($this->_loaded);
}
/**
* Debug friendly object properties.
*
* @return array<string, mixed>
*/
public function __debugInfo(): array
{
$properties = get_object_vars($this);
if (isset($properties['_loaded'])) {
$properties['_loaded'] = array_keys($properties['_loaded']);
}
return $properties;
}
}

136
vendor/cakephp/core/Plugin.php vendored Normal file
View File

@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 2.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
/**
* Plugin is used to load and locate plugins.
*
* It also can retrieve plugin paths and load their bootstrap and routes files.
*
* @link https://book.cakephp.org/4/en/plugins.html
*/
class Plugin
{
/**
* Holds a list of all loaded plugins and their configuration
*
* @var \Cake\Core\PluginCollection|null
*/
protected static $plugins;
/**
* Returns the filesystem path for a plugin
*
* @param string $name name of the plugin in CamelCase format
* @return string path to the plugin folder
* @throws \Cake\Core\Exception\MissingPluginException If the folder for plugin was not found
* or plugin has not been loaded.
*/
public static function path(string $name): string
{
$plugin = static::getCollection()->get($name);
return $plugin->getPath();
}
/**
* Returns the filesystem path for plugin's folder containing class files.
*
* @param string $name name of the plugin in CamelCase format.
* @return string Path to the plugin folder containing class files.
* @throws \Cake\Core\Exception\MissingPluginException If plugin has not been loaded.
*/
public static function classPath(string $name): string
{
$plugin = static::getCollection()->get($name);
return $plugin->getClassPath();
}
/**
* Returns the filesystem path for plugin's folder containing config files.
*
* @param string $name name of the plugin in CamelCase format.
* @return string Path to the plugin folder containing config files.
* @throws \Cake\Core\Exception\MissingPluginException If plugin has not been loaded.
*/
public static function configPath(string $name): string
{
$plugin = static::getCollection()->get($name);
return $plugin->getConfigPath();
}
/**
* Returns the filesystem path for plugin's folder containing template files.
*
* @param string $name name of the plugin in CamelCase format.
* @return string Path to the plugin folder containing template files.
* @throws \Cake\Core\Exception\MissingPluginException If plugin has not been loaded.
*/
public static function templatePath(string $name): string
{
$plugin = static::getCollection()->get($name);
return $plugin->getTemplatePath();
}
/**
* Returns true if the plugin $plugin is already loaded.
*
* @param string $plugin Plugin name.
* @return bool
* @since 3.7.0
*/
public static function isLoaded(string $plugin): bool
{
return static::getCollection()->has($plugin);
}
/**
* Return a list of loaded plugins.
*
* @return array<string> A list of plugins that have been loaded
*/
public static function loaded(): array
{
$names = [];
foreach (static::getCollection() as $plugin) {
$names[] = $plugin->getName();
}
sort($names);
return $names;
}
/**
* Get the shared plugin collection.
*
* This method should generally not be used during application
* runtime as plugins should be set during Application startup.
*
* @return \Cake\Core\PluginCollection
*/
public static function getCollection(): PluginCollection
{
if (!isset(static::$plugins)) {
static::$plugins = new PluginCollection();
}
return static::$plugins;
}
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Console\CommandCollection;
use Cake\Event\EventDispatcherInterface;
use Cake\Http\MiddlewareQueue;
use Cake\Routing\RouteBuilder;
/**
* Interface for Applications that leverage plugins & events.
*
* Events can be bound to the application event manager during
* the application's bootstrap and plugin bootstrap.
*/
interface PluginApplicationInterface extends EventDispatcherInterface
{
/**
* Add a plugin to the loaded plugin set.
*
* If the named plugin does not exist, or does not define a Plugin class, an
* instance of `Cake\Core\BasePlugin` will be used. This generated class will have
* all plugin hooks enabled.
*
* @param \Cake\Core\PluginInterface|string $name The plugin name or plugin object.
* @param array<string, mixed> $config The configuration data for the plugin if using a string for $name
* @return $this
*/
public function addPlugin($name, array $config = []);
/**
* Run bootstrap logic for loaded plugins.
*
* @return void
*/
public function pluginBootstrap(): void;
/**
* Run routes hooks for loaded plugins
*
* @param \Cake\Routing\RouteBuilder $routes The route builder to use.
* @return \Cake\Routing\RouteBuilder
*/
public function pluginRoutes(RouteBuilder $routes): RouteBuilder;
/**
* Run middleware hooks for plugins
*
* @param \Cake\Http\MiddlewareQueue $middleware The MiddlewareQueue to use.
* @return \Cake\Http\MiddlewareQueue
*/
public function pluginMiddleware(MiddlewareQueue $middleware): MiddlewareQueue;
/**
* Run console hooks for plugins
*
* @param \Cake\Console\CommandCollection $commands The CommandCollection to use.
* @return \Cake\Console\CommandCollection
*/
public function pluginConsole(CommandCollection $commands): CommandCollection;
}

363
vendor/cakephp/core/PluginCollection.php vendored Normal file
View File

@ -0,0 +1,363 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Core\Exception\CakeException;
use Cake\Core\Exception\MissingPluginException;
use Countable;
use Generator;
use InvalidArgumentException;
use Iterator;
/**
* Plugin Collection
*
* Holds onto plugin objects loaded into an application, and
* provides methods for iterating, and finding plugins based
* on criteria.
*
* This class implements the Iterator interface to allow plugins
* to be iterated, handling the situation where a plugin's hook
* method (usually bootstrap) loads another plugin during iteration.
*
* While its implementation supported nested iteration it does not
* support using `continue` or `break` inside loops.
*
* @template-implements \Iterator<string, \Cake\Core\PluginInterface>
*/
class PluginCollection implements Iterator, Countable
{
/**
* Plugin list
*
* @var array<\Cake\Core\PluginInterface>
*/
protected $plugins = [];
/**
* Names of plugins
*
* @var array<string>
*/
protected $names = [];
/**
* Iterator position stack.
*
* @var array<int>
*/
protected $positions = [];
/**
* Loop depth
*
* @var int
*/
protected $loopDepth = -1;
/**
* Constructor
*
* @param array<\Cake\Core\PluginInterface> $plugins The map of plugins to add to the collection.
*/
public function __construct(array $plugins = [])
{
foreach ($plugins as $plugin) {
$this->add($plugin);
}
$this->loadConfig();
}
/**
* Load the path information stored in vendor/cakephp-plugins.php
*
* This file is generated by the cakephp/plugin-installer package and used
* to locate plugins on the filesystem as applications can use `extra.plugin-paths`
* in their composer.json file to move plugin outside of vendor/
*
* @internal
* @return void
*/
protected function loadConfig(): void
{
if (Configure::check('plugins')) {
return;
}
$vendorFile = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'cakephp-plugins.php';
if (!is_file($vendorFile)) {
$vendorFile = dirname(dirname(dirname(dirname(__DIR__)))) . DIRECTORY_SEPARATOR . 'cakephp-plugins.php';
if (!is_file($vendorFile)) {
Configure::write(['plugins' => []]);
return;
}
}
$config = require $vendorFile;
Configure::write($config);
}
/**
* Locate a plugin path by looking at configuration data.
*
* This will use the `plugins` Configure key, and fallback to enumerating `App::path('plugins')`
*
* This method is not part of the official public API as plugins with
* no plugin class are being phased out.
*
* @param string $name The plugin name to locate a path for.
* @return string
* @throws \Cake\Core\Exception\MissingPluginException when a plugin path cannot be resolved.
* @internal
*/
public function findPath(string $name): string
{
// Ensure plugin config is loaded each time. This is necessary primarily
// for testing because the Configure::clear() call in TestCase::tearDown()
// wipes out all configuration including plugin paths config.
$this->loadConfig();
$path = Configure::read('plugins.' . $name);
if ($path) {
return $path;
}
$pluginPath = str_replace('/', DIRECTORY_SEPARATOR, $name);
$paths = App::path('plugins');
foreach ($paths as $path) {
if (is_dir($path . $pluginPath)) {
return $path . $pluginPath . DIRECTORY_SEPARATOR;
}
}
throw new MissingPluginException(['plugin' => $name]);
}
/**
* Add a plugin to the collection
*
* Plugins will be keyed by their names.
*
* @param \Cake\Core\PluginInterface $plugin The plugin to load.
* @return $this
*/
public function add(PluginInterface $plugin)
{
$name = $plugin->getName();
$this->plugins[$name] = $plugin;
$this->names = array_keys($this->plugins);
return $this;
}
/**
* Remove a plugin from the collection if it exists.
*
* @param string $name The named plugin.
* @return $this
*/
public function remove(string $name)
{
unset($this->plugins[$name]);
$this->names = array_keys($this->plugins);
return $this;
}
/**
* Remove all plugins from the collection
*
* @return $this
*/
public function clear()
{
$this->plugins = [];
$this->names = [];
$this->positions = [];
$this->loopDepth = -1;
return $this;
}
/**
* Check whether the named plugin exists in the collection.
*
* @param string $name The named plugin.
* @return bool
*/
public function has(string $name): bool
{
return isset($this->plugins[$name]);
}
/**
* Get the a plugin by name.
*
* If a plugin isn't already loaded it will be autoloaded on first access
* and that plugins loaded this way may miss some hook methods.
*
* @param string $name The plugin to get.
* @return \Cake\Core\PluginInterface The plugin.
* @throws \Cake\Core\Exception\MissingPluginException when unknown plugins are fetched.
*/
public function get(string $name): PluginInterface
{
if ($this->has($name)) {
return $this->plugins[$name];
}
$plugin = $this->create($name);
$this->add($plugin);
return $plugin;
}
/**
* Create a plugin instance from a name/classname and configuration.
*
* @param string $name The plugin name or classname
* @param array<string, mixed> $config Configuration options for the plugin.
* @return \Cake\Core\PluginInterface
* @throws \Cake\Core\Exception\MissingPluginException When plugin instance could not be created.
*/
public function create(string $name, array $config = []): PluginInterface
{
if ($name === '') {
throw new CakeException('Cannot create a plugin with empty name');
}
if (strpos($name, '\\') !== false) {
/** @var \Cake\Core\PluginInterface */
return new $name($config);
}
$config += ['name' => $name];
$namespace = str_replace('/', '\\', $name);
$className = $namespace . '\\' . 'Plugin';
// Check for [Vendor/]Foo/Plugin class
if (!class_exists($className)) {
$pos = strpos($name, '/');
if ($pos === false) {
$className = $namespace . '\\' . $name . 'Plugin';
} else {
$className = $namespace . '\\' . substr($name, $pos + 1) . 'Plugin';
}
// Check for [Vendor/]Foo/FooPlugin
if (!class_exists($className)) {
$className = BasePlugin::class;
if (empty($config['path'])) {
$config['path'] = $this->findPath($name);
}
}
}
/** @var class-string<\Cake\Core\PluginInterface> $className */
return new $className($config);
}
/**
* Implementation of Countable.
*
* Get the number of plugins in the collection.
*
* @return int
*/
public function count(): int
{
return count($this->plugins);
}
/**
* Part of Iterator Interface
*
* @return void
*/
public function next(): void
{
$this->positions[$this->loopDepth]++;
}
/**
* Part of Iterator Interface
*
* @return string
*/
public function key(): string
{
return $this->names[$this->positions[$this->loopDepth]];
}
/**
* Part of Iterator Interface
*
* @return \Cake\Core\PluginInterface
*/
public function current(): PluginInterface
{
$position = $this->positions[$this->loopDepth];
$name = $this->names[$position];
return $this->plugins[$name];
}
/**
* Part of Iterator Interface
*
* @return void
*/
public function rewind(): void
{
$this->positions[] = 0;
$this->loopDepth += 1;
}
/**
* Part of Iterator Interface
*
* @return bool
*/
public function valid(): bool
{
$valid = isset($this->names[$this->positions[$this->loopDepth]]);
if (!$valid) {
array_pop($this->positions);
$this->loopDepth -= 1;
}
return $valid;
}
/**
* Filter the plugins to those with the named hook enabled.
*
* @param string $hook The hook to filter plugins by
* @return \Generator<\Cake\Core\PluginInterface> A generator containing matching plugins.
* @throws \InvalidArgumentException on invalid hooks
*/
public function with(string $hook): Generator
{
if (!in_array($hook, PluginInterface::VALID_HOOKS, true)) {
throw new InvalidArgumentException("The `{$hook}` hook is not a known plugin hook.");
}
foreach ($this as $plugin) {
if ($plugin->isEnabled($hook)) {
yield $plugin;
}
}
}
}

135
vendor/cakephp/core/PluginInterface.php vendored Normal file
View File

@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (https://cakefoundation.org)
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Console\CommandCollection;
use Cake\Http\MiddlewareQueue;
use Cake\Routing\RouteBuilder;
/**
* Plugin Interface
*
* @method void services(\Cake\Core\ContainerInterface $container) Register plugin services to
* the application's container
*/
interface PluginInterface
{
/**
* List of valid hooks.
*
* @var array<string>
*/
public const VALID_HOOKS = ['bootstrap', 'console', 'middleware', 'routes', 'services'];
/**
* Get the name of this plugin.
*
* @return string
*/
public function getName(): string;
/**
* Get the filesystem path to this plugin
*
* @return string
*/
public function getPath(): string;
/**
* Get the filesystem path to configuration for this plugin
*
* @return string
*/
public function getConfigPath(): string;
/**
* Get the filesystem path to configuration for this plugin
*
* @return string
*/
public function getClassPath(): string;
/**
* Get the filesystem path to templates for this plugin
*
* @return string
*/
public function getTemplatePath(): string;
/**
* Load all the application configuration and bootstrap logic.
*
* The default implementation of this method will include the `config/bootstrap.php` in the plugin if it exist. You
* can override this method to replace that behavior.
*
* The host application is provided as an argument. This allows you to load additional
* plugin dependencies, or attach events.
*
* @param \Cake\Core\PluginApplicationInterface $app The host application
* @return void
*/
public function bootstrap(PluginApplicationInterface $app): void;
/**
* Add console commands for the plugin.
*
* @param \Cake\Console\CommandCollection $commands The command collection to update
* @return \Cake\Console\CommandCollection
*/
public function console(CommandCollection $commands): CommandCollection;
/**
* Add middleware for the plugin.
*
* @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to update.
* @return \Cake\Http\MiddlewareQueue
*/
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue;
/**
* Add routes for the plugin.
*
* The default implementation of this method will include the `config/routes.php` in the plugin if it exists. You
* can override this method to replace that behavior.
*
* @param \Cake\Routing\RouteBuilder $routes The route builder to update.
* @return void
*/
public function routes(RouteBuilder $routes): void;
/**
* Disables the named hook
*
* @param string $hook The hook to disable
* @return $this
*/
public function disable(string $hook);
/**
* Enables the named hook
*
* @param string $hook The hook to disable
* @return $this
*/
public function enable(string $hook);
/**
* Check if the named hook is enabled
*
* @param string $hook The hook to check
* @return bool
*/
public function isEnabled(string $hook): bool;
}

37
vendor/cakephp/core/README.md vendored Normal file
View File

@ -0,0 +1,37 @@
[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/core.svg?style=flat-square)](https://packagist.org/packages/cakephp/core)
[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt)
# CakePHP Core Classes
A set of classes used for configuration files reading and storing.
This repository contains the classes that are used as glue for creating the CakePHP framework.
## Usage
You can use the `Configure` class to store arbitrary configuration data:
```php
use Cake\Core\Configure;
use Cake\Core\Configure\Engine\PhpConfig;
Configure::write('Company.name','Pizza, Inc.');
Configure::read('Company.name'); // Returns: 'Pizza, Inc.'
```
It also possible to load configuration from external files:
```php
Configure::config('default', new PhpConfig('/path/to/config/folder'));
Configure::load('app', 'default', false);
Configure::load('other_config', 'default');
```
And write the configuration back into files:
```php
Configure::dump('my_config', 'default');
```
## Documentation
Please make sure you check the [official documentation](https://book.cakephp.org/4/en/development/configuration.html)

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Retry;
use Exception;
/**
* Allows any action to be retried in case of an exception.
*
* This class can be parametrized with a strategy, which will be followed
* to determine whether the action should be retried.
*/
class CommandRetry
{
/**
* The strategy to follow should the executed action fail.
*
* @var \Cake\Core\Retry\RetryStrategyInterface
*/
protected $strategy;
/**
* @var int
*/
protected $maxRetries;
/**
* @var int
*/
protected $numRetries;
/**
* Creates the CommandRetry object with the given strategy and retry count
*
* @param \Cake\Core\Retry\RetryStrategyInterface $strategy The strategy to follow should the action fail
* @param int $maxRetries The maximum number of retry attempts allowed
*/
public function __construct(RetryStrategyInterface $strategy, int $maxRetries = 1)
{
$this->strategy = $strategy;
$this->maxRetries = $maxRetries;
}
/**
* The number of retries to perform in case of failure
*
* @param callable $action The callable action to execute with a retry strategy
* @return mixed The return value of the passed action callable
* @throws \Exception
*/
public function run(callable $action)
{
$this->numRetries = 0;
while (true) {
try {
return $action();
} catch (Exception $e) {
if (
$this->numRetries < $this->maxRetries &&
$this->strategy->shouldRetry($e, $this->numRetries)
) {
$this->numRetries++;
continue;
}
throw $e;
}
}
}
/**
* Returns the last number of retry attemps.
*
* @return int
*/
public function getRetries(): int
{
return $this->numRetries;
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Retry;
use Exception;
/**
* Used to instruct a CommandRetry object on whether a retry
* for an action should be performed
*/
interface RetryStrategyInterface
{
/**
* Returns true if the action can be retried, false otherwise.
*
* @param \Exception $exception The exception that caused the action to fail
* @param int $retryCount The number of times action has been retried
* @return bool Whether it is OK to retry the action
*/
public function shouldRetry(Exception $exception, int $retryCount): bool;
}

50
vendor/cakephp/core/ServiceConfig.php vendored Normal file
View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
/**
* Read-only wrapper for configuration data
*
* Intended for use with {@link \Cake\Core\Container} as
* a typehintable way for services to have application
* configuration injected as arrays cannot be typehinted.
*/
class ServiceConfig
{
/**
* Read a configuration key
*
* @param string $path The path to read.
* @param mixed $default The default value to use if $path does not exist.
* @return mixed The configuration data or $default value.
*/
public function get(string $path, $default = null)
{
return Configure::read($path, $default);
}
/**
* Check if $path exists and has a non-null value.
*
* @param string $path The path to check.
* @return bool True if the configuration data exists.
*/
public function has(string $path): bool
{
return Configure::check($path);
}
}

132
vendor/cakephp/core/ServiceProvider.php vendored Normal file
View File

@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use League\Container\DefinitionContainerInterface;
use League\Container\ServiceProvider\AbstractServiceProvider;
use League\Container\ServiceProvider\BootableServiceProviderInterface;
use RuntimeException;
/**
* Container ServiceProvider
*
* Service provider bundle related services together helping
* to organize your application's dependencies. They also help
* improve performance of applications with many services by
* allowing service registration to be deferred until services are needed.
*/
abstract class ServiceProvider extends AbstractServiceProvider implements BootableServiceProviderInterface
{
/**
* List of ids of services this provider provides.
*
* @var array<string>
* @see ServiceProvider::provides()
*/
protected $provides = [];
/**
* Get the container.
*
* This method's actual return type and documented return type differ
* because PHP 7.2 doesn't support return type narrowing.
*
* @return \Cake\Core\ContainerInterface
*/
public function getContainer(): DefinitionContainerInterface
{
$container = parent::getContainer();
if (!($container instanceof ContainerInterface)) {
$message = sprintf(
'Unexpected container type. Expected `%s` got `%s` instead.',
ContainerInterface::class,
getTypeName($container)
);
throw new RuntimeException($message);
}
return $container;
}
/**
* Delegate to the bootstrap() method
*
* This method wraps the league/container function so users
* only need to use the CakePHP bootstrap() interface.
*
* @return void
*/
public function boot(): void
{
$this->bootstrap($this->getContainer());
}
/**
* Bootstrap hook for ServiceProviders
*
* This hook should be implemented if your service provider
* needs to register additional service providers, load configuration
* files or do any other work when the service provider is added to the
* container.
*
* @param \Cake\Core\ContainerInterface $container The container to add services to.
* @return void
*/
public function bootstrap(ContainerInterface $container): void
{
}
/**
* Call the abstract services() method.
*
* This method primarily exists as a shim between the interface
* that league/container has and the one we want to offer in CakePHP.
*
* @return void
*/
public function register(): void
{
$this->services($this->getContainer());
}
/**
* The provides method is a way to let the container know that a service
* is provided by this service provider.
*
* Every service that is registered via this service provider must have an
* alias added to this array or it will be ignored.
*
* @param string $id Identifier.
* @return bool
*/
public function provides(string $id): bool
{
return in_array($id, $this->provides, true);
}
/**
* Register the services in a provider.
*
* All services registered in this method should also be included in the $provides
* property so that services can be located.
*
* @param \Cake\Core\ContainerInterface $container The container to add services to.
* @return void
*/
abstract public function services(ContainerInterface $container): void;
}

View File

@ -0,0 +1,325 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use BadMethodCallException;
use InvalidArgumentException;
use LogicException;
/**
* A trait that provides a set of static methods to manage configuration
* for classes that provide an adapter facade or need to have sets of
* configuration data registered and manipulated.
*
* Implementing objects are expected to declare a static `$_dsnClassMap` property.
*/
trait StaticConfigTrait
{
/**
* Configuration sets.
*
* @var array<string, mixed>
*/
protected static $_config = [];
/**
* This method can be used to define configuration adapters for an application.
*
* To change an adapter's configuration at runtime, first drop the adapter and then
* reconfigure it.
*
* Adapters will not be constructed until the first operation is done.
*
* ### Usage
*
* Assuming that the class' name is `Cache` the following scenarios
* are supported:
*
* Setting a cache engine up.
*
* ```
* Cache::setConfig('default', $settings);
* ```
*
* Injecting a constructed adapter in:
*
* ```
* Cache::setConfig('default', $instance);
* ```
*
* Configure multiple adapters at once:
*
* ```
* Cache::setConfig($arrayOfConfig);
* ```
*
* @param array<string, mixed>|string $key The name of the configuration, or an array of multiple configs.
* @param mixed $config Configuration value. Generally an array of name => configuration data for adapter.
* @throws \BadMethodCallException When trying to modify an existing config.
* @throws \LogicException When trying to store an invalid structured config array.
* @return void
*/
public static function setConfig($key, $config = null): void
{
if ($config === null) {
if (!is_array($key)) {
throw new LogicException('If config is null, key must be an array.');
}
foreach ($key as $name => $settings) {
static::setConfig($name, $settings);
}
return;
}
if (isset(static::$_config[$key])) {
/** @psalm-suppress PossiblyInvalidArgument */
throw new BadMethodCallException(sprintf('Cannot reconfigure existing key "%s"', $key));
}
if (is_object($config)) {
$config = ['className' => $config];
}
if (is_array($config) && isset($config['url'])) {
$parsed = static::parseDsn($config['url']);
unset($config['url']);
$config = $parsed + $config;
}
if (isset($config['engine']) && empty($config['className'])) {
$config['className'] = $config['engine'];
unset($config['engine']);
}
/** @psalm-suppress InvalidPropertyAssignmentValue */
static::$_config[$key] = $config;
}
/**
* Reads existing configuration.
*
* @param string $key The name of the configuration.
* @return mixed|null Configuration data at the named key or null if the key does not exist.
*/
public static function getConfig(string $key)
{
return static::$_config[$key] ?? null;
}
/**
* Reads existing configuration for a specific key.
*
* The config value for this key must exist, it can never be null.
*
* @param string $key The name of the configuration.
* @return mixed Configuration data at the named key.
* @throws \InvalidArgumentException If value does not exist.
*/
public static function getConfigOrFail(string $key)
{
if (!isset(static::$_config[$key])) {
throw new InvalidArgumentException(sprintf('Expected configuration `%s` not found.', $key));
}
return static::$_config[$key];
}
/**
* Drops a constructed adapter.
*
* If you wish to modify an existing configuration, you should drop it,
* change configuration and then re-add it.
*
* If the implementing objects supports a `$_registry` object the named configuration
* will also be unloaded from the registry.
*
* @param string $config An existing configuration you wish to remove.
* @return bool Success of the removal, returns false when the config does not exist.
*/
public static function drop(string $config): bool
{
if (!isset(static::$_config[$config])) {
return false;
}
/** @psalm-suppress RedundantPropertyInitializationCheck */
if (isset(static::$_registry)) {
static::$_registry->unload($config);
}
unset(static::$_config[$config]);
return true;
}
/**
* Returns an array containing the named configurations
*
* @return array<string> Array of configurations.
*/
public static function configured(): array
{
$configurations = array_keys(static::$_config);
return array_map(function ($key) {
return (string)$key;
}, $configurations);
}
/**
* Parses a DSN into a valid connection configuration
*
* This method allows setting a DSN using formatting similar to that used by PEAR::DB.
* The following is an example of its usage:
*
* ```
* $dsn = 'mysql://user:pass@localhost/database?';
* $config = ConnectionManager::parseDsn($dsn);
*
* $dsn = 'Cake\Log\Engine\FileLog://?types=notice,info,debug&file=debug&path=LOGS';
* $config = Log::parseDsn($dsn);
*
* $dsn = 'smtp://user:secret@localhost:25?timeout=30&client=null&tls=null';
* $config = Email::parseDsn($dsn);
*
* $dsn = 'file:///?className=\My\Cache\Engine\FileEngine';
* $config = Cache::parseDsn($dsn);
*
* $dsn = 'File://?prefix=myapp_cake_core_&serialize=true&duration=+2 minutes&path=/tmp/persistent/';
* $config = Cache::parseDsn($dsn);
* ```
*
* For all classes, the value of `scheme` is set as the value of both the `className`
* unless they have been otherwise specified.
*
* Note that querystring arguments are also parsed and set as values in the returned configuration.
*
* @param string $dsn The DSN string to convert to a configuration array
* @return array<string, mixed> The configuration array to be stored after parsing the DSN
* @throws \InvalidArgumentException If not passed a string, or passed an invalid string
*/
public static function parseDsn(string $dsn): array
{
if (empty($dsn)) {
return [];
}
$pattern = <<<'REGEXP'
{
^
(?P<_scheme>
(?P<scheme>[\w\\\\]+)://
)
(?P<_username>
(?P<username>.*?)
(?P<_password>
:(?P<password>.*?)
)?
@
)?
(?P<_host>
(?P<host>[^?#/:@]+)
(?P<_port>
:(?P<port>\d+)
)?
)?
(?P<_path>
(?P<path>/[^?#]*)
)?
(?P<_query>
\?(?P<query>[^#]*)
)?
(?P<_fragment>
\#(?P<fragment>.*)
)?
$
}x
REGEXP;
preg_match($pattern, $dsn, $parsed);
if (!$parsed) {
throw new InvalidArgumentException("The DSN string '{$dsn}' could not be parsed.");
}
$exists = [];
foreach ($parsed as $k => $v) {
if (is_int($k)) {
unset($parsed[$k]);
} elseif (strpos($k, '_') === 0) {
$exists[substr($k, 1)] = ($v !== '');
unset($parsed[$k]);
} elseif ($v === '' && !$exists[$k]) {
unset($parsed[$k]);
}
}
$query = '';
if (isset($parsed['query'])) {
$query = $parsed['query'];
unset($parsed['query']);
}
parse_str($query, $queryArgs);
foreach ($queryArgs as $key => $value) {
if ($value === 'true') {
$queryArgs[$key] = true;
} elseif ($value === 'false') {
$queryArgs[$key] = false;
} elseif ($value === 'null') {
$queryArgs[$key] = null;
}
}
$parsed = $queryArgs + $parsed;
if (empty($parsed['className'])) {
$classMap = static::getDsnClassMap();
$parsed['className'] = $parsed['scheme'];
if (isset($classMap[$parsed['scheme']])) {
/** @psalm-suppress PossiblyNullArrayOffset */
$parsed['className'] = $classMap[$parsed['scheme']];
}
}
return $parsed;
}
/**
* Updates the DSN class map for this class.
*
* @param array<string, string> $map Additions/edits to the class map to apply.
* @return void
* @psalm-param array<string, class-string> $map
*/
public static function setDsnClassMap(array $map): void
{
static::$_dsnClassMap = $map + static::$_dsnClassMap;
}
/**
* Returns the DSN class map for this class.
*
* @return array<string, string>
* @psalm-return array<string, class-string>
*/
public static function getDsnClassMap(): array
{
return static::$_dsnClassMap;
}
}

View File

@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @since 4.2.0
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\TestSuite;
use Cake\Core\Configure;
use Cake\Core\ContainerInterface;
use Cake\Event\EventInterface;
use Closure;
use League\Container\Exception\NotFoundException;
use LogicException;
/**
* A set of methods used for defining container services
* in test cases.
*
* This trait leverages the `Application.buildContainer` event
* to inject the mocked services into the container that the
* application uses.
*/
trait ContainerStubTrait
{
/**
* The customized application class name.
*
* @psalm-var class-string<\Cake\Core\HttpApplicationInterface>|class-string<\Cake\Core\ConsoleApplicationInterface>|null
* @var string|null
*/
protected $_appClass;
/**
* The customized application constructor arguments.
*
* @var array|null
*/
protected $_appArgs;
/**
* The collection of container services.
*
* @var array
*/
private $containerServices = [];
/**
* Configure the application class to use in integration tests.
*
* @param string $class The application class name.
* @param array|null $constructorArgs The constructor arguments for your application class.
* @return void
* @psalm-param class-string<\Cake\Core\HttpApplicationInterface>|class-string<\Cake\Core\ConsoleApplicationInterface> $class
*/
public function configApplication(string $class, ?array $constructorArgs): void
{
$this->_appClass = $class;
$this->_appArgs = $constructorArgs;
}
/**
* Create an application instance.
*
* Uses the configuration set in `configApplication()`.
*
* @return \Cake\Core\HttpApplicationInterface|\Cake\Core\ConsoleApplicationInterface
*/
protected function createApp()
{
if ($this->_appClass) {
$appClass = $this->_appClass;
} else {
/** @psalm-var class-string<\Cake\Http\BaseApplication> */
$appClass = Configure::read('App.namespace') . '\Application';
}
if (!class_exists($appClass)) {
throw new LogicException("Cannot load `{$appClass}` for use in integration testing.");
}
$appArgs = $this->_appArgs ?: [CONFIG];
$app = new $appClass(...$appArgs);
if (!empty($this->containerServices) && method_exists($app, 'getEventManager')) {
$app->getEventManager()->on('Application.buildContainer', [$this, 'modifyContainer']);
}
return $app;
}
/**
* Add a mocked service to the container.
*
* When the container is created the provided classname
* will be mapped to the factory function. The factory
* function will be used to create mocked services.
*
* @param string $class The class or interface you want to define.
* @param \Closure $factory The factory function for mocked services.
* @return $this
*/
public function mockService(string $class, Closure $factory)
{
$this->containerServices[$class] = $factory;
return $this;
}
/**
* Remove a mocked service to the container.
*
* @param string $class The class or interface you want to remove.
* @return $this
*/
public function removeMockService(string $class)
{
unset($this->containerServices[$class]);
return $this;
}
/**
* Wrap the application's container with one containing mocks.
*
* If any mocked services are defined, the application's container
* will be replaced with one containing mocks. The original
* container will be set as a delegate to the mock container.
*
* @param \Cake\Event\EventInterface $event The event
* @param \Cake\Core\ContainerInterface $container The container to wrap.
* @return \Cake\Core\ContainerInterface|null
*/
public function modifyContainer(EventInterface $event, ContainerInterface $container): ?ContainerInterface
{
if (empty($this->containerServices)) {
return null;
}
foreach ($this->containerServices as $key => $factory) {
if ($container->has($key)) {
try {
$container->extend($key)->setConcrete($factory);
} catch (NotFoundException $e) {
$container->add($key, $factory);
}
} else {
$container->add($key, $factory);
}
}
return $container;
}
/**
* Clears any mocks that were defined and cleans
* up application class configuration.
*
* @after
* @return void
*/
public function cleanupContainer(): void
{
$this->_appArgs = null;
$this->_appClass = null;
$this->containerServices = [];
}
}
// phpcs:disable
class_alias(
'Cake\Core\TestSuite\ContainerStubTrait',
'Cake\TestSuite\ContainerStubTrait'
);
// phpcs:enable

44
vendor/cakephp/core/composer.json vendored Normal file
View File

@ -0,0 +1,44 @@
{
"name": "cakephp/core",
"description": "CakePHP Framework Core classes",
"type": "library",
"keywords": [
"cakephp",
"framework",
"core"
],
"homepage": "https://cakephp.org",
"license": "MIT",
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/core/graphs/contributors"
}
],
"support": {
"issues": "https://github.com/cakephp/cakephp/issues",
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"source": "https://github.com/cakephp/core"
},
"require": {
"php": ">=7.4.0",
"cakephp/utility": "^4.0"
},
"provide": {
"psr/container-implementation": "^1.0 || ^2.0"
},
"suggest": {
"cakephp/event": "To use PluginApplicationInterface or plugin applications.",
"cakephp/cache": "To use Configure::store() and restore().",
"league/container": "To use Container and ServiceProvider classes"
},
"autoload": {
"psr-4": {
"Cake\\Core\\": "."
},
"files": [
"functions.php"
]
}
}

340
vendor/cakephp/core/functions.php vendored Normal file
View File

@ -0,0 +1,340 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.5.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
// phpcs:disable PSR1.Files.SideEffects
namespace Cake\Core;
if (!function_exists('Cake\Core\h')) {
/**
* Convenience method for htmlspecialchars.
*
* @param mixed $text Text to wrap through htmlspecialchars. Also works with arrays, and objects.
* Arrays will be mapped and have all their elements escaped. Objects will be string cast if they
* implement a `__toString` method. Otherwise, the class name will be used.
* Other scalar types will be returned unchanged.
* @param bool $double Encode existing html entities.
* @param string|null $charset Character set to use when escaping.
* Defaults to config value in `mb_internal_encoding()` or 'UTF-8'.
* @return mixed Wrapped text.
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#h
*/
function h($text, bool $double = true, ?string $charset = null)
{
if (is_string($text)) {
//optimize for strings
} elseif (is_array($text)) {
$texts = [];
foreach ($text as $k => $t) {
$texts[$k] = h($t, $double, $charset);
}
return $texts;
} elseif (is_object($text)) {
if (method_exists($text, '__toString')) {
$text = $text->__toString();
} else {
$text = '(object)' . get_class($text);
}
} elseif ($text === null || is_scalar($text)) {
return $text;
}
static $defaultCharset = false;
if ($defaultCharset === false) {
$defaultCharset = mb_internal_encoding() ?: 'UTF-8';
}
return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, $charset ?: $defaultCharset, $double);
}
}
if (!function_exists('Cake\Core\pluginSplit')) {
/**
* Splits a dot syntax plugin name into its plugin and class name.
* If $name does not have a dot, then index 0 will be null.
*
* Commonly used like
* ```
* list($plugin, $name) = pluginSplit($name);
* ```
*
* @param string $name The name you want to plugin split.
* @param bool $dotAppend Set to true if you want the plugin to have a '.' appended to it.
* @param string|null $plugin Optional default plugin to use if no plugin is found. Defaults to null.
* @return array Array with 2 indexes. 0 => plugin name, 1 => class name.
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#pluginSplit
* @psalm-return array{string|null, string}
*/
function pluginSplit(string $name, bool $dotAppend = false, ?string $plugin = null): array
{
if (strpos($name, '.') !== false) {
$parts = explode('.', $name, 2);
if ($dotAppend) {
$parts[0] .= '.';
}
/** @psalm-var array{string, string} */
return $parts;
}
return [$plugin, $name];
}
}
if (!function_exists('Cake\Core\namespaceSplit')) {
/**
* Split the namespace from the classname.
*
* Commonly used like `list($namespace, $className) = namespaceSplit($class);`.
*
* @param string $class The full class name, ie `Cake\Core\App`.
* @return array<string> Array with 2 indexes. 0 => namespace, 1 => classname.
*/
function namespaceSplit(string $class): array
{
$pos = strrpos($class, '\\');
if ($pos === false) {
return ['', $class];
}
return [substr($class, 0, $pos), substr($class, $pos + 1)];
}
}
if (!function_exists('Cake\Core\pr')) {
/**
* print_r() convenience function.
*
* In terminals this will act similar to using print_r() directly, when not run on CLI
* print_r() will also wrap `<pre>` tags around the output of given variable. Similar to debug().
*
* This function returns the same variable that was passed.
*
* @param mixed $var Variable to print out.
* @return mixed the same $var that was passed to this function
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#pr
* @see debug()
*/
function pr($var)
{
if (!Configure::read('debug')) {
return $var;
}
$template = PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg' ? '<pre class="pr">%s</pre>' : "\n%s\n\n";
printf($template, trim(print_r($var, true)));
return $var;
}
}
if (!function_exists('Cake\Core\pj')) {
/**
* JSON pretty print convenience function.
*
* In terminals this will act similar to using json_encode() with JSON_PRETTY_PRINT directly, when not run on CLI
* will also wrap `<pre>` tags around the output of given variable. Similar to pr().
*
* This function returns the same variable that was passed.
*
* @param mixed $var Variable to print out.
* @return mixed the same $var that was passed to this function
* @see pr()
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#pj
*/
function pj($var)
{
if (!Configure::read('debug')) {
return $var;
}
$template = PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg' ? '<pre class="pj">%s</pre>' : "\n%s\n\n";
printf($template, trim(json_encode($var, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)));
return $var;
}
}
if (!function_exists('Cake\Core\env')) {
/**
* Gets an environment variable from available sources, and provides emulation
* for unsupported or inconsistent environment variables (i.e. DOCUMENT_ROOT on
* IIS, or SCRIPT_NAME in CGI mode). Also exposes some additional custom
* environment information.
*
* @param string $key Environment variable name.
* @param string|bool|null $default Specify a default value in case the environment variable is not defined.
* @return string|bool|null Environment variable setting.
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#env
*/
function env(string $key, $default = null)
{
if ($key === 'HTTPS') {
if (isset($_SERVER['HTTPS'])) {
return !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
}
return strpos((string)env('SCRIPT_URI'), 'https://') === 0;
}
if ($key === 'SCRIPT_NAME' && env('CGI_MODE') && isset($_ENV['SCRIPT_URL'])) {
$key = 'SCRIPT_URL';
}
/** @var string|null $val */
$val = $_SERVER[$key] ?? $_ENV[$key] ?? null;
if ($val == null && getenv($key) !== false) {
/** @var string|false $val */
$val = getenv($key);
}
if ($key === 'REMOTE_ADDR' && $val === env('SERVER_ADDR')) {
$addr = env('HTTP_PC_REMOTE_ADDR');
if ($addr !== null) {
$val = $addr;
}
}
if ($val !== null) {
return $val;
}
switch ($key) {
case 'DOCUMENT_ROOT':
$name = (string)env('SCRIPT_NAME');
$filename = (string)env('SCRIPT_FILENAME');
$offset = 0;
if (!strpos($name, '.php')) {
$offset = 4;
}
return substr($filename, 0, -(strlen($name) + $offset));
case 'PHP_SELF':
return str_replace((string)env('DOCUMENT_ROOT'), '', (string)env('SCRIPT_FILENAME'));
case 'CGI_MODE':
return PHP_SAPI === 'cgi';
}
return $default;
}
}
if (!function_exists('Cake\Core\triggerWarning')) {
/**
* Triggers an E_USER_WARNING.
*
* @param string $message The warning message.
* @return void
*/
function triggerWarning(string $message): void
{
$trace = debug_backtrace();
if (isset($trace[1])) {
$frame = $trace[1];
$frame += ['file' => '[internal]', 'line' => '??'];
$message = sprintf(
'%s - %s, line: %s',
$message,
$frame['file'],
$frame['line']
);
}
trigger_error($message, E_USER_WARNING);
}
}
if (!function_exists('Cake\Core\deprecationWarning')) {
/**
* Helper method for outputting deprecation warnings
*
* @param string $message The message to output as a deprecation warning.
* @param int $stackFrame The stack frame to include in the error. Defaults to 1
* as that should point to application/plugin code.
* @return void
*/
function deprecationWarning(string $message, int $stackFrame = 1): void
{
if (!(error_reporting() & E_USER_DEPRECATED)) {
return;
}
$trace = debug_backtrace();
if (isset($trace[$stackFrame])) {
$frame = $trace[$stackFrame];
$frame += ['file' => '[internal]', 'line' => '??'];
// Assuming we're installed in vendor/cakephp/cakephp/src/Core/functions.php
$root = dirname(__DIR__, 5);
if (defined('ROOT')) {
$root = ROOT;
}
$relative = str_replace(
DIRECTORY_SEPARATOR,
'/',
substr($frame['file'], strlen($root) + 1)
);
$patterns = (array)Configure::read('Error.ignoredDeprecationPaths');
foreach ($patterns as $pattern) {
$pattern = str_replace(DIRECTORY_SEPARATOR, '/', $pattern);
if (fnmatch($pattern, $relative)) {
return;
}
}
$message = sprintf(
"%s\n%s, line: %s\n" . 'You can disable all deprecation warnings by setting `Error.errorLevel` to ' .
'`E_ALL & ~E_USER_DEPRECATED`. Adding `%s` to `Error.ignoredDeprecationPaths` ' .
'in your `config/app.php` config will mute deprecations from that file only.',
$message,
$frame['file'],
$frame['line'],
$relative
);
}
static $errors = [];
$checksum = md5($message);
$duplicate = (bool)Configure::read('Error.allowDuplicateDeprecations', false);
if (isset($errors[$checksum]) && !$duplicate) {
return;
}
if (!$duplicate) {
$errors[$checksum] = true;
}
trigger_error($message, E_USER_DEPRECATED);
}
}
if (!function_exists('Cake\Core\getTypeName')) {
/**
* Returns the objects class or var type of it's not an object
*
* @param mixed $var Variable to check
* @return string Returns the class name or variable type
*/
function getTypeName($var): string
{
return is_object($var) ? get_class($var) : gettype($var);
}
}
/**
* Include global functions.
*/
if (!getenv('CAKE_DISABLE_GLOBAL_FUNCS')) {
include 'functions_global.php';
}

190
vendor/cakephp/core/functions_global.php vendored Normal file
View File

@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
use function Cake\Core\deprecationWarning as cakeDeprecationWarning;
use function Cake\Core\env as cakeEnv;
use function Cake\Core\getTypeName as cakeGetTypeName;
use function Cake\Core\h as cakeH;
use function Cake\Core\namespaceSplit as cakeNamespaceSplit;
use function Cake\Core\pj as cakePj;
use function Cake\Core\pluginSplit as cakePluginSplit;
use function Cake\Core\pr as cakePr;
use function Cake\Core\triggerWarning as cakeTriggerWarning;
if (!defined('DS')) {
/**
* Defines DS as short form of DIRECTORY_SEPARATOR.
*/
define('DS', DIRECTORY_SEPARATOR);
}
if (!function_exists('h')) {
/**
* Convenience method for htmlspecialchars.
*
* @param mixed $text Text to wrap through htmlspecialchars. Also works with arrays, and objects.
* Arrays will be mapped and have all their elements escaped. Objects will be string cast if they
* implement a `__toString` method. Otherwise, the class name will be used.
* Other scalar types will be returned unchanged.
* @param bool $double Encode existing html entities.
* @param string|null $charset Character set to use when escaping.
* Defaults to config value in `mb_internal_encoding()` or 'UTF-8'.
* @return mixed Wrapped text.
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#h
*/
function h($text, bool $double = true, ?string $charset = null)
{
return cakeH($text, $double, $charset);
}
}
if (!function_exists('pluginSplit')) {
/**
* Splits a dot syntax plugin name into its plugin and class name.
* If $name does not have a dot, then index 0 will be null.
*
* Commonly used like
* ```
* list($plugin, $name) = pluginSplit($name);
* ```
*
* @param string $name The name you want to plugin split.
* @param bool $dotAppend Set to true if you want the plugin to have a '.' appended to it.
* @param string|null $plugin Optional default plugin to use if no plugin is found. Defaults to null.
* @return array Array with 2 indexes. 0 => plugin name, 1 => class name.
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#pluginSplit
* @psalm-return array{string|null, string}
*/
function pluginSplit(string $name, bool $dotAppend = false, ?string $plugin = null): array
{
return cakePluginSplit($name, $dotAppend, $plugin);
}
}
if (!function_exists('namespaceSplit')) {
/**
* Split the namespace from the classname.
*
* Commonly used like `list($namespace, $className) = namespaceSplit($class);`.
*
* @param string $class The full class name, ie `Cake\Core\App`.
* @return array<string> Array with 2 indexes. 0 => namespace, 1 => classname.
*/
function namespaceSplit(string $class): array
{
return cakeNamespaceSplit($class);
}
}
if (!function_exists('pr')) {
/**
* print_r() convenience function.
*
* In terminals this will act similar to using print_r() directly, when not run on CLI
* print_r() will also wrap `<pre>` tags around the output of given variable. Similar to debug().
*
* This function returns the same variable that was passed.
*
* @param mixed $var Variable to print out.
* @return mixed the same $var that was passed to this function
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#pr
* @see debug()
*/
function pr($var)
{
return cakePr($var);
}
}
if (!function_exists('pj')) {
/**
* JSON pretty print convenience function.
*
* In terminals this will act similar to using json_encode() with JSON_PRETTY_PRINT directly, when not run on CLI
* will also wrap `<pre>` tags around the output of given variable. Similar to pr().
*
* This function returns the same variable that was passed.
*
* @param mixed $var Variable to print out.
* @return mixed the same $var that was passed to this function
* @see pr()
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#pj
*/
function pj($var)
{
return cakePj($var);
}
}
if (!function_exists('env')) {
/**
* Gets an environment variable from available sources, and provides emulation
* for unsupported or inconsistent environment variables (i.e. DOCUMENT_ROOT on
* IIS, or SCRIPT_NAME in CGI mode). Also exposes some additional custom
* environment information.
*
* @param string $key Environment variable name.
* @param string|bool|null $default Specify a default value in case the environment variable is not defined.
* @return string|bool|null Environment variable setting.
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#env
*/
function env(string $key, $default = null)
{
return cakeEnv($key, $default);
}
}
if (!function_exists('triggerWarning')) {
/**
* Triggers an E_USER_WARNING.
*
* @param string $message The warning message.
* @return void
*/
function triggerWarning(string $message): void
{
cakeTriggerWarning($message);
}
}
if (!function_exists('deprecationWarning')) {
/**
* Helper method for outputting deprecation warnings
*
* @param string $message The message to output as a deprecation warning.
* @param int $stackFrame The stack frame to include in the error. Defaults to 1
* as that should point to application/plugin code.
* @return void
*/
function deprecationWarning(string $message, int $stackFrame = 1): void
{
cakeDeprecationWarning($message, $stackFrame + 1);
}
}
if (!function_exists('getTypeName')) {
/**
* Returns the objects class or var type of it's not an object
*
* @param mixed $var Variable to check
* @return string Returns the class name or variable type
*/
function getTypeName($var): string
{
return cakeGetTypeName($var);
}
}

1197
vendor/cakephp/database/Connection.php vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database;
use Cake\Datasource\ConnectionInterface;
/**
* Defines the interface for a fixture that needs to manage constraints.
*
* If an implementation of `Cake\Datasource\FixtureInterface` also implements
* this interface, the FixtureManager will use these methods to manage
* a fixtures constraints.
*/
interface ConstraintsInterface
{
/**
* Build and execute SQL queries necessary to create the constraints for the
* fixture
*
* @param \Cake\Datasource\ConnectionInterface $connection An instance of the database
* into which the constraints will be created.
* @return bool on success or if there are no constraints to create, or false on failure
*/
public function createConstraints(ConnectionInterface $connection): bool;
/**
* Build and execute SQL queries necessary to drop the constraints for the
* fixture
*
* @param \Cake\Datasource\ConnectionInterface $connection An instance of the database
* into which the constraints will be dropped.
* @return bool on success or if there are no constraints to drop, or false on failure
*/
public function dropConstraints(ConnectionInterface $connection): bool;
}

559
vendor/cakephp/database/Driver.php vendored Normal file
View File

@ -0,0 +1,559 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database;
use Cake\Core\App;
use Cake\Core\Retry\CommandRetry;
use Cake\Database\Exception\MissingConnectionException;
use Cake\Database\Retry\ErrorCodeWaitStrategy;
use Cake\Database\Schema\SchemaDialect;
use Cake\Database\Schema\TableSchema;
use Cake\Database\Statement\PDOStatement;
use Closure;
use InvalidArgumentException;
use PDO;
use PDOException;
use function Cake\Core\deprecationWarning;
/**
* Represents a database driver containing all specificities for
* a database engine including its SQL dialect.
*/
abstract class Driver implements DriverInterface
{
/**
* @var int|null Maximum alias length or null if no limit
*/
protected const MAX_ALIAS_LENGTH = null;
/**
* @var array<int> DB-specific error codes that allow connect retry
*/
protected const RETRY_ERROR_CODES = [];
/**
* Instance of PDO.
*
* @var \PDO
*/
protected $_connection;
/**
* Configuration data.
*
* @var array<string, mixed>
*/
protected $_config;
/**
* Base configuration that is merged into the user
* supplied configuration data.
*
* @var array<string, mixed>
*/
protected $_baseConfig = [];
/**
* Indicates whether the driver is doing automatic identifier quoting
* for all queries
*
* @var bool
*/
protected $_autoQuoting = false;
/**
* The server version
*
* @var string|null
*/
protected $_version;
/**
* The last number of connection retry attempts.
*
* @var int
*/
protected $connectRetries = 0;
/**
* Constructor
*
* @param array<string, mixed> $config The configuration for the driver.
* @throws \InvalidArgumentException
*/
public function __construct(array $config = [])
{
if (empty($config['username']) && !empty($config['login'])) {
throw new InvalidArgumentException(
'Please pass "username" instead of "login" for connecting to the database'
);
}
$config += $this->_baseConfig;
$this->_config = $config;
if (!empty($config['quoteIdentifiers'])) {
$this->enableAutoQuoting();
}
}
/**
* Get the configuration data used to create the driver.
*
* @return array<string, mixed>
*/
public function config(): array
{
return $this->_config;
}
/**
* Establishes a connection to the database server
*
* @param string $dsn A Driver-specific PDO-DSN
* @param array<string, mixed> $config configuration to be used for creating connection
* @return bool true on success
*/
protected function _connect(string $dsn, array $config): bool
{
$action = function () use ($dsn, $config) {
$this->setConnection(new PDO(
$dsn,
$config['username'] ?: null,
$config['password'] ?: null,
$config['flags']
));
};
$retry = new CommandRetry(new ErrorCodeWaitStrategy(static::RETRY_ERROR_CODES, 5), 4);
try {
$retry->run($action);
} catch (PDOException $e) {
throw new MissingConnectionException(
[
'driver' => App::shortName(static::class, 'Database/Driver'),
'reason' => $e->getMessage(),
],
null,
$e
);
} finally {
$this->connectRetries = $retry->getRetries();
}
return true;
}
/**
* @inheritDoc
*/
abstract public function connect(): bool;
/**
* @inheritDoc
*/
public function disconnect(): void
{
/** @psalm-suppress PossiblyNullPropertyAssignmentValue */
$this->_connection = null;
$this->_version = null;
}
/**
* Returns connected server version.
*
* @return string
*/
public function version(): string
{
if ($this->_version === null) {
$this->connect();
$this->_version = (string)$this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION);
}
return $this->_version;
}
/**
* Get the internal PDO connection instance.
*
* @return \PDO
*/
public function getConnection()
{
if ($this->_connection === null) {
throw new MissingConnectionException([
'driver' => App::shortName(static::class, 'Database/Driver'),
'reason' => 'Unknown',
]);
}
return $this->_connection;
}
/**
* Set the internal PDO connection instance.
*
* @param \PDO $connection PDO instance.
* @return $this
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function setConnection($connection)
{
$this->_connection = $connection;
return $this;
}
/**
* @inheritDoc
*/
abstract public function enabled(): bool;
/**
* @inheritDoc
*/
public function prepare($query): StatementInterface
{
$this->connect();
$statement = $this->_connection->prepare($query instanceof Query ? $query->sql() : $query);
return new PDOStatement($statement, $this);
}
/**
* @inheritDoc
*/
public function beginTransaction(): bool
{
$this->connect();
if ($this->_connection->inTransaction()) {
return true;
}
return $this->_connection->beginTransaction();
}
/**
* @inheritDoc
*/
public function commitTransaction(): bool
{
$this->connect();
if (!$this->_connection->inTransaction()) {
return false;
}
return $this->_connection->commit();
}
/**
* @inheritDoc
*/
public function rollbackTransaction(): bool
{
$this->connect();
if (!$this->_connection->inTransaction()) {
return false;
}
return $this->_connection->rollBack();
}
/**
* Returns whether a transaction is active for connection.
*
* @return bool
*/
public function inTransaction(): bool
{
$this->connect();
return $this->_connection->inTransaction();
}
/**
* @inheritDoc
*/
public function supportsSavePoints(): bool
{
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
return $this->supports(static::FEATURE_SAVEPOINT);
}
/**
* Returns true if the server supports common table expressions.
*
* @return bool
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_QUOTE)` instead
*/
public function supportsCTEs(): bool
{
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
return $this->supports(static::FEATURE_CTE);
}
/**
* @inheritDoc
*/
public function quote($value, $type = PDO::PARAM_STR): string
{
$this->connect();
return $this->_connection->quote((string)$value, $type);
}
/**
* Checks if the driver supports quoting, as PDO_ODBC does not support it.
*
* @return bool
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_QUOTE)` instead
*/
public function supportsQuoting(): bool
{
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
return $this->supports(static::FEATURE_QUOTE);
}
/**
* @inheritDoc
*/
abstract public function queryTranslator(string $type): Closure;
/**
* @inheritDoc
*/
abstract public function schemaDialect(): SchemaDialect;
/**
* @inheritDoc
*/
abstract public function quoteIdentifier(string $identifier): string;
/**
* @inheritDoc
*/
public function schemaValue($value): string
{
if ($value === null) {
return 'NULL';
}
if ($value === false) {
return 'FALSE';
}
if ($value === true) {
return 'TRUE';
}
if (is_float($value)) {
return str_replace(',', '.', (string)$value);
}
/** @psalm-suppress InvalidArgument */
if (
(
is_int($value) ||
$value === '0'
) ||
(
is_numeric($value) &&
strpos($value, ',') === false &&
substr($value, 0, 1) !== '0' &&
strpos($value, 'e') === false
)
) {
return (string)$value;
}
return $this->_connection->quote((string)$value, PDO::PARAM_STR);
}
/**
* @inheritDoc
*/
public function schema(): string
{
return $this->_config['schema'];
}
/**
* @inheritDoc
*/
public function lastInsertId(?string $table = null, ?string $column = null)
{
$this->connect();
if ($this->_connection instanceof PDO) {
return $this->_connection->lastInsertId($table);
}
return $this->_connection->lastInsertId($table);
}
/**
* @inheritDoc
*/
public function isConnected(): bool
{
if ($this->_connection === null) {
$connected = false;
} else {
try {
$connected = (bool)$this->_connection->query('SELECT 1');
} catch (PDOException $e) {
$connected = false;
}
}
return $connected;
}
/**
* @inheritDoc
*/
public function enableAutoQuoting(bool $enable = true)
{
$this->_autoQuoting = $enable;
return $this;
}
/**
* @inheritDoc
*/
public function disableAutoQuoting()
{
$this->_autoQuoting = false;
return $this;
}
/**
* @inheritDoc
*/
public function isAutoQuotingEnabled(): bool
{
return $this->_autoQuoting;
}
/**
* Returns whether the driver supports the feature.
*
* Defaults to true for FEATURE_QUOTE and FEATURE_SAVEPOINT.
*
* @param string $feature Driver feature name
* @return bool
*/
public function supports(string $feature): bool
{
switch ($feature) {
case static::FEATURE_DISABLE_CONSTRAINT_WITHOUT_TRANSACTION:
case static::FEATURE_QUOTE:
case static::FEATURE_SAVEPOINT:
return true;
}
return false;
}
/**
* @inheritDoc
*/
public function compileQuery(Query $query, ValueBinder $binder): array
{
$processor = $this->newCompiler();
$translator = $this->queryTranslator($query->type());
$query = $translator($query);
return [$query, $processor->compile($query, $binder)];
}
/**
* @inheritDoc
*/
public function newCompiler(): QueryCompiler
{
return new QueryCompiler();
}
/**
* @inheritDoc
*/
public function newTableSchema(string $table, array $columns = []): TableSchema
{
$className = TableSchema::class;
if (isset($this->_config['tableSchema'])) {
/** @var class-string<\Cake\Database\Schema\TableSchema> $className */
$className = $this->_config['tableSchema'];
}
return new $className($table, $columns);
}
/**
* Returns the maximum alias length allowed.
* This can be different from the maximum identifier length for columns.
*
* @return int|null Maximum alias length or null if no limit
*/
public function getMaxAliasLength(): ?int
{
return static::MAX_ALIAS_LENGTH;
}
/**
* Returns the number of connection retry attempts made.
*
* @return int
*/
public function getConnectRetries(): int
{
return $this->connectRetries;
}
/**
* Returns the connection role this driver performs.
*
* @return string
*/
public function getRole(): string
{
return $this->_config['_role'] ?? Connection::ROLE_WRITE;
}
/**
* Destructor
*/
public function __destruct()
{
/** @psalm-suppress PossiblyNullPropertyAssignmentValue */
$this->_connection = null;
}
/**
* Returns an array that can be used to describe the internal state of this
* object.
*
* @return array<string, mixed>
*/
public function __debugInfo(): array
{
return [
'connected' => $this->_connection !== null,
'role' => $this->getRole(),
];
}
}

345
vendor/cakephp/database/Driver/Mysql.php vendored Normal file
View File

@ -0,0 +1,345 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Driver;
use Cake\Database\Driver;
use Cake\Database\Query;
use Cake\Database\Schema\MysqlSchemaDialect;
use Cake\Database\Schema\SchemaDialect;
use Cake\Database\Statement\MysqlStatement;
use Cake\Database\StatementInterface;
use PDO;
use function Cake\Core\deprecationWarning;
/**
* MySQL Driver
*/
class Mysql extends Driver
{
use SqlDialectTrait;
/**
* @inheritDoc
*/
protected const MAX_ALIAS_LENGTH = 256;
/**
* Server type MySQL
*
* @var string
*/
protected const SERVER_TYPE_MYSQL = 'mysql';
/**
* Server type MariaDB
*
* @var string
*/
protected const SERVER_TYPE_MARIADB = 'mariadb';
/**
* Base configuration settings for MySQL driver
*
* @var array<string, mixed>
*/
protected $_baseConfig = [
'persistent' => true,
'host' => 'localhost',
'username' => 'root',
'password' => '',
'database' => 'cake',
'port' => '3306',
'flags' => [],
'encoding' => 'utf8mb4',
'timezone' => null,
'init' => [],
];
/**
* The schema dialect for this driver
*
* @var \Cake\Database\Schema\MysqlSchemaDialect|null
*/
protected $_schemaDialect;
/**
* String used to start a database identifier quoting to make it safe
*
* @var string
*/
protected $_startQuote = '`';
/**
* String used to end a database identifier quoting to make it safe
*
* @var string
*/
protected $_endQuote = '`';
/**
* Server type.
*
* If the underlying server is MariaDB, its value will get set to `'mariadb'`
* after `version()` method is called.
*
* @var string
*/
protected $serverType = self::SERVER_TYPE_MYSQL;
/**
* Mapping of feature to db server version for feature availability checks.
*
* @var array<string, array<string, string>>
*/
protected $featureVersions = [
'mysql' => [
'json' => '5.7.0',
'cte' => '8.0.0',
'window' => '8.0.0',
],
'mariadb' => [
'json' => '10.2.7',
'cte' => '10.2.1',
'window' => '10.2.0',
],
];
/**
* Establishes a connection to the database server
*
* @return bool true on success
*/
public function connect(): bool
{
if ($this->_connection) {
return true;
}
$config = $this->_config;
if ($config['timezone'] === 'UTC') {
$config['timezone'] = '+0:00';
}
if (!empty($config['timezone'])) {
$config['init'][] = sprintf("SET time_zone = '%s'", $config['timezone']);
}
$config['flags'] += [
PDO::ATTR_PERSISTENT => $config['persistent'],
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
if (!empty($config['ssl_key']) && !empty($config['ssl_cert'])) {
$config['flags'][PDO::MYSQL_ATTR_SSL_KEY] = $config['ssl_key'];
$config['flags'][PDO::MYSQL_ATTR_SSL_CERT] = $config['ssl_cert'];
}
if (!empty($config['ssl_ca'])) {
$config['flags'][PDO::MYSQL_ATTR_SSL_CA] = $config['ssl_ca'];
}
if (empty($config['unix_socket'])) {
$dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']}";
} else {
$dsn = "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}";
}
if (!empty($config['encoding'])) {
$dsn .= ";charset={$config['encoding']}";
}
$this->_connect($dsn, $config);
if (!empty($config['init'])) {
$connection = $this->getConnection();
foreach ((array)$config['init'] as $command) {
$connection->exec($command);
}
}
return true;
}
/**
* Returns whether php is able to use this driver for connecting to database
*
* @return bool true if it is valid to use this driver
*/
public function enabled(): bool
{
return in_array('mysql', PDO::getAvailableDrivers(), true);
}
/**
* Prepares a sql statement to be executed
*
* @param \Cake\Database\Query|string $query The query to prepare.
* @return \Cake\Database\StatementInterface
*/
public function prepare($query): StatementInterface
{
$this->connect();
$isObject = $query instanceof Query;
/**
* @psalm-suppress PossiblyInvalidMethodCall
* @psalm-suppress PossiblyInvalidArgument
*/
$statement = $this->_connection->prepare($isObject ? $query->sql() : $query);
$result = new MysqlStatement($statement, $this);
/** @psalm-suppress PossiblyInvalidMethodCall */
if ($isObject && $query->isBufferedResultsEnabled() === false) {
$result->bufferResults(false);
}
return $result;
}
/**
* @inheritDoc
*/
public function schemaDialect(): SchemaDialect
{
if ($this->_schemaDialect === null) {
$this->_schemaDialect = new MysqlSchemaDialect($this);
}
return $this->_schemaDialect;
}
/**
* @inheritDoc
*/
public function schema(): string
{
return $this->_config['database'];
}
/**
* @inheritDoc
*/
public function disableForeignKeySQL(): string
{
return 'SET foreign_key_checks = 0';
}
/**
* @inheritDoc
*/
public function enableForeignKeySQL(): string
{
return 'SET foreign_key_checks = 1';
}
/**
* @inheritDoc
*/
public function supports(string $feature): bool
{
switch ($feature) {
case static::FEATURE_CTE:
case static::FEATURE_JSON:
case static::FEATURE_WINDOW:
return version_compare(
$this->version(),
$this->featureVersions[$this->serverType][$feature],
'>='
);
}
return parent::supports($feature);
}
/**
* @inheritDoc
*/
public function supportsDynamicConstraints(): bool
{
return true;
}
/**
* Returns true if the connected server is MariaDB.
*
* @return bool
*/
public function isMariadb(): bool
{
$this->version();
return $this->serverType === static::SERVER_TYPE_MARIADB;
}
/**
* Returns connected server version.
*
* @return string
*/
public function version(): string
{
if ($this->_version === null) {
$this->connect();
$this->_version = (string)$this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION);
if (strpos($this->_version, 'MariaDB') !== false) {
$this->serverType = static::SERVER_TYPE_MARIADB;
preg_match('/^(?:5\.5\.5-)?(\d+\.\d+\.\d+.*-MariaDB[^:]*)/', $this->_version, $matches);
$this->_version = $matches[1];
}
}
return $this->_version;
}
/**
* Returns true if the server supports common table expressions.
*
* @return bool
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_CTE)` instead
*/
public function supportsCTEs(): bool
{
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
return $this->supports(static::FEATURE_CTE);
}
/**
* Returns true if the server supports native JSON columns
*
* @return bool
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_JSON)` instead
*/
public function supportsNativeJson(): bool
{
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
return $this->supports(static::FEATURE_JSON);
}
/**
* Returns true if the connected server supports window functions.
*
* @return bool
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_WINDOW)` instead
*/
public function supportsWindowFunctions(): bool
{
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
return $this->supports(static::FEATURE_WINDOW);
}
}

View File

@ -0,0 +1,348 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Driver;
use Cake\Database\Driver;
use Cake\Database\Expression\FunctionExpression;
use Cake\Database\Expression\IdentifierExpression;
use Cake\Database\Expression\StringExpression;
use Cake\Database\PostgresCompiler;
use Cake\Database\Query;
use Cake\Database\QueryCompiler;
use Cake\Database\Schema\PostgresSchemaDialect;
use Cake\Database\Schema\SchemaDialect;
use PDO;
/**
* Class Postgres
*/
class Postgres extends Driver
{
use SqlDialectTrait;
/**
* @inheritDoc
*/
protected const MAX_ALIAS_LENGTH = 63;
/**
* Base configuration settings for Postgres driver
*
* @var array<string, mixed>
*/
protected $_baseConfig = [
'persistent' => true,
'host' => 'localhost',
'username' => 'root',
'password' => '',
'database' => 'cake',
'schema' => 'public',
'port' => 5432,
'encoding' => 'utf8',
'timezone' => null,
'flags' => [],
'init' => [],
];
/**
* The schema dialect class for this driver
*
* @var \Cake\Database\Schema\PostgresSchemaDialect|null
*/
protected $_schemaDialect;
/**
* String used to start a database identifier quoting to make it safe
*
* @var string
*/
protected $_startQuote = '"';
/**
* String used to end a database identifier quoting to make it safe
*
* @var string
*/
protected $_endQuote = '"';
/**
* Establishes a connection to the database server
*
* @return bool true on success
*/
public function connect(): bool
{
if ($this->_connection) {
return true;
}
$config = $this->_config;
$config['flags'] += [
PDO::ATTR_PERSISTENT => $config['persistent'],
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
if (empty($config['unix_socket'])) {
$dsn = "pgsql:host={$config['host']};port={$config['port']};dbname={$config['database']}";
} else {
$dsn = "pgsql:dbname={$config['database']}";
}
$this->_connect($dsn, $config);
$this->_connection = $connection = $this->getConnection();
if (!empty($config['encoding'])) {
$this->setEncoding($config['encoding']);
}
if (!empty($config['schema'])) {
$this->setSchema($config['schema']);
}
if (!empty($config['timezone'])) {
$config['init'][] = sprintf('SET timezone = %s', $connection->quote($config['timezone']));
}
foreach ($config['init'] as $command) {
$connection->exec($command);
}
return true;
}
/**
* Returns whether php is able to use this driver for connecting to database
*
* @return bool true if it is valid to use this driver
*/
public function enabled(): bool
{
return in_array('pgsql', PDO::getAvailableDrivers(), true);
}
/**
* @inheritDoc
*/
public function schemaDialect(): SchemaDialect
{
if ($this->_schemaDialect === null) {
$this->_schemaDialect = new PostgresSchemaDialect($this);
}
return $this->_schemaDialect;
}
/**
* Sets connection encoding
*
* @param string $encoding The encoding to use.
* @return void
*/
public function setEncoding(string $encoding): void
{
$this->connect();
$this->_connection->exec('SET NAMES ' . $this->_connection->quote($encoding));
}
/**
* Sets connection default schema, if any relation defined in a query is not fully qualified
* postgres will fallback to looking the relation into defined default schema
*
* @param string $schema The schema names to set `search_path` to.
* @return void
*/
public function setSchema(string $schema): void
{
$this->connect();
$this->_connection->exec('SET search_path TO ' . $this->_connection->quote($schema));
}
/**
* @inheritDoc
*/
public function disableForeignKeySQL(): string
{
return 'SET CONSTRAINTS ALL DEFERRED';
}
/**
* @inheritDoc
*/
public function enableForeignKeySQL(): string
{
return 'SET CONSTRAINTS ALL IMMEDIATE';
}
/**
* @inheritDoc
*/
public function supports(string $feature): bool
{
switch ($feature) {
case static::FEATURE_CTE:
case static::FEATURE_JSON:
case static::FEATURE_TRUNCATE_WITH_CONSTRAINTS:
case static::FEATURE_WINDOW:
return true;
case static::FEATURE_DISABLE_CONSTRAINT_WITHOUT_TRANSACTION:
return false;
}
return parent::supports($feature);
}
/**
* @inheritDoc
*/
public function supportsDynamicConstraints(): bool
{
return true;
}
/**
* @inheritDoc
*/
protected function _transformDistinct(Query $query): Query
{
return $query;
}
/**
* @inheritDoc
*/
protected function _insertQueryTranslator(Query $query): Query
{
if (!$query->clause('epilog')) {
$query->epilog('RETURNING *');
}
return $query;
}
/**
* @inheritDoc
*/
protected function _expressionTranslators(): array
{
return [
IdentifierExpression::class => '_transformIdentifierExpression',
FunctionExpression::class => '_transformFunctionExpression',
StringExpression::class => '_transformStringExpression',
];
}
/**
* Changes identifer expression into postgresql format.
*
* @param \Cake\Database\Expression\IdentifierExpression $expression The expression to tranform.
* @return void
*/
protected function _transformIdentifierExpression(IdentifierExpression $expression): void
{
$collation = $expression->getCollation();
if ($collation) {
// use trim() to work around expression being transformed multiple times
$expression->setCollation('"' . trim($collation, '"') . '"');
}
}
/**
* Receives a FunctionExpression and changes it so that it conforms to this
* SQL dialect.
*
* @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert
* to postgres SQL.
* @return void
*/
protected function _transformFunctionExpression(FunctionExpression $expression): void
{
switch ($expression->getName()) {
case 'CONCAT':
// CONCAT function is expressed as exp1 || exp2
$expression->setName('')->setConjunction(' ||');
break;
case 'DATEDIFF':
$expression
->setName('')
->setConjunction('-')
->iterateParts(function ($p) {
if (is_string($p)) {
$p = ['value' => [$p => 'literal'], 'type' => null];
} else {
$p['value'] = [$p['value']];
}
return new FunctionExpression('DATE', $p['value'], [$p['type']]);
});
break;
case 'CURRENT_DATE':
$time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']);
$expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'date' => 'literal']);
break;
case 'CURRENT_TIME':
$time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']);
$expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'time' => 'literal']);
break;
case 'NOW':
$expression->setName('LOCALTIMESTAMP')->add([' 0 ' => 'literal']);
break;
case 'RAND':
$expression->setName('RANDOM');
break;
case 'DATE_ADD':
$expression
->setName('')
->setConjunction(' + INTERVAL')
->iterateParts(function ($p, $key) {
if ($key === 1) {
$p = sprintf("'%s'", $p);
}
return $p;
});
break;
case 'DAYOFWEEK':
$expression
->setName('EXTRACT')
->setConjunction(' ')
->add(['DOW FROM' => 'literal'], [], true)
->add([') + (1' => 'literal']); // Postgres starts on index 0 but Sunday should be 1
break;
}
}
/**
* Changes string expression into postgresql format.
*
* @param \Cake\Database\Expression\StringExpression $expression The string expression to tranform.
* @return void
*/
protected function _transformStringExpression(StringExpression $expression): void
{
// use trim() to work around expression being transformed multiple times
$expression->setCollation('"' . trim($expression->getCollation(), '"') . '"');
}
/**
* {@inheritDoc}
*
* @return \Cake\Database\PostgresCompiler
*/
public function newCompiler(): QueryCompiler
{
return new PostgresCompiler();
}
}

View File

@ -0,0 +1,315 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Driver;
use Cake\Database\Expression\ComparisonExpression;
use Cake\Database\Expression\IdentifierExpression;
use Cake\Database\IdentifierQuoter;
use Cake\Database\Query;
use Closure;
use RuntimeException;
/**
* Sql dialect trait
*
* @internal
*/
trait SqlDialectTrait
{
/**
* Quotes a database identifier (a column name, table name, etc..) to
* be used safely in queries without the risk of using reserved words
*
* @param string $identifier The identifier to quote.
* @return string
*/
public function quoteIdentifier(string $identifier): string
{
$identifier = trim($identifier);
if ($identifier === '*' || $identifier === '') {
return $identifier;
}
// string
if (preg_match('/^[\w-]+$/u', $identifier)) {
return $this->_startQuote . $identifier . $this->_endQuote;
}
// string.string
if (preg_match('/^[\w-]+\.[^ \*]*$/u', $identifier)) {
$items = explode('.', $identifier);
return $this->_startQuote . implode($this->_endQuote . '.' . $this->_startQuote, $items) . $this->_endQuote;
}
// string.*
if (preg_match('/^[\w-]+\.\*$/u', $identifier)) {
return $this->_startQuote . str_replace('.*', $this->_endQuote . '.*', $identifier);
}
// Functions
if (preg_match('/^([\w-]+)\((.*)\)$/', $identifier, $matches)) {
return $matches[1] . '(' . $this->quoteIdentifier($matches[2]) . ')';
}
// Alias.field AS thing
if (preg_match('/^([\w-]+(\.[\w\s-]+|\(.*\))*)\s+AS\s*([\w-]+)$/ui', $identifier, $matches)) {
return $this->quoteIdentifier($matches[1]) . ' AS ' . $this->quoteIdentifier($matches[3]);
}
// string.string with spaces
if (preg_match('/^([\w-]+\.[\w][\w\s-]*[\w])(.*)/u', $identifier, $matches)) {
$items = explode('.', $matches[1]);
$field = implode($this->_endQuote . '.' . $this->_startQuote, $items);
return $this->_startQuote . $field . $this->_endQuote . $matches[2];
}
if (preg_match('/^[\w\s-]*[\w-]+/u', $identifier)) {
return $this->_startQuote . $identifier . $this->_endQuote;
}
return $identifier;
}
/**
* Returns a callable function that will be used to transform a passed Query object.
* This function, in turn, will return an instance of a Query object that has been
* transformed to accommodate any specificities of the SQL dialect in use.
*
* @param string $type the type of query to be transformed
* (select, insert, update, delete)
* @return \Closure
*/
public function queryTranslator(string $type): Closure
{
return function ($query) use ($type) {
if ($this->isAutoQuotingEnabled()) {
$query = (new IdentifierQuoter($this))->quote($query);
}
/** @var \Cake\ORM\Query $query */
$query = $this->{'_' . $type . 'QueryTranslator'}($query);
$translators = $this->_expressionTranslators();
if (!$translators) {
return $query;
}
$query->traverseExpressions(function ($expression) use ($translators, $query): void {
foreach ($translators as $class => $method) {
if ($expression instanceof $class) {
$this->{$method}($expression, $query);
}
}
});
return $query;
};
}
/**
* Returns an associative array of methods that will transform Expression
* objects to conform with the specific SQL dialect. Keys are class names
* and values a method in this class.
*
* @psalm-return array<class-string, string>
* @return array<string>
*/
protected function _expressionTranslators(): array
{
return [];
}
/**
* Apply translation steps to select queries.
*
* @param \Cake\Database\Query $query The query to translate
* @return \Cake\Database\Query The modified query
*/
protected function _selectQueryTranslator(Query $query): Query
{
return $this->_transformDistinct($query);
}
/**
* Returns the passed query after rewriting the DISTINCT clause, so that drivers
* that do not support the "ON" part can provide the actual way it should be done
*
* @param \Cake\Database\Query $query The query to be transformed
* @return \Cake\Database\Query
*/
protected function _transformDistinct(Query $query): Query
{
if (is_array($query->clause('distinct'))) {
$query->group($query->clause('distinct'), true);
$query->distinct(false);
}
return $query;
}
/**
* Apply translation steps to delete queries.
*
* Chops out aliases on delete query conditions as most database dialects do not
* support aliases in delete queries. This also removes aliases
* in table names as they frequently don't work either.
*
* We are intentionally not supporting deletes with joins as they have even poorer support.
*
* @param \Cake\Database\Query $query The query to translate
* @return \Cake\Database\Query The modified query
*/
protected function _deleteQueryTranslator(Query $query): Query
{
$hadAlias = false;
$tables = [];
foreach ($query->clause('from') as $alias => $table) {
if (is_string($alias)) {
$hadAlias = true;
}
$tables[] = $table;
}
if ($hadAlias) {
$query->from($tables, true);
}
if (!$hadAlias) {
return $query;
}
return $this->_removeAliasesFromConditions($query);
}
/**
* Apply translation steps to update queries.
*
* Chops out aliases on update query conditions as not all database dialects do support
* aliases in update queries.
*
* Just like for delete queries, joins are currently not supported for update queries.
*
* @param \Cake\Database\Query $query The query to translate
* @return \Cake\Database\Query The modified query
*/
protected function _updateQueryTranslator(Query $query): Query
{
return $this->_removeAliasesFromConditions($query);
}
/**
* Removes aliases from the `WHERE` clause of a query.
*
* @param \Cake\Database\Query $query The query to process.
* @return \Cake\Database\Query The modified query.
* @throws \RuntimeException In case the processed query contains any joins, as removing
* aliases from the conditions can break references to the joined tables.
*/
protected function _removeAliasesFromConditions(Query $query): Query
{
if ($query->clause('join')) {
throw new RuntimeException(
'Aliases are being removed from conditions for UPDATE/DELETE queries, ' .
'this can break references to joined tables.'
);
}
$conditions = $query->clause('where');
if ($conditions) {
$conditions->traverse(function ($expression) {
if ($expression instanceof ComparisonExpression) {
$field = $expression->getField();
if (
is_string($field) &&
strpos($field, '.') !== false
) {
[, $unaliasedField] = explode('.', $field, 2);
$expression->setField($unaliasedField);
}
return $expression;
}
if ($expression instanceof IdentifierExpression) {
$identifier = $expression->getIdentifier();
if (strpos($identifier, '.') !== false) {
[, $unaliasedIdentifier] = explode('.', $identifier, 2);
$expression->setIdentifier($unaliasedIdentifier);
}
return $expression;
}
return $expression;
});
}
return $query;
}
/**
* Apply translation steps to insert queries.
*
* @param \Cake\Database\Query $query The query to translate
* @return \Cake\Database\Query The modified query
*/
protected function _insertQueryTranslator(Query $query): Query
{
return $query;
}
/**
* Returns a SQL snippet for creating a new transaction savepoint
*
* @param string|int $name save point name
* @return string
*/
public function savePointSQL($name): string
{
return 'SAVEPOINT LEVEL' . $name;
}
/**
* Returns a SQL snippet for releasing a previously created save point
*
* @param string|int $name save point name
* @return string
*/
public function releaseSavePointSQL($name): string
{
return 'RELEASE SAVEPOINT LEVEL' . $name;
}
/**
* Returns a SQL snippet for rollbacking a previously created save point
*
* @param string|int $name save point name
* @return string
*/
public function rollbackSavePointSQL($name): string
{
return 'ROLLBACK TO SAVEPOINT LEVEL' . $name;
}
}
// phpcs:disable
class_alias(
'Cake\Database\Driver\SqlDialectTrait',
'Cake\Database\SqlDialectTrait'
);
// phpcs:enable

View File

@ -0,0 +1,385 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Driver;
use Cake\Database\Driver;
use Cake\Database\Expression\FunctionExpression;
use Cake\Database\Expression\TupleComparison;
use Cake\Database\Query;
use Cake\Database\QueryCompiler;
use Cake\Database\Schema\SchemaDialect;
use Cake\Database\Schema\SqliteSchemaDialect;
use Cake\Database\SqliteCompiler;
use Cake\Database\Statement\PDOStatement;
use Cake\Database\Statement\SqliteStatement;
use Cake\Database\StatementInterface;
use InvalidArgumentException;
use PDO;
use RuntimeException;
use function Cake\Core\deprecationWarning;
/**
* Class Sqlite
*/
class Sqlite extends Driver
{
use SqlDialectTrait;
use TupleComparisonTranslatorTrait;
/**
* Base configuration settings for Sqlite driver
*
* - `mask` The mask used for created database
*
* @var array<string, mixed>
*/
protected $_baseConfig = [
'persistent' => false,
'username' => null,
'password' => null,
'database' => ':memory:',
'encoding' => 'utf8',
'mask' => 0644,
'cache' => null,
'mode' => null,
'flags' => [],
'init' => [],
];
/**
* The schema dialect class for this driver
*
* @var \Cake\Database\Schema\SqliteSchemaDialect|null
*/
protected $_schemaDialect;
/**
* Whether the connected server supports window functions.
*
* @var bool|null
*/
protected $_supportsWindowFunctions;
/**
* String used to start a database identifier quoting to make it safe
*
* @var string
*/
protected $_startQuote = '"';
/**
* String used to end a database identifier quoting to make it safe
*
* @var string
*/
protected $_endQuote = '"';
/**
* Mapping of date parts.
*
* @var array<string, string>
*/
protected $_dateParts = [
'day' => 'd',
'hour' => 'H',
'month' => 'm',
'minute' => 'M',
'second' => 'S',
'week' => 'W',
'year' => 'Y',
];
/**
* Mapping of feature to db server version for feature availability checks.
*
* @var array<string, string>
*/
protected $featureVersions = [
'cte' => '3.8.3',
'window' => '3.28.0',
];
/**
* Establishes a connection to the database server
*
* @return bool true on success
*/
public function connect(): bool
{
if ($this->_connection) {
return true;
}
$config = $this->_config;
$config['flags'] += [
PDO::ATTR_PERSISTENT => $config['persistent'],
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
if (!is_string($config['database']) || $config['database'] === '') {
$name = $config['name'] ?? 'unknown';
throw new InvalidArgumentException(
"The `database` key for the `{$name}` SQLite connection needs to be a non-empty string."
);
}
$chmodFile = false;
if ($config['database'] !== ':memory:' && $config['mode'] !== 'memory') {
$chmodFile = !file_exists($config['database']);
}
$params = [];
if ($config['cache']) {
$params[] = 'cache=' . $config['cache'];
}
if ($config['mode']) {
$params[] = 'mode=' . $config['mode'];
}
if ($params) {
if (PHP_VERSION_ID < 80100) {
throw new RuntimeException('SQLite URI support requires PHP 8.1.');
}
$dsn = 'sqlite:file:' . $config['database'] . '?' . implode('&', $params);
} else {
$dsn = 'sqlite:' . $config['database'];
}
$this->_connect($dsn, $config);
if ($chmodFile) {
// phpcs:disable
@chmod($config['database'], $config['mask']);
// phpcs:enable
}
if (!empty($config['init'])) {
foreach ((array)$config['init'] as $command) {
$this->getConnection()->exec($command);
}
}
return true;
}
/**
* Returns whether php is able to use this driver for connecting to database
*
* @return bool true if it is valid to use this driver
*/
public function enabled(): bool
{
return in_array('sqlite', PDO::getAvailableDrivers(), true);
}
/**
* Prepares a sql statement to be executed
*
* @param \Cake\Database\Query|string $query The query to prepare.
* @return \Cake\Database\StatementInterface
*/
public function prepare($query): StatementInterface
{
$this->connect();
$isObject = $query instanceof Query;
/**
* @psalm-suppress PossiblyInvalidMethodCall
* @psalm-suppress PossiblyInvalidArgument
*/
$statement = $this->_connection->prepare($isObject ? $query->sql() : $query);
$result = new SqliteStatement(new PDOStatement($statement, $this), $this);
/** @psalm-suppress PossiblyInvalidMethodCall */
if ($isObject && $query->isBufferedResultsEnabled() === false) {
$result->bufferResults(false);
}
return $result;
}
/**
* @inheritDoc
*/
public function disableForeignKeySQL(): string
{
return 'PRAGMA foreign_keys = OFF';
}
/**
* @inheritDoc
*/
public function enableForeignKeySQL(): string
{
return 'PRAGMA foreign_keys = ON';
}
/**
* @inheritDoc
*/
public function supports(string $feature): bool
{
switch ($feature) {
case static::FEATURE_CTE:
case static::FEATURE_WINDOW:
return version_compare(
$this->version(),
$this->featureVersions[$feature],
'>='
);
case static::FEATURE_TRUNCATE_WITH_CONSTRAINTS:
return true;
}
return parent::supports($feature);
}
/**
* @inheritDoc
*/
public function supportsDynamicConstraints(): bool
{
return false;
}
/**
* @inheritDoc
*/
public function schemaDialect(): SchemaDialect
{
if ($this->_schemaDialect === null) {
$this->_schemaDialect = new SqliteSchemaDialect($this);
}
return $this->_schemaDialect;
}
/**
* @inheritDoc
*/
public function newCompiler(): QueryCompiler
{
return new SqliteCompiler();
}
/**
* @inheritDoc
*/
protected function _expressionTranslators(): array
{
return [
FunctionExpression::class => '_transformFunctionExpression',
TupleComparison::class => '_transformTupleComparison',
];
}
/**
* Receives a FunctionExpression and changes it so that it conforms to this
* SQL dialect.
*
* @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert to TSQL.
* @return void
*/
protected function _transformFunctionExpression(FunctionExpression $expression): void
{
switch ($expression->getName()) {
case 'CONCAT':
// CONCAT function is expressed as exp1 || exp2
$expression->setName('')->setConjunction(' ||');
break;
case 'DATEDIFF':
$expression
->setName('ROUND')
->setConjunction('-')
->iterateParts(function ($p) {
return new FunctionExpression('JULIANDAY', [$p['value']], [$p['type']]);
});
break;
case 'NOW':
$expression->setName('DATETIME')->add(["'now'" => 'literal']);
break;
case 'RAND':
$expression
->setName('ABS')
->add(['RANDOM() % 1' => 'literal'], [], true);
break;
case 'CURRENT_DATE':
$expression->setName('DATE')->add(["'now'" => 'literal']);
break;
case 'CURRENT_TIME':
$expression->setName('TIME')->add(["'now'" => 'literal']);
break;
case 'EXTRACT':
$expression
->setName('STRFTIME')
->setConjunction(' ,')
->iterateParts(function ($p, $key) {
if ($key === 0) {
$value = rtrim(strtolower($p), 's');
if (isset($this->_dateParts[$value])) {
$p = ['value' => '%' . $this->_dateParts[$value], 'type' => null];
}
}
return $p;
});
break;
case 'DATE_ADD':
$expression
->setName('DATE')
->setConjunction(',')
->iterateParts(function ($p, $key) {
if ($key === 1) {
$p = ['value' => $p, 'type' => null];
}
return $p;
});
break;
case 'DAYOFWEEK':
$expression
->setName('STRFTIME')
->setConjunction(' ')
->add(["'%w', " => 'literal'], [], true)
->add([') + (1' => 'literal']); // Sqlite starts on index 0 but Sunday should be 1
break;
}
}
/**
* Returns true if the server supports common table expressions.
*
* @return bool
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_CTE)` instead
*/
public function supportsCTEs(): bool
{
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
return $this->supports(static::FEATURE_CTE);
}
/**
* Returns true if the connected server supports window functions.
*
* @return bool
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_WINDOW)` instead
*/
public function supportsWindowFunctions(): bool
{
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
return $this->supports(static::FEATURE_WINDOW);
}
}

View File

@ -0,0 +1,569 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Driver;
use Cake\Database\Driver;
use Cake\Database\Expression\FunctionExpression;
use Cake\Database\Expression\OrderByExpression;
use Cake\Database\Expression\OrderClauseExpression;
use Cake\Database\Expression\TupleComparison;
use Cake\Database\Expression\UnaryExpression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\QueryCompiler;
use Cake\Database\Schema\SchemaDialect;
use Cake\Database\Schema\SqlserverSchemaDialect;
use Cake\Database\SqlserverCompiler;
use Cake\Database\Statement\SqlserverStatement;
use Cake\Database\StatementInterface;
use InvalidArgumentException;
use PDO;
/**
* SQLServer driver.
*/
class Sqlserver extends Driver
{
use SqlDialectTrait;
use TupleComparisonTranslatorTrait;
/**
* @inheritDoc
*/
protected const MAX_ALIAS_LENGTH = 128;
/**
* @inheritDoc
*/
protected const RETRY_ERROR_CODES = [
40613, // Azure Sql Database paused
];
/**
* Base configuration settings for Sqlserver driver
*
* @var array<string, mixed>
*/
protected $_baseConfig = [
'host' => 'localhost\SQLEXPRESS',
'username' => '',
'password' => '',
'database' => 'cake',
'port' => '',
// PDO::SQLSRV_ENCODING_UTF8
'encoding' => 65001,
'flags' => [],
'init' => [],
'settings' => [],
'attributes' => [],
'app' => null,
'connectionPooling' => null,
'failoverPartner' => null,
'loginTimeout' => null,
'multiSubnetFailover' => null,
'encrypt' => null,
'trustServerCertificate' => null,
];
/**
* The schema dialect class for this driver
*
* @var \Cake\Database\Schema\SqlserverSchemaDialect|null
*/
protected $_schemaDialect;
/**
* String used to start a database identifier quoting to make it safe
*
* @var string
*/
protected $_startQuote = '[';
/**
* String used to end a database identifier quoting to make it safe
*
* @var string
*/
protected $_endQuote = ']';
/**
* Establishes a connection to the database server.
*
* Please note that the PDO::ATTR_PERSISTENT attribute is not supported by
* the SQL Server PHP PDO drivers. As a result you cannot use the
* persistent config option when connecting to a SQL Server (for more
* information see: https://github.com/Microsoft/msphpsql/issues/65).
*
* @throws \InvalidArgumentException if an unsupported setting is in the driver config
* @return bool true on success
*/
public function connect(): bool
{
if ($this->_connection) {
return true;
}
$config = $this->_config;
if (isset($config['persistent']) && $config['persistent']) {
throw new InvalidArgumentException(
'Config setting "persistent" cannot be set to true, '
. 'as the Sqlserver PDO driver does not support PDO::ATTR_PERSISTENT'
);
}
$config['flags'] += [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
if (!empty($config['encoding'])) {
$config['flags'][PDO::SQLSRV_ATTR_ENCODING] = $config['encoding'];
}
$port = '';
if ($config['port']) {
$port = ',' . $config['port'];
}
$dsn = "sqlsrv:Server={$config['host']}{$port};Database={$config['database']};MultipleActiveResultSets=false";
if ($config['app'] !== null) {
$dsn .= ";APP={$config['app']}";
}
if ($config['connectionPooling'] !== null) {
$dsn .= ";ConnectionPooling={$config['connectionPooling']}";
}
if ($config['failoverPartner'] !== null) {
$dsn .= ";Failover_Partner={$config['failoverPartner']}";
}
if ($config['loginTimeout'] !== null) {
$dsn .= ";LoginTimeout={$config['loginTimeout']}";
}
if ($config['multiSubnetFailover'] !== null) {
$dsn .= ";MultiSubnetFailover={$config['multiSubnetFailover']}";
}
if ($config['encrypt'] !== null) {
$dsn .= ";Encrypt={$config['encrypt']}";
}
if ($config['trustServerCertificate'] !== null) {
$dsn .= ";TrustServerCertificate={$config['trustServerCertificate']}";
}
$this->_connect($dsn, $config);
$connection = $this->getConnection();
if (!empty($config['init'])) {
foreach ((array)$config['init'] as $command) {
$connection->exec($command);
}
}
if (!empty($config['settings']) && is_array($config['settings'])) {
foreach ($config['settings'] as $key => $value) {
$connection->exec("SET {$key} {$value}");
}
}
if (!empty($config['attributes']) && is_array($config['attributes'])) {
foreach ($config['attributes'] as $key => $value) {
$connection->setAttribute($key, $value);
}
}
return true;
}
/**
* Returns whether PHP is able to use this driver for connecting to database
*
* @return bool true if it is valid to use this driver
*/
public function enabled(): bool
{
return in_array('sqlsrv', PDO::getAvailableDrivers(), true);
}
/**
* Prepares a sql statement to be executed
*
* @param \Cake\Database\Query|string $query The query to prepare.
* @return \Cake\Database\StatementInterface
*/
public function prepare($query): StatementInterface
{
$this->connect();
$sql = $query;
$options = [
PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL,
PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED,
];
if ($query instanceof Query) {
$sql = $query->sql();
if (count($query->getValueBinder()->bindings()) > 2100) {
throw new InvalidArgumentException(
'Exceeded maximum number of parameters (2100) for prepared statements in Sql Server. ' .
'This is probably due to a very large WHERE IN () clause which generates a parameter ' .
'for each value in the array. ' .
'If using an Association, try changing the `strategy` from select to subquery.'
);
}
if (!$query->isBufferedResultsEnabled()) {
$options = [];
}
}
/** @psalm-suppress PossiblyInvalidArgument */
$statement = $this->_connection->prepare($sql, $options);
return new SqlserverStatement($statement, $this);
}
/**
* @inheritDoc
*/
public function savePointSQL($name): string
{
return 'SAVE TRANSACTION t' . $name;
}
/**
* @inheritDoc
*/
public function releaseSavePointSQL($name): string
{
// SQLServer has no release save point operation.
return '';
}
/**
* @inheritDoc
*/
public function rollbackSavePointSQL($name): string
{
return 'ROLLBACK TRANSACTION t' . $name;
}
/**
* @inheritDoc
*/
public function disableForeignKeySQL(): string
{
return 'EXEC sp_MSforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT all"';
}
/**
* @inheritDoc
*/
public function enableForeignKeySQL(): string
{
return 'EXEC sp_MSforeachtable "ALTER TABLE ? WITH CHECK CHECK CONSTRAINT all"';
}
/**
* @inheritDoc
*/
public function supports(string $feature): bool
{
switch ($feature) {
case static::FEATURE_CTE:
case static::FEATURE_TRUNCATE_WITH_CONSTRAINTS:
case static::FEATURE_WINDOW:
return true;
case static::FEATURE_QUOTE:
$this->connect();
return $this->_connection->getAttribute(PDO::ATTR_DRIVER_NAME) !== 'odbc';
}
return parent::supports($feature);
}
/**
* @inheritDoc
*/
public function supportsDynamicConstraints(): bool
{
return true;
}
/**
* @inheritDoc
*/
public function schemaDialect(): SchemaDialect
{
if ($this->_schemaDialect === null) {
$this->_schemaDialect = new SqlserverSchemaDialect($this);
}
return $this->_schemaDialect;
}
/**
* {@inheritDoc}
*
* @return \Cake\Database\SqlserverCompiler
*/
public function newCompiler(): QueryCompiler
{
return new SqlserverCompiler();
}
/**
* @inheritDoc
*/
protected function _selectQueryTranslator(Query $query): Query
{
$limit = $query->clause('limit');
$offset = $query->clause('offset');
if ($limit && $offset === null) {
$query->modifier(['_auto_top_' => sprintf('TOP %d', $limit)]);
}
if ($offset !== null && !$query->clause('order')) {
$query->order($query->newExpr()->add('(SELECT NULL)'));
}
if ($this->version() < 11 && $offset !== null) {
return $this->_pagingSubquery($query, $limit, $offset);
}
return $this->_transformDistinct($query);
}
/**
* Generate a paging subquery for older versions of SQLserver.
*
* Prior to SQLServer 2012 there was no equivalent to LIMIT OFFSET, so a subquery must
* be used.
*
* @param \Cake\Database\Query $original The query to wrap in a subquery.
* @param int|null $limit The number of rows to fetch.
* @param int|null $offset The number of rows to offset.
* @return \Cake\Database\Query Modified query object.
*/
protected function _pagingSubquery(Query $original, ?int $limit, ?int $offset): Query
{
$field = '_cake_paging_._cake_page_rownum_';
if ($original->clause('order')) {
// SQL server does not support column aliases in OVER clauses. But
// the only practical way to specify the use of calculated columns
// is with their alias. So substitute the select SQL in place of
// any column aliases for those entries in the order clause.
$select = $original->clause('select');
$order = new OrderByExpression();
$original
->clause('order')
->iterateParts(function ($direction, $orderBy) use ($select, $order) {
$key = $orderBy;
if (
isset($select[$orderBy]) &&
$select[$orderBy] instanceof ExpressionInterface
) {
$order->add(new OrderClauseExpression($select[$orderBy], $direction));
} else {
$order->add([$key => $direction]);
}
// Leave original order clause unchanged.
return $orderBy;
});
} else {
$order = new OrderByExpression('(SELECT NULL)');
}
$query = clone $original;
$query->select([
'_cake_page_rownum_' => new UnaryExpression('ROW_NUMBER() OVER', $order),
])->limit(null)
->offset(null)
->order([], true);
$outer = new Query($query->getConnection());
$outer->select('*')
->from(['_cake_paging_' => $query]);
if ($offset) {
$outer->where(["$field > " . $offset]);
}
if ($limit) {
$value = (int)$offset + $limit;
$outer->where(["$field <= $value"]);
}
// Decorate the original query as that is what the
// end developer will be calling execute() on originally.
$original->decorateResults(function ($row) {
if (isset($row['_cake_page_rownum_'])) {
unset($row['_cake_page_rownum_']);
}
return $row;
});
return $outer;
}
/**
* @inheritDoc
*/
protected function _transformDistinct(Query $query): Query
{
if (!is_array($query->clause('distinct'))) {
return $query;
}
$original = $query;
$query = clone $original;
$distinct = $query->clause('distinct');
$query->distinct(false);
$order = new OrderByExpression($distinct);
$query
->select(function ($q) use ($distinct, $order) {
$over = $q->newExpr('ROW_NUMBER() OVER')
->add('(PARTITION BY')
->add($q->newExpr()->add($distinct)->setConjunction(','))
->add($order)
->add(')')
->setConjunction(' ');
return [
'_cake_distinct_pivot_' => $over,
];
})
->limit(null)
->offset(null)
->order([], true);
$outer = new Query($query->getConnection());
$outer->select('*')
->from(['_cake_distinct_' => $query])
->where(['_cake_distinct_pivot_' => 1]);
// Decorate the original query as that is what the
// end developer will be calling execute() on originally.
$original->decorateResults(function ($row) {
if (isset($row['_cake_distinct_pivot_'])) {
unset($row['_cake_distinct_pivot_']);
}
return $row;
});
return $outer;
}
/**
* @inheritDoc
*/
protected function _expressionTranslators(): array
{
return [
FunctionExpression::class => '_transformFunctionExpression',
TupleComparison::class => '_transformTupleComparison',
];
}
/**
* Receives a FunctionExpression and changes it so that it conforms to this
* SQL dialect.
*
* @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert to TSQL.
* @return void
*/
protected function _transformFunctionExpression(FunctionExpression $expression): void
{
switch ($expression->getName()) {
case 'CONCAT':
// CONCAT function is expressed as exp1 + exp2
$expression->setName('')->setConjunction(' +');
break;
case 'DATEDIFF':
/** @var bool $hasDay */
$hasDay = false;
$visitor = function ($value) use (&$hasDay) {
if ($value === 'day') {
$hasDay = true;
}
return $value;
};
$expression->iterateParts($visitor);
if (!$hasDay) {
$expression->add(['day' => 'literal'], [], true);
}
break;
case 'CURRENT_DATE':
$time = new FunctionExpression('GETUTCDATE');
$expression->setName('CONVERT')->add(['date' => 'literal', $time]);
break;
case 'CURRENT_TIME':
$time = new FunctionExpression('GETUTCDATE');
$expression->setName('CONVERT')->add(['time' => 'literal', $time]);
break;
case 'NOW':
$expression->setName('GETUTCDATE');
break;
case 'EXTRACT':
$expression->setName('DATEPART')->setConjunction(' ,');
break;
case 'DATE_ADD':
$params = [];
$visitor = function ($p, $key) use (&$params) {
if ($key === 0) {
$params[2] = $p;
} else {
$valueUnit = explode(' ', $p);
$params[0] = rtrim($valueUnit[1], 's');
$params[1] = $valueUnit[0];
}
return $p;
};
$manipulator = function ($p, $key) use (&$params) {
return $params[$key];
};
$expression
->setName('DATEADD')
->setConjunction(',')
->iterateParts($visitor)
->iterateParts($manipulator)
->add([$params[2] => 'literal']);
break;
case 'DAYOFWEEK':
$expression
->setName('DATEPART')
->setConjunction(' ')
->add(['weekday, ' => 'literal'], [], true);
break;
case 'SUBSTR':
$expression->setName('SUBSTRING');
if (count($expression) < 4) {
$params = [];
$expression
->iterateParts(function ($p) use (&$params) {
return $params[] = $p;
})
->add([new FunctionExpression('LEN', [$params[0]]), ['string']]);
}
break;
}
}
}

View File

@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Driver;
use Cake\Database\Expression\IdentifierExpression;
use Cake\Database\Expression\QueryExpression;
use Cake\Database\Expression\TupleComparison;
use Cake\Database\Query;
use RuntimeException;
/**
* Provides a translator method for tuple comparisons
*
* @internal
*/
trait TupleComparisonTranslatorTrait
{
/**
* Receives a TupleExpression and changes it so that it conforms to this
* SQL dialect.
*
* It transforms expressions looking like '(a, b) IN ((c, d), (e, f))' into an
* equivalent expression of the form '((a = c) AND (b = d)) OR ((a = e) AND (b = f))'.
*
* It can also transform transform expressions where the right hand side is a query
* selecting the same amount of columns as the elements in the left hand side of
* the expression:
*
* (a, b) IN (SELECT c, d FROM a_table) is transformed into
*
* 1 = (SELECT 1 FROM a_table WHERE (a = c) AND (b = d))
*
* @param \Cake\Database\Expression\TupleComparison $expression The expression to transform
* @param \Cake\Database\Query $query The query to update.
* @return void
*/
protected function _transformTupleComparison(TupleComparison $expression, Query $query): void
{
$fields = $expression->getField();
if (!is_array($fields)) {
return;
}
$operator = strtoupper($expression->getOperator());
if (!in_array($operator, ['IN', '='])) {
throw new RuntimeException(
sprintf(
'Tuple comparison transform only supports the `IN` and `=` operators, `%s` given.',
$operator
)
);
}
$value = $expression->getValue();
$true = new QueryExpression('1');
if ($value instanceof Query) {
$selected = array_values($value->clause('select'));
foreach ($fields as $i => $field) {
$value->andWhere([$field => new IdentifierExpression($selected[$i])]);
}
$value->select($true, true);
$expression->setField($true);
$expression->setOperator('=');
return;
}
$type = $expression->getType();
if ($type) {
/** @var array<string, string> $typeMap */
$typeMap = array_combine($fields, $type) ?: [];
} else {
$typeMap = [];
}
$surrogate = $query->getConnection()
->selectQuery($true);
if (!is_array(current($value))) {
$value = [$value];
}
$conditions = ['OR' => []];
foreach ($value as $tuple) {
$item = [];
foreach (array_values($tuple) as $i => $value2) {
$item[] = [$fields[$i] => $value2];
}
$conditions['OR'][] = $item;
}
$surrogate->where($conditions, $typeMap);
$expression->setField($true);
$expression->setValue($surrogate);
$expression->setOperator('=');
}
}

View File

@ -0,0 +1,336 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database;
use Cake\Database\Schema\SchemaDialect;
use Cake\Database\Schema\TableSchema;
use Closure;
/**
* Interface for database driver.
*
* @method int|null getMaxAliasLength() Returns the maximum alias length allowed.
* @method int getConnectRetries() Returns the number of connection retry attempts made.
* @method bool supports(string $feature) Checks whether a feature is supported by the driver.
* @method bool inTransaction() Returns whether a transaction is active.
* @method array config() Get the configuration data used to create the driver.
* @method string getRole() Returns the connection role this driver prforms.
*/
interface DriverInterface
{
/**
* Common Table Expressions (with clause) support.
*
* @var string
*/
public const FEATURE_CTE = 'cte';
/**
* Disabling constraints without being in transaction support.
*
* @var string
*/
public const FEATURE_DISABLE_CONSTRAINT_WITHOUT_TRANSACTION = 'disable-constraint-without-transaction';
/**
* Native JSON data type support.
*
* @var string
*/
public const FEATURE_JSON = 'json';
/**
* PDO::quote() support.
*
* @var string
*/
public const FEATURE_QUOTE = 'quote';
/**
* Transaction savepoint support.
*
* @var string
*/
public const FEATURE_SAVEPOINT = 'savepoint';
/**
* Truncate with foreign keys attached support.
*
* @var string
*/
public const FEATURE_TRUNCATE_WITH_CONSTRAINTS = 'truncate-with-constraints';
/**
* Window function support (all or partial clauses).
*
* @var string
*/
public const FEATURE_WINDOW = 'window';
/**
* Establishes a connection to the database server.
*
* @throws \Cake\Database\Exception\MissingConnectionException If database connection could not be established.
* @return bool True on success, false on failure.
*/
public function connect(): bool;
/**
* Disconnects from database server.
*
* @return void
*/
public function disconnect(): void;
/**
* Returns correct connection resource or object that is internally used.
*
* @return object Connection object used internally.
*/
public function getConnection();
/**
* Set the internal connection object.
*
* @param object $connection The connection instance.
* @return $this
*/
public function setConnection($connection);
/**
* Returns whether php is able to use this driver for connecting to database.
*
* @return bool True if it is valid to use this driver.
*/
public function enabled(): bool;
/**
* Prepares a sql statement to be executed.
*
* @param \Cake\Database\Query|string $query The query to turn into a prepared statement.
* @return \Cake\Database\StatementInterface
*/
public function prepare($query): StatementInterface;
/**
* Starts a transaction.
*
* @return bool True on success, false otherwise.
*/
public function beginTransaction(): bool;
/**
* Commits a transaction.
*
* @return bool True on success, false otherwise.
*/
public function commitTransaction(): bool;
/**
* Rollbacks a transaction.
*
* @return bool True on success, false otherwise.
*/
public function rollbackTransaction(): bool;
/**
* Get the SQL for releasing a save point.
*
* @param string|int $name Save point name or id
* @return string
*/
public function releaseSavePointSQL($name): string;
/**
* Get the SQL for creating a save point.
*
* @param string|int $name Save point name or id
* @return string
*/
public function savePointSQL($name): string;
/**
* Get the SQL for rollingback a save point.
*
* @param string|int $name Save point name or id
* @return string
*/
public function rollbackSavePointSQL($name): string;
/**
* Get the SQL for disabling foreign keys.
*
* @return string
*/
public function disableForeignKeySQL(): string;
/**
* Get the SQL for enabling foreign keys.
*
* @return string
*/
public function enableForeignKeySQL(): string;
/**
* Returns whether the driver supports adding or dropping constraints
* to already created tables.
*
* @return bool True if driver supports dynamic constraints.
* @deprecated 4.3.0 Fixtures no longer dynamically drop and create constraints.
*/
public function supportsDynamicConstraints(): bool;
/**
* Returns whether this driver supports save points for nested transactions.
*
* @return bool True if save points are supported, false otherwise.
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_SAVEPOINT)` instead
*/
public function supportsSavePoints(): bool;
/**
* Returns a value in a safe representation to be used in a query string
*
* @param mixed $value The value to quote.
* @param int $type Must be one of the \PDO::PARAM_* constants
* @return string
*/
public function quote($value, $type): string;
/**
* Checks if the driver supports quoting.
*
* @return bool
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_QUOTE)` instead
*/
public function supportsQuoting(): bool;
/**
* Returns a callable function that will be used to transform a passed Query object.
* This function, in turn, will return an instance of a Query object that has been
* transformed to accommodate any specificities of the SQL dialect in use.
*
* @param string $type The type of query to be transformed
* (select, insert, update, delete).
* @return \Closure
*/
public function queryTranslator(string $type): Closure;
/**
* Get the schema dialect.
*
* Used by {@link \Cake\Database\Schema} package to reflect schema and
* generate schema.
*
* If all the tables that use this Driver specify their
* own schemas, then this may return null.
*
* @return \Cake\Database\Schema\SchemaDialect
*/
public function schemaDialect(): SchemaDialect;
/**
* Quotes a database identifier (a column name, table name, etc..) to
* be used safely in queries without the risk of using reserved words.
*
* @param string $identifier The identifier expression to quote.
* @return string
*/
public function quoteIdentifier(string $identifier): string;
/**
* Escapes values for use in schema definitions.
*
* @param mixed $value The value to escape.
* @return string String for use in schema definitions.
*/
public function schemaValue($value): string;
/**
* Returns the schema name that's being used.
*
* @return string
*/
public function schema(): string;
/**
* Returns last id generated for a table or sequence in database.
*
* @param string|null $table table name or sequence to get last insert value from.
* @param string|null $column the name of the column representing the primary key.
* @return string|int
*/
public function lastInsertId(?string $table = null, ?string $column = null);
/**
* Checks whether the driver is connected.
*
* @return bool
*/
public function isConnected(): bool;
/**
* Sets whether this driver should automatically quote identifiers
* in queries.
*
* @param bool $enable Whether to enable auto quoting
* @return $this
*/
public function enableAutoQuoting(bool $enable = true);
/**
* Disable auto quoting of identifiers in queries.
*
* @return $this
*/
public function disableAutoQuoting();
/**
* Returns whether this driver should automatically quote identifiers
* in queries.
*
* @return bool
*/
public function isAutoQuotingEnabled(): bool;
/**
* Transforms the passed query to this Driver's dialect and returns an instance
* of the transformed query and the full compiled SQL string.
*
* @param \Cake\Database\Query $query The query to compile.
* @param \Cake\Database\ValueBinder $binder The value binder to use.
* @return array containing 2 entries. The first entity is the transformed query
* and the second one the compiled SQL.
*/
public function compileQuery(Query $query, ValueBinder $binder): array;
/**
* Returns an instance of a QueryCompiler.
*
* @return \Cake\Database\QueryCompiler
*/
public function newCompiler(): QueryCompiler;
/**
* Constructs new TableSchema.
*
* @param string $table The table name.
* @param array $columns The list of columns for the schema.
* @return \Cake\Database\Schema\TableSchema
*/
public function newTableSchema(string $table, array $columns = []): TableSchema;
}

10
vendor/cakephp/database/Exception.php vendored Normal file
View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
use function Cake\Core\deprecationWarning;
deprecationWarning(
'Since 4.2.0: Cake\Database\Exception is deprecated. ' .
'Use Cake\Database\Exception\DatabaseException instead.'
);
class_exists('Cake\Database\Exception\DatabaseException');

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Exception;
use Cake\Core\Exception\CakeException;
/**
* Exception for the database package.
*/
class DatabaseException extends CakeException
{
/**
* @inheritDoc
*/
protected $_messageTemplate = '%s';
}
// phpcs:disable
class_alias(
'Cake\Database\Exception\DatabaseException',
'Cake\Database\Exception'
);
// phpcs:enable

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Exception;
use Cake\Core\Exception\CakeException;
/**
* Class MissingConnectionException
*/
class MissingConnectionException extends CakeException
{
/**
* @inheritDoc
*/
protected $_messageTemplate = 'Connection to %s could not be established: %s';
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Exception;
use Cake\Core\Exception\CakeException;
/**
* Class MissingDriverException
*/
class MissingDriverException extends CakeException
{
/**
* @inheritDoc
*/
protected $_messageTemplate = 'Could not find driver `%s` for connection `%s`.';
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Exception;
use Cake\Core\Exception\CakeException;
/**
* Class MissingExtensionException
*/
class MissingExtensionException extends CakeException
{
/**
* @inheritDoc
*/
// phpcs:ignore Generic.Files.LineLength
protected $_messageTemplate = 'Database driver %s cannot be used due to a missing PHP extension or unmet dependency. Requested by connection "%s"';
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.4.3
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Exception;
use Cake\Core\Exception\CakeException;
use Throwable;
/**
* Class NestedTransactionRollbackException
*/
class NestedTransactionRollbackException extends CakeException
{
/**
* Constructor
*
* @param string|null $message If no message is given a default meesage will be used.
* @param int|null $code Status code, defaults to 500.
* @param \Throwable|null $previous the previous exception.
*/
public function __construct(?string $message = null, ?int $code = 500, ?Throwable $previous = null)
{
if ($message === null) {
$message = 'Cannot commit transaction - rollback() has been already called in the nested transaction';
}
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,253 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ValueBinder;
use Closure;
/**
* This represents an SQL aggregate function expression in an SQL statement.
* Calls can be constructed by passing the name of the function and a list of params.
* For security reasons, all params passed are quoted by default unless
* explicitly told otherwise.
*/
class AggregateExpression extends FunctionExpression implements WindowInterface
{
/**
* @var \Cake\Database\Expression\QueryExpression
*/
protected $filter;
/**
* @var \Cake\Database\Expression\WindowExpression
*/
protected $window;
/**
* Adds conditions to the FILTER clause. The conditions are the same format as
* `Query::where()`.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions The conditions to filter on.
* @param array<string, string> $types Associative array of type names used to bind values to query
* @return $this
* @see \Cake\Database\Query::where()
*/
public function filter($conditions, array $types = [])
{
if ($this->filter === null) {
$this->filter = new QueryExpression();
}
if ($conditions instanceof Closure) {
$conditions = $conditions(new QueryExpression());
}
$this->filter->add($conditions, $types);
return $this;
}
/**
* Adds an empty `OVER()` window expression or a named window epression.
*
* @param string|null $name Window name
* @return $this
*/
public function over(?string $name = null)
{
if ($this->window === null) {
$this->window = new WindowExpression();
}
if ($name) {
// Set name manually in case this was chained from FunctionsBuilder wrapper
$this->window->name($name);
}
return $this;
}
/**
* @inheritDoc
*/
public function partition($partitions)
{
$this->over();
$this->window->partition($partitions);
return $this;
}
/**
* @inheritDoc
*/
public function order($fields)
{
$this->over();
$this->window->order($fields);
return $this;
}
/**
* @inheritDoc
*/
public function range($start, $end = 0)
{
$this->over();
$this->window->range($start, $end);
return $this;
}
/**
* @inheritDoc
*/
public function rows(?int $start, ?int $end = 0)
{
$this->over();
$this->window->rows($start, $end);
return $this;
}
/**
* @inheritDoc
*/
public function groups(?int $start, ?int $end = 0)
{
$this->over();
$this->window->groups($start, $end);
return $this;
}
/**
* @inheritDoc
*/
public function frame(
string $type,
$startOffset,
string $startDirection,
$endOffset,
string $endDirection
) {
$this->over();
$this->window->frame($type, $startOffset, $startDirection, $endOffset, $endDirection);
return $this;
}
/**
* @inheritDoc
*/
public function excludeCurrent()
{
$this->over();
$this->window->excludeCurrent();
return $this;
}
/**
* @inheritDoc
*/
public function excludeGroup()
{
$this->over();
$this->window->excludeGroup();
return $this;
}
/**
* @inheritDoc
*/
public function excludeTies()
{
$this->over();
$this->window->excludeTies();
return $this;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$sql = parent::sql($binder);
if ($this->filter !== null) {
$sql .= ' FILTER (WHERE ' . $this->filter->sql($binder) . ')';
}
if ($this->window !== null) {
if ($this->window->isNamedOnly()) {
$sql .= ' OVER ' . $this->window->sql($binder);
} else {
$sql .= ' OVER (' . $this->window->sql($binder) . ')';
}
}
return $sql;
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
parent::traverse($callback);
if ($this->filter !== null) {
$callback($this->filter);
$this->filter->traverse($callback);
}
if ($this->window !== null) {
$callback($this->window);
$this->window->traverse($callback);
}
return $this;
}
/**
* @inheritDoc
*/
public function count(): int
{
$count = parent::count();
if ($this->window !== null) {
$count = $count + 1;
}
return $count;
}
/**
* Clone this object and its subtree of expressions.
*
* @return void
*/
public function __clone()
{
parent::__clone();
if ($this->filter !== null) {
$this->filter = clone $this->filter;
}
if ($this->window !== null) {
$this->window = clone $this->window;
}
}
}

View File

@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Type\ExpressionTypeCasterTrait;
use Cake\Database\ValueBinder;
use Closure;
/**
* An expression object that represents a SQL BETWEEN snippet
*/
class BetweenExpression implements ExpressionInterface, FieldInterface
{
use ExpressionTypeCasterTrait;
use FieldTrait;
/**
* The first value in the expression
*
* @var mixed
*/
protected $_from;
/**
* The second value in the expression
*
* @var mixed
*/
protected $_to;
/**
* The data type for the from and to arguments
*
* @var mixed
*/
protected $_type;
/**
* Constructor
*
* @param \Cake\Database\ExpressionInterface|string $field The field name to compare for values inbetween the range.
* @param mixed $from The initial value of the range.
* @param mixed $to The ending value in the comparison range.
* @param string|null $type The data type name to bind the values with.
*/
public function __construct($field, $from, $to, $type = null)
{
if ($type !== null) {
$from = $this->_castToExpression($from, $type);
$to = $this->_castToExpression($to, $type);
}
$this->_field = $field;
$this->_from = $from;
$this->_to = $to;
$this->_type = $type;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$parts = [
'from' => $this->_from,
'to' => $this->_to,
];
/** @var \Cake\Database\ExpressionInterface|string $field */
$field = $this->_field;
if ($field instanceof ExpressionInterface) {
$field = $field->sql($binder);
}
foreach ($parts as $name => $part) {
if ($part instanceof ExpressionInterface) {
$parts[$name] = $part->sql($binder);
continue;
}
$parts[$name] = $this->_bindValue($part, $binder, $this->_type);
}
return sprintf('%s BETWEEN %s AND %s', $field, $parts['from'], $parts['to']);
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
foreach ([$this->_field, $this->_from, $this->_to] as $part) {
if ($part instanceof ExpressionInterface) {
$callback($part);
}
}
return $this;
}
/**
* Registers a value in the placeholder generator and returns the generated placeholder
*
* @param mixed $value The value to bind
* @param \Cake\Database\ValueBinder $binder The value binder to use
* @param string $type The type of $value
* @return string generated placeholder
*/
protected function _bindValue($value, $binder, $type): string
{
$placeholder = $binder->placeholder('c');
$binder->bind($placeholder, $value, $type);
return $placeholder;
}
/**
* Do a deep clone of this expression.
*
* @return void
*/
public function __clone()
{
foreach (['_field', '_from', '_to'] as $part) {
if ($this->{$part} instanceof ExpressionInterface) {
$this->{$part} = clone $this->{$part};
}
}
}
}

View File

@ -0,0 +1,251 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Type\ExpressionTypeCasterTrait;
use Cake\Database\ValueBinder;
use Closure;
/**
* This class represents a SQL Case statement
*
* @deprecated 4.3.0 Use QueryExpression::case() or CaseStatementExpression instead
*/
class CaseExpression implements ExpressionInterface
{
use ExpressionTypeCasterTrait;
/**
* A list of strings or other expression objects that represent the conditions of
* the case statement. For example one key of the array might look like "sum > :value"
*
* @var array
*/
protected $_conditions = [];
/**
* Values that are associated with the conditions in the $_conditions array.
* Each value represents the 'true' value for the condition with the corresponding key.
*
* @var array
*/
protected $_values = [];
/**
* The `ELSE` value for the case statement. If null then no `ELSE` will be included.
*
* @var \Cake\Database\ExpressionInterface|array|string|null
*/
protected $_elseValue;
/**
* Constructs the case expression
*
* @param \Cake\Database\ExpressionInterface|array $conditions The conditions to test. Must be a ExpressionInterface
* instance, or an array of ExpressionInterface instances.
* @param \Cake\Database\ExpressionInterface|array $values Associative array of values to be associated with the
* conditions passed in $conditions. If there are more $values than $conditions,
* the last $value is used as the `ELSE` value.
* @param array<string> $types Associative array of types to be associated with the values
* passed in $values
*/
public function __construct($conditions = [], $values = [], $types = [])
{
$conditions = is_array($conditions) ? $conditions : [$conditions];
$values = is_array($values) ? $values : [$values];
$types = is_array($types) ? $types : [$types];
if (!empty($conditions)) {
$this->add($conditions, $values, $types);
}
if (count($values) > count($conditions)) {
end($values);
$key = key($values);
$this->elseValue($values[$key], $types[$key] ?? null);
}
}
/**
* Adds one or more conditions and their respective true values to the case object.
* Conditions must be a one dimensional array or a QueryExpression.
* The trueValues must be a similar structure, but may contain a string value.
*
* @param \Cake\Database\ExpressionInterface|array $conditions Must be a ExpressionInterface instance,
* or an array of ExpressionInterface instances.
* @param \Cake\Database\ExpressionInterface|array $values Associative array of values of each condition
* @param array<string> $types Associative array of types to be associated with the values
* @return $this
*/
public function add($conditions = [], $values = [], $types = [])
{
$conditions = is_array($conditions) ? $conditions : [$conditions];
$values = is_array($values) ? $values : [$values];
$types = is_array($types) ? $types : [$types];
$this->_addExpressions($conditions, $values, $types);
return $this;
}
/**
* Iterates over the passed in conditions and ensures that there is a matching true value for each.
* If no matching true value, then it is defaulted to '1'.
*
* @param array $conditions Array of ExpressionInterface instances.
* @param array<mixed> $values Associative array of values of each condition
* @param array<string> $types Associative array of types to be associated with the values
* @return void
*/
protected function _addExpressions(array $conditions, array $values, array $types): void
{
$rawValues = array_values($values);
$keyValues = array_keys($values);
foreach ($conditions as $k => $c) {
$numericKey = is_numeric($k);
if ($numericKey && empty($c)) {
continue;
}
if (!$c instanceof ExpressionInterface) {
continue;
}
$this->_conditions[] = $c;
$value = $rawValues[$k] ?? 1;
if ($value === 'literal') {
$value = $keyValues[$k];
$this->_values[] = $value;
continue;
}
if ($value === 'identifier') {
/** @var string $identifier */
$identifier = $keyValues[$k];
$value = new IdentifierExpression($identifier);
$this->_values[] = $value;
continue;
}
$type = $types[$k] ?? null;
if ($type !== null && !$value instanceof ExpressionInterface) {
$value = $this->_castToExpression($value, $type);
}
if ($value instanceof ExpressionInterface) {
$this->_values[] = $value;
continue;
}
$this->_values[] = ['value' => $value, 'type' => $type];
}
}
/**
* Sets the default value
*
* @param \Cake\Database\ExpressionInterface|array|string|null $value Value to set
* @param string|null $type Type of value
* @return void
*/
public function elseValue($value = null, ?string $type = null): void
{
if (is_array($value)) {
end($value);
$value = key($value);
}
if ($value !== null && !$value instanceof ExpressionInterface) {
$value = $this->_castToExpression($value, $type);
}
if (!$value instanceof ExpressionInterface) {
$value = ['value' => $value, 'type' => $type];
}
$this->_elseValue = $value;
}
/**
* Compiles the relevant parts into sql
*
* @param \Cake\Database\ExpressionInterface|array|string $part The part to compile
* @param \Cake\Database\ValueBinder $binder Sql generator
* @return string
*/
protected function _compile($part, ValueBinder $binder): string
{
if ($part instanceof ExpressionInterface) {
$part = $part->sql($binder);
} elseif (is_array($part)) {
$placeholder = $binder->placeholder('param');
$binder->bind($placeholder, $part['value'], $part['type']);
$part = $placeholder;
}
return $part;
}
/**
* Converts the Node into a SQL string fragment.
*
* @param \Cake\Database\ValueBinder $binder Placeholder generator object
* @return string
*/
public function sql(ValueBinder $binder): string
{
$parts = [];
$parts[] = 'CASE';
foreach ($this->_conditions as $k => $part) {
$value = $this->_values[$k];
$parts[] = 'WHEN ' . $this->_compile($part, $binder) . ' THEN ' . $this->_compile($value, $binder);
}
if ($this->_elseValue !== null) {
$parts[] = 'ELSE';
$parts[] = $this->_compile($this->_elseValue, $binder);
}
$parts[] = 'END';
return implode(' ', $parts);
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
foreach (['_conditions', '_values'] as $part) {
foreach ($this->{$part} as $c) {
if ($c instanceof ExpressionInterface) {
$callback($c);
$c->traverse($callback);
}
}
}
if ($this->_elseValue instanceof ExpressionInterface) {
$callback($this->_elseValue);
$this->_elseValue->traverse($callback);
}
return $this;
}
}

View File

@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Chronos\ChronosDate;
use Cake\Chronos\MutableDate;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\TypedResultInterface;
use Cake\Database\ValueBinder;
use DateTimeInterface;
/**
* Trait that holds shared functionality for case related expressions.
*
* @property \Cake\Database\TypeMap $_typeMap The type map to use when using an array of conditions for the `WHEN`
* value.
* @internal
*/
trait CaseExpressionTrait
{
/**
* Infers the abstract type for the given value.
*
* @param mixed $value The value for which to infer the type.
* @return string|null The abstract type, or `null` if it could not be inferred.
*/
protected function inferType($value): ?string
{
$type = null;
if (is_string($value)) {
$type = 'string';
} elseif (is_int($value)) {
$type = 'integer';
} elseif (is_float($value)) {
$type = 'float';
} elseif (is_bool($value)) {
$type = 'boolean';
} elseif (
$value instanceof ChronosDate ||
$value instanceof MutableDate
) {
$type = 'date';
} elseif ($value instanceof DateTimeInterface) {
$type = 'datetime';
} elseif (
is_object($value) &&
method_exists($value, '__toString')
) {
$type = 'string';
} elseif (
$this->_typeMap !== null &&
$value instanceof IdentifierExpression
) {
$type = $this->_typeMap->type($value->getIdentifier());
} elseif ($value instanceof TypedResultInterface) {
$type = $value->getReturnType();
}
return $type;
}
/**
* Compiles a nullable value to SQL.
*
* @param \Cake\Database\ValueBinder $binder The value binder to use.
* @param \Cake\Database\ExpressionInterface|object|scalar|null $value The value to compile.
* @param string|null $type The value type.
* @return string
*/
protected function compileNullableValue(ValueBinder $binder, $value, ?string $type = null): string
{
if (
$type !== null &&
!($value instanceof ExpressionInterface)
) {
$value = $this->_castToExpression($value, $type);
}
if ($value === null) {
$value = 'NULL';
} elseif ($value instanceof Query) {
$value = sprintf('(%s)', $value->sql($binder));
} elseif ($value instanceof ExpressionInterface) {
$value = $value->sql($binder);
} else {
$placeholder = $binder->placeholder('c');
$binder->bind($placeholder, $value, $type);
$value = $placeholder;
}
return $value;
}
}

View File

@ -0,0 +1,597 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Type\ExpressionTypeCasterTrait;
use Cake\Database\TypedResultInterface;
use Cake\Database\TypeMapTrait;
use Cake\Database\ValueBinder;
use Closure;
use InvalidArgumentException;
use LogicException;
use function Cake\Core\getTypeName;
/**
* Represents a SQL case statement with a fluid API
*/
class CaseStatementExpression implements ExpressionInterface, TypedResultInterface
{
use CaseExpressionTrait;
use ExpressionTypeCasterTrait;
use TypeMapTrait;
/**
* The names of the clauses that are valid for use with the
* `clause()` method.
*
* @var array<string>
*/
protected $validClauseNames = [
'value',
'when',
'else',
];
/**
* Whether this is a simple case expression.
*
* @var bool
*/
protected $isSimpleVariant = false;
/**
* The case value.
*
* @var \Cake\Database\ExpressionInterface|object|scalar|null
*/
protected $value = null;
/**
* The case value type.
*
* @var string|null
*/
protected $valueType = null;
/**
* The `WHEN ... THEN ...` expressions.
*
* @var array<\Cake\Database\Expression\WhenThenExpression>
*/
protected $when = [];
/**
* Buffer that holds values and types for use with `then()`.
*
* @var array|null
*/
protected $whenBuffer = null;
/**
* The else part result value.
*
* @var \Cake\Database\ExpressionInterface|object|scalar|null
*/
protected $else = null;
/**
* The else part result type.
*
* @var string|null
*/
protected $elseType = null;
/**
* The return type.
*
* @var string|null
*/
protected $returnType = null;
/**
* Constructor.
*
* When a value is set, the syntax generated is
* `CASE case_value WHEN when_value ... END` (simple case),
* where the `when_value`'s are compared against the
* `case_value`.
*
* When no value is set, the syntax generated is
* `CASE WHEN when_conditions ... END` (searched case),
* where the conditions hold the comparisons.
*
* Note that `null` is a valid case value, and thus should
* only be passed if you actually want to create the simple
* case expression variant!
*
* @param \Cake\Database\ExpressionInterface|object|scalar|null $value The case value.
* @param string|null $type The case value type. If no type is provided, the type will be tried to be inferred
* from the value.
*/
public function __construct($value = null, ?string $type = null)
{
if (func_num_args() > 0) {
if (
$value !== null &&
!is_scalar($value) &&
!(is_object($value) && !($value instanceof Closure))
) {
throw new InvalidArgumentException(sprintf(
'The `$value` argument must be either `null`, a scalar value, an object, ' .
'or an instance of `\%s`, `%s` given.',
ExpressionInterface::class,
getTypeName($value)
));
}
$this->value = $value;
if (
$value !== null &&
$type === null &&
!($value instanceof ExpressionInterface)
) {
$type = $this->inferType($value);
}
$this->valueType = $type;
$this->isSimpleVariant = true;
}
}
/**
* Sets the `WHEN` value for a `WHEN ... THEN ...` expression, or a
* self-contained expression that holds both the value for `WHEN`
* and the value for `THEN`.
*
* ### Order based syntax
*
* When passing a value other than a self-contained
* `\Cake\Database\Expression\WhenThenExpression`,
* instance, the `WHEN ... THEN ...` statement must be closed off with
* a call to `then()` before invoking `when()` again or `else()`:
*
* ```
* $queryExpression
* ->case($query->identifier('Table.column'))
* ->when(true)
* ->then('Yes')
* ->when(false)
* ->then('No')
* ->else('Maybe');
* ```
*
* ### Self-contained expressions
*
* When passing an instance of `\Cake\Database\Expression\WhenThenExpression`,
* being it directly, or via a callable, then there is no need to close
* using `then()` on this object, instead the statement will be closed
* on the `\Cake\Database\Expression\WhenThenExpression`
* object using
* `\Cake\Database\Expression\WhenThenExpression::then()`.
*
* Callables will receive an instance of `\Cake\Database\Expression\WhenThenExpression`,
* and must return one, being it the same object, or a custom one:
*
* ```
* $queryExpression
* ->case()
* ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
* return $whenThen
* ->when(['Table.column' => true])
* ->then('Yes');
* })
* ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
* return $whenThen
* ->when(['Table.column' => false])
* ->then('No');
* })
* ->else('Maybe');
* ```
*
* ### Type handling
*
* The types provided via the `$type` argument will be merged with the
* type map set for this expression. When using callables for `$when`,
* the `\Cake\Database\Expression\WhenThenExpression`
* instance received by the callables will inherit that type map, however
* the types passed here will _not_ be merged in case of using callables,
* instead the types must be passed in
* `\Cake\Database\Expression\WhenThenExpression::when()`:
*
* ```
* $queryExpression
* ->case()
* ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
* return $whenThen
* ->when(['unmapped_column' => true], ['unmapped_column' => 'bool'])
* ->then('Yes');
* })
* ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
* return $whenThen
* ->when(['unmapped_column' => false], ['unmapped_column' => 'bool'])
* ->then('No');
* })
* ->else('Maybe');
* ```
*
* ### User data safety
*
* When passing user data, be aware that allowing a user defined array
* to be passed, is a potential SQL injection vulnerability, as it
* allows for raw SQL to slip in!
*
* The following is _unsafe_ usage that must be avoided:
*
* ```
* $case
* ->when($userData)
* ```
*
* A safe variant for the above would be to define a single type for
* the value:
*
* ```
* $case
* ->when($userData, 'integer')
* ```
*
* This way an exception would be triggered when an array is passed for
* the value, thus preventing raw SQL from slipping in, and all other
* types of values would be forced to be bound as an integer.
*
* Another way to safely pass user data is when using a conditions
* array, and passing user data only on the value side of the array
* entries, which will cause them to be bound:
*
* ```
* $case
* ->when([
* 'Table.column' => $userData,
* ])
* ```
*
* Lastly, data can also be bound manually:
*
* ```
* $query
* ->select([
* 'val' => $query->newExpr()
* ->case()
* ->when($query->newExpr(':userData'))
* ->then(123)
* ])
* ->bind(':userData', $userData, 'integer')
* ```
*
* @param \Cake\Database\ExpressionInterface|\Closure|object|array|scalar $when The `WHEN` value. When using an
* array of conditions, it must be compatible with `\Cake\Database\Query::where()`. Note that this argument is
* _not_ completely safe for use with user data, as a user supplied array would allow for raw SQL to slip in! If
* you plan to use user data, either pass a single type for the `$type` argument (which forces the `$when` value to
* be a non-array, and then always binds the data), use a conditions array where the user data is only passed on
* the value side of the array entries, or custom bindings!
* @param array<string, string>|string|null $type The when value type. Either an associative array when using array style
* conditions, or else a string. If no type is provided, the type will be tried to be inferred from the value.
* @return $this
* @throws \LogicException In case this a closing `then()` call is required before calling this method.
* @throws \LogicException In case the callable doesn't return an instance of
* `\Cake\Database\Expression\WhenThenExpression`.
*/
public function when($when, $type = null)
{
if ($this->whenBuffer !== null) {
throw new LogicException('Cannot call `when()` between `when()` and `then()`.');
}
if ($when instanceof Closure) {
$when = $when(new WhenThenExpression($this->getTypeMap()));
if (!($when instanceof WhenThenExpression)) {
throw new LogicException(sprintf(
'`when()` callables must return an instance of `\%s`, `%s` given.',
WhenThenExpression::class,
getTypeName($when)
));
}
}
if ($when instanceof WhenThenExpression) {
$this->when[] = $when;
} else {
$this->whenBuffer = ['when' => $when, 'type' => $type];
}
return $this;
}
/**
* Sets the `THEN` result value for the last `WHEN ... THEN ...`
* statement that was opened using `when()`.
*
* ### Order based syntax
*
* This method can only be invoked in case `when()` was previously
* used with a value other than a closure or an instance of
* `\Cake\Database\Expression\WhenThenExpression`:
*
* ```
* $case
* ->when(['Table.column' => true])
* ->then('Yes')
* ->when(['Table.column' => false])
* ->then('No')
* ->else('Maybe');
* ```
*
* The following would all fail with an exception:
*
* ```
* $case
* ->when(['Table.column' => true])
* ->when(['Table.column' => false])
* // ...
* ```
*
* ```
* $case
* ->when(['Table.column' => true])
* ->else('Maybe')
* // ...
* ```
*
* ```
* $case
* ->then('Yes')
* // ...
* ```
*
* ```
* $case
* ->when(['Table.column' => true])
* ->then('Yes')
* ->then('No')
* // ...
* ```
*
* @param \Cake\Database\ExpressionInterface|object|scalar|null $result The result value.
* @param string|null $type The result type. If no type is provided, the type will be tried to be inferred from the
* value.
* @return $this
* @throws \LogicException In case `when()` wasn't previously called with a value other than a closure or an
* instance of `\Cake\Database\Expression\WhenThenExpression`.
*/
public function then($result, ?string $type = null)
{
if ($this->whenBuffer === null) {
throw new LogicException('Cannot call `then()` before `when()`.');
}
$whenThen = (new WhenThenExpression($this->getTypeMap()))
->when($this->whenBuffer['when'], $this->whenBuffer['type'])
->then($result, $type);
$this->whenBuffer = null;
$this->when[] = $whenThen;
return $this;
}
/**
* Sets the `ELSE` result value.
*
* @param \Cake\Database\ExpressionInterface|object|scalar|null $result The result value.
* @param string|null $type The result type. If no type is provided, the type will be tried to be inferred from the
* value.
* @return $this
* @throws \LogicException In case a closing `then()` call is required before calling this method.
* @throws \InvalidArgumentException In case the `$result` argument is neither a scalar value, nor an object, an
* instance of `\Cake\Database\ExpressionInterface`, or `null`.
*/
public function else($result, ?string $type = null)
{
if ($this->whenBuffer !== null) {
throw new LogicException('Cannot call `else()` between `when()` and `then()`.');
}
if (
$result !== null &&
!is_scalar($result) &&
!(is_object($result) && !($result instanceof Closure))
) {
throw new InvalidArgumentException(sprintf(
'The `$result` argument must be either `null`, a scalar value, an object, ' .
'or an instance of `\%s`, `%s` given.',
ExpressionInterface::class,
getTypeName($result)
));
}
if ($type === null) {
$type = $this->inferType($result);
}
$this->else = $result;
$this->elseType = $type;
return $this;
}
/**
* Returns the abstract type that this expression will return.
*
* If no type has been explicitly set via `setReturnType()`, this
* method will try to obtain the type from the result types of the
* `then()` and `else() `calls. All types must be identical in order
* for this to work, otherwise the type will default to `string`.
*
* @return string
* @see CaseStatementExpression::then()
*/
public function getReturnType(): string
{
if ($this->returnType !== null) {
return $this->returnType;
}
$types = [];
foreach ($this->when as $when) {
$type = $when->getResultType();
if ($type !== null) {
$types[] = $type;
}
}
if ($this->elseType !== null) {
$types[] = $this->elseType;
}
$types = array_unique($types);
if (count($types) === 1) {
return $types[0];
}
return 'string';
}
/**
* Sets the abstract type that this expression will return.
*
* If no type is being explicitly set via this method, then the
* `getReturnType()` method will try to infer the type from the
* result types of the `then()` and `else() `calls.
*
* @param string $type The type name to use.
* @return $this
*/
public function setReturnType(string $type)
{
$this->returnType = $type;
return $this;
}
/**
* Returns the available data for the given clause.
*
* ### Available clauses
*
* The following clause names are available:
*
* * `value`: The case value for a `CASE case_value WHEN ...` expression.
* * `when`: An array of `WHEN ... THEN ...` expressions.
* * `else`: The `ELSE` result value.
*
* @param string $clause The name of the clause to obtain.
* @return \Cake\Database\ExpressionInterface|object|array<\Cake\Database\Expression\WhenThenExpression>|scalar|null
* @throws \InvalidArgumentException In case the given clause name is invalid.
*/
public function clause(string $clause)
{
if (!in_array($clause, $this->validClauseNames, true)) {
throw new InvalidArgumentException(
sprintf(
'The `$clause` argument must be one of `%s`, the given value `%s` is invalid.',
implode('`, `', $this->validClauseNames),
$clause
)
);
}
return $this->{$clause};
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
if ($this->whenBuffer !== null) {
throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.');
}
if (empty($this->when)) {
throw new LogicException('Case expression must have at least one when statement.');
}
$value = '';
if ($this->isSimpleVariant) {
$value = $this->compileNullableValue($binder, $this->value, $this->valueType) . ' ';
}
$whenThenExpressions = [];
foreach ($this->when as $whenThen) {
$whenThenExpressions[] = $whenThen->sql($binder);
}
$whenThen = implode(' ', $whenThenExpressions);
$else = $this->compileNullableValue($binder, $this->else, $this->elseType);
return "CASE {$value}{$whenThen} ELSE $else END";
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
if ($this->whenBuffer !== null) {
throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.');
}
if ($this->value instanceof ExpressionInterface) {
$callback($this->value);
$this->value->traverse($callback);
}
foreach ($this->when as $when) {
$callback($when);
$when->traverse($callback);
}
if ($this->else instanceof ExpressionInterface) {
$callback($this->else);
$this->else->traverse($callback);
}
return $this;
}
/**
* Clones the inner expression objects.
*
* @return void
*/
public function __clone()
{
if ($this->whenBuffer !== null) {
throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.');
}
if ($this->value instanceof ExpressionInterface) {
$this->value = clone $this->value;
}
foreach ($this->when as $key => $when) {
$this->when[$key] = clone $this->when[$key];
}
if ($this->else instanceof ExpressionInterface) {
$this->else = clone $this->else;
}
}
}

View File

@ -0,0 +1,239 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use Closure;
use RuntimeException;
/**
* An expression that represents a common table expression definition.
*/
class CommonTableExpression implements ExpressionInterface
{
/**
* The CTE name.
*
* @var \Cake\Database\Expression\IdentifierExpression
*/
protected $name;
/**
* The field names to use for the CTE.
*
* @var array<\Cake\Database\Expression\IdentifierExpression>
*/
protected $fields = [];
/**
* The CTE query definition.
*
* @var \Cake\Database\ExpressionInterface|null
*/
protected $query;
/**
* Whether the CTE is materialized or not materialized.
*
* @var string|null
*/
protected $materialized = null;
/**
* Whether the CTE is recursive.
*
* @var bool
*/
protected $recursive = false;
/**
* Constructor.
*
* @param string $name The CTE name.
* @param \Cake\Database\ExpressionInterface|\Closure $query CTE query
*/
public function __construct(string $name = '', $query = null)
{
$this->name = new IdentifierExpression($name);
if ($query) {
$this->query($query);
}
}
/**
* Sets the name of this CTE.
*
* This is the named you used to reference the expression
* in select, insert, etc queries.
*
* @param string $name The CTE name.
* @return $this
*/
public function name(string $name)
{
$this->name = new IdentifierExpression($name);
return $this;
}
/**
* Sets the query for this CTE.
*
* @param \Cake\Database\ExpressionInterface|\Closure $query CTE query
* @return $this
*/
public function query($query)
{
if ($query instanceof Closure) {
$query = $query();
if (!($query instanceof ExpressionInterface)) {
throw new RuntimeException(
'You must return an `ExpressionInterface` from a Closure passed to `query()`.'
);
}
}
$this->query = $query;
return $this;
}
/**
* Adds one or more fields (arguments) to the CTE.
*
* @param \Cake\Database\Expression\IdentifierExpression|array<\Cake\Database\Expression\IdentifierExpression>|array<string>|string $fields Field names
* @return $this
*/
public function field($fields)
{
$fields = (array)$fields;
foreach ($fields as &$field) {
if (!($field instanceof IdentifierExpression)) {
$field = new IdentifierExpression($field);
}
}
$this->fields = array_merge($this->fields, $fields);
return $this;
}
/**
* Sets this CTE as materialized.
*
* @return $this
*/
public function materialized()
{
$this->materialized = 'MATERIALIZED';
return $this;
}
/**
* Sets this CTE as not materialized.
*
* @return $this
*/
public function notMaterialized()
{
$this->materialized = 'NOT MATERIALIZED';
return $this;
}
/**
* Gets whether this CTE is recursive.
*
* @return bool
*/
public function isRecursive(): bool
{
return $this->recursive;
}
/**
* Sets this CTE as recursive.
*
* @return $this
*/
public function recursive()
{
$this->recursive = true;
return $this;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$fields = '';
if ($this->fields) {
$expressions = array_map(function (IdentifierExpression $e) use ($binder) {
return $e->sql($binder);
}, $this->fields);
$fields = sprintf('(%s)', implode(', ', $expressions));
}
$suffix = $this->materialized ? $this->materialized . ' ' : '';
return sprintf(
'%s%s AS %s(%s)',
$this->name->sql($binder),
$fields,
$suffix,
$this->query ? $this->query->sql($binder) : ''
);
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
$callback($this->name);
foreach ($this->fields as $field) {
$callback($field);
$field->traverse($callback);
}
if ($this->query) {
$callback($this->query);
$this->query->traverse($callback);
}
return $this;
}
/**
* Clones the inner expression objects.
*
* @return void
*/
public function __clone()
{
$this->name = clone $this->name;
if ($this->query) {
$this->query = clone $this->query;
}
foreach ($this->fields as $key => $field) {
$this->fields[$key] = clone $field;
}
}
}

View File

@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
use function Cake\Core\deprecationWarning;
deprecationWarning('Since 4.1.0: `Comparison` deprecated. Use `ComparisonExpression` instead.');
class_exists('Cake\Database\Expression\ComparisonExpression');

View File

@ -0,0 +1,324 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\Exception\DatabaseException;
use Cake\Database\ExpressionInterface;
use Cake\Database\Type\ExpressionTypeCasterTrait;
use Cake\Database\ValueBinder;
use Closure;
/**
* A Comparison is a type of query expression that represents an operation
* involving a field an operator and a value. In its most common form the
* string representation of a comparison is `field = value`
*/
class ComparisonExpression implements ExpressionInterface, FieldInterface
{
use ExpressionTypeCasterTrait;
use FieldTrait;
/**
* The value to be used in the right hand side of the operation
*
* @var mixed
*/
protected $_value;
/**
* The type to be used for casting the value to a database representation
*
* @var string|null
*/
protected $_type;
/**
* The operator used for comparing field and value
*
* @var string
*/
protected $_operator = '=';
/**
* Whether the value in this expression is a traversable
*
* @var bool
*/
protected $_isMultiple = false;
/**
* A cached list of ExpressionInterface objects that were
* found in the value for this expression.
*
* @var array<\Cake\Database\ExpressionInterface>
*/
protected $_valueExpressions = [];
/**
* Constructor
*
* @param \Cake\Database\ExpressionInterface|string $field the field name to compare to a value
* @param mixed $value The value to be used in comparison
* @param string|null $type the type name used to cast the value
* @param string $operator the operator used for comparing field and value
*/
public function __construct($field, $value, ?string $type = null, string $operator = '=')
{
$this->_type = $type;
$this->setField($field);
$this->setValue($value);
$this->_operator = $operator;
}
/**
* Sets the value
*
* @param mixed $value The value to compare
* @return void
*/
public function setValue($value): void
{
$value = $this->_castToExpression($value, $this->_type);
$isMultiple = $this->_type && strpos($this->_type, '[]') !== false;
if ($isMultiple) {
[$value, $this->_valueExpressions] = $this->_collectExpressions($value);
}
$this->_isMultiple = $isMultiple;
$this->_value = $value;
}
/**
* Returns the value used for comparison
*
* @return mixed
*/
public function getValue()
{
return $this->_value;
}
/**
* Sets the operator to use for the comparison
*
* @param string $operator The operator to be used for the comparison.
* @return void
*/
public function setOperator(string $operator): void
{
$this->_operator = $operator;
}
/**
* Returns the operator used for comparison
*
* @return string
*/
public function getOperator(): string
{
return $this->_operator;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
/** @var \Cake\Database\ExpressionInterface|string $field */
$field = $this->_field;
if ($field instanceof ExpressionInterface) {
$field = $field->sql($binder);
}
if ($this->_value instanceof IdentifierExpression) {
$template = '%s %s %s';
$value = $this->_value->sql($binder);
} elseif ($this->_value instanceof ExpressionInterface) {
$template = '%s %s (%s)';
$value = $this->_value->sql($binder);
} else {
[$template, $value] = $this->_stringExpression($binder);
}
return sprintf($template, $field, $this->_operator, $value);
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
if ($this->_field instanceof ExpressionInterface) {
$callback($this->_field);
$this->_field->traverse($callback);
}
if ($this->_value instanceof ExpressionInterface) {
$callback($this->_value);
$this->_value->traverse($callback);
}
foreach ($this->_valueExpressions as $v) {
$callback($v);
$v->traverse($callback);
}
return $this;
}
/**
* Create a deep clone.
*
* Clones the field and value if they are expression objects.
*
* @return void
*/
public function __clone()
{
foreach (['_value', '_field'] as $prop) {
if ($this->{$prop} instanceof ExpressionInterface) {
$this->{$prop} = clone $this->{$prop};
}
}
}
/**
* Returns a template and a placeholder for the value after registering it
* with the placeholder $binder
*
* @param \Cake\Database\ValueBinder $binder The value binder to use.
* @return array First position containing the template and the second a placeholder
*/
protected function _stringExpression(ValueBinder $binder): array
{
$template = '%s ';
if ($this->_field instanceof ExpressionInterface && !$this->_field instanceof IdentifierExpression) {
$template = '(%s) ';
}
if ($this->_isMultiple) {
$template .= '%s (%s)';
$type = $this->_type;
if ($type !== null) {
$type = str_replace('[]', '', $type);
}
$value = $this->_flattenValue($this->_value, $binder, $type);
// To avoid SQL errors when comparing a field to a list of empty values,
// better just throw an exception here
if ($value === '') {
$field = $this->_field instanceof ExpressionInterface ? $this->_field->sql($binder) : $this->_field;
/** @psalm-suppress PossiblyInvalidCast */
throw new DatabaseException(
"Impossible to generate condition with empty list of values for field ($field)"
);
}
} else {
$template .= '%s %s';
$value = $this->_bindValue($this->_value, $binder, $this->_type);
}
return [$template, $value];
}
/**
* Registers a value in the placeholder generator and returns the generated placeholder
*
* @param mixed $value The value to bind
* @param \Cake\Database\ValueBinder $binder The value binder to use
* @param string|null $type The type of $value
* @return string generated placeholder
*/
protected function _bindValue($value, ValueBinder $binder, ?string $type = null): string
{
$placeholder = $binder->placeholder('c');
$binder->bind($placeholder, $value, $type);
return $placeholder;
}
/**
* Converts a traversable value into a set of placeholders generated by
* $binder and separated by `,`
*
* @param iterable $value the value to flatten
* @param \Cake\Database\ValueBinder $binder The value binder to use
* @param string|null $type the type to cast values to
* @return string
*/
protected function _flattenValue(iterable $value, ValueBinder $binder, ?string $type = null): string
{
$parts = [];
if (is_array($value)) {
foreach ($this->_valueExpressions as $k => $v) {
$parts[$k] = $v->sql($binder);
unset($value[$k]);
}
}
if (!empty($value)) {
$parts += $binder->generateManyNamed($value, $type);
}
return implode(',', $parts);
}
/**
* Returns an array with the original $values in the first position
* and all ExpressionInterface objects that could be found in the second
* position.
*
* @param \Cake\Database\ExpressionInterface|iterable $values The rows to insert
* @return array
*/
protected function _collectExpressions($values): array
{
if ($values instanceof ExpressionInterface) {
return [$values, []];
}
$expressions = $result = [];
$isArray = is_array($values);
if ($isArray) {
/** @var array $result */
$result = $values;
}
foreach ($values as $k => $v) {
if ($v instanceof ExpressionInterface) {
$expressions[$k] = $v;
}
if ($isArray) {
$result[$k] = $v;
}
}
return [$result, $expressions];
}
}
// phpcs:disable
class_alias(
'Cake\Database\Expression\ComparisonExpression',
'Cake\Database\Expression\Comparison'
);
// phpcs:enable

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
/**
* Describes a getter and a setter for the a field property. Useful for expressions
* that contain an identifier to compare against.
*/
interface FieldInterface
{
/**
* Sets the field name
*
* @param \Cake\Database\ExpressionInterface|array|string $field The field to compare with.
* @return void
*/
public function setField($field): void;
/**
* Returns the field name
*
* @return \Cake\Database\ExpressionInterface|array|string
*/
public function getField();
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
/**
* Contains the field property with a getter and a setter for it
*/
trait FieldTrait
{
/**
* The field name or expression to be used in the left hand side of the operator
*
* @var \Cake\Database\ExpressionInterface|array|string
*/
protected $_field;
/**
* Sets the field name
*
* @param \Cake\Database\ExpressionInterface|array|string $field The field to compare with.
* @return void
*/
public function setField($field): void
{
$this->_field = $field;
}
/**
* Returns the field name
*
* @return \Cake\Database\ExpressionInterface|array|string
*/
public function getField()
{
return $this->_field;
}
}

View File

@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\Type\ExpressionTypeCasterTrait;
use Cake\Database\TypedResultInterface;
use Cake\Database\TypedResultTrait;
use Cake\Database\ValueBinder;
/**
* This class represents a function call string in a SQL statement. Calls can be
* constructed by passing the name of the function and a list of params.
* For security reasons, all params passed are quoted by default unless
* explicitly told otherwise.
*/
class FunctionExpression extends QueryExpression implements TypedResultInterface
{
use ExpressionTypeCasterTrait;
use TypedResultTrait;
/**
* The name of the function to be constructed when generating the SQL string
*
* @var string
*/
protected $_name;
/**
* Constructor. Takes a name for the function to be invoked and a list of params
* to be passed into the function. Optionally you can pass a list of types to
* be used for each bound param.
*
* By default, all params that are passed will be quoted. If you wish to use
* literal arguments, you need to explicitly hint this function.
*
* ### Examples:
*
* `$f = new FunctionExpression('CONCAT', ['CakePHP', ' rules']);`
*
* Previous line will generate `CONCAT('CakePHP', ' rules')`
*
* `$f = new FunctionExpression('CONCAT', ['name' => 'literal', ' rules']);`
*
* Will produce `CONCAT(name, ' rules')`
*
* @param string $name the name of the function to be constructed
* @param array $params list of arguments to be passed to the function
* If associative the key would be used as argument when value is 'literal'
* @param array<string, string>|array<string|null> $types Associative array of types to be associated with the
* passed arguments
* @param string $returnType The return type of this expression
*/
public function __construct(string $name, array $params = [], array $types = [], string $returnType = 'string')
{
$this->_name = $name;
$this->_returnType = $returnType;
parent::__construct($params, $types, ',');
}
/**
* Sets the name of the SQL function to be invoke in this expression.
*
* @param string $name The name of the function
* @return $this
*/
public function setName(string $name)
{
$this->_name = $name;
return $this;
}
/**
* Gets the name of the SQL function to be invoke in this expression.
*
* @return string
*/
public function getName(): string
{
return $this->_name;
}
/**
* Adds one or more arguments for the function call.
*
* @param array $conditions list of arguments to be passed to the function
* If associative the key would be used as argument when value is 'literal'
* @param array<string, string> $types Associative array of types to be associated with the
* passed arguments
* @param bool $prepend Whether to prepend or append to the list of arguments
* @see \Cake\Database\Expression\FunctionExpression::__construct() for more details.
* @return $this
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function add($conditions, array $types = [], bool $prepend = false)
{
$put = $prepend ? 'array_unshift' : 'array_push';
$typeMap = $this->getTypeMap()->setTypes($types);
foreach ($conditions as $k => $p) {
if ($p === 'literal') {
$put($this->_conditions, $k);
continue;
}
if ($p === 'identifier') {
$put($this->_conditions, new IdentifierExpression($k));
continue;
}
$type = $typeMap->type($k);
if ($type !== null && !$p instanceof ExpressionInterface) {
$p = $this->_castToExpression($p, $type);
}
if ($p instanceof ExpressionInterface) {
$put($this->_conditions, $p);
continue;
}
$put($this->_conditions, ['value' => $p, 'type' => $type]);
}
return $this;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$parts = [];
foreach ($this->_conditions as $condition) {
if ($condition instanceof Query) {
$condition = sprintf('(%s)', $condition->sql($binder));
} elseif ($condition instanceof ExpressionInterface) {
$condition = $condition->sql($binder);
} elseif (is_array($condition)) {
$p = $binder->placeholder('param');
$binder->bind($p, $condition['value'], $condition['type']);
$condition = $p;
}
$parts[] = $condition;
}
return $this->_name . sprintf('(%s)', implode(
$this->_conjunction . ' ',
$parts
));
}
/**
* The name of the function is in itself an expression to generate, thus
* always adding 1 to the amount of expressions stored in this object.
*
* @return int
*/
public function count(): int
{
return 1 + count($this->_conditions);
}
}

View File

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use Closure;
/**
* Represents a single identifier name in the database.
*
* Identifier values are unsafe with user supplied data.
* Values will be quoted when identifier quoting is enabled.
*
* @see \Cake\Database\Query::identifier()
*/
class IdentifierExpression implements ExpressionInterface
{
/**
* Holds the identifier string
*
* @var string
*/
protected $_identifier;
/**
* @var string|null
*/
protected $collation;
/**
* Constructor
*
* @param string $identifier The identifier this expression represents
* @param string|null $collation The identifier collation
*/
public function __construct(string $identifier, ?string $collation = null)
{
$this->_identifier = $identifier;
$this->collation = $collation;
}
/**
* Sets the identifier this expression represents
*
* @param string $identifier The identifier
* @return void
*/
public function setIdentifier(string $identifier): void
{
$this->_identifier = $identifier;
}
/**
* Returns the identifier this expression represents
*
* @return string
*/
public function getIdentifier(): string
{
return $this->_identifier;
}
/**
* Sets the collation.
*
* @param string $collation Identifier collation
* @return void
*/
public function setCollation(string $collation): void
{
$this->collation = $collation;
}
/**
* Returns the collation.
*
* @return string|null
*/
public function getCollation(): ?string
{
return $this->collation;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$sql = $this->_identifier;
if ($this->collation) {
$sql .= ' COLLATE ' . $this->collation;
}
return $sql;
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
return $this;
}
}

View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use RuntimeException;
/**
* An expression object for ORDER BY clauses
*/
class OrderByExpression extends QueryExpression
{
/**
* Constructor
*
* @param \Cake\Database\ExpressionInterface|array|string $conditions The sort columns
* @param \Cake\Database\TypeMap|array<string, string> $types The types for each column.
* @param string $conjunction The glue used to join conditions together.
*/
public function __construct($conditions = [], $types = [], $conjunction = '')
{
parent::__construct($conditions, $types, $conjunction);
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$order = [];
foreach ($this->_conditions as $k => $direction) {
if ($direction instanceof ExpressionInterface) {
$direction = $direction->sql($binder);
}
$order[] = is_numeric($k) ? $direction : sprintf('%s %s', $k, $direction);
}
return sprintf('ORDER BY %s', implode(', ', $order));
}
/**
* Auxiliary function used for decomposing a nested array of conditions and
* building a tree structure inside this object to represent the full SQL expression.
*
* New order by expressions are merged to existing ones
*
* @param array $conditions list of order by expressions
* @param array $types list of types associated on fields referenced in $conditions
* @return void
*/
protected function _addConditions(array $conditions, array $types): void
{
foreach ($conditions as $key => $val) {
if (
is_string($key) &&
is_string($val) &&
!in_array(strtoupper($val), ['ASC', 'DESC'], true)
) {
throw new RuntimeException(
sprintf(
'Passing extra expressions by associative array (`\'%s\' => \'%s\'`) ' .
'is not allowed to avoid potential SQL injection. ' .
'Use QueryExpression or numeric array instead.',
$key,
$val
)
);
}
}
$this->_conditions = array_merge($this->_conditions, $conditions);
}
}

View File

@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\ValueBinder;
use Closure;
/**
* An expression object for complex ORDER BY clauses
*/
class OrderClauseExpression implements ExpressionInterface, FieldInterface
{
use FieldTrait;
/**
* The direction of sorting.
*
* @var string
*/
protected $_direction;
/**
* Constructor
*
* @param \Cake\Database\ExpressionInterface|string $field The field to order on.
* @param string $direction The direction to sort on.
*/
public function __construct($field, $direction)
{
$this->_field = $field;
$this->_direction = strtolower($direction) === 'asc' ? 'ASC' : 'DESC';
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
/** @var \Cake\Database\ExpressionInterface|string $field */
$field = $this->_field;
if ($field instanceof Query) {
$field = sprintf('(%s)', $field->sql($binder));
} elseif ($field instanceof ExpressionInterface) {
$field = $field->sql($binder);
}
return sprintf('%s %s', $field, $this->_direction);
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
if ($this->_field instanceof ExpressionInterface) {
$callback($this->_field);
$this->_field->traverse($callback);
}
return $this;
}
/**
* Create a deep clone of the order clause.
*
* @return void
*/
public function __clone()
{
if ($this->_field instanceof ExpressionInterface) {
$this->_field = clone $this->_field;
}
}
}

View File

@ -0,0 +1,876 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\TypeMapTrait;
use Cake\Database\ValueBinder;
use Closure;
use Countable;
use InvalidArgumentException;
use function Cake\Core\deprecationWarning;
/**
* Represents a SQL Query expression. Internally it stores a tree of
* expressions that can be compiled by converting this object to string
* and will contain a correctly parenthesized and nested expression.
*/
class QueryExpression implements ExpressionInterface, Countable
{
use TypeMapTrait;
/**
* String to be used for joining each of the internal expressions
* this object internally stores for example "AND", "OR", etc.
*
* @var string
*/
protected $_conjunction;
/**
* A list of strings or other expression objects that represent the "branches" of
* the expression tree. For example one key of the array might look like "sum > :value"
*
* @var array
*/
protected $_conditions = [];
/**
* Constructor. A new expression object can be created without any params and
* be built dynamically. Otherwise, it is possible to pass an array of conditions
* containing either a tree-like array structure to be parsed and/or other
* expression objects. Optionally, you can set the conjunction keyword to be used
* for joining each part of this level of the expression tree.
*
* @param \Cake\Database\ExpressionInterface|array|string $conditions Tree like array structure
* containing all the conditions to be added or nested inside this expression object.
* @param \Cake\Database\TypeMap|array $types Associative array of types to be associated with the values
* passed in $conditions.
* @param string $conjunction the glue that will join all the string conditions at this
* level of the expression tree. For example "AND", "OR", "XOR"...
* @see \Cake\Database\Expression\QueryExpression::add() for more details on $conditions and $types
*/
public function __construct($conditions = [], $types = [], $conjunction = 'AND')
{
$this->setTypeMap($types);
$this->setConjunction(strtoupper($conjunction));
if (!empty($conditions)) {
$this->add($conditions, $this->getTypeMap()->getTypes());
}
}
/**
* Changes the conjunction for the conditions at this level of the expression tree.
*
* @param string $conjunction Value to be used for joining conditions
* @return $this
*/
public function setConjunction(string $conjunction)
{
$this->_conjunction = strtoupper($conjunction);
return $this;
}
/**
* Gets the currently configured conjunction for the conditions at this level of the expression tree.
*
* @return string
*/
public function getConjunction(): string
{
return $this->_conjunction;
}
/**
* Adds one or more conditions to this expression object. Conditions can be
* expressed in a one dimensional array, that will cause all conditions to
* be added directly at this level of the tree or they can be nested arbitrarily
* making it create more expression objects that will be nested inside and
* configured to use the specified conjunction.
*
* If the type passed for any of the fields is expressed "type[]" (note braces)
* then it will cause the placeholder to be re-written dynamically so if the
* value is an array, it will create as many placeholders as values are in it.
*
* @param \Cake\Database\ExpressionInterface|array|string $conditions single or multiple conditions to
* be added. When using an array and the key is 'OR' or 'AND' a new expression
* object will be created with that conjunction and internal array value passed
* as conditions.
* @param array<int|string, string> $types Associative array of fields pointing to the type of the
* values that are being passed. Used for correctly binding values to statements.
* @see \Cake\Database\Query::where() for examples on conditions
* @return $this
*/
public function add($conditions, array $types = [])
{
if (is_string($conditions) || $conditions instanceof ExpressionInterface) {
$this->_conditions[] = $conditions;
return $this;
}
$this->_addConditions($conditions, $types);
return $this;
}
/**
* Adds a new condition to the expression object in the form "field = value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* If it is suffixed with "[]" and the value is an array then multiple placeholders
* will be created, one per each value in the array.
* @return $this
*/
public function eq($field, $value, ?string $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
return $this->add(new ComparisonExpression($field, $value, $type, '='));
}
/**
* Adds a new condition to the expression object in the form "field != value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* If it is suffixed with "[]" and the value is an array then multiple placeholders
* will be created, one per each value in the array.
* @return $this
*/
public function notEq($field, $value, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
return $this->add(new ComparisonExpression($field, $value, $type, '!='));
}
/**
* Adds a new condition to the expression object in the form "field > value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function gt($field, $value, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
return $this->add(new ComparisonExpression($field, $value, $type, '>'));
}
/**
* Adds a new condition to the expression object in the form "field < value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function lt($field, $value, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
return $this->add(new ComparisonExpression($field, $value, $type, '<'));
}
/**
* Adds a new condition to the expression object in the form "field >= value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function gte($field, $value, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
return $this->add(new ComparisonExpression($field, $value, $type, '>='));
}
/**
* Adds a new condition to the expression object in the form "field <= value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function lte($field, $value, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
return $this->add(new ComparisonExpression($field, $value, $type, '<='));
}
/**
* Adds a new condition to the expression object in the form "field IS NULL".
*
* @param \Cake\Database\ExpressionInterface|string $field database field to be
* tested for null
* @return $this
*/
public function isNull($field)
{
if (!($field instanceof ExpressionInterface)) {
$field = new IdentifierExpression($field);
}
return $this->add(new UnaryExpression('IS NULL', $field, UnaryExpression::POSTFIX));
}
/**
* Adds a new condition to the expression object in the form "field IS NOT NULL".
*
* @param \Cake\Database\ExpressionInterface|string $field database field to be
* tested for not null
* @return $this
*/
public function isNotNull($field)
{
if (!($field instanceof ExpressionInterface)) {
$field = new IdentifierExpression($field);
}
return $this->add(new UnaryExpression('IS NOT NULL', $field, UnaryExpression::POSTFIX));
}
/**
* Adds a new condition to the expression object in the form "field LIKE value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function like($field, $value, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
return $this->add(new ComparisonExpression($field, $value, $type, 'LIKE'));
}
/**
* Adds a new condition to the expression object in the form "field NOT LIKE value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function notLike($field, $value, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
return $this->add(new ComparisonExpression($field, $value, $type, 'NOT LIKE'));
}
/**
* Adds a new condition to the expression object in the form
* "field IN (value1, value2)".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param \Cake\Database\ExpressionInterface|array|string $values the value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function in($field, $values, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
$type = $type ?: 'string';
$type .= '[]';
$values = $values instanceof ExpressionInterface ? $values : (array)$values;
return $this->add(new ComparisonExpression($field, $values, $type, 'IN'));
}
/**
* Adds a new case expression to the expression object
*
* @param \Cake\Database\ExpressionInterface|array $conditions The conditions to test. Must be a ExpressionInterface
* instance, or an array of ExpressionInterface instances.
* @param \Cake\Database\ExpressionInterface|array $values Associative array of values to be associated with the
* conditions passed in $conditions. If there are more $values than $conditions,
* the last $value is used as the `ELSE` value.
* @param array<string> $types Associative array of types to be associated with the values
* passed in $values
* @return $this
* @deprecated 4.3.0 Use QueryExpression::case() or CaseStatementExpression instead
*/
public function addCase($conditions, $values = [], $types = [])
{
deprecationWarning('QueryExpression::addCase() is deprecated, use case() instead.');
return $this->add(new CaseExpression($conditions, $values, $types));
}
/**
* Returns a new case expression object.
*
* When a value is set, the syntax generated is
* `CASE case_value WHEN when_value ... END` (simple case),
* where the `when_value`'s are compared against the
* `case_value`.
*
* When no value is set, the syntax generated is
* `CASE WHEN when_conditions ... END` (searched case),
* where the conditions hold the comparisons.
*
* Note that `null` is a valid case value, and thus should
* only be passed if you actually want to create the simple
* case expression variant!
*
* @param \Cake\Database\ExpressionInterface|object|scalar|null $value The case value.
* @param string|null $type The case value type. If no type is provided, the type will be tried to be inferred
* from the value.
* @return \Cake\Database\Expression\CaseStatementExpression
*/
public function case($value = null, ?string $type = null): CaseStatementExpression
{
if (func_num_args() > 0) {
$expression = new CaseStatementExpression($value, $type);
} else {
$expression = new CaseStatementExpression();
}
return $expression->setTypeMap($this->getTypeMap());
}
/**
* Adds a new condition to the expression object in the form
* "field NOT IN (value1, value2)".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param \Cake\Database\ExpressionInterface|array|string $values the value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function notIn($field, $values, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
$type = $type ?: 'string';
$type .= '[]';
$values = $values instanceof ExpressionInterface ? $values : (array)$values;
return $this->add(new ComparisonExpression($field, $values, $type, 'NOT IN'));
}
/**
* Adds a new condition to the expression object in the form
* "(field NOT IN (value1, value2) OR field IS NULL".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param \Cake\Database\ExpressionInterface|array|string $values the value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function notInOrNull($field, $values, ?string $type = null)
{
$or = new static([], [], 'OR');
$or
->notIn($field, $values, $type)
->isNull($field);
return $this->add($or);
}
/**
* Adds a new condition to the expression object in the form "EXISTS (...)".
*
* @param \Cake\Database\ExpressionInterface $expression the inner query
* @return $this
*/
public function exists(ExpressionInterface $expression)
{
return $this->add(new UnaryExpression('EXISTS', $expression, UnaryExpression::PREFIX));
}
/**
* Adds a new condition to the expression object in the form "NOT EXISTS (...)".
*
* @param \Cake\Database\ExpressionInterface $expression the inner query
* @return $this
*/
public function notExists(ExpressionInterface $expression)
{
return $this->add(new UnaryExpression('NOT EXISTS', $expression, UnaryExpression::PREFIX));
}
/**
* Adds a new condition to the expression object in the form
* "field BETWEEN from AND to".
*
* @param \Cake\Database\ExpressionInterface|string $field The field name to compare for values inbetween the range.
* @param mixed $from The initial value of the range.
* @param mixed $to The ending value in the comparison range.
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function between($field, $from, $to, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
return $this->add(new BetweenExpression($field, $from, $to, $type));
}
/**
* Returns a new QueryExpression object containing all the conditions passed
* and set up the conjunction to be "AND"
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be joined with AND
* @param array<string, string> $types Associative array of fields pointing to the type of the
* values that are being passed. Used for correctly binding values to statements.
* @return \Cake\Database\Expression\QueryExpression
*/
public function and($conditions, $types = [])
{
if ($conditions instanceof Closure) {
return $conditions(new static([], $this->getTypeMap()->setTypes($types)));
}
return new static($conditions, $this->getTypeMap()->setTypes($types));
}
/**
* Returns a new QueryExpression object containing all the conditions passed
* and set up the conjunction to be "OR"
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be joined with OR
* @param array<string, string> $types Associative array of fields pointing to the type of the
* values that are being passed. Used for correctly binding values to statements.
* @return \Cake\Database\Expression\QueryExpression
*/
public function or($conditions, $types = [])
{
if ($conditions instanceof Closure) {
return $conditions(new static([], $this->getTypeMap()->setTypes($types), 'OR'));
}
return new static($conditions, $this->getTypeMap()->setTypes($types), 'OR');
}
// phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
/**
* Returns a new QueryExpression object containing all the conditions passed
* and set up the conjunction to be "AND"
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be joined with AND
* @param array<string, string> $types Associative array of fields pointing to the type of the
* values that are being passed. Used for correctly binding values to statements.
* @return \Cake\Database\Expression\QueryExpression
* @deprecated 4.0.0 Use {@link and()} instead.
*/
public function and_($conditions, $types = [])
{
deprecationWarning('QueryExpression::and_() is deprecated use and() instead.');
return $this->and($conditions, $types);
}
/**
* Returns a new QueryExpression object containing all the conditions passed
* and set up the conjunction to be "OR"
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be joined with OR
* @param array<string, string> $types Associative array of fields pointing to the type of the
* values that are being passed. Used for correctly binding values to statements.
* @return \Cake\Database\Expression\QueryExpression
* @deprecated 4.0.0 Use {@link or()} instead.
*/
public function or_($conditions, $types = [])
{
deprecationWarning('QueryExpression::or_() is deprecated use or() instead.');
return $this->or($conditions, $types);
}
// phpcs:enable
/**
* Adds a new set of conditions to this level of the tree and negates
* the final result by prepending a NOT, it will look like
* "NOT ( (condition1) AND (conditions2) )" conjunction depends on the one
* currently configured for this object.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be added and negated
* @param array<string, string> $types Associative array of fields pointing to the type of the
* values that are being passed. Used for correctly binding values to statements.
* @return $this
*/
public function not($conditions, $types = [])
{
return $this->add(['NOT' => $conditions], $types);
}
/**
* Returns the number of internal conditions that are stored in this expression.
* Useful to determine if this expression object is void or it will generate
* a non-empty string when compiled
*
* @return int
*/
public function count(): int
{
return count($this->_conditions);
}
/**
* Builds equal condition or assignment with identifier wrapping.
*
* @param string $leftField Left join condition field name.
* @param string $rightField Right join condition field name.
* @return $this
*/
public function equalFields(string $leftField, string $rightField)
{
$wrapIdentifier = function ($field) {
if ($field instanceof ExpressionInterface) {
return $field;
}
return new IdentifierExpression($field);
};
return $this->eq($wrapIdentifier($leftField), $wrapIdentifier($rightField));
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$len = $this->count();
if ($len === 0) {
return '';
}
$conjunction = $this->_conjunction;
$template = $len === 1 ? '%s' : '(%s)';
$parts = [];
foreach ($this->_conditions as $part) {
if ($part instanceof Query) {
$part = '(' . $part->sql($binder) . ')';
} elseif ($part instanceof ExpressionInterface) {
$part = $part->sql($binder);
}
if ($part !== '') {
$parts[] = $part;
}
}
return sprintf($template, implode(" $conjunction ", $parts));
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
foreach ($this->_conditions as $c) {
if ($c instanceof ExpressionInterface) {
$callback($c);
$c->traverse($callback);
}
}
return $this;
}
/**
* Executes a callable function for each of the parts that form this expression.
*
* The callable function is required to return a value with which the currently
* visited part will be replaced. If the callable function returns null then
* the part will be discarded completely from this expression.
*
* The callback function will receive each of the conditions as first param and
* the key as second param. It is possible to declare the second parameter as
* passed by reference, this will enable you to change the key under which the
* modified part is stored.
*
* @param callable $callback The callable to apply to each part.
* @return $this
*/
public function iterateParts(callable $callback)
{
$parts = [];
foreach ($this->_conditions as $k => $c) {
$key = &$k;
$part = $callback($c, $key);
if ($part !== null) {
$parts[$key] = $part;
}
}
$this->_conditions = $parts;
return $this;
}
/**
* Check whether a callable is acceptable.
*
* We don't accept ['class', 'method'] style callbacks,
* as they often contain user input and arrays of strings
* are easy to sneak in.
*
* @param \Cake\Database\ExpressionInterface|callable|array|string $callable The callable to check.
* @return bool Valid callable.
* @deprecated 4.2.0 This method is unused.
* @codeCoverageIgnore
*/
public function isCallable($callable): bool
{
if (is_string($callable)) {
return false;
}
if (is_object($callable) && is_callable($callable)) {
return true;
}
return is_array($callable) && isset($callable[0]) && is_object($callable[0]) && is_callable($callable);
}
/**
* Returns true if this expression contains any other nested
* ExpressionInterface objects
*
* @return bool
*/
public function hasNestedExpression(): bool
{
foreach ($this->_conditions as $c) {
if ($c instanceof ExpressionInterface) {
return true;
}
}
return false;
}
/**
* Auxiliary function used for decomposing a nested array of conditions and build
* a tree structure inside this object to represent the full SQL expression.
* String conditions are stored directly in the conditions, while any other
* representation is wrapped around an adequate instance or of this class.
*
* @param array $conditions list of conditions to be stored in this object
* @param array<int|string, string> $types list of types associated on fields referenced in $conditions
* @return void
*/
protected function _addConditions(array $conditions, array $types): void
{
$operators = ['and', 'or', 'xor'];
$typeMap = $this->getTypeMap()->setTypes($types);
foreach ($conditions as $k => $c) {
$numericKey = is_numeric($k);
if ($c instanceof Closure) {
$expr = new static([], $typeMap);
$c = $c($expr, $this);
}
if ($numericKey && empty($c)) {
continue;
}
$isArray = is_array($c);
$isOperator = $isNot = false;
if (!$numericKey) {
$normalizedKey = strtolower($k);
$isOperator = in_array($normalizedKey, $operators);
$isNot = $normalizedKey === 'not';
}
if (($isOperator || $isNot) && ($isArray || $c instanceof Countable) && count($c) === 0) {
continue;
}
if ($numericKey && $c instanceof ExpressionInterface) {
$this->_conditions[] = $c;
continue;
}
if ($numericKey && is_string($c)) {
$this->_conditions[] = $c;
continue;
}
if ($numericKey && $isArray || $isOperator) {
$this->_conditions[] = new static($c, $typeMap, $numericKey ? 'AND' : $k);
continue;
}
if ($isNot) {
$this->_conditions[] = new UnaryExpression('NOT', new static($c, $typeMap));
continue;
}
if (!$numericKey) {
$this->_conditions[] = $this->_parseCondition($k, $c);
}
}
}
/**
* Parses a string conditions by trying to extract the operator inside it if any
* and finally returning either an adequate QueryExpression object or a plain
* string representation of the condition. This function is responsible for
* generating the placeholders and replacing the values by them, while storing
* the value elsewhere for future binding.
*
* @param string $field The value from which the actual field and operator will
* be extracted.
* @param mixed $value The value to be bound to a placeholder for the field
* @return \Cake\Database\ExpressionInterface
* @throws \InvalidArgumentException If operator is invalid or missing on NULL usage.
*/
protected function _parseCondition(string $field, $value)
{
$field = trim($field);
$operator = '=';
$expression = $field;
$spaces = substr_count($field, ' ');
// Handle field values that contain multiple spaces, such as
// operators with a space in them like `field IS NOT` and
// `field NOT LIKE`, or combinations with function expressions
// like `CONCAT(first_name, ' ', last_name) IN`.
if ($spaces > 1) {
$parts = explode(' ', $field);
if (preg_match('/(is not|not \w+)$/i', $field)) {
$last = array_pop($parts);
$second = array_pop($parts);
$parts[] = "{$second} {$last}";
}
$operator = array_pop($parts);
$expression = implode(' ', $parts);
} elseif ($spaces == 1) {
$parts = explode(' ', $field, 2);
[$expression, $operator] = $parts;
}
$operator = strtolower(trim($operator));
$type = $this->getTypeMap()->type($expression);
$typeMultiple = (is_string($type) && strpos($type, '[]') !== false);
if (in_array($operator, ['in', 'not in']) || $typeMultiple) {
$type = $type ?: 'string';
if (!$typeMultiple) {
$type .= '[]';
}
$operator = $operator === '=' ? 'IN' : $operator;
$operator = $operator === '!=' ? 'NOT IN' : $operator;
$typeMultiple = true;
}
if ($typeMultiple) {
$value = $value instanceof ExpressionInterface ? $value : (array)$value;
}
if ($operator === 'is' && $value === null) {
return new UnaryExpression(
'IS NULL',
new IdentifierExpression($expression),
UnaryExpression::POSTFIX
);
}
if ($operator === 'is not' && $value === null) {
return new UnaryExpression(
'IS NOT NULL',
new IdentifierExpression($expression),
UnaryExpression::POSTFIX
);
}
if ($operator === 'is' && $value !== null) {
$operator = '=';
}
if ($operator === 'is not' && $value !== null) {
$operator = '!=';
}
if ($value === null && $this->_conjunction !== ',') {
throw new InvalidArgumentException(
sprintf('Expression `%s` is missing operator (IS, IS NOT) with `null` value.', $expression)
);
}
return new ComparisonExpression($expression, $value, $type, $operator);
}
/**
* Returns the type name for the passed field if it was stored in the typeMap
*
* @param \Cake\Database\ExpressionInterface|string $field The field name to get a type for.
* @return string|null The computed type or null, if the type is unknown.
*/
protected function _calculateType($field): ?string
{
$field = $field instanceof IdentifierExpression ? $field->getIdentifier() : $field;
if (is_string($field)) {
return $this->getTypeMap()->type($field);
}
return null;
}
/**
* Clone this object and its subtree of expressions.
*
* @return void
*/
public function __clone()
{
foreach ($this->_conditions as $i => $condition) {
if ($condition instanceof ExpressionInterface) {
$this->_conditions[$i] = clone $condition;
}
}
}
}

View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use Closure;
/**
* String expression with collation.
*/
class StringExpression implements ExpressionInterface
{
/**
* @var string
*/
protected $string;
/**
* @var string
*/
protected $collation;
/**
* @param string $string String value
* @param string $collation String collation
*/
public function __construct(string $string, string $collation)
{
$this->string = $string;
$this->collation = $collation;
}
/**
* Sets the string collation.
*
* @param string $collation String collation
* @return void
*/
public function setCollation(string $collation): void
{
$this->collation = $collation;
}
/**
* Returns the string collation.
*
* @return string
*/
public function getCollation(): string
{
return $this->collation;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$placeholder = $binder->placeholder('c');
$binder->bind($placeholder, $this->string, 'string');
return $placeholder . ' COLLATE ' . $this->collation;
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
return $this;
}
}

View File

@ -0,0 +1,231 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use Closure;
use InvalidArgumentException;
/**
* This expression represents SQL fragments that are used for comparing one tuple
* to another, one tuple to a set of other tuples or one tuple to an expression
*/
class TupleComparison extends ComparisonExpression
{
/**
* The type to be used for casting the value to a database representation
*
* @var array<string|null>
* @psalm-suppress NonInvariantDocblockPropertyType
*/
protected $_type;
/**
* Constructor
*
* @param \Cake\Database\ExpressionInterface|array|string $fields the fields to use to form a tuple
* @param \Cake\Database\ExpressionInterface|array $values the values to use to form a tuple
* @param array<string|null> $types the types names to use for casting each of the values, only
* one type per position in the value array in needed
* @param string $conjunction the operator used for comparing field and value
*/
public function __construct($fields, $values, array $types = [], string $conjunction = '=')
{
$this->_type = $types;
$this->setField($fields);
$this->_operator = $conjunction;
$this->setValue($values);
}
/**
* Returns the type to be used for casting the value to a database representation
*
* @return array<string|null>
*/
public function getType(): array
{
return $this->_type;
}
/**
* Sets the value
*
* @param mixed $value The value to compare
* @return void
*/
public function setValue($value): void
{
if ($this->isMulti()) {
if (is_array($value) && !is_array(current($value))) {
throw new InvalidArgumentException(
'Multi-tuple comparisons require a multi-tuple value, single-tuple given.'
);
}
} else {
if (is_array($value) && is_array(current($value))) {
throw new InvalidArgumentException(
'Single-tuple comparisons require a single-tuple value, multi-tuple given.'
);
}
}
$this->_value = $value;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$template = '(%s) %s (%s)';
$fields = [];
$originalFields = $this->getField();
if (!is_array($originalFields)) {
$originalFields = [$originalFields];
}
foreach ($originalFields as $field) {
$fields[] = $field instanceof ExpressionInterface ? $field->sql($binder) : $field;
}
$values = $this->_stringifyValues($binder);
$field = implode(', ', $fields);
return sprintf($template, $field, $this->_operator, $values);
}
/**
* Returns a string with the values as placeholders in a string to be used
* for the SQL version of this expression
*
* @param \Cake\Database\ValueBinder $binder The value binder to convert expressions with.
* @return string
*/
protected function _stringifyValues(ValueBinder $binder): string
{
$values = [];
$parts = $this->getValue();
if ($parts instanceof ExpressionInterface) {
return $parts->sql($binder);
}
foreach ($parts as $i => $value) {
if ($value instanceof ExpressionInterface) {
$values[] = $value->sql($binder);
continue;
}
$type = $this->_type;
$isMultiOperation = $this->isMulti();
if (empty($type)) {
$type = null;
}
if ($isMultiOperation) {
$bound = [];
foreach ($value as $k => $val) {
/** @var string $valType */
$valType = $type && isset($type[$k]) ? $type[$k] : $type;
$bound[] = $this->_bindValue($val, $binder, $valType);
}
$values[] = sprintf('(%s)', implode(',', $bound));
continue;
}
/** @var string $valType */
$valType = $type && isset($type[$i]) ? $type[$i] : $type;
$values[] = $this->_bindValue($value, $binder, $valType);
}
return implode(', ', $values);
}
/**
* @inheritDoc
*/
protected function _bindValue($value, ValueBinder $binder, ?string $type = null): string
{
$placeholder = $binder->placeholder('tuple');
$binder->bind($placeholder, $value, $type);
return $placeholder;
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
/** @var array<string> $fields */
$fields = $this->getField();
foreach ($fields as $field) {
$this->_traverseValue($field, $callback);
}
$value = $this->getValue();
if ($value instanceof ExpressionInterface) {
$callback($value);
$value->traverse($callback);
return $this;
}
foreach ($value as $val) {
if ($this->isMulti()) {
foreach ($val as $v) {
$this->_traverseValue($v, $callback);
}
} else {
$this->_traverseValue($val, $callback);
}
}
return $this;
}
/**
* Conditionally executes the callback for the passed value if
* it is an ExpressionInterface
*
* @param mixed $value The value to traverse
* @param \Closure $callback The callable to use when traversing
* @return void
*/
protected function _traverseValue($value, Closure $callback): void
{
if ($value instanceof ExpressionInterface) {
$callback($value);
$value->traverse($callback);
}
}
/**
* Determines if each of the values in this expressions is a tuple in
* itself
*
* @return bool
*/
public function isMulti(): bool
{
return in_array(strtolower($this->_operator), ['in', 'not in']);
}
}

View File

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use Closure;
/**
* An expression object that represents an expression with only a single operand.
*/
class UnaryExpression implements ExpressionInterface
{
/**
* Indicates that the operation is in pre-order
*
* @var int
*/
public const PREFIX = 0;
/**
* Indicates that the operation is in post-order
*
* @var int
*/
public const POSTFIX = 1;
/**
* The operator this unary expression represents
*
* @var string
*/
protected $_operator;
/**
* Holds the value which the unary expression operates
*
* @var mixed
*/
protected $_value;
/**
* Where to place the operator
*
* @var int
*/
protected $position;
/**
* Constructor
*
* @param string $operator The operator to used for the expression
* @param mixed $value the value to use as the operand for the expression
* @param int $position either UnaryExpression::PREFIX or UnaryExpression::POSTFIX
*/
public function __construct(string $operator, $value, $position = self::PREFIX)
{
$this->_operator = $operator;
$this->_value = $value;
$this->position = $position;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$operand = $this->_value;
if ($operand instanceof ExpressionInterface) {
$operand = $operand->sql($binder);
}
if ($this->position === self::POSTFIX) {
return '(' . $operand . ') ' . $this->_operator;
}
return $this->_operator . ' (' . $operand . ')';
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
if ($this->_value instanceof ExpressionInterface) {
$callback($this->_value);
$this->_value->traverse($callback);
}
return $this;
}
/**
* Perform a deep clone of the inner expression.
*
* @return void
*/
public function __clone()
{
if ($this->_value instanceof ExpressionInterface) {
$this->_value = clone $this->_value;
}
}
}

View File

@ -0,0 +1,325 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\Exception\DatabaseException;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\Type\ExpressionTypeCasterTrait;
use Cake\Database\TypeMap;
use Cake\Database\TypeMapTrait;
use Cake\Database\ValueBinder;
use Closure;
/**
* An expression object to contain values being inserted.
*
* Helps generate SQL with the correct number of placeholders and bind
* values correctly into the statement.
*/
class ValuesExpression implements ExpressionInterface
{
use ExpressionTypeCasterTrait;
use TypeMapTrait;
/**
* Array of values to insert.
*
* @var array
*/
protected $_values = [];
/**
* List of columns to ensure are part of the insert.
*
* @var array
*/
protected $_columns = [];
/**
* The Query object to use as a values expression
*
* @var \Cake\Database\Query|null
*/
protected $_query;
/**
* Whether values have been casted to expressions
* already.
*
* @var bool
*/
protected $_castedExpressions = false;
/**
* Constructor
*
* @param array $columns The list of columns that are going to be part of the values.
* @param \Cake\Database\TypeMap $typeMap A dictionary of column -> type names
*/
public function __construct(array $columns, TypeMap $typeMap)
{
$this->_columns = $columns;
$this->setTypeMap($typeMap);
}
/**
* Add a row of data to be inserted.
*
* @param \Cake\Database\Query|array $values Array of data to append into the insert, or
* a query for doing INSERT INTO .. SELECT style commands
* @return void
* @throws \Cake\Database\Exception\DatabaseException When mixing array + Query data types.
*/
public function add($values): void
{
if (
(
count($this->_values) &&
$values instanceof Query
) ||
(
$this->_query &&
is_array($values)
)
) {
throw new DatabaseException(
'You cannot mix subqueries and array values in inserts.'
);
}
if ($values instanceof Query) {
$this->setQuery($values);
return;
}
$this->_values[] = $values;
$this->_castedExpressions = false;
}
/**
* Sets the columns to be inserted.
*
* @param array $columns Array with columns to be inserted.
* @return $this
*/
public function setColumns(array $columns)
{
$this->_columns = $columns;
$this->_castedExpressions = false;
return $this;
}
/**
* Gets the columns to be inserted.
*
* @return array
*/
public function getColumns(): array
{
return $this->_columns;
}
/**
* Get the bare column names.
*
* Because column names could be identifier quoted, we
* need to strip the identifiers off of the columns.
*
* @return array
*/
protected function _columnNames(): array
{
$columns = [];
foreach ($this->_columns as $col) {
if (is_string($col)) {
$col = trim($col, '`[]"');
}
$columns[] = $col;
}
return $columns;
}
/**
* Sets the values to be inserted.
*
* @param array $values Array with values to be inserted.
* @return $this
*/
public function setValues(array $values)
{
$this->_values = $values;
$this->_castedExpressions = false;
return $this;
}
/**
* Gets the values to be inserted.
*
* @return array
*/
public function getValues(): array
{
if (!$this->_castedExpressions) {
$this->_processExpressions();
}
return $this->_values;
}
/**
* Sets the query object to be used as the values expression to be evaluated
* to insert records in the table.
*
* @param \Cake\Database\Query $query The query to set
* @return $this
*/
public function setQuery(Query $query)
{
$this->_query = $query;
return $this;
}
/**
* Gets the query object to be used as the values expression to be evaluated
* to insert records in the table.
*
* @return \Cake\Database\Query|null
*/
public function getQuery(): ?Query
{
return $this->_query;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
if (empty($this->_values) && empty($this->_query)) {
return '';
}
if (!$this->_castedExpressions) {
$this->_processExpressions();
}
$columns = $this->_columnNames();
$defaults = array_fill_keys($columns, null);
$placeholders = [];
$types = [];
$typeMap = $this->getTypeMap();
foreach ($defaults as $col => $v) {
$types[$col] = $typeMap->type($col);
}
foreach ($this->_values as $row) {
$row += $defaults;
$rowPlaceholders = [];
foreach ($columns as $column) {
$value = $row[$column];
if ($value instanceof ExpressionInterface) {
$rowPlaceholders[] = '(' . $value->sql($binder) . ')';
continue;
}
$placeholder = $binder->placeholder('c');
$rowPlaceholders[] = $placeholder;
$binder->bind($placeholder, $value, $types[$column]);
}
$placeholders[] = implode(', ', $rowPlaceholders);
}
$query = $this->getQuery();
if ($query) {
return ' ' . $query->sql($binder);
}
return sprintf(' VALUES (%s)', implode('), (', $placeholders));
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
if ($this->_query) {
return $this;
}
if (!$this->_castedExpressions) {
$this->_processExpressions();
}
foreach ($this->_values as $v) {
if ($v instanceof ExpressionInterface) {
$v->traverse($callback);
}
if (!is_array($v)) {
continue;
}
foreach ($v as $field) {
if ($field instanceof ExpressionInterface) {
$callback($field);
$field->traverse($callback);
}
}
}
return $this;
}
/**
* Converts values that need to be casted to expressions
*
* @return void
*/
protected function _processExpressions(): void
{
$types = [];
$typeMap = $this->getTypeMap();
$columns = $this->_columnNames();
foreach ($columns as $c) {
if (!is_string($c) && !is_int($c)) {
continue;
}
$types[$c] = $typeMap->type($c);
}
$types = $this->_requiresToExpressionCasting($types);
if (empty($types)) {
return;
}
foreach ($this->_values as $row => $values) {
foreach ($types as $col => $type) {
/** @var \Cake\Database\Type\ExpressionTypeInterface $type */
$this->_values[$row][$col] = $type->toExpression($values[$col]);
}
}
$this->_castedExpressions = true;
}
}

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