This commit is contained in:
Sebastian Molenda
2026-05-12 21:10:38 +02:00
commit ab96d82fcf
2544 changed files with 721700 additions and 0 deletions
+833
View File
@@ -0,0 +1,833 @@
<?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\Cache\Cache;
use Cake\Core\App;
use Cake\Core\Exception\CakeException;
use Cake\Core\Retry\CommandRetry;
use Cake\Database\Exception\MissingDriverException;
use Cake\Database\Exception\MissingExtensionException;
use Cake\Database\Exception\NestedTransactionRollbackException;
use Cake\Database\Query\DeleteQuery;
use Cake\Database\Query\InsertQuery;
use Cake\Database\Query\QueryFactory;
use Cake\Database\Query\SelectQuery;
use Cake\Database\Query\UpdateQuery;
use Cake\Database\Retry\ReconnectStrategy;
use Cake\Database\Schema\CachedCollection;
use Cake\Database\Schema\Collection as SchemaCollection;
use Cake\Database\Schema\CollectionInterface as SchemaCollectionInterface;
use Cake\Datasource\ConnectionInterface;
use Cake\Log\Log;
use Closure;
use Psr\SimpleCache\CacheInterface;
use Throwable;
use function Cake\Core\env;
/**
* Represents a connection with a database server.
*/
class Connection implements ConnectionInterface
{
/**
* Contains the configuration params for this connection.
*
* @var array<string, mixed>
*/
protected array $_config;
/**
* @var \Cake\Database\Driver
*/
protected Driver $readDriver;
/**
* @var \Cake\Database\Driver
*/
protected Driver $writeDriver;
/**
* Contains how many nested transactions have been started.
*
* @var int
*/
protected int $_transactionLevel = 0;
/**
* Whether a transaction is active in this connection.
*
* @var bool
*/
protected bool $_transactionStarted = false;
/**
* Whether this connection can and should use savepoints for nested
* transactions.
*
* @var bool
*/
protected bool $_useSavePoints = false;
/**
* Cacher object instance.
*
* @var \Psr\SimpleCache\CacheInterface|null
*/
protected ?CacheInterface $cacher = null;
/**
* The schema collection object
*
* @var \Cake\Database\Schema\CollectionInterface|null
*/
protected ?SchemaCollectionInterface $_schemaCollection = null;
/**
* NestedTransactionRollbackException object instance, will be stored if
* the rollback method is called in some nested transaction.
*
* @var \Cake\Database\Exception\NestedTransactionRollbackException|null
*/
protected ?NestedTransactionRollbackException $nestedTransactionRollbackException = null;
protected QueryFactory $queryFactory;
/**
* Constructor.
*
* ### Available options:
*
* - `driver` Sort name or FQCN for driver.
* - `log` Boolean indicating whether to use query logging.
* - `name` Connection name.
* - `cacheMetaData` Boolean indicating whether metadata (datasource schemas) should be cached.
* If set to a string it will be used as the name of cache config to use.
* - `cacheKeyPrefix` Custom prefix to use when generation cache keys. Defaults to connection name.
*
* @param array<string, mixed> $config Configuration array.
* @throws \Cake\Database\Exception\MissingDriverException when the driver class cannot be found
* @throws \Cake\Database\Exception\MissingExtensionException when the database extension is not enabled
*/
public function __construct(array $config)
{
$this->_config = $config;
[self::ROLE_READ => $this->readDriver, self::ROLE_WRITE => $this->writeDriver] = $this->createDrivers($config);
}
/**
* Creates read and write drivers.
*
* @param array<string, mixed> $config Connection config
* @return array<string, \Cake\Database\Driver>
* @phpstan-return array{read: \Cake\Database\Driver, write: \Cake\Database\Driver}
*/
protected function createDrivers(array $config): array
{
$driver = $config['driver'] ?? '';
if (!is_string($driver)) {
assert($driver instanceof Driver);
if (!$driver->enabled()) {
throw new MissingExtensionException(['driver' => $driver::class, 'name' => $this->configName()]);
}
// Legacy support for setting instance instead of driver class
return [self::ROLE_READ => $driver, self::ROLE_WRITE => $driver];
}
/** @var class-string<\Cake\Database\Driver>|null $driverClass */
$driverClass = App::className($driver, 'Database/Driver');
if ($driverClass === null) {
throw new MissingDriverException(['driver' => $driver, 'connection' => $this->configName()]);
}
$sharedConfig = array_diff_key($config, array_flip([
'name',
'className',
'driver',
'cacheMetaData',
'cacheKeyPrefix',
'read',
'write',
]));
$writeConfig = ($config['write'] ?? []) + $sharedConfig;
$readConfig = ($config['read'] ?? []) + $sharedConfig;
if (array_key_exists('write', $config) || array_key_exists('read', $config)) {
$readDriver = new $driverClass(['_role' => self::ROLE_READ] + $readConfig);
$writeDriver = new $driverClass(['_role' => self::ROLE_WRITE] + $writeConfig);
} else {
$readDriver = new $driverClass(['_role' => self::ROLE_WRITE] + $writeConfig);
$writeDriver = $readDriver;
}
if (!$writeDriver->enabled()) {
throw new MissingExtensionException(['driver' => $writeDriver::class, 'name' => $this->configName()]);
}
return [self::ROLE_READ => $readDriver, self::ROLE_WRITE => $writeDriver];
}
/**
* Destructor
*
* Disconnects the driver to release the connection.
*/
public function __destruct()
{
if ($this->_transactionStarted && class_exists(Log::class)) {
$message = 'The connection is going to be closed but there is an active transaction.';
$requestUrl = env('REQUEST_URI');
if ($requestUrl) {
$message .= "\nRequest URL: " . $requestUrl;
}
$clientIp = env('REMOTE_ADDR');
if ($clientIp) {
$message .= "\nClient IP: " . $clientIp;
}
Log::warning($message);
}
}
/**
* @inheritDoc
*/
public function config(): array
{
return $this->_config;
}
/**
* @inheritDoc
*/
public function configName(): string
{
return $this->_config['name'] ?? '';
}
/**
* Returns the connection role: read or write.
*
* @return string
*/
public function role(): string
{
return preg_match('/:read$/', $this->configName()) === 1 ? static::ROLE_READ : static::ROLE_WRITE;
}
/**
* Get the retry wrapper object that is allows recovery from server disconnects
* while performing certain database actions, such as executing a query.
*
* @return \Cake\Core\Retry\CommandRetry The retry wrapper
*/
public function getDisconnectRetry(): CommandRetry
{
return new CommandRetry(new ReconnectStrategy($this));
}
/**
* Gets the role-specific driver instance.
*
* @param string $role Connection role ('read' or 'write')
* @return \Cake\Database\Driver
*/
public function getDriver(string $role = self::ROLE_WRITE): Driver
{
assert($role === self::ROLE_READ || $role === self::ROLE_WRITE);
return $role === self::ROLE_READ ? $this->getReadDriver() : $this->getWriteDriver();
}
/**
* Gets the read-role driver instance.
*
* @return \Cake\Database\Driver
*/
public function getReadDriver(): Driver
{
return $this->readDriver;
}
/**
* Gets the write-role driver instance.
*
* @return \Cake\Database\Driver
*/
public function getWriteDriver(): Driver
{
return $this->writeDriver;
}
/**
* Executes a query using $params for interpolating values and $types as a hint for each
* those params.
*
* @param string $sql SQL to be executed and interpolated with $params
* @param array $params list or associative array of params to be interpolated in $sql as values
* @param array $types list or associative array of types to be used for casting values in query
* @return \Cake\Database\StatementInterface executed statement
*/
public function execute(string $sql, array $params = [], array $types = []): StatementInterface
{
return $this->getDisconnectRetry()->run(fn() => $this->getWriteDriver()->execute($sql, $params, $types));
}
/**
* Executes the provided query after compiling it for the specific driver
* dialect and returns the executed Statement object.
*
* @param \Cake\Database\Query $query The query to be executed
* @return \Cake\Database\StatementInterface executed statement
*/
public function run(Query $query): StatementInterface
{
return $this->getDisconnectRetry()->run(fn() => $this->getDriver($query->getConnectionRole())->run($query));
}
/**
* Get query factory instance.
*
* @return \Cake\Database\Query\QueryFactory
*/
public function queryFactory(): QueryFactory
{
return $this->queryFactory ??= new QueryFactory($this);
}
/**
* Create a new SelectQuery instance for this connection.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string|float|int $fields Fields/columns list for the query.
* @param array|string $table The table or list of tables to query.
* @param array<string, string> $types Associative array containing the types to be used for casting.
* @return \Cake\Database\Query\SelectQuery<mixed>
*/
public function selectQuery(
ExpressionInterface|Closure|array|string|float|int $fields = [],
array|string $table = [],
array $types = [],
): SelectQuery {
return $this->queryFactory()->select($fields, $table, $types);
}
/**
* Create a new InsertQuery instance for this connection.
*
* @param string|null $table The table to insert rows into.
* @param array $values Associative array of column => value to be inserted.
* @param array<int|string, string> $types Associative array containing the types to be used for casting.
* @return \Cake\Database\Query\InsertQuery
*/
public function insertQuery(?string $table = null, array $values = [], array $types = []): InsertQuery
{
return $this->queryFactory()->insert($table, $values, $types);
}
/**
* Create a new UpdateQuery instance for this connection.
*
* @param \Cake\Database\ExpressionInterface|string|null $table The table to update rows of.
* @param array $values Values to be updated.
* @param array $conditions Conditions to be set for the update statement.
* @param array<string, string> $types Associative array containing the types to be used for casting.
* @return \Cake\Database\Query\UpdateQuery
*/
public function updateQuery(
ExpressionInterface|string|null $table = null,
array $values = [],
array $conditions = [],
array $types = [],
): UpdateQuery {
return $this->queryFactory()->update($table, $values, $conditions, $types);
}
/**
* Create a new DeleteQuery instance for this connection.
*
* @param string|null $table The table to delete rows from.
* @param array $conditions Conditions to be set for the delete statement.
* @param array<string, string> $types Associative array containing the types to be used for casting.
* @return \Cake\Database\Query\DeleteQuery
*/
public function deleteQuery(?string $table = null, array $conditions = [], array $types = []): DeleteQuery
{
return $this->queryFactory()->delete($table, $conditions, $types);
}
/**
* Sets a Schema\Collection object for this connection.
*
* @param \Cake\Database\Schema\CollectionInterface $collection The schema collection object
* @return $this
*/
public function setSchemaCollection(SchemaCollectionInterface $collection)
{
$this->_schemaCollection = $collection;
return $this;
}
/**
* Gets a Schema\Collection object for this connection.
*
* @return \Cake\Database\Schema\CollectionInterface
*/
public function getSchemaCollection(): SchemaCollectionInterface
{
if ($this->_schemaCollection !== null) {
return $this->_schemaCollection;
}
if (!empty($this->_config['cacheMetadata'])) {
return $this->_schemaCollection = new CachedCollection(
new SchemaCollection($this),
empty($this->_config['cacheKeyPrefix']) ? $this->configName() : $this->_config['cacheKeyPrefix'],
$this->getCacher(),
);
}
return $this->_schemaCollection = new SchemaCollection($this);
}
/**
* Executes an INSERT query on the specified table.
*
* @param string $table the table to insert values in
* @param array $values values to be inserted
* @param array<string, string> $types Array containing the types to be used for casting
* @return \Cake\Database\StatementInterface
*/
public function insert(string $table, array $values, array $types = []): StatementInterface
{
return $this->insertQuery($table, $values, $types)->execute();
}
/**
* Executes an UPDATE statement on the specified table.
*
* @param string $table the table to update rows from
* @param array $values values to be updated
* @param array $conditions conditions to be set for update statement
* @param array<string, string> $types list of associative array containing the types to be used for casting
* @return \Cake\Database\StatementInterface
*/
public function update(string $table, array $values, array $conditions = [], array $types = []): StatementInterface
{
return $this->updateQuery($table, $values, $conditions, $types)->execute();
}
/**
* Executes a DELETE statement on the specified table.
*
* @param string $table the table to delete rows from
* @param array $conditions conditions to be set for delete statement
* @param array<string, string> $types list of associative array containing the types to be used for casting
* @return \Cake\Database\StatementInterface
*/
public function delete(string $table, array $conditions = [], array $types = []): StatementInterface
{
return $this->deleteQuery($table, $conditions, $types)->execute();
}
/**
* Starts a new transaction.
*
* @return void
*/
public function begin(): void
{
if (!$this->_transactionStarted) {
$this->getDisconnectRetry()->run(function (): void {
$this->getWriteDriver()->beginTransaction();
});
$this->_transactionLevel = 0;
$this->_transactionStarted = true;
$this->nestedTransactionRollbackException = null;
return;
}
$this->_transactionLevel++;
if ($this->isSavePointsEnabled()) {
$this->createSavePoint((string)$this->_transactionLevel);
}
}
/**
* Commits current transaction.
*
* @return bool true on success, false otherwise
* @throws \Cake\Database\Exception\NestedTransactionRollbackException when a nested transaction was rolled back
*/
public function commit(): bool
{
if (!$this->_transactionStarted) {
return false;
}
if ($this->_transactionLevel === 0) {
if ($this->wasNestedTransactionRolledback()) {
$e = $this->nestedTransactionRollbackException;
assert($e !== null);
$this->nestedTransactionRollbackException = null;
throw $e;
}
$this->_transactionStarted = false;
$this->nestedTransactionRollbackException = null;
return $this->getWriteDriver()->commitTransaction();
}
if ($this->isSavePointsEnabled()) {
$this->releaseSavePoint((string)$this->_transactionLevel);
}
$this->_transactionLevel--;
return true;
}
/**
* Rollback current transaction.
*
* @param bool|null $toBeginning Whether the transaction should be rolled back to the
* beginning of it. Defaults to false if using savepoints, or true if not.
* @return bool
*/
public function rollback(?bool $toBeginning = null): bool
{
if (!$this->_transactionStarted) {
return false;
}
$useSavePoint = $this->isSavePointsEnabled();
$toBeginning ??= !$useSavePoint;
if ($this->_transactionLevel === 0 || $toBeginning) {
$this->_transactionLevel = 0;
$this->_transactionStarted = false;
$this->nestedTransactionRollbackException = null;
$this->getWriteDriver()->rollbackTransaction();
return true;
}
$savePoint = $this->_transactionLevel--;
if ($useSavePoint) {
$this->rollbackSavepoint($savePoint);
} else {
$this->nestedTransactionRollbackException ??= new NestedTransactionRollbackException();
}
return true;
}
/**
* Enables/disables the usage of savepoints, enables only if the driver allows it.
*
* If you are trying to enable this feature, make sure you check
* `isSavePointsEnabled()` to verify that savepoints were enabled successfully.
*
* @param bool $enable Whether save points should be used.
* @return $this
*/
public function enableSavePoints(bool $enable = true)
{
if ($enable === false) {
$this->_useSavePoints = false;
} else {
$this->_useSavePoints = $this->getWriteDriver()->supports(DriverFeatureEnum::SAVEPOINT);
}
return $this;
}
/**
* Disables the usage of savepoints.
*
* @return $this
*/
public function disableSavePoints()
{
$this->_useSavePoints = false;
return $this;
}
/**
* Returns whether this connection is using savepoints for nested transactions
*
* @return bool true if enabled, false otherwise
*/
public function isSavePointsEnabled(): bool
{
return $this->_useSavePoints;
}
/**
* Creates a new save point for nested transactions.
*
* @param string|int $name Save point name or id
* @return void
*/
public function createSavePoint(string|int $name): void
{
$this->execute($this->getWriteDriver()->savePointSQL($name));
}
/**
* Releases a save point by its name.
*
* @param string|int $name Save point name or id
* @return void
*/
public function releaseSavePoint(string|int $name): void
{
$sql = $this->getWriteDriver()->releaseSavePointSQL($name);
if ($sql) {
$this->execute($sql);
}
}
/**
* Rollback a save point by its name.
*
* @param string|int $name Save point name or id
* @return void
*/
public function rollbackSavepoint(string|int $name): void
{
$this->execute($this->getWriteDriver()->rollbackSavePointSQL($name));
}
/**
* Run driver specific SQL to disable foreign key checks.
*
* @return void
*/
public function disableForeignKeys(): void
{
$this->getDisconnectRetry()->run(function (): void {
$this->execute($this->getWriteDriver()->disableForeignKeySQL());
});
}
/**
* Run driver specific SQL to enable foreign key checks.
*
* @return void
*/
public function enableForeignKeys(): void
{
$this->getDisconnectRetry()->run(function (): void {
$this->execute($this->getWriteDriver()->enableForeignKeySQL());
});
}
/**
* Executes a callback inside a transaction, if any exception occurs
* while executing the passed callback, the transaction will be rolled back
* If the result of the callback is `false`, the transaction will
* also be rolled back. Otherwise the transaction is committed after executing
* the callback.
*
* The callback will receive the connection instance as its first argument.
*
* ### Example:
*
* ```
* $connection->transactional(function ($connection) {
* $connection->deleteQuery('users')->execute();
* });
* ```
*
* @param \Closure $callback The callback to execute within a transaction.
* @return mixed The return value of the callback.
* @throws \Exception Will re-throw any exception raised in $callback after
* rolling back the transaction.
*/
public function transactional(Closure $callback): mixed
{
$this->begin();
try {
$result = $callback($this);
} catch (Throwable $e) {
$this->rollback(false);
throw $e;
}
if ($result === false) {
$this->rollback(false);
return false;
}
try {
$this->commit();
} catch (NestedTransactionRollbackException $e) {
$this->rollback(false);
throw $e;
}
return $result;
}
/**
* Returns whether some nested transaction has been already rolled back.
*
* @return bool
*/
protected function wasNestedTransactionRolledback(): bool
{
return $this->nestedTransactionRollbackException instanceof NestedTransactionRollbackException;
}
/**
* Run an operation with constraints disabled.
*
* Constraints should be re-enabled after the callback succeeds/fails.
*
* ### Example:
*
* ```
* $connection->disableConstraints(function ($connection) {
* $connection->insertQuery('users')->execute();
* });
* ```
*
* @param \Closure $callback Callback to run with constraints disabled
* @return mixed The return value of the callback.
* @throws \Exception Will re-throw any exception raised in $callback after
* rolling back the transaction.
*/
public function disableConstraints(Closure $callback): mixed
{
return $this->getDisconnectRetry()->run(function () use ($callback) {
$this->disableForeignKeys();
try {
$result = $callback($this);
} finally {
$this->enableForeignKeys();
}
return $result;
});
}
/**
* Checks if a transaction is running.
*
* @return bool True if a transaction is running else false.
*/
public function inTransaction(): bool
{
return $this->_transactionStarted;
}
/**
* Enables or disables metadata caching for this connection
*
* Changing this setting will not modify existing schema collections objects.
*
* @param string|bool $cache Either boolean false to disable metadata caching, or
* true to use `_cake_model_` or the name of the cache config to use.
* @return void
*/
public function cacheMetadata(string|bool $cache): void
{
$this->_schemaCollection = null;
$this->_config['cacheMetadata'] = $cache;
if (is_string($cache)) {
$this->cacher = null;
}
}
/**
* @inheritDoc
*/
public function setCacher(CacheInterface $cacher)
{
$this->cacher = $cacher;
return $this;
}
/**
* @inheritDoc
*/
public function getCacher(): CacheInterface
{
if ($this->cacher !== null) {
return $this->cacher;
}
$configName = $this->_config['cacheMetadata'] ?? '_cake_model_';
if (!is_string($configName)) {
$configName = '_cake_model_';
}
if (!class_exists(Cache::class)) {
throw new CakeException(
'To use caching you must either set a cacher using Connection::setCacher()' .
' or require the cakephp/cache package in your composer config.',
);
}
return $this->cacher = Cache::pool($configName);
}
/**
* Returns an array that can be used to describe the internal state of this
* object.
*
* @return array<string, mixed>
*/
public function __debugInfo(): array
{
$secrets = [
'password' => '*****',
'username' => '*****',
'host' => '*****',
'database' => '*****',
'port' => '*****',
];
$replace = array_intersect_key($secrets, $this->_config);
$config = $replace + $this->_config;
if (isset($config['read'])) {
$config['read'] = array_intersect_key($secrets, $config['read']) + $config['read'];
}
if (isset($config['write'])) {
$config['write'] = array_intersect_key($secrets, $config['write']) + $config['write'];
}
return [
'config' => $config,
'readDriver' => $this->readDriver,
'writeDriver' => $this->writeDriver,
'transactionLevel' => $this->_transactionLevel,
'transactionStarted' => $this->_transactionStarted,
'useSavePoints' => $this->_useSavePoints,
];
}
}
+47
View File
@@ -0,0 +1,47 @@
<?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.
*
* @deprecated 5.2.5 This interface is no longer used.
*/
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;
}
File diff suppressed because it is too large Load Diff
+337
View File
@@ -0,0 +1,337 @@
<?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\DriverFeatureEnum;
use Cake\Database\Query;
use Cake\Database\Query\SelectQuery;
use Cake\Database\Schema\MysqlSchemaDialect;
use Cake\Database\Schema\SchemaDialect;
use Cake\Database\StatementInterface;
use PDO;
use Pdo\Mysql as PdoMysql;
/**
* MySQL Driver
*/
class Mysql extends Driver
{
/**
* @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 array $_baseConfig = [
'persistent' => true,
'host' => 'localhost',
'username' => 'root',
'password' => '',
'database' => 'cake',
'port' => '3306',
'flags' => [],
'encoding' => 'utf8mb4',
'timezone' => null,
'init' => [],
];
/**
* String used to start a database identifier quoting to make it safe
*
* @var string
*/
protected string $_startQuote = '`';
/**
* String used to end a database identifier quoting to make it safe
*
* @var string
*/
protected string $_endQuote = '`';
/**
* Server type.
*
* If the underlying server is MariaDB, its value will get set to `'mariadb'`
* after `version()` method is called.
*
* @var string
*/
protected string $serverType = self::SERVER_TYPE_MYSQL;
/**
* Mapping of feature to db server version for feature availability checks.
*
* @var array<string, array<string, string>>
*/
protected array $featureVersions = [
'mysql' => [
'json' => '5.7.0',
'cte' => '8.0.0',
'window' => '8.0.0',
'intersect' => '8.0.31',
'intersect-all' => '8.0.31',
'check-constraints' => '8.0.16',
],
'mariadb' => [
'json' => '10.2.7',
'cte' => '10.2.1',
'window' => '10.2.0',
'intersect' => '10.3.0',
'intersect-all' => '10.5.0',
'check-constraints' => '10.2.1',
],
];
/**
* @inheritDoc
*/
public function connect(): void
{
if ($this->pdo !== null) {
return;
}
$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'],
$this->attrUseBufferedQueryId() => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
if (!empty($config['ssl_key']) && !empty($config['ssl_cert'])) {
$config['flags'][$this->attrSslKeyId()] = $config['ssl_key'];
$config['flags'][$this->attrSslCertId()] = $config['ssl_cert'];
}
if (!empty($config['ssl_ca'])) {
$config['flags'][$this->attrSslCaId()] = $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->pdo = $this->createPdo($dsn, $config);
if (!empty($config['init'])) {
foreach ((array)$config['init'] as $command) {
$this->pdo->exec($command);
}
}
}
/**
* @inheritDoc
*/
public function run(Query $query): StatementInterface
{
$statement = $this->prepare($query);
$query->getValueBinder()->attachTo($statement);
if ($query instanceof SelectQuery) {
try {
$this->getPdo()->setAttribute($this->attrUseBufferedQueryId(), $query->isBufferedResultsEnabled());
$this->executeStatement($statement);
} finally {
$this->getPdo()->setAttribute($this->attrUseBufferedQueryId(), true);
}
} else {
$this->executeStatement($statement);
}
return $statement;
}
/**
* 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);
}
/**
* @inheritDoc
*/
public function schemaDialect(): SchemaDialect
{
return $this->_schemaDialect ?? ($this->_schemaDialect = new MysqlSchemaDialect($this));
}
/**
* @inheritDoc
*/
public function schema(): string
{
return $this->_config['database'];
}
/**
* Get the SQL for disabling foreign keys.
*
* @return string
*/
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(DriverFeatureEnum $feature): bool
{
$versionCompare = function () use ($feature) {
return version_compare(
$this->version(),
$this->featureVersions[$this->serverType][$feature->value],
'>=',
);
};
return match ($feature) {
DriverFeatureEnum::DISABLE_CONSTRAINT_WITHOUT_TRANSACTION,
DriverFeatureEnum::SAVEPOINT => true,
DriverFeatureEnum::TRUNCATE_WITH_CONSTRAINTS => false,
DriverFeatureEnum::CTE,
DriverFeatureEnum::JSON,
DriverFeatureEnum::WINDOW => $versionCompare(),
DriverFeatureEnum::INTERSECT => $versionCompare(),
DriverFeatureEnum::INTERSECT_ALL => $versionCompare(),
DriverFeatureEnum::CHECK_CONSTRAINTS => $versionCompare(),
DriverFeatureEnum::SET_OPERATIONS_ORDER_BY => true,
DriverFeatureEnum::OPTIMIZER_HINT_COMMENT => 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->_version = (string)$this->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION);
if (str_contains($this->_version, 'MariaDB')) {
$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;
}
/**
* Get PDO ATTR_SSL_KEY id.
*
* @return int
*/
private function attrSslKeyId(): int
{
return PHP_VERSION_ID < 80400 ? PDO::MYSQL_ATTR_SSL_KEY : PdoMysql::ATTR_SSL_KEY;
}
/**
* Get PDO ATTR_SSL_CERT id.
*
* @return int
*/
private function attrSslCertId(): int
{
return PHP_VERSION_ID < 80400 ? PDO::MYSQL_ATTR_SSL_CERT : PdoMysql::ATTR_SSL_CERT;
}
/**
* Get PDO ATTR_SSL_CA id.
*
* @return int
*/
private function attrSslCaId(): int
{
return PHP_VERSION_ID < 80400 ? PDO::MYSQL_ATTR_SSL_CA : PdoMysql::ATTR_SSL_CA;
}
/**
* Get PDO ATTR_USE_BUFFERED_QUERY id.
*
* @return int
*/
private function attrUseBufferedQueryId(): int
{
return PHP_VERSION_ID < 80400 ? PDO::MYSQL_ATTR_USE_BUFFERED_QUERY : PdoMysql::ATTR_USE_BUFFERED_QUERY;
}
}
+363
View File
@@ -0,0 +1,363 @@
<?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\DriverFeatureEnum;
use Cake\Database\Expression\FunctionExpression;
use Cake\Database\Expression\IdentifierExpression;
use Cake\Database\Expression\StringExpression;
use Cake\Database\PostgresCompiler;
use Cake\Database\Query\InsertQuery;
use Cake\Database\Query\SelectQuery;
use Cake\Database\QueryCompiler;
use Cake\Database\Schema\PostgresSchemaDialect;
use Cake\Database\Schema\SchemaDialect;
use PDO;
/**
* Class Postgres
*/
class Postgres extends Driver
{
/**
* @inheritDoc
*/
protected const MAX_ALIAS_LENGTH = 63;
/**
* Base configuration settings for Postgres driver
*
* @var array<string, mixed>
*/
protected array $_baseConfig = [
'persistent' => true,
'host' => 'localhost',
'username' => 'root',
'password' => '',
'database' => 'cake',
'schema' => 'public',
'port' => 5432,
'encoding' => 'utf8',
'timezone' => null,
'flags' => [],
'init' => [],
'ssl_key' => null,
'ssl_cert' => null,
'ssl_ca' => null,
'ssl' => false,
'ssl_mode' => null,
];
/**
* String used to start a database identifier quoting to make it safe
*
* @var string
*/
protected string $_startQuote = '"';
/**
* String used to end a database identifier quoting to make it safe
*
* @var string
*/
protected string $_endQuote = '"';
/**
* @inheritDoc
*/
public function connect(): void
{
if ($this->pdo !== null) {
return;
}
$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']}";
}
if ($this->_config['ssl']) {
if ($this->_config['ssl_mode']) {
$dsn .= ';sslmode=' . $this->_config['ssl_mode'];
} else {
$dsn .= ';sslmode=allow';
}
if ($this->_config['ssl_key']) {
$dsn .= ';sslkey=' . $this->_config['ssl_key'];
}
if ($this->_config['ssl_cert']) {
$dsn .= ';sslcert=' . $this->_config['ssl_cert'];
}
if ($this->_config['ssl_ca']) {
$dsn .= ';sslrootcert=' . $this->_config['ssl_ca'];
}
}
$this->pdo = $this->createPdo($dsn, $config);
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', $this->getPdo()->quote($config['timezone']));
}
foreach ($config['init'] as $command) {
/** @phpstan-ignore-next-line */
$this->pdo->exec($command);
}
}
/**
* 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
{
return $this->_schemaDialect ?? ($this->_schemaDialect = new PostgresSchemaDialect($this));
}
/**
* Sets connection encoding
*
* @param string $encoding The encoding to use.
* @return void
*/
public function setEncoding(string $encoding): void
{
$pdo = $this->getPdo();
$pdo->exec('SET NAMES ' . $pdo->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
{
$pdo = $this->getPdo();
$pdo->exec('SET search_path TO ' . $pdo->quote($schema));
}
/**
* Get the SQL for disabling foreign keys.
*
* @return string
*/
public function disableForeignKeySQL(): string
{
return 'SET CONSTRAINTS ALL DEFERRED';
}
/**
* @inheritDoc
*/
public function enableForeignKeySQL(): string
{
return 'SET CONSTRAINTS ALL IMMEDIATE';
}
/**
* @inheritDoc
*/
public function supports(DriverFeatureEnum $feature): bool
{
return match ($feature) {
DriverFeatureEnum::CTE,
DriverFeatureEnum::JSON,
DriverFeatureEnum::SAVEPOINT,
DriverFeatureEnum::TRUNCATE_WITH_CONSTRAINTS,
DriverFeatureEnum::WINDOW => true,
DriverFeatureEnum::INTERSECT => true,
DriverFeatureEnum::INTERSECT_ALL => true,
DriverFeatureEnum::SET_OPERATIONS_ORDER_BY => true,
DriverFeatureEnum::DISABLE_CONSTRAINT_WITHOUT_TRANSACTION => false,
DriverFeatureEnum::OPTIMIZER_HINT_COMMENT => true,
DriverFeatureEnum::CHECK_CONSTRAINTS => true,
};
}
/**
* @inheritDoc
*/
protected function _transformDistinct(SelectQuery $query): SelectQuery
{
return $query;
}
/**
* @inheritDoc
*/
protected function _insertQueryTranslator(InsertQuery $query): InsertQuery
{
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 transform.
* @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) {
return 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;
case 'JSON_VALUE':
$expression->setName('JSONB_PATH_QUERY')
->iterateParts(function ($p, $key) {
if ($key === 0) {
$p = sprintf('%s::jsonb', $p);
} elseif ($key === 1) {
$p = sprintf("'%s'::jsonpath", $this->quoteIdentifier($p['value']));
}
return $p;
});
break;
}
}
/**
* Changes string expression into postgresql format.
*
* @param \Cake\Database\Expression\StringExpression $expression The string expression to transform.
* @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();
}
}
+309
View File
@@ -0,0 +1,309 @@
<?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\DriverFeatureEnum;
use Cake\Database\Expression\FunctionExpression;
use Cake\Database\Expression\TupleComparison;
use Cake\Database\Schema\SchemaDialect;
use Cake\Database\Schema\SqliteSchemaDialect;
use Cake\Database\Statement\SqliteStatement;
use InvalidArgumentException;
use PDO;
/**
* Class Sqlite
*/
class Sqlite extends Driver
{
use TupleComparisonTranslatorTrait;
/**
* @inheritDoc
*/
protected const STATEMENT_CLASS = SqliteStatement::class;
/**
* Base configuration settings for Sqlite driver
*
* - `mask` The mask used for created database
*
* @var array<string, mixed>
*/
protected array $_baseConfig = [
'persistent' => false,
'username' => null,
'password' => null,
'database' => ':memory:',
'encoding' => 'utf8',
'mask' => 0644,
'cache' => null,
'mode' => null,
'flags' => [],
'init' => [],
];
/**
* Whether the connected server supports window functions.
*
* @var bool|null
*/
protected ?bool $_supportsWindowFunctions = null;
/**
* String used to start a database identifier quoting to make it safe
*
* @var string
*/
protected string $_startQuote = '"';
/**
* String used to end a database identifier quoting to make it safe
*
* @var string
*/
protected string $_endQuote = '"';
/**
* Mapping of date parts.
*
* @var array<string, string>
*/
protected array $_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 array $featureVersions = [
'cte' => '3.8.3',
'window' => '3.28.0',
];
/**
* @inheritDoc
*/
public function connect(): void
{
if ($this->pdo !== null) {
return;
}
$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) {
$dsn = 'sqlite:file:' . $config['database'] . '?' . implode('&', $params);
} else {
$dsn = 'sqlite:' . $config['database'];
}
$this->pdo = $this->createPdo($dsn, $config);
if ($chmodFile) {
// phpcs:disable
@chmod($config['database'], $config['mask']);
// phpcs:enable
}
if (!empty($config['init'])) {
foreach ((array)$config['init'] as $command) {
$this->pdo->exec($command);
}
}
}
/**
* 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);
}
/**
* Get the SQL for disabling foreign keys.
*
* @return string
*/
public function disableForeignKeySQL(): string
{
return 'PRAGMA foreign_keys = OFF';
}
/**
* @inheritDoc
*/
public function enableForeignKeySQL(): string
{
return 'PRAGMA foreign_keys = ON';
}
/**
* @inheritDoc
*/
public function supports(DriverFeatureEnum $feature): bool
{
return match ($feature) {
DriverFeatureEnum::DISABLE_CONSTRAINT_WITHOUT_TRANSACTION,
DriverFeatureEnum::SAVEPOINT,
DriverFeatureEnum::TRUNCATE_WITH_CONSTRAINTS => true,
DriverFeatureEnum::JSON => false,
DriverFeatureEnum::CTE,
DriverFeatureEnum::WINDOW => version_compare(
$this->version(),
$this->featureVersions[$feature->value],
'>=',
),
DriverFeatureEnum::INTERSECT => true,
DriverFeatureEnum::INTERSECT_ALL => false,
DriverFeatureEnum::SET_OPERATIONS_ORDER_BY => false,
DriverFeatureEnum::OPTIMIZER_HINT_COMMENT => false,
DriverFeatureEnum::CHECK_CONSTRAINTS => true,
};
}
/**
* @inheritDoc
*/
public function schemaDialect(): SchemaDialect
{
return $this->_schemaDialect ?? ($this->_schemaDialect = new SqliteSchemaDialect($this));
}
/**
* @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) {
return ['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;
case 'JSON_VALUE':
$expression->setName('JSON_EXTRACT');
break;
}
}
}
+561
View File
@@ -0,0 +1,561 @@
<?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\DriverFeatureEnum;
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\Query\SelectQuery;
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 TupleComparisonTranslatorTrait;
/**
* @inheritDoc
*/
protected const MAX_ALIAS_LENGTH = 128;
/**
* @inheritDoc
*/
protected const RETRY_ERROR_CODES = [
40613, // Azure Sql Database paused
];
/**
* @inheritDoc
*/
protected const STATEMENT_CLASS = SqlserverStatement::class;
/**
* Base configuration settings for Sqlserver driver
*
* @var array<string, mixed>
*/
protected array $_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,
'accessToken' => null,
'authentication' => null,
];
/**
* String used to start a database identifier quoting to make it safe
*
* @var string
*/
protected string $_startQuote = '[';
/**
* String used to end a database identifier quoting to make it safe
*
* @var string
*/
protected string $_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 void
*/
public function connect(): void
{
if ($this->pdo !== null) {
return;
}
$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']}";
}
if ($config['accessToken'] !== null) {
$dsn .= ";AccessToken={$config['accessToken']}";
}
if ($config['authentication'] !== null) {
$dsn .= ";Authentication={$config['authentication']}";
}
$this->pdo = $this->createPdo($dsn, $config);
if (!empty($config['init'])) {
foreach ((array)$config['init'] as $command) {
$this->pdo->exec($command);
}
}
if (!empty($config['settings']) && is_array($config['settings'])) {
foreach ($config['settings'] as $key => $value) {
$this->pdo->exec("SET {$key} {$value}");
}
}
if (!empty($config['attributes']) && is_array($config['attributes'])) {
foreach ($config['attributes'] as $key => $value) {
$this->pdo->setAttribute($key, $value);
}
}
}
/**
* 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);
}
/**
* @inheritDoc
*/
public function prepare(Query|string $query): StatementInterface
{
$options = [
PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL,
PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED,
];
$sql = $query;
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 instanceof SelectQuery && !$query->isBufferedResultsEnabled()) {
$options = [];
}
}
/** @var string $sql */
$statement = $this->getPdo()->prepare(
$sql,
$options,
);
/** @var \Cake\Database\StatementInterface */
return new (static::STATEMENT_CLASS)($statement, $this, $this->getResultSetDecorators($query));
}
/**
* @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(DriverFeatureEnum $feature): bool
{
return match ($feature) {
DriverFeatureEnum::CTE,
DriverFeatureEnum::DISABLE_CONSTRAINT_WITHOUT_TRANSACTION,
DriverFeatureEnum::SAVEPOINT,
DriverFeatureEnum::TRUNCATE_WITH_CONSTRAINTS,
DriverFeatureEnum::WINDOW => true,
DriverFeatureEnum::INTERSECT => true,
DriverFeatureEnum::INTERSECT_ALL => false,
DriverFeatureEnum::JSON => false,
DriverFeatureEnum::SET_OPERATIONS_ORDER_BY => false,
DriverFeatureEnum::OPTIMIZER_HINT_COMMENT => false,
DriverFeatureEnum::CHECK_CONSTRAINTS => false,
};
}
/**
* @inheritDoc
*/
public function schemaDialect(): SchemaDialect
{
return $this->_schemaDialect ??= new SqlserverSchemaDialect($this);
}
/**
* {@inheritDoc}
*
* @return \Cake\Database\SqlserverCompiler
*/
public function newCompiler(): QueryCompiler
{
return new SqlserverCompiler();
}
/**
* @inheritDoc
*/
protected function _selectQueryTranslator(SelectQuery $query): SelectQuery
{
$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->orderBy($query->expr()->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\SelectQuery<mixed> $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\SelectQuery<mixed> Modified query object.
*/
protected function _pagingSubquery(SelectQuery $original, ?int $limit, ?int $offset): SelectQuery
{
$field = '_cake_paging_._cake_page_rownum_';
/** @var \Cake\Database\Expression\OrderByExpression $originalOrder */
$originalOrder = $original->clause('order');
if ($originalOrder) {
// 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();
$originalOrder
->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)
->orderBy([], true);
$outer = $query->getConnection()->selectQuery();
$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(SelectQuery $query): SelectQuery
{
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 (Query $q) use ($distinct, $order) {
$over = $q->expr('ROW_NUMBER() OVER')
->add('(PARTITION BY')
->add($q->expr()->add($distinct)->setConjunction(','))
->add($order)
->add(')')
->setConjunction(' ');
return [
'_cake_distinct_pivot_' => $over,
];
})
->limit(null)
->offset(null)
->orderBy([], true);
$outer = new SelectQuery($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':
$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;
}
}
}
@@ -0,0 +1,116 @@
<?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 Cake\Database\Query\SelectQuery;
use InvalidArgumentException;
/**
* 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 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 InvalidArgumentException(
sprintf(
'Tuple comparison transform only supports the `IN` and `=` operators, `%s` given.',
$operator,
),
);
}
$value = $expression->getValue();
$true = new QueryExpression('1');
if ($value instanceof SelectQuery) {
/** @var array<string> $selected */
$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()
->select($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('=');
}
}
+75
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 5.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database;
enum DriverFeatureEnum: string
{
/**
* Common Table Expressions (with clause) support.
*/
case CTE = 'cte';
/**
* Disabling constraints without being in transaction support.
*/
case DISABLE_CONSTRAINT_WITHOUT_TRANSACTION = 'disable-constraint-without-transaction';
/**
* Native JSON data type support.
*/
case JSON = 'json';
/**
* Transaction savepoint support.
*/
case SAVEPOINT = 'savepoint';
/**
* Truncate with foreign keys attached support.
*/
case TRUNCATE_WITH_CONSTRAINTS = 'truncate-with-constraints';
/**
* Window function support (all or partial clauses).
*/
case WINDOW = 'window';
/**
* Intersect feature support
*/
case INTERSECT = 'intersect';
/**
* Intersect all feature support
*/
case INTERSECT_ALL = 'intersect-all';
/**
* Support for order by in set operations (union, intersect)
*/
case SET_OPERATIONS_ORDER_BY = 'set-operations-order-by';
/**
* Support for optimizer hints in comment form after statement keyword (SELECT <hint>, etc)
*/
case OPTIMIZER_HINT_COMMENT = 'optimizer-hint-comment';
/**
* Support for CHECK constraints.
*/
case CHECK_CONSTRAINTS = 'check-constraints';
}
@@ -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;
/**
* Exception for the database package.
*/
class DatabaseException extends CakeException
{
/**
* @inheritDoc
*/
protected string $_messageTemplate = '%s';
}
@@ -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 string $_messageTemplate = 'Connection to %s could not be established: %s';
}
@@ -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 string $_messageTemplate = 'Could not find driver `%s` for connection `%s`.';
}
@@ -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 string $_messageTemplate = 'Database driver `%s` cannot be used due to a missing PHP extension or unmet dependency. Requested by connection `%s`';
}
@@ -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.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 message 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)
{
$message ??= 'Cannot commit transaction - rollback() has been already called in the nested transaction';
parent::__construct($message, $code, $previous);
}
}
@@ -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 5.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Exception;
use Cake\Database\Log\LoggedQuery;
use PDOException;
class QueryException extends PDOException
{
/**
* Constructor
*
* @param \Cake\Database\Log\LoggedQuery|string $query
* @param \PDOException $previous
*/
public function __construct(protected LoggedQuery|string $query, PDOException $previous)
{
$message = $previous->getMessage() . "\nQuery: " . $this->getQueryString();
parent::__construct($message, (int)$previous->getCode(), $previous);
}
/**
* Get the query string that caused this exception.
*
* @return string
*/
public function getQueryString(): string
{
if ($this->query instanceof LoggedQuery) {
return (string)$this->query;
}
return $this->query;
}
}
@@ -0,0 +1,265 @@
<?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 function Cake\Core\deprecationWarning;
/**
* 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|null
*/
protected ?QueryExpression $filter = null;
/**
* @var \Cake\Database\Expression\WindowExpression|null
*/
protected ?WindowExpression $window = null;
/**
* 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(ExpressionInterface|Closure|array|string $conditions, array $types = [])
{
$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 expression.
*
* @param string|null $name Window name
* @return $this
*/
public function over(?string $name = null)
{
$window = $this->getWindow();
if ($name) {
// Set name manually in case this was chained from FunctionsBuilder wrapper
$window->name($name);
}
return $this;
}
/**
* @inheritDoc
*/
public function partition(ExpressionInterface|Closure|array|string $partitions)
{
$this->getWindow()->partition($partitions);
return $this;
}
/**
* @inheritDoc
*/
public function order(ExpressionInterface|Closure|array|string $fields)
{
deprecationWarning(
'5.0.0',
'AggregateExpression::order() is deprecated. Use AggregateExpression::orderBy() instead.',
);
return $this->orderBy($fields);
}
/**
* @inheritDoc
*/
public function orderBy(ExpressionInterface|Closure|array|string $fields)
{
$this->getWindow()->orderBy($fields);
return $this;
}
/**
* @inheritDoc
*/
public function range(ExpressionInterface|string|int|null $start, ExpressionInterface|string|int|null $end = 0)
{
$this->getWindow()->range($start, $end);
return $this;
}
/**
* @inheritDoc
*/
public function rows(?int $start, ?int $end = 0)
{
$this->getWindow()->rows($start, $end);
return $this;
}
/**
* @inheritDoc
*/
public function groups(?int $start, ?int $end = 0)
{
$this->getWindow()->groups($start, $end);
return $this;
}
/**
* @inheritDoc
*/
public function frame(
string $type,
ExpressionInterface|string|int|null $startOffset,
string $startDirection,
ExpressionInterface|string|int|null $endOffset,
string $endDirection,
) {
$this->getWindow()->frame($type, $startOffset, $startDirection, $endOffset, $endDirection);
return $this;
}
/**
* @inheritDoc
*/
public function excludeCurrent()
{
$this->getWindow()->excludeCurrent();
return $this;
}
/**
* @inheritDoc
*/
public function excludeGroup()
{
$this->getWindow()->excludeGroup();
return $this;
}
/**
* @inheritDoc
*/
public function excludeTies()
{
$this->getWindow()->excludeTies();
return $this;
}
/**
* Returns or creates WindowExpression for function.
*
* @return \Cake\Database\Expression\WindowExpression
*/
protected function getWindow(): WindowExpression
{
return $this->window ??= new WindowExpression();
}
/**
* @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 += 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;
}
}
}
@@ -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 mixed $_from;
/**
* The second value in the expression
*
* @var mixed
*/
protected mixed $_to;
/**
* The data type for the from and to arguments
*
* @var mixed
*/
protected mixed $_type;
/**
* Constructor
*
* @param \Cake\Database\ExpressionInterface|string $field The field name to compare for values in between 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(ExpressionInterface|string $field, mixed $from, mixed $to, ?string $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,
];
$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);
}
assert(is_string($field));
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|null $type The type of $value
* @return string generated placeholder
*/
protected function _bindValue(mixed $value, ValueBinder $binder, ?string $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};
}
}
}
}
@@ -0,0 +1,103 @@
<?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\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\TypedResultInterface;
use Cake\Database\ValueBinder;
use DateTimeInterface;
use Stringable;
/**
* Trait that holds shared functionality for case related expressions.
*
* @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(mixed $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) {
$type = 'date';
} elseif ($value instanceof DateTimeInterface) {
$type = 'datetime';
} elseif (
$value instanceof Stringable
) {
$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, mixed $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;
}
}
@@ -0,0 +1,594 @@
<?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;
/**
* 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 array $validClauseNames = [
'value',
'when',
'else',
];
/**
* Whether this is a simple case expression.
*
* @var bool
*/
protected bool $isSimpleVariant = false;
/**
* The case value.
*
* @var \Cake\Database\ExpressionInterface|object|scalar|null
*/
protected mixed $value = null;
/**
* The case value type.
*
* @var string|null
*/
protected ?string $valueType = null;
/**
* The `WHEN ... THEN ...` expressions.
*
* @var array<\Cake\Database\Expression\WhenThenExpression>
*/
protected array $when = [];
/**
* Buffer that holds values and types for use with `then()`.
*
* @var array|null
*/
protected ?array $whenBuffer = null;
/**
* The else part result value.
*
* @var \Cake\Database\ExpressionInterface|object|scalar|null
*/
protected mixed $else = null;
/**
* The else part result type.
*
* @var string|null
*/
protected ?string $elseType = null;
/**
* The return type.
*
* @var string|null
*/
protected ?string $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(mixed $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,
get_debug_type($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->expr()
* ->case()
* ->when($query->expr(':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(mixed $when, array|string|null $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,
get_debug_type($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(mixed $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(mixed $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,
get_debug_type($result),
));
}
$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): mixed
{
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 (!$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;
}
}
}
@@ -0,0 +1,240 @@
<?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\Exception\DatabaseException;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use Closure;
/**
* An expression that represents a common table expression definition.
*/
class CommonTableExpression implements ExpressionInterface
{
/**
* The CTE name.
*
* @var \Cake\Database\Expression\IdentifierExpression
*/
protected IdentifierExpression $name;
/**
* The field names to use for the CTE.
*
* @var array<\Cake\Database\Expression\IdentifierExpression>
*/
protected array $fields = [];
/**
* The CTE query definition.
*
* @var \Cake\Database\ExpressionInterface|null
*/
protected ?ExpressionInterface $query = null;
/**
* Whether the CTE is materialized or not materialized.
*
* @var string|null
*/
protected ?string $materialized = null;
/**
* Whether the CTE is recursive.
*
* @var bool
*/
protected bool $recursive = false;
/**
* Constructor.
*
* @param string $name The CTE name.
* @param \Cake\Database\ExpressionInterface|\Closure|null $query CTE query
*/
public function __construct(string $name = '', ExpressionInterface|Closure|null $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(ExpressionInterface|Closure $query)
{
if ($query instanceof Closure) {
$query = $query();
if (!($query instanceof ExpressionInterface)) {
throw new DatabaseException(
'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<string>|array<\Cake\Database\Expression\IdentifierExpression>|string $fields Field names
* @return $this
*/
public function field(IdentifierExpression|array|string $fields)
{
$fields = (array)$fields;
/** @var array<string|\Cake\Database\Expression\IdentifierExpression> $fields */
foreach ($fields as &$field) {
if (!($field instanceof IdentifierExpression)) {
$field = new IdentifierExpression($field);
}
}
/** @var array<\Cake\Database\Expression\IdentifierExpression> $mergedFields */
$mergedFields = array_merge($this->fields, $fields);
$this->fields = $mergedFields;
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(fn(IdentifierExpression $e) => $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;
}
}
}
@@ -0,0 +1,320 @@
<?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 mixed $_value;
/**
* The type to be used for casting the value to a database representation
*
* @var string|null
*/
protected ?string $_type = null;
/**
* The operator used for comparing field and value
*
* @var string
*/
protected string $_operator = '=';
/**
* Whether the value in this expression is a traversable
*
* @var bool
*/
protected bool $_isMultiple = false;
/**
* A cached list of ExpressionInterface objects that were
* found in the value for this expression.
*
* @var array<\Cake\Database\ExpressionInterface>
*/
protected array $_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(
ExpressionInterface|string $field,
mixed $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(mixed $value): void
{
$value = $this->_castToExpression($value, $this->_type);
$isMultiple = $this->_type && str_contains($this->_type, '[]');
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(): mixed
{
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
{
$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);
}
assert(is_string($field));
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;
/** @var string $field */
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(mixed $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 ($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(ExpressionInterface|iterable $values): array
{
if ($values instanceof ExpressionInterface) {
return [$values, []];
}
$expressions = [];
$result = [];
$isArray = is_array($values);
if ($isArray) {
$result = (array)$values;
}
foreach ($values as $k => $v) {
if ($v instanceof ExpressionInterface) {
$expressions[$k] = $v;
}
if ($isArray) {
$result[$k] = $v;
}
}
return [$result, $expressions];
}
}
@@ -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.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
/**
* 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(ExpressionInterface|array|string $field): void;
/**
* Returns the field name
*
* @return \Cake\Database\ExpressionInterface|array|string
*/
public function getField(): ExpressionInterface|array|string;
}
@@ -0,0 +1,53 @@
<?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;
/**
* 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 ExpressionInterface|array|string $_field;
/**
* Sets the field name
*
* @param \Cake\Database\ExpressionInterface|array|string $field The field to compare with.
* @return void
*/
public function setField(ExpressionInterface|array|string $field): void
{
$this->_field = $field;
}
/**
* Returns the field name
*
* @return \Cake\Database\ExpressionInterface|array|string
*/
public function getField(): ExpressionInterface|array|string
{
return $this->_field;
}
}
@@ -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 string $_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 \Cake\Database\ExpressionInterface|array|string $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
*/
public function add(ExpressionInterface|array|string $conditions, array $types = [], bool $prepend = false)
{
$put = $prepend ? 'array_unshift' : 'array_push';
$typeMap = $this->getTypeMap()->setTypes($types);
/** @var array $conditions */
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);
}
}
@@ -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 string $_identifier;
/**
* @var string|null
*/
protected ?string $collation = null;
/**
* 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;
}
}
@@ -0,0 +1,92 @@
<?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\TypeMap;
use Cake\Database\ValueBinder;
use InvalidArgumentException;
/**
* 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(
ExpressionInterface|array|string $conditions = [],
TypeMap|array $types = [],
string $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 InvalidArgumentException(
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);
}
}
@@ -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 string $_direction;
/**
* Constructor
*
* @param \Cake\Database\ExpressionInterface|string $field The field to order on.
* @param string $direction The direction to sort on.
*/
public function __construct(ExpressionInterface|string $field, string $direction)
{
$this->_field = $field;
$this->_direction = strtolower($direction) === 'asc' ? 'ASC' : 'DESC';
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$field = $this->_field;
if ($field instanceof Query) {
$field = sprintf('(%s)', $field->sql($binder));
} elseif ($field instanceof ExpressionInterface) {
$field = $field->sql($binder);
}
assert(is_string($field));
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;
}
}
}
@@ -0,0 +1,788 @@
<?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\TypeMap;
use Cake\Database\TypeMapTrait;
use Cake\Database\ValueBinder;
use Closure;
use Countable;
use InvalidArgumentException;
/**
* 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 string $_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 array $_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(
ExpressionInterface|array|string $conditions = [],
TypeMap|array $types = [],
string $conjunction = 'AND',
) {
$this->setTypeMap($types);
$this->setConjunction(strtoupper($conjunction));
if ($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(ExpressionInterface|array|string $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(ExpressionInterface|string $field, mixed $value, ?string $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(ExpressionInterface|string $field, mixed $value, ?string $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(ExpressionInterface|string $field, mixed $value, ?string $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(ExpressionInterface|string $field, mixed $value, ?string $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(ExpressionInterface|string $field, mixed $value, ?string $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(ExpressionInterface|string $field, mixed $value, ?string $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(ExpressionInterface|string $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(ExpressionInterface|string $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(ExpressionInterface|string $field, mixed $value, ?string $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(ExpressionInterface|string $field, mixed $value, ?string $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(
ExpressionInterface|string $field,
ExpressionInterface|array|string $values,
?string $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'));
}
/**
* 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(mixed $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(
ExpressionInterface|string $field,
ExpressionInterface|array|string $values,
?string $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(
ExpressionInterface|string $field,
ExpressionInterface|array|string $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 in between 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(ExpressionInterface|string $field, mixed $from, mixed $to, ?string $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 static
*/
public function and(ExpressionInterface|Closure|array|string $conditions, array $types = []): static
{
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 static
*/
public function or(ExpressionInterface|Closure|array|string $conditions, array $types = []): static
{
if ($conditions instanceof Closure) {
return $conditions(new static([], $this->getTypeMap()->setTypes($types), 'OR'));
}
return new static($conditions, $this->getTypeMap()->setTypes($types), 'OR');
}
/**
* 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(ExpressionInterface|Closure|array|string $conditions, array $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): ExpressionInterface {
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 callback for each of the parts that form this expression.
*
* The callback is required to return a value with which the currently
* visited part will be replaced. If the callback 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 \Closure $callback The callback to run for each part
* @return $this
*/
public function iterateParts(Closure $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;
}
/**
* 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 = false;
$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 $condition 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|string
* @throws \InvalidArgumentException If operator is invalid or missing on NULL usage.
*/
protected function _parseCondition(string $condition, mixed $value): ExpressionInterface|string
{
$expression = trim($condition);
$operator = '=';
$spaces = substr_count($expression, ' ');
// Handle expression 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(' ', $expression);
if (preg_match('/(is not|not \w+)$/i', $expression)) {
$last = array_pop($parts);
$second = array_pop($parts);
$parts[] = "{$second} {$last}";
}
$operator = array_pop($parts);
$expression = implode(' ', $parts);
} elseif ($spaces == 1) {
$parts = explode(' ', $expression, 2);
[$expression, $operator] = $parts;
}
$operator = strtoupper(trim($operator));
$type = $this->getTypeMap()->type($expression);
$typeMultiple = (is_string($type) && str_contains($type, '[]'));
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` has invalid `null` value.'
. ' If `null` is a valid value, operator (IS, IS NOT) is missing.',
$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(ExpressionInterface|string $field): ?string
{
$field = $field instanceof IdentifierExpression ? $field->getIdentifier() : $field;
if (!is_string($field)) {
return null;
}
return $this->getTypeMap()->type($field);
}
/**
* 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;
}
}
}
}
@@ -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 $string;
/**
* @var string
*/
protected string $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;
}
}
@@ -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>
*/
protected array $types;
/**
* 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(
ExpressionInterface|array|string $fields,
ExpressionInterface|array $values,
array $types = [],
string $conjunction = '=',
) {
$this->types = $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->types;
}
/**
* Sets the value
*
* @param mixed $value The value to compare
* @return void
*/
public function setValue(mixed $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.',
);
}
} elseif (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->types;
$isMultiOperation = $this->isMulti();
if (!$type) {
$type = null;
}
if ($isMultiOperation) {
$bound = [];
foreach ($value as $k => $val) {
$valType = $type && isset($type[$k]) ? $type[$k] : $type;
assert($valType === null || is_scalar($valType));
$bound[] = $this->_bindValue($val, $binder, $valType);
}
$values[] = sprintf('(%s)', implode(',', $bound));
continue;
}
$valType = $type && isset($type[$i]) ? $type[$i] : $type;
assert($valType === null || is_scalar($valType));
$values[] = $this->_bindValue($value, $binder, $valType);
}
return implode(', ', $values);
}
/**
* @inheritDoc
*/
protected function _bindValue(mixed $value, ValueBinder $binder, ?string $type = null): string
{
$placeholder = $binder->placeholder('tuple');
$binder->bind($placeholder, $value, $type);
return $placeholder;
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
$fields = (array)$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 callback to use when traversing
* @return void
*/
protected function _traverseValue(mixed $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']);
}
}
@@ -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 string $_operator;
/**
* Holds the value which the unary expression operates
*
* @var mixed
*/
protected mixed $_value;
/**
* Where to place the operator
*
* @var int
*/
protected int $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, mixed $value, int $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;
}
}
}
@@ -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 array $_values = [];
/**
* List of columns to ensure are part of the insert.
*
* @var array
*/
protected array $_columns = [];
/**
* The Query object to use as a values expression
*
* @var \Cake\Database\Query|null
*/
protected ?Query $_query = null;
/**
* Whether values have been casted to expressions
* already.
*
* @var bool
*/
protected bool $_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 and Query data types.
*/
public function add(Query|array $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 (!$this->_values && $this->_query === null) {
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 (!$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;
}
}
@@ -0,0 +1,320 @@
<?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\Query;
use Cake\Database\Type\ExpressionTypeCasterTrait;
use Cake\Database\TypeMap;
use Cake\Database\ValueBinder;
use Closure;
use InvalidArgumentException;
use LogicException;
/**
* Represents a SQL when/then clause with a fluid API
*/
class WhenThenExpression implements ExpressionInterface
{
use CaseExpressionTrait;
use ExpressionTypeCasterTrait;
/**
* The names of the clauses that are valid for use with the
* `clause()` method.
*
* @var array<string>
*/
protected array $validClauseNames = [
'when',
'then',
];
/**
* The type map to use when using an array of conditions for the
* `WHEN` value.
*
* @var \Cake\Database\TypeMap
*/
protected TypeMap $_typeMap;
/**
* Then `WHEN` value.
*
* @var \Cake\Database\ExpressionInterface|object|scalar|null
*/
protected mixed $when = null;
/**
* The `WHEN` value type.
*
* @var array|string|null
*/
protected array|string|null $whenType = null;
/**
* The `THEN` value.
*
* @var \Cake\Database\ExpressionInterface|object|scalar|null
*/
protected mixed $then = null;
/**
* Whether the `THEN` value has been defined, eg whether `then()`
* has been invoked.
*
* @var bool
*/
protected bool $hasThenBeenDefined = false;
/**
* The `THEN` result type.
*
* @var string|null
*/
protected ?string $thenType = null;
/**
* Constructor.
*
* @param \Cake\Database\TypeMap|null $typeMap The type map to use when using an array of conditions for the `WHEN`
* value.
*/
public function __construct(?TypeMap $typeMap = null)
{
$this->_typeMap = $typeMap ?? new TypeMap();
}
/**
* Sets the `WHEN` value.
*
* @param object|array|string|float|int|bool $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 \InvalidArgumentException In case the `$when` argument is an empty array.
* @throws \InvalidArgumentException In case the `$when` argument is an array, and the `$type` argument is neither
* an array, nor null.
* @throws \InvalidArgumentException In case the `$when` argument is a non-array value, and the `$type` argument is
* neither a string, nor null.
* @see CaseStatementExpression::when() for a more detailed usage explanation.
*/
public function when(object|array|string|float|int|bool $when, array|string|null $type = null)
{
if (is_array($when)) {
if (!$when) {
throw new InvalidArgumentException('The `$when` argument must be a non-empty array');
}
if (
$type !== null &&
!is_array($type)
) {
throw new InvalidArgumentException(sprintf(
'When using an array for the `$when` argument, the `$type` argument must be an ' .
'array too, `%s` given.',
get_debug_type($type),
));
}
// avoid dirtying the type map for possible consecutive `when()` calls
$typeMap = clone $this->_typeMap;
if (
is_array($type) &&
$type !== []
) {
$typeMap = $typeMap->setTypes($type);
}
$when = new QueryExpression($when, $typeMap);
} else {
if (
$type !== null &&
!is_string($type)
) {
throw new InvalidArgumentException(sprintf(
'When using a non-array value for the `$when` argument, the `$type` argument must ' .
'be a string, `%s` given.',
get_debug_type($type),
));
}
if (
$type === null &&
!($when instanceof ExpressionInterface)
) {
$type = $this->inferType($when);
}
}
$this->when = $when;
$this->whenType = $type;
return $this;
}
/**
* Sets the `THEN` 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 inferred from the given
* result value.
* @return $this
*/
public function then(mixed $result, ?string $type = null)
{
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,
get_debug_type($result),
));
}
$this->then = $result;
$this->thenType = $type ?? $this->inferType($result);
$this->hasThenBeenDefined = true;
return $this;
}
/**
* Returns the expression's result value type.
*
* @return string|null
* @see WhenThenExpression::then()
*/
public function getResultType(): ?string
{
return $this->thenType;
}
/**
* Returns the available data for the given clause.
*
* ### Available clauses
*
* The following clause names are available:
*
* * `when`: The `WHEN` value.
* * `then`: The `THEN` result value.
*
* @param string $clause The name of the clause to obtain.
* @return \Cake\Database\ExpressionInterface|object|scalar|null
* @throws \InvalidArgumentException In case the given clause name is invalid.
*/
public function clause(string $clause): mixed
{
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->when === null) {
throw new LogicException('Case expression has incomplete when clause. Missing `when()`.');
}
if (!$this->hasThenBeenDefined) {
throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.');
}
$when = $this->when;
if (
is_string($this->whenType) &&
!($when instanceof ExpressionInterface)
) {
$when = $this->_castToExpression($when, $this->whenType);
}
if ($when instanceof Query) {
$when = sprintf('(%s)', $when->sql($binder));
} elseif ($when instanceof ExpressionInterface) {
$when = $when->sql($binder);
} else {
$placeholder = $binder->placeholder('c');
if (is_string($this->whenType)) {
$whenType = $this->whenType;
} else {
$whenType = null;
}
$binder->bind($placeholder, $when, $whenType);
$when = $placeholder;
}
$then = $this->compileNullableValue($binder, $this->then, $this->thenType);
return "WHEN {$when} THEN {$then}";
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
if ($this->when instanceof ExpressionInterface) {
$callback($this->when);
$this->when->traverse($callback);
}
if ($this->then instanceof ExpressionInterface) {
$callback($this->then);
$this->then->traverse($callback);
}
return $this;
}
/**
* Clones the inner expression objects.
*
* @return void
*/
public function __clone()
{
if ($this->when instanceof ExpressionInterface) {
$this->when = clone $this->when;
}
if ($this->then instanceof ExpressionInterface) {
$this->then = clone $this->then;
}
}
}
@@ -0,0 +1,350 @@
<?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 function Cake\Core\deprecationWarning;
/**
* This represents a SQL window expression used by aggregate and window functions.
*/
class WindowExpression implements ExpressionInterface, WindowInterface
{
/**
* @var \Cake\Database\Expression\IdentifierExpression
*/
protected IdentifierExpression $name;
/**
* @var array<\Cake\Database\ExpressionInterface>
*/
protected array $partitions = [];
/**
* @var \Cake\Database\Expression\OrderByExpression|null
*/
protected ?OrderByExpression $order = null;
/**
* @var array|null
*/
protected ?array $frame = null;
/**
* @var string|null
*/
protected ?string $exclusion = null;
/**
* @param string $name Window name
*/
public function __construct(string $name = '')
{
$this->name = new IdentifierExpression($name);
}
/**
* Return whether is only a named window expression.
*
* These window expressions only specify a named window and do not
* specify their own partitions, frame or order.
*
* @return bool
*/
public function isNamedOnly(): bool
{
return $this->name->getIdentifier() && (!$this->partitions && !$this->frame && !$this->order);
}
/**
* Sets the window name.
*
* @param string $name Window name
* @return $this
*/
public function name(string $name)
{
$this->name = new IdentifierExpression($name);
return $this;
}
/**
* @inheritDoc
*/
public function partition(ExpressionInterface|Closure|array|string $partitions)
{
if (!$partitions) {
return $this;
}
if ($partitions instanceof Closure) {
$partitions = $partitions(new QueryExpression([], [], ''));
}
if (!is_array($partitions)) {
$partitions = [$partitions];
}
foreach ($partitions as &$partition) {
if (is_string($partition)) {
$partition = new IdentifierExpression($partition);
}
}
$this->partitions = array_merge($this->partitions, $partitions);
return $this;
}
/**
* @inheritDoc
*/
public function order(ExpressionInterface|Closure|array|string $fields)
{
deprecationWarning(
'5.0.0',
'WindowExpression::order() is deprecated. Use WindowExpression::orderBy() instead.',
);
return $this->orderBy($fields);
}
/**
* @inheritDoc
*/
public function orderBy(ExpressionInterface|Closure|array|string $fields)
{
if (!$fields) {
return $this;
}
$this->order ??= new OrderByExpression();
if ($fields instanceof Closure) {
$fields = $fields(new QueryExpression([], [], ''));
}
$this->order->add($fields);
return $this;
}
/**
* @inheritDoc
*/
public function range(ExpressionInterface|string|int|null $start, ExpressionInterface|string|int|null $end = 0)
{
return $this->frame(self::RANGE, $start, self::PRECEDING, $end, self::FOLLOWING);
}
/**
* @inheritDoc
*/
public function rows(?int $start, ?int $end = 0)
{
return $this->frame(self::ROWS, $start, self::PRECEDING, $end, self::FOLLOWING);
}
/**
* @inheritDoc
*/
public function groups(?int $start, ?int $end = 0)
{
return $this->frame(self::GROUPS, $start, self::PRECEDING, $end, self::FOLLOWING);
}
/**
* @inheritDoc
*/
public function frame(
string $type,
ExpressionInterface|string|int|null $startOffset,
string $startDirection,
ExpressionInterface|string|int|null $endOffset,
string $endDirection,
) {
$this->frame = [
'type' => $type,
'start' => [
'offset' => $startOffset,
'direction' => $startDirection,
],
'end' => [
'offset' => $endOffset,
'direction' => $endDirection,
],
];
return $this;
}
/**
* @inheritDoc
*/
public function excludeCurrent()
{
$this->exclusion = 'CURRENT ROW';
return $this;
}
/**
* @inheritDoc
*/
public function excludeGroup()
{
$this->exclusion = 'GROUP';
return $this;
}
/**
* @inheritDoc
*/
public function excludeTies()
{
$this->exclusion = 'TIES';
return $this;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$clauses = [];
if ($this->name->getIdentifier()) {
$clauses[] = $this->name->sql($binder);
}
if ($this->partitions) {
$expressions = [];
foreach ($this->partitions as $partition) {
$expressions[] = $partition->sql($binder);
}
$clauses[] = 'PARTITION BY ' . implode(', ', $expressions);
}
if ($this->order) {
$clauses[] = $this->order->sql($binder);
}
if ($this->frame) {
$start = $this->buildOffsetSql(
$binder,
$this->frame['start']['offset'],
$this->frame['start']['direction'],
);
$end = $this->buildOffsetSql(
$binder,
$this->frame['end']['offset'],
$this->frame['end']['direction'],
);
$frameSql = sprintf('%s BETWEEN %s AND %s', $this->frame['type'], $start, $end);
if ($this->exclusion !== null) {
$frameSql .= ' EXCLUDE ' . $this->exclusion;
}
$clauses[] = $frameSql;
}
return implode(' ', $clauses);
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
$callback($this->name);
foreach ($this->partitions as $partition) {
$callback($partition);
$partition->traverse($callback);
}
if ($this->order) {
$callback($this->order);
$this->order->traverse($callback);
}
if ($this->frame !== null) {
$offset = $this->frame['start']['offset'];
if ($offset instanceof ExpressionInterface) {
$callback($offset);
$offset->traverse($callback);
}
$offset = $this->frame['end']['offset'] ?? null;
if ($offset instanceof ExpressionInterface) {
$callback($offset);
$offset->traverse($callback);
}
}
return $this;
}
/**
* Builds frame offset sql.
*
* @param \Cake\Database\ValueBinder $binder Value binder
* @param \Cake\Database\ExpressionInterface|string|int|null $offset Frame offset
* @param string $direction Frame offset direction
* @return string
*/
protected function buildOffsetSql(
ValueBinder $binder,
ExpressionInterface|string|int|null $offset,
string $direction,
): string {
if ($offset === 0) {
return 'CURRENT ROW';
}
if ($offset instanceof ExpressionInterface) {
$offset = $offset->sql($binder);
}
return sprintf(
'%s %s',
$offset ?? 'UNBOUNDED',
$direction,
);
}
/**
* Clone this object and its subtree of expressions.
*
* @return void
*/
public function __clone()
{
$this->name = clone $this->name;
foreach ($this->partitions as $i => $partition) {
$this->partitions[$i] = clone $partition;
}
if ($this->order !== null) {
$this->order = clone $this->order;
}
}
}
@@ -0,0 +1,175 @@
<?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 Closure;
/**
* This defines the functions used for building window expressions.
*/
interface WindowInterface
{
/**
* @var string
*/
public const PRECEDING = 'PRECEDING';
/**
* @var string
*/
public const FOLLOWING = 'FOLLOWING';
/**
* @var string
*/
public const RANGE = 'RANGE';
/**
* @var string
*/
public const ROWS = 'ROWS';
/**
* @var string
*/
public const GROUPS = 'GROUPS';
/**
* Adds one or more partition expressions to the window.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array<\Cake\Database\ExpressionInterface|string>|string $partitions Partition expressions
* @return $this
*/
public function partition(ExpressionInterface|Closure|array|string $partitions);
/**
* Adds one or more order by clauses to the window.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array<\Cake\Database\ExpressionInterface|string>|string $fields Order expressions
* @return $this
* @deprecated 5.0.0 Use orderBy() instead.
*/
public function order(ExpressionInterface|Closure|array|string $fields);
/**
* Adds one or more order by clauses to the window.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array<\Cake\Database\ExpressionInterface|string>|string $fields Order expressions
* @return $this
*/
public function orderBy(ExpressionInterface|Closure|array|string $fields);
/**
* Adds a simple range frame to the window.
*
* `$start`:
* - `0` - 'CURRENT ROW'
* - `null` - 'UNBOUNDED PRECEDING'
* - offset - 'offset PRECEDING'
*
* `$end`:
* - `0` - 'CURRENT ROW'
* - `null` - 'UNBOUNDED FOLLOWING'
* - offset - 'offset FOLLOWING'
*
* If you need to use 'FOLLOWING' with frame start or
* 'PRECEDING' with frame end, use `frame()` instead.
*
* @param \Cake\Database\ExpressionInterface|string|int|null $start Frame start
* @param \Cake\Database\ExpressionInterface|string|int|null $end Frame end
* If not passed in, only frame start SQL will be generated.
* @return $this
*/
public function range(ExpressionInterface|string|int|null $start, ExpressionInterface|string|int|null $end = 0);
/**
* Adds a simple rows frame to the window.
*
* See `range()` for details.
*
* @param int|null $start Frame start
* @param int|null $end Frame end
* If not passed in, only frame start SQL will be generated.
* @return $this
*/
public function rows(?int $start, ?int $end = 0);
/**
* Adds a simple groups frame to the window.
*
* See `range()` for details.
*
* @param int|null $start Frame start
* @param int|null $end Frame end
* If not passed in, only frame start SQL will be generated.
* @return $this
*/
public function groups(?int $start, ?int $end = 0);
/**
* Adds a frame to the window.
*
* Use the `range()`, `rows()` or `groups()` helpers if you need simple
* 'BETWEEN offset PRECEDING and offset FOLLOWING' frames.
*
* You can specify any direction for both frame start and frame end.
*
* With both `$startOffset` and `$endOffset`:
* - `0` - 'CURRENT ROW'
* - `null` - 'UNBOUNDED'
*
* @param string $type Frame type
* @param \Cake\Database\ExpressionInterface|string|int|null $startOffset Frame start offset
* @param string $startDirection Frame start direction
* @param \Cake\Database\ExpressionInterface|string|int|null $endOffset Frame end offset
* @param string $endDirection Frame end direction
* @return $this
* @throws \InvalidArgumentException WHen offsets are negative.
* @phpstan-param self::RANGE|self::ROWS|self::GROUPS $type
* @phpstan-param self::PRECEDING|self::FOLLOWING $startDirection
* @phpstan-param self::PRECEDING|self::FOLLOWING $endDirection
*/
public function frame(
string $type,
ExpressionInterface|string|int|null $startOffset,
string $startDirection,
ExpressionInterface|string|int|null $endOffset,
string $endDirection,
);
/**
* Adds current row frame exclusion.
*
* @return $this
*/
public function excludeCurrent();
/**
* Adds group frame exclusion.
*
* @return $this
*/
public function excludeGroup();
/**
* Adds ties frame exclusion.
*
* @return $this
*/
public function excludeTies();
}
+44
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 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database;
use Closure;
/**
* An interface used by Expression objects.
*/
interface ExpressionInterface
{
/**
* Converts the Node into a SQL string fragment.
*
* @param \Cake\Database\ValueBinder $binder Parameter binder
* @return string
*/
public function sql(ValueBinder $binder): string;
/**
* Iterates over each part of the expression recursively for every
* level of the expressions tree and executes the callback,
* passing as first parameter the instance of the expression currently
* being iterated.
*
* @param \Closure $callback The callback to run for all nodes.
* @return $this
*/
public function traverse(Closure $callback);
}
+97
View File
@@ -0,0 +1,97 @@
<?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.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database;
use Cake\Database\Type\BatchCastingInterface;
use Cake\Database\Type\OptionalConvertInterface;
/**
* An invokable class to be used for processing each of the rows in a statement
* result, so that the values are converted to the right PHP types.
*
* @internal
*/
class FieldTypeConverter
{
/**
* @var \Cake\Database\Driver
*/
protected Driver $driver;
/**
* Maps type names to conversion settings.
*
* @var array
*/
protected array $conversions = [];
/**
* Builds the type map
*
* @param \Cake\Database\TypeMap $typeMap Contains the types to use for converting results
* @param \Cake\Database\Driver $driver The driver to use for the type conversion
*/
public function __construct(TypeMap $typeMap, Driver $driver)
{
$this->driver = $driver;
$types = TypeFactory::buildAll();
foreach ($typeMap->toArray() as $field => $typeName) {
$type = $types[$typeName] ?? null;
if (!$type || ($type instanceof OptionalConvertInterface && !$type->requiresToPhpCast())) {
continue;
}
$this->conversions[$typeName] ??= [
'type' => $type,
'hasBatch' => $type instanceof BatchCastingInterface,
'fields' => [],
];
$this->conversions[$typeName]['fields'][] = $field;
}
}
/**
* Converts each of the fields in the array that are present in the type map
* using the corresponding Type class.
*
* @param mixed $row The array with the fields to be casted
* @return mixed
*/
public function __invoke(mixed $row): mixed
{
if (!is_array($row)) {
return $row;
}
foreach ($this->conversions as $conversion) {
/** @var \Cake\Database\TypeInterface $type */
$type = $conversion['type'];
if ($conversion['hasBatch']) {
/** @var \Cake\Database\Type\BatchCastingInterface $type */
$row = $type->manyToPHP($row, $conversion['fields'], $this->driver);
continue;
}
foreach ($conversion['fields'] as $field) {
$row[$field] = $type->toPHP($row[$field], $this->driver);
}
}
return $row;
}
}
+391
View File
@@ -0,0 +1,391 @@
<?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\Database\Expression\AggregateExpression;
use Cake\Database\Expression\FunctionExpression;
use InvalidArgumentException;
/**
* Contains methods related to generating FunctionExpression objects
* with most commonly used SQL functions.
* This acts as a factory for FunctionExpression objects.
*/
class FunctionsBuilder
{
/**
* Returns a FunctionExpression representing a call to SQL RAND function.
*
* @return \Cake\Database\Expression\FunctionExpression
*/
public function rand(): FunctionExpression
{
return new FunctionExpression('RAND', [], [], 'float');
}
/**
* Returns a AggregateExpression representing a call to SQL SUM function.
*
* @param \Cake\Database\ExpressionInterface|string $expression the function argument
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\AggregateExpression
*/
public function sum(ExpressionInterface|string $expression, array $types = []): AggregateExpression
{
$returnType = 'float';
if (current($types) === 'integer') {
$returnType = 'integer';
}
return $this->aggregate('SUM', $this->toLiteralParam($expression), $types, $returnType);
}
/**
* Returns a AggregateExpression representing a call to SQL AVG function.
*
* @param \Cake\Database\ExpressionInterface|string $expression the function argument
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\AggregateExpression
*/
public function avg(ExpressionInterface|string $expression, array $types = []): AggregateExpression
{
return $this->aggregate('AVG', $this->toLiteralParam($expression), $types, 'float');
}
/**
* Returns a AggregateExpression representing a call to SQL MAX function.
*
* @param \Cake\Database\ExpressionInterface|string $expression the function argument
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\AggregateExpression
*/
public function max(ExpressionInterface|string $expression, array $types = []): AggregateExpression
{
return $this->aggregate('MAX', $this->toLiteralParam($expression), $types, current($types) ?: 'float');
}
/**
* Returns a AggregateExpression representing a call to SQL MIN function.
*
* @param \Cake\Database\ExpressionInterface|string $expression the function argument
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\AggregateExpression
*/
public function min(ExpressionInterface|string $expression, array $types = []): AggregateExpression
{
return $this->aggregate('MIN', $this->toLiteralParam($expression), $types, current($types) ?: 'float');
}
/**
* Returns a AggregateExpression representing a call to SQL COUNT function.
*
* @param \Cake\Database\ExpressionInterface|string $expression the function argument
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\AggregateExpression
*/
public function count(ExpressionInterface|string $expression, array $types = []): AggregateExpression
{
return $this->aggregate('COUNT', $this->toLiteralParam($expression), $types, 'integer');
}
/**
* Returns a FunctionExpression representing a string concatenation
*
* @param array $args List of strings or expressions to concatenate
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\FunctionExpression
*/
public function concat(array $args, array $types = []): FunctionExpression
{
return new FunctionExpression('CONCAT', $args, $types, 'string');
}
/**
* Returns a FunctionExpression representing a call to SQL COALESCE function.
*
* @param array $args List of expressions to evaluate as function parameters
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\FunctionExpression
*/
public function coalesce(array $args, array $types = []): FunctionExpression
{
return new FunctionExpression('COALESCE', $args, $types, current($types) ?: 'string');
}
/**
* Returns a FunctionExpression representing a SQL CAST.
*
* The `$type` parameter is a SQL type. The return type for the returned expression
* is the default type name. Use `setReturnType()` to update it.
*
* @param \Cake\Database\ExpressionInterface|string $field Field or expression to cast.
* @param string $dataType The SQL data type
* @return \Cake\Database\Expression\FunctionExpression
*/
public function cast(ExpressionInterface|string $field, string $dataType): FunctionExpression
{
$expression = new FunctionExpression('CAST', $this->toLiteralParam($field));
return $expression->setConjunction(' AS')->add([$dataType => 'literal']);
}
/**
* Returns a FunctionExpression representing the difference in days between
* two dates.
*
* @param array $args List of expressions to obtain the difference in days.
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\FunctionExpression
*/
public function dateDiff(array $args, array $types = []): FunctionExpression
{
return new FunctionExpression('DATEDIFF', $args, $types, 'integer');
}
/**
* Returns the specified date part from the SQL expression.
*
* @param string $part Part of the date to return.
* @param \Cake\Database\ExpressionInterface|string $expression Expression to obtain the date part from.
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\FunctionExpression
*/
public function datePart(
string $part,
ExpressionInterface|string $expression,
array $types = [],
): FunctionExpression {
return $this->extract($part, $expression, $types);
}
/**
* Returns the specified date part from the SQL expression.
*
* @param string $part Part of the date to return.
* @param \Cake\Database\ExpressionInterface|string $expression Expression to obtain the date part from.
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\FunctionExpression
*/
public function extract(string $part, ExpressionInterface|string $expression, array $types = []): FunctionExpression
{
$expression = new FunctionExpression('EXTRACT', $this->toLiteralParam($expression), $types, 'integer');
return $expression->setConjunction(' FROM')->add([$part => 'literal'], [], true);
}
/**
* Add the time unit to the date expression
*
* @param \Cake\Database\ExpressionInterface|string $expression Expression to obtain the date part from.
* @param string|int $value Value to be added. Use negative to subtract.
* @param string $unit Unit of the value e.g. hour or day.
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\FunctionExpression
*/
public function dateAdd(
ExpressionInterface|string $expression,
string|int $value,
string $unit,
array $types = [],
): FunctionExpression {
if (!is_numeric($value)) {
$value = 0;
}
$interval = $value . ' ' . $unit;
$expression = new FunctionExpression('DATE_ADD', $this->toLiteralParam($expression), $types, 'datetime');
return $expression->setConjunction(', INTERVAL')->add([$interval => 'literal']);
}
/**
* Returns a FunctionExpression representing a call to SQL WEEKDAY function.
* 1 - Sunday, 2 - Monday, 3 - Tuesday...
*
* @param \Cake\Database\ExpressionInterface|string $expression the function argument
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\FunctionExpression
*/
public function dayOfWeek(ExpressionInterface|string $expression, array $types = []): FunctionExpression
{
return new FunctionExpression('DAYOFWEEK', $this->toLiteralParam($expression), $types, 'integer');
}
/**
* Returns a FunctionExpression representing a call to SQL WEEKDAY function.
* 1 - Sunday, 2 - Monday, 3 - Tuesday...
*
* @param \Cake\Database\ExpressionInterface|string $expression the function argument
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\FunctionExpression
*/
public function weekday(ExpressionInterface|string $expression, array $types = []): FunctionExpression
{
return $this->dayOfWeek($expression, $types);
}
/**
* Returns a FunctionExpression representing a call that will return the current
* date and time. By default it returns both date and time, but you can also
* make it generate only the date or only the time.
*
* @param string $type (datetime|date|time)
* @return \Cake\Database\Expression\FunctionExpression
*/
public function now(string $type = 'datetime'): FunctionExpression
{
return match ($type) {
'datetime' => new FunctionExpression('NOW', [], [], 'datetime'),
'date' => new FunctionExpression('CURRENT_DATE', [], [], 'date'),
'time' => new FunctionExpression('CURRENT_TIME', [], [], 'time'),
default => throw new InvalidArgumentException('Invalid argument for FunctionsBuilder::now(): ' . $type),
};
}
/**
* Returns an AggregateExpression representing call to SQL ROW_NUMBER().
*
* @return \Cake\Database\Expression\AggregateExpression
*/
public function rowNumber(): AggregateExpression
{
return (new AggregateExpression('ROW_NUMBER', [], [], 'integer'))->over();
}
/**
* Returns an AggregateExpression representing call to SQL LAG().
*
* @param \Cake\Database\ExpressionInterface|string $expression The value evaluated at offset
* @param int $offset The row offset
* @param mixed $default The default value if offset doesn't exist
* @param string|null $type The output type of the lag expression. Defaults to float.
* @return \Cake\Database\Expression\AggregateExpression
*/
public function lag(
ExpressionInterface|string $expression,
int $offset,
mixed $default = null,
?string $type = null,
): AggregateExpression {
$params = $this->toLiteralParam($expression) + [$offset => 'literal'];
if ($default !== null) {
$params[] = $default;
}
$types = [];
if ($type !== null) {
$types = [$type, 'integer', $type];
}
return (new AggregateExpression('LAG', $params, $types, $type ?? 'float'))->over();
}
/**
* Returns an AggregateExpression representing call to SQL LEAD().
*
* @param \Cake\Database\ExpressionInterface|string $expression The value evaluated at offset
* @param int $offset The row offset
* @param mixed $default The default value if offset doesn't exist
* @param string|null $type The output type of the lead expression. Defaults to float.
* @return \Cake\Database\Expression\AggregateExpression
*/
public function lead(
ExpressionInterface|string $expression,
int $offset,
mixed $default = null,
?string $type = null,
): AggregateExpression {
$params = $this->toLiteralParam($expression) + [$offset => 'literal'];
if ($default !== null) {
$params[] = $default;
}
$types = [];
if ($type !== null) {
$types = [$type, 'integer', $type];
}
return (new AggregateExpression('LEAD', $params, $types, $type ?? 'float'))->over();
}
/**
* Returns a FunctionExpression representing the Json Value
*
* @param \Cake\Database\ExpressionInterface|string $expression The Json value or json field
* @param string $jsonPath A valid JSON PATH Query
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\FunctionExpression
*/
public function jsonValue(
ExpressionInterface|string $expression,
string $jsonPath,
array $types = [],
): FunctionExpression {
$params = $this->toLiteralParam($expression) + [$jsonPath];
return new FunctionExpression('JSON_VALUE', $params, $types);
}
/**
* Helper method to create arbitrary SQL aggregate function calls.
*
* @param string $name The SQL aggregate function name
* @param array $params Array of arguments to be passed to the function.
* Can be an associative array with the literal value or identifier:
* `['value' => 'literal']` or `['value' => 'identifier']
* @param array $types Array of types that match the names used in `$params`:
* `['name' => 'type']`
* @param string $return Return type of the entire expression. Defaults to float.
* @return \Cake\Database\Expression\AggregateExpression
*/
public function aggregate(
string $name,
array $params = [],
array $types = [],
string $return = 'float',
): AggregateExpression {
return new AggregateExpression($name, $params, $types, $return);
}
/**
* Magic method dispatcher to create custom SQL function calls
*
* @param string $name the SQL function name to construct
* @param array $args list with up to 3 arguments, first one being an array with
* parameters for the SQL function, the second one a list of types to bind to those
* params, and the third one the return type of the function
* @return \Cake\Database\Expression\FunctionExpression
*/
public function __call(string $name, array $args): FunctionExpression
{
return new FunctionExpression($name, ...$args);
}
/**
* Creates function parameter array from expression or string literal.
*
* @param \Cake\Database\ExpressionInterface|string $expression function argument
* @return array<\Cake\Database\ExpressionInterface|string>
*/
protected function toLiteralParam(ExpressionInterface|string $expression): array
{
if (is_string($expression)) {
return [$expression => 'literal'];
}
return [$expression];
}
}
+354
View File
@@ -0,0 +1,354 @@
<?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\Database\Exception\DatabaseException;
use Cake\Database\Expression\FieldInterface;
use Cake\Database\Expression\IdentifierExpression;
use Cake\Database\Expression\OrderByExpression;
use Cake\Database\Query\DeleteQuery;
use Cake\Database\Query\InsertQuery;
use Cake\Database\Query\SelectQuery;
use Cake\Database\Query\UpdateQuery;
/**
* Contains all the logic related to quoting identifiers in a Query object
*
* @internal
*/
class IdentifierQuoter
{
/**
* Constructor
*
* @param string $startQuote String used to start a database identifier quoting to make it safe.
* @param string $endQuote String used to end a database identifier quoting to make it safe.
*/
public function __construct(
protected string $startQuote,
protected string $endQuote,
) {
}
/**
* 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;
}
/**
* Iterates over each of the clauses in a query looking for identifiers and
* quotes them
*
* @param \Cake\Database\Query $query The query to have its identifiers quoted
* @return \Cake\Database\Query
*/
public function quote(Query $query): Query
{
$binder = $query->getValueBinder();
$query->setValueBinder(null);
match (true) {
$query instanceof InsertQuery => $this->_quoteInsert($query),
$query instanceof SelectQuery => $this->_quoteSelect($query),
$query instanceof UpdateQuery => $this->_quoteUpdate($query),
$query instanceof DeleteQuery => $this->_quoteDelete($query),
default =>
throw new DatabaseException(sprintf(
'Instance of SelectQuery, UpdateQuery, InsertQuery, DeleteQuery expected. Found `%s` instead.',
get_debug_type($query),
))
};
$query->traverseExpressions($this->quoteExpression(...));
return $query->setValueBinder($binder);
}
/**
* Quotes identifiers inside expression objects
*
* @param \Cake\Database\ExpressionInterface $expression The expression object to walk and quote.
* @return void
*/
public function quoteExpression(ExpressionInterface $expression): void
{
match (true) {
$expression instanceof FieldInterface => $this->_quoteComparison($expression),
$expression instanceof OrderByExpression => $this->_quoteOrderBy($expression),
$expression instanceof IdentifierExpression => $this->_quoteIdentifierExpression($expression),
default => null // Nothing to do if there is no match
};
}
/**
* Quotes all identifiers in each of the clauses/parts of a query
*
* @param \Cake\Database\Query $query The query to quote.
* @param array<string> $parts Query clauses.
* @return void
*/
protected function _quoteParts(Query $query, array $parts): void
{
foreach ($parts as $part) {
$contents = $query->clause($part);
if (!is_array($contents)) {
continue;
}
$result = $this->_basicQuoter($contents);
if ($result) {
$part = match ($part) {
'group' => 'groupBy',
'order' => 'orderBy',
default => $part,
};
$query->{$part}($result, true);
}
}
}
/**
* A generic identifier quoting function used for various parts of the query
*
* @param array<string, mixed> $part the part of the query to quote
* @return array<string, mixed>
*/
protected function _basicQuoter(array $part): array
{
$result = [];
foreach ($part as $alias => $value) {
$value = is_string($value) ? $this->quoteIdentifier($value) : $value;
$alias = is_numeric($alias) ? $alias : $this->quoteIdentifier($alias);
$result[$alias] = $value;
}
return $result;
}
/**
* Quotes both the table and alias for an array of joins as stored in a Query
* object
*
* @param array<array> $joins The joins to quote.
* @return array<string, array>
*/
protected function _quoteJoins(array $joins): array
{
$result = [];
foreach ($joins as $value) {
$alias = '';
if (!empty($value['alias'])) {
$alias = $this->quoteIdentifier($value['alias']);
$value['alias'] = $alias;
}
if (is_string($value['table'])) {
$value['table'] = $this->quoteIdentifier($value['table']);
}
$result[$alias] = $value;
}
return $result;
}
/**
* Quotes all identifiers in each of the clauses of a SELECT query
*
* @param \Cake\Database\Query\SelectQuery<mixed> $query The query to quote.
* @return void
*/
protected function _quoteSelect(SelectQuery $query): void
{
$this->_quoteParts($query, ['select', 'distinct', 'from', 'group']);
$joins = $query->clause('join');
if ($joins) {
$joins = $this->_quoteJoins($joins);
$query->join($joins, [], true);
}
}
/**
* Quotes all identifiers in each of the clauses of a DELETE query
*
* @param \Cake\Database\Query\DeleteQuery $query The query to quote.
* @return void
*/
protected function _quoteDelete(DeleteQuery $query): void
{
$this->_quoteParts($query, ['from']);
$joins = $query->clause('join');
if ($joins) {
$joins = $this->_quoteJoins($joins);
$query->join($joins, [], true);
}
}
/**
* Quotes the table name and columns for an insert query
*
* @param \Cake\Database\Query\InsertQuery $query The insert query to quote.
* @return void
*/
protected function _quoteInsert(InsertQuery $query): void
{
/** @var array{0?: string, 1?: array} $insert */
$insert = $query->clause('insert');
if (!isset($insert[0]) || !isset($insert[1])) {
return;
}
[$table, $columns] = $insert;
$table = $this->quoteIdentifier($table);
foreach ($columns as &$column) {
if (is_scalar($column)) {
$column = $this->quoteIdentifier((string)$column);
}
}
$query->insert($columns)->into($table);
}
/**
* Quotes the table name for an update query
*
* @param \Cake\Database\Query\UpdateQuery $query The update query to quote.
* @return void
*/
protected function _quoteUpdate(UpdateQuery $query): void
{
$table = $query->clause('update')[0];
if (is_string($table)) {
$query->update($this->quoteIdentifier($table));
}
}
/**
* Quotes identifiers in expression objects implementing the field interface
*
* @param \Cake\Database\Expression\FieldInterface $expression The expression to quote.
* @return void
*/
protected function _quoteComparison(FieldInterface $expression): void
{
$field = $expression->getField();
if (is_string($field)) {
$expression->setField($this->quoteIdentifier($field));
} elseif (is_array($field)) {
$quoted = [];
foreach ($field as $f) {
$quoted[] = $this->quoteIdentifier($f);
}
$expression->setField($quoted);
} else {
$this->quoteExpression($field);
}
}
/**
* Quotes identifiers in "order by" expression objects
*
* Strings with spaces are treated as literal expressions
* and will not have identifiers quoted.
*
* @param \Cake\Database\Expression\OrderByExpression $expression The expression to quote.
* @return void
*/
protected function _quoteOrderBy(OrderByExpression $expression): void
{
$expression->iterateParts(function ($part, &$field) {
if (is_string($field)) {
$field = $this->quoteIdentifier($field);
return $part;
}
if (is_string($part) && !str_contains($part, ' ')) {
return $this->quoteIdentifier($part);
}
return $part;
});
}
/**
* Quotes identifiers in "order by" expression objects
*
* @param \Cake\Database\Expression\IdentifierExpression $expression The identifiers to quote.
* @return void
*/
protected function _quoteIdentifierExpression(IdentifierExpression $expression): void
{
$expression->setIdentifier(
$this->quoteIdentifier($expression->getIdentifier()),
);
}
}
+22
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.
+194
View File
@@ -0,0 +1,194 @@
<?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\Log;
use Cake\Database\Driver;
use Cake\Database\Driver\Sqlserver;
use Exception;
use JsonSerializable;
use Stringable;
/**
* Contains a query string, the params used to executed it, time taken to do it
* and the number of rows found or affected by its execution.
*
* @internal
*/
class LoggedQuery implements JsonSerializable, Stringable
{
/**
* Driver executing the query
*
* @var \Cake\Database\Driver|null
*/
protected ?Driver $driver = null;
/**
* Query string that was executed
*
* @var string
*/
protected string $query = '';
/**
* Number of milliseconds this query took to complete
*
* @var float
*/
protected float $took = 0;
/**
* Associative array with the params bound to the query string
*
* @var array
*/
protected array $params = [];
/**
* Number of rows affected or returned by the query execution
*
* @var int
*/
protected int $numRows = 0;
/**
* The exception that was thrown by the execution of this query
*
* @var \Exception|null
*/
protected ?Exception $error = null;
/**
* Helper function used to replace query placeholders by the real
* params used to execute the query
*
* @return string
*/
protected function interpolate(): string
{
$params = array_map(function ($p) {
if ($p === null) {
return 'NULL';
}
if (is_bool($p)) {
if ($this->driver instanceof Sqlserver) {
return $p ? '1' : '0';
}
return $p ? 'TRUE' : 'FALSE';
}
if (is_string($p)) {
// Likely binary data like a blob or binary uuid.
// pattern matches ascii control chars.
if (preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $p) !== $p) {
$p = bin2hex($p);
}
$replacements = [
'$' => '\\$',
'\\' => '\\\\\\\\',
"'" => "''",
];
$p = strtr($p, $replacements);
return "'{$p}'";
}
return $p;
}, $this->params);
$keys = [];
$limit = is_int(key($params)) ? 1 : -1;
foreach ($params as $key => $param) {
$keys[] = is_string($key) ? "/:{$key}\b/" : '/[?]/';
}
return (string)preg_replace($keys, $params, $this->query, $limit);
}
/**
* Get the logging context data for a query.
*
* @return array<string, mixed>
*/
public function getContext(): array
{
return [
'query' => $this->query,
'numRows' => $this->numRows,
'took' => $this->took,
'role' => $this->driver ? $this->driver->getRole() : '',
];
}
/**
* Set logging context for this query.
*
* @param array $context Context data.
* @return void
*/
public function setContext(array $context): void
{
foreach ($context as $key => $val) {
if (property_exists($this, $key)) {
$this->{$key} = $val;
}
}
}
/**
* Returns data that will be serialized as JSON
*
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
$error = $this->error;
if ($error !== null) {
$error = [
'class' => $error::class,
'message' => $error->getMessage(),
'code' => $error->getCode(),
];
}
return [
'query' => $this->query,
'numRows' => $this->numRows,
'params' => $this->params,
'took' => $this->took,
'error' => $error,
];
}
/**
* Returns the string representation of this logged query
*
* @return string
*/
public function __toString(): string
{
if ($this->params) {
return $this->interpolate();
}
return $this->query;
}
}
+61
View File
@@ -0,0 +1,61 @@
<?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\Log;
use Cake\Log\Engine\BaseLog;
use Cake\Log\Log;
use Stringable;
/**
* This class is a bridge used to write LoggedQuery objects into a real log.
* by default this class use the built-in CakePHP Log class to accomplish this
*
* @internal
*/
class QueryLogger extends BaseLog
{
/**
* Constructor.
*
* @param array<string, mixed> $config Configuration array
*/
public function __construct(array $config = [])
{
$this->_defaultConfig['scopes'] = ['queriesLog', 'cake.database.queries'];
$this->_defaultConfig['connection'] = '';
parent::__construct($config);
}
/**
* @inheritDoc
*/
public function log($level, string|Stringable $message, array $context = []): void
{
$context += [
'scope' => $this->scopes() ?: ['queriesLog', 'cake.database.queries'],
'connection' => $this->getConfig('connection'),
'query' => null,
];
if ($context['query'] instanceof LoggedQuery) {
$context = $context['query']->getContext() + $context;
$message = 'connection={connection} role={role} duration={took} rows={numRows} ' . $message;
}
Log::write('debug', (string)$message, $context);
}
}
+96
View File
@@ -0,0 +1,96 @@
<?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.3
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database;
use Cake\Database\Expression\FunctionExpression;
/**
* Responsible for compiling a Query object into its SQL representation
* for Postgres
*
* @internal
*/
class PostgresCompiler extends QueryCompiler
{
/**
* Always quote aliases in SELECT clause.
*
* Postgres auto converts unquoted identifiers to lower case.
*
* @var bool
*/
protected bool $_quotedSelectAliases = true;
/**
* {@inheritDoc}
*
* @var array<string, string>
*/
protected array $_templates = [
'delete' => 'DELETE',
'where' => ' WHERE %s',
'group' => ' GROUP BY %s',
'order' => ' %s',
'limit' => ' LIMIT %s',
'offset' => ' OFFSET %s',
'epilog' => ' %s',
'comment' => '/* %s */ ',
];
/**
* Helper function used to build the string representation of a HAVING clause,
* it constructs the field list taking care of aliasing and
* converting expression objects to string.
*
* @param array $parts list of fields to be transformed to string
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
* @return string
*/
protected function _buildHavingPart(array $parts, Query $query, ValueBinder $binder): string
{
$selectParts = $query->clause('select');
foreach ($selectParts as $selectKey => $selectPart) {
if (!$selectPart instanceof FunctionExpression) {
continue;
}
foreach ($parts as $k => $p) {
if (!is_string($p)) {
continue;
}
preg_match_all(
'/\b' . trim($selectKey, '"') . '\b/i',
$p,
$matches,
);
if (empty($matches[0])) {
continue;
}
$parts[$k] = preg_replace(
['/"/', '/\b' . trim($selectKey, '"') . '\b/i'],
['', $selectPart->sql($binder)],
$p,
);
}
}
return sprintf(' HAVING %s', implode(', ', $parts));
}
}
File diff suppressed because it is too large Load Diff
+70
View File
@@ -0,0 +1,70 @@
<?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
*/
namespace Cake\Database\Query;
use Cake\Database\Query;
/**
* This class is used to generate DELETE queries for the relational database.
*/
class DeleteQuery extends Query
{
/**
* Type of this query.
*
* @var string
*/
protected string $_type = self::TYPE_DELETE;
/**
* List of SQL parts that will be used to build this query.
*
* @var array<string, mixed>
*/
protected array $_parts = [
'comment' => null,
'with' => [],
'delete' => true,
'optimizerHint' => [],
'modifier' => [],
'from' => [],
'join' => [],
'where' => null,
'order' => null,
'limit' => null,
'epilog' => null,
];
/**
* Create a delete query.
*
* Can be combined with from(), where() and other methods to
* create delete queries with specific conditions.
*
* @param string|null $table The table to use when deleting.
* @return $this
*/
public function delete(?string $table = null)
{
$this->_dirty();
if ($table !== null) {
$this->from($table);
}
return $this;
}
}
+127
View File
@@ -0,0 +1,127 @@
<?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
*/
namespace Cake\Database\Query;
use Cake\Database\Exception\DatabaseException;
use Cake\Database\Expression\ValuesExpression;
use Cake\Database\Query;
use InvalidArgumentException;
/**
* This class is used to generate INSERT queries for the relational database.
*/
class InsertQuery extends Query
{
/**
* Type of this query.
*
* @var string
*/
protected string $_type = self::TYPE_INSERT;
/**
* List of SQL parts that will be used to build this query.
*
* @var array<string, mixed>
*/
protected array $_parts = [
'comment' => null,
'with' => [],
'insert' => [],
'optimizerHint' => [],
'modifier' => [],
'values' => [],
'epilog' => null,
];
/**
* Create an insert query.
*
* Note calling this method will reset any data previously set
* with Query::values().
*
* @param array $columns The columns to insert into.
* @param array<int|string, string> $types A map between columns & their datatypes.
* @return $this
* @throws \InvalidArgumentException When there are 0 columns.
*/
public function insert(array $columns, array $types = [])
{
if (!$columns) {
throw new InvalidArgumentException('At least 1 column is required to perform an insert.');
}
$this->_dirty();
$this->_parts['insert'][1] = $columns;
if (!$this->_parts['values']) {
$this->_parts['values'] = new ValuesExpression($columns, $this->getTypeMap()->setTypes($types));
} else {
/** @var \Cake\Database\Expression\ValuesExpression $valuesExpr */
$valuesExpr = $this->_parts['values'];
$valuesExpr->setColumns($columns);
}
return $this;
}
/**
* Set the table name for insert queries.
*
* @param string $table The table name to insert into.
* @return $this
*/
public function into(string $table)
{
$this->_dirty();
$this->_parts['insert'][0] = $table;
return $this;
}
/**
* Set the values for an insert query.
*
* Multi inserts can be performed by calling values() more than one time,
* or by providing an array of value sets. Additionally $data can be a Query
* instance to insert data from another SELECT statement.
*
* @param \Cake\Database\Expression\ValuesExpression|\Cake\Database\Query|array $data The data to insert.
* @return $this
* @throws \Cake\Database\Exception\DatabaseException if you try to set values before declaring columns.
* Or if you try to set values on non-insert queries.
*/
public function values(ValuesExpression|Query|array $data)
{
if (empty($this->_parts['insert'])) {
throw new DatabaseException(
'You cannot add values before defining columns to use.',
);
}
$this->_dirty();
if ($data instanceof ValuesExpression) {
$this->_parts['values'] = $data;
return $this;
}
/** @var \Cake\Database\Expression\ValuesExpression $valuesExpr */
$valuesExpr = $this->_parts['values'];
$valuesExpr->add($data);
return $this;
}
}
+136
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 5.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Query;
use Cake\Database\Connection;
use Cake\Database\ExpressionInterface;
use Closure;
/**
* Factory class for generating instances of Select, Insert, Update, Delete queries.
*/
class QueryFactory
{
/**
* Constructor/
*
* @param \Cake\Database\Connection $connection Connection instance.
*/
public function __construct(
protected Connection $connection,
) {
}
/**
* Create a new SelectQuery instance.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string|float|int $fields Fields/columns list for the query.
* @param array|string $table List of tables to query.
* @param array<string, string> $types Associative array containing the types to be used for casting.
* @return \Cake\Database\Query\SelectQuery
*/
public function select(
ExpressionInterface|Closure|array|string|float|int $fields = [],
array|string $table = [],
array $types = [],
): SelectQuery {
$query = new SelectQuery($this->connection);
$query
->select($fields)
->from($table)
->setDefaultTypes($types);
return $query;
}
/**
* Create a new InsertQuery instance.
*
* @param string|null $table The table to insert rows into.
* @param array $values Associative array of column => value to be inserted.
* @param array<int|string, string> $types Associative array containing the types to be used for casting.
* @return \Cake\Database\Query\InsertQuery
*/
public function insert(?string $table = null, array $values = [], array $types = []): InsertQuery
{
$query = new InsertQuery($this->connection);
if ($table) {
$query->into($table);
}
if ($values) {
$columns = array_keys($values);
$query
->insert($columns, $types)
->values($values);
}
return $query;
}
/**
* Create a new UpdateQuery instance.
*
* @param \Cake\Database\ExpressionInterface|string|null $table The table to update rows of.
* @param array $values Values to be updated.
* @param array $conditions Conditions to be set for the update statement.
* @param array<string, string> $types Associative array containing the types to be used for casting.
* @return \Cake\Database\Query\UpdateQuery
*/
public function update(
ExpressionInterface|string|null $table = null,
array $values = [],
array $conditions = [],
array $types = [],
): UpdateQuery {
$query = new UpdateQuery($this->connection);
if ($table) {
$query->update($table);
}
if ($values) {
$query->set($values, $types);
}
if ($conditions) {
$query->where($conditions, $types);
}
return $query;
}
/**
* Create a new DeleteQuery instance.
*
* @param string|null $table The table to delete rows from.
* @param array $conditions Conditions to be set for the delete statement.
* @param array<string, string> $types Associative array containing the types to be used for casting.
* @return \Cake\Database\Query\DeleteQuery
*/
public function delete(?string $table = null, array $conditions = [], array $types = []): DeleteQuery
{
$query = (new DeleteQuery($this->connection))
->delete($table);
if ($conditions) {
$query->where($conditions, $types);
}
return $query;
}
}
+835
View File
@@ -0,0 +1,835 @@
<?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
*/
namespace Cake\Database\Query;
use ArrayIterator;
use Cake\Core\Exception\CakeException;
use Cake\Database\Connection;
use Cake\Database\Expression\IdentifierExpression;
use Cake\Database\Expression\WindowExpression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\StatementInterface;
use Cake\Database\TypeMap;
use Closure;
use InvalidArgumentException;
use IteratorAggregate;
use Traversable;
use function Cake\Core\deprecationWarning;
/**
* This class is used to generate SELECT queries for the relational database.
*
* @template T of mixed
* @implements \IteratorAggregate<T>
*/
class SelectQuery extends Query implements IteratorAggregate
{
/**
* Type of this query.
*
* @var string
*/
protected string $_type = self::TYPE_SELECT;
/**
* List of SQL parts that will be used to build this query.
*
* @var array<string, mixed>
*/
protected array $_parts = [
'comment' => null,
'with' => [],
'select' => [],
'optimizerHint' => [],
'modifier' => [],
'distinct' => false,
'from' => [],
'join' => [],
'where' => null,
'group' => [],
'having' => null,
'window' => [],
'order' => null,
'limit' => null,
'offset' => null,
'union' => [],
'epilog' => null,
'intersect' => [],
];
/**
* A list of callbacks to be called to alter each row from resulting
* statement upon retrieval. Each one of the callback function will receive
* the row array as first argument.
*
* @var array<\Closure>
*/
protected array $_resultDecorators = [];
/**
* Result set from executed SELECT query.
*
* @var iterable|null
*/
protected ?iterable $_results = null;
/**
* Boolean for tracking whether buffered results
* are enabled.
*
* @var bool
*/
protected bool $bufferedResults = true;
/**
* The Type map for fields in the select clause
*
* @var \Cake\Database\TypeMap|null
*/
protected ?TypeMap $_selectTypeMap = null;
/**
* Tracking flag to disable casting
*
* @var bool
*/
protected bool $typeCastEnabled = true;
/**
* Executes query and returns set of decorated results.
*
* The results are cached until the query is modified and marked dirty.
*
* @return iterable
* @throws \Cake\Core\Exception\CakeException When query is not a SELECT query.
*/
public function all(): iterable
{
if ($this->_results === null || $this->_dirty) {
$this->_results = $this->execute()->fetchAll(StatementInterface::FETCH_TYPE_ASSOC);
}
return $this->_results;
}
/**
* Adds new fields to be returned by a `SELECT` statement when this query is
* executed. Fields can be passed as an array of strings, array of expression
* objects, a single expression or a single string.
*
* If an array is passed, keys will be used to alias fields using the value as the
* real field to be aliased. It is possible to alias strings, Expression objects or
* even other Query objects.
*
* If a callback is passed, the returning array of the function will
* be used as the list of fields.
*
* By default this function will append any passed argument to the list of fields
* to be selected, unless the second argument is set to true.
*
* ### Examples:
*
* ```
* $query->select(['id', 'title']); // Produces SELECT id, title
* $query->select(['author' => 'author_id']); // Appends author: SELECT id, title, author_id as author
* $query->select('id', true); // Resets the list: SELECT id
* $query->select(['total' => $countQuery]); // SELECT id, (SELECT ...) AS total
* $query->select(function ($query) {
* return ['article_id', 'total' => $query->func()->count('*')];
* })
* ```
*
* By default no fields are selected, if you have an instance of `Cake\ORM\Query` and try to append
* fields you should also call `Cake\ORM\Query::enableAutoFields()` to select the default fields
* from the table.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string|float|int $fields fields to be added to the list.
* @param bool $overwrite whether to reset fields with passed list or not
* @return $this
*/
public function select(ExpressionInterface|Closure|array|string|float|int $fields = [], bool $overwrite = false)
{
if (!is_string($fields) && $fields instanceof Closure) {
$fields = $fields($this);
}
if (!is_array($fields)) {
$fields = [$fields];
}
if ($overwrite) {
$this->_parts['select'] = $fields;
} else {
$this->_parts['select'] = array_merge($this->_parts['select'], $fields);
}
$this->_dirty();
return $this;
}
/**
* Adds a `DISTINCT` clause to the query to remove duplicates from the result set.
* This clause can only be used for select statements.
*
* If you wish to filter duplicates based of those rows sharing a particular field
* or set of fields, you may pass an array of fields to filter on. Beware that
* this option might not be fully supported in all database systems.
*
* ### Examples:
*
* ```
* // Filters products with the same name and city
* $query->select(['name', 'city'])->from('products')->distinct();
*
* // Filters products in the same city
* $query->distinct(['city']);
* $query->distinct('city');
*
* // Filter products with the same name
* $query->distinct(['name'], true);
* $query->distinct('name', true);
* ```
*
* @param \Cake\Database\ExpressionInterface|array|string|bool $on Enable/disable distinct class
* or list of fields to be filtered on
* @param bool $overwrite whether to reset fields with passed list or not
* @return $this
*/
public function distinct(ExpressionInterface|array|string|bool $on = [], bool $overwrite = false)
{
if ($on === []) {
$on = true;
} elseif (is_string($on)) {
$on = [$on];
}
if (is_array($on)) {
$merge = [];
if (is_array($this->_parts['distinct'])) {
$merge = $this->_parts['distinct'];
}
$on = $overwrite ? array_values($on) : array_merge($merge, array_values($on));
}
$this->_parts['distinct'] = $on;
$this->_dirty();
return $this;
}
/**
* Adds a single or multiple fields to be used in the GROUP BY clause for this query.
* Fields can be passed as an array of strings, array of expression
* objects, a single expression or a single string.
*
* By default this function will append any passed argument to the list of fields
* to be grouped, unless the second argument is set to true.
*
* ### Examples:
*
* ```
* // Produces GROUP BY id, title
* $query->groupBy(['id', 'title']);
*
* // Produces GROUP BY title
* $query->groupBy('title');
* ```
*
* Group fields are not suitable for use with user supplied data as they are
* not sanitized by the query builder.
*
* @param \Cake\Database\ExpressionInterface|array|string $fields fields to be added to the list
* @param bool $overwrite whether to reset fields with passed list or not
* @return $this
* @deprecated 5.0.0 Use groupBy() instead now that CollectionInterface methods are no longer proxied.
*/
public function group(ExpressionInterface|array|string $fields, bool $overwrite = false)
{
deprecationWarning('5.0.0', 'SelectQuery::group() is deprecated. Use SelectQuery::groupBy() instead.');
return $this->groupBy($fields, $overwrite);
}
/**
* Adds a single or multiple fields to be used in the GROUP BY clause for this query.
* Fields can be passed as an array of strings, array of expression
* objects, a single expression or a single string.
*
* By default this function will append any passed argument to the list of fields
* to be grouped, unless the second argument is set to true.
*
* ### Examples:
*
* ```
* // Produces GROUP BY id, title
* $query->groupBy(['id', 'title']);
*
* // Produces GROUP BY title
* $query->groupBy('title');
* ```
*
* Group fields are not suitable for use with user supplied data as they are
* not sanitized by the query builder.
*
* @param \Cake\Database\ExpressionInterface|array|string $fields fields to be added to the list
* @param bool $overwrite whether to reset fields with passed list or not
* @return $this
*/
public function groupBy(ExpressionInterface|array|string $fields, bool $overwrite = false)
{
if ($overwrite) {
$this->_parts['group'] = [];
}
if (!is_array($fields)) {
$fields = [$fields];
}
$this->_parts['group'] = array_merge($this->_parts['group'], array_values($fields));
$this->_dirty();
return $this;
}
/**
* Adds a condition or set of conditions to be used in the `HAVING` clause for this
* query. This method operates in exactly the same way as the method `where()`
* does. Please refer to its documentation for an insight on how to using each
* parameter.
*
* Having fields are not suitable for use with user supplied data as they are
* not sanitized by the query builder.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string|null $conditions The having conditions.
* @param array<string, string> $types Associative array of type names used to bind values to query
* @param bool $overwrite whether to reset conditions with passed list or not
* @see \Cake\Database\Query::where()
* @return $this
*/
public function having(
ExpressionInterface|Closure|array|string|null $conditions = null,
array $types = [],
bool $overwrite = false,
) {
if ($overwrite) {
$this->_parts['having'] = $this->expr();
}
$this->_conjugate('having', $conditions, 'AND', $types);
return $this;
}
/**
* Connects any previously defined set of conditions to the provided list
* using the AND operator in the HAVING clause. This method operates in exactly
* the same way as the method `andWhere()` does. Please refer to its
* documentation for an insight on how to using each parameter.
*
* Having fields are not suitable for use with user supplied data as they are
* not sanitized by the query builder.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions The AND conditions for HAVING.
* @param array<string, string> $types Associative array of type names used to bind values to query
* @see \Cake\Database\Query::andWhere()
* @return $this
*/
public function andHaving(ExpressionInterface|Closure|array|string $conditions, array $types = [])
{
$this->_conjugate('having', $conditions, 'AND', $types);
return $this;
}
/**
* Adds a named window expression.
*
* You are responsible for adding windows in the order your database requires.
*
* @param string $name Window name
* @param \Cake\Database\Expression\WindowExpression|\Closure $window Window expression
* @param bool $overwrite Clear all previous query window expressions
* @return $this
*/
public function window(string $name, WindowExpression|Closure $window, bool $overwrite = false)
{
if ($overwrite) {
$this->_parts['window'] = [];
}
if ($window instanceof Closure) {
$window = $window(new WindowExpression(), $this);
if (!($window instanceof WindowExpression)) {
throw new CakeException('You must return a `WindowExpression` from a Closure passed to `window()`.');
}
}
$this->_parts['window'][] = ['name' => new IdentifierExpression($name), 'window' => $window];
$this->_dirty();
return $this;
}
/**
* Set the page of results you want.
*
* This method provides an easier to use interface to set the limit + offset
* in the record set you want as results. If empty the limit will default to
* the existing limit clause, and if that too is empty, then `25` will be used.
*
* Pages must start at 1.
*
* @param int $num The page number you want.
* @param int|null $limit The number of rows you want in the page. If null
* the current limit clause will be used.
* @return $this
* @throws \InvalidArgumentException If page number < 1.
*/
public function page(int $num, ?int $limit = null)
{
if ($num < 1) {
throw new InvalidArgumentException('Pages must start at 1.');
}
if ($limit !== null) {
$this->limit($limit);
}
$limit = $this->clause('limit');
if ($limit === null) {
$limit = 25;
$this->limit($limit);
}
$offset = ($num - 1) * $limit;
if (PHP_INT_MAX <= $offset) {
$offset = PHP_INT_MAX;
}
$this->offset((int)$offset);
return $this;
}
/**
* Adds a complete query to be used in conjunction with an UNION operator with
* this query. This is used to combine the result set of this query with the one
* that will be returned by the passed query. You can add as many queries as you
* required by calling multiple times this method with different queries.
*
* By default, the UNION operator will remove duplicate rows, if you wish to include
* every row for all queries, use unionAll().
*
* ### Examples
*
* ```
* $union = (new SelectQuery($conn))->select(['id', 'title'])->from(['a' => 'articles']);
* $query->select(['id', 'name'])->from(['d' => 'things'])->union($union);
* ```
*
* Will produce:
*
* `SELECT id, name FROM things d UNION SELECT id, title FROM articles a`
*
* @param \Cake\Database\Query|string $query full SQL query to be used in UNION operator
* @param bool $overwrite whether to reset the list of queries to be operated or not
* @return $this
*/
public function union(Query|string $query, bool $overwrite = false)
{
if ($overwrite) {
$this->_parts['union'] = [];
}
$this->_parts['union'][] = [
'all' => false,
'query' => $query,
];
$this->_dirty();
return $this;
}
/**
* Adds a complete query to be used in conjunction with the UNION ALL operator with
* this query. This is used to combine the result set of this query with the one
* that will be returned by the passed query. You can add as many queries as you
* required by calling multiple times this method with different queries.
*
* Unlike UNION, UNION ALL will not remove duplicate rows.
*
* ```
* $union = (new SelectQuery($conn))->select(['id', 'title'])->from(['a' => 'articles']);
* $query->select(['id', 'name'])->from(['d' => 'things'])->unionAll($union);
* ```
*
* Will produce:
*
* `SELECT id, name FROM things d UNION ALL SELECT id, title FROM articles a`
*
* @param \Cake\Database\Query|string $query full SQL query to be used in UNION operator
* @param bool $overwrite whether to reset the list of queries to be operated or not
* @return $this
*/
public function unionAll(Query|string $query, bool $overwrite = false)
{
if ($overwrite) {
$this->_parts['union'] = [];
}
$this->_parts['union'][] = [
'all' => true,
'query' => $query,
];
$this->_dirty();
return $this;
}
/**
* Adds a complete query to be used in conjunction with an INTERSECT operator with
* this query. This is used to combine the result set of this query with the one
* that will be returned by the passed query. You can add as many queries as you
* required by calling multiple times this method with different queries.
*
* By default, the INTERSECT operator will remove duplicate rows, if you wish to include
* every row for all queries, use intersectAll().
*
* ### Examples
*
* ```
* $intersect = (new SelectQuery($conn))->select(['id', 'title'])->from(['a' => 'articles']);
* $query->select(['id', 'name'])->from(['d' => 'things'])->intersect($intersect);
* ```
*
* Will produce:
*
* `SELECT id, name FROM things d INTERSECT SELECT id, title FROM articles a`
*
* @param \Cake\Database\Query|string $query full SQL query to be used in INTERSECT operator
* @param bool $overwrite whether to reset the list of queries to be operated or not
* @return $this
*/
public function intersect(Query|string $query, bool $overwrite = false)
{
if ($overwrite) {
$this->_parts['intersect'] = [];
}
$this->_parts['intersect'][] = [
'all' => false,
'query' => $query,
];
$this->_dirty();
return $this;
}
/**
* Adds a complete query to be used in conjunction with the INTERSECT ALL operator with
* this query. This is used to combine the result set of this query with the one
* that will be returned by the passed query. You can add as many queries as you
* required by calling multiple times this method with different queries.
*
* Unlike INTERSECT, INTERSECT ALL will not remove duplicate rows.
*
* ```
* $intersect = (new SelectQuery($conn))->select(['id', 'title'])->from(['a' => 'articles']);
* $query->select(['id', 'name'])->from(['d' => 'things'])->intersectAll($intersect);
* ```
*
* Will produce:
*
* `SELECT id, name FROM things d INTERSECT ALL SELECT id, title FROM articles a`
*
* @param \Cake\Database\Query|string $query full SQL query to be used in INTERSECT operator
* @param bool $overwrite whether to reset the list of queries to be operated or not
* @return $this
*/
public function intersectAll(Query|string $query, bool $overwrite = false)
{
if ($overwrite) {
$this->_parts['intersect'] = [];
}
$this->_parts['intersect'][] = [
'all' => true,
'query' => $query,
];
$this->_dirty();
return $this;
}
/**
* Executes this query and returns a results iterator. This function is required
* for implementing the IteratorAggregate interface and allows the query to be
* iterated without having to call all() manually, thus making it look like
* a result set instead of the query itself.
*
* @return \Traversable
*/
public function getIterator(): Traversable
{
if ($this->bufferedResults) {
/** @var \Traversable|array $results */
$results = $this->all();
if (is_array($results)) {
return new ArrayIterator($results);
}
return $results;
}
return $this->execute();
}
/**
* Registers a callback to be executed for each result that is fetched from the
* result set, the callback function will receive as first parameter an array with
* the raw data from the database for every row that is fetched and must return the
* row with any possible modifications.
*
* Callbacks will be executed lazily, if only 3 rows are fetched for database it will
* be called 3 times, event though there might be more rows to be fetched in the cursor.
*
* Callbacks are stacked in the order they are registered, if you wish to reset the stack
* the call this function with the second parameter set to true.
*
* If you wish to remove all decorators from the stack, set the first parameter
* to null and the second to true.
*
* ### Example
*
* ```
* $query->decorateResults(function ($row) {
* $row['order_total'] = $row['subtotal'] + ($row['subtotal'] * $row['tax']);
* return $row;
* });
* ```
*
* @param \Closure|null $callback The callback to invoke when results are fetched.
* @param bool $overwrite Whether this should append or replace all existing decorators.
* @return $this
*/
public function decorateResults(?Closure $callback, bool $overwrite = false)
{
$this->_dirty();
if ($overwrite) {
$this->_resultDecorators = [];
}
if ($callback !== null) {
$this->_resultDecorators[] = $callback;
}
return $this;
}
/**
* Get result decorators.
*
* @return array
*/
public function getResultDecorators(): array
{
return $this->_resultDecorators;
}
/**
* Enables buffered results.
*
* When enabled the results returned by this query will be
* buffered. This enables you to iterate a result set multiple times, or
* both cache and iterate it.
*
* When disabled it will consume less memory as fetched results are not
* remembered for future iterations.
*
* @return $this
*/
public function enableBufferedResults()
{
$this->_dirty();
$this->bufferedResults = true;
return $this;
}
/**
* Disables buffered results.
*
* Disabling buffering will consume less memory as fetched results are not
* remembered for future iterations.
*
* @return $this
*/
public function disableBufferedResults()
{
$this->_dirty();
$this->bufferedResults = false;
return $this;
}
/**
* Returns whether buffered results are enabled/disabled.
*
* When enabled the results returned by this query will be
* buffered. This enables you to iterate a result set multiple times, or
* both cache and iterate it.
*
* When disabled it will consume less memory as fetched results are not
* remembered for future iterations.
*
* @return bool
*/
public function isBufferedResultsEnabled(): bool
{
return $this->bufferedResults;
}
/**
* Sets the TypeMap class where the types for each of the fields in the
* select clause are stored.
*
* @param \Cake\Database\TypeMap|array $typeMap Creates a TypeMap if array, otherwise sets the given TypeMap.
* @return $this
*/
public function setSelectTypeMap(TypeMap|array $typeMap)
{
$this->_selectTypeMap = is_array($typeMap) ? new TypeMap($typeMap) : $typeMap;
$this->_dirty();
return $this;
}
/**
* Gets the TypeMap class where the types for each of the fields in the
* select clause are stored.
*
* @return \Cake\Database\TypeMap
*/
public function getSelectTypeMap(): TypeMap
{
return $this->_selectTypeMap ??= new TypeMap();
}
/**
* Disables result casting.
*
* When disabled, the fields will be returned as received from the database
* driver (which in most environments means they are being returned as
* strings), which can improve performance with larger datasets.
*
* @return $this
*/
public function disableResultsCasting()
{
$this->typeCastEnabled = false;
return $this;
}
/**
* Enables result casting.
*
* When enabled, the fields in the results returned by this Query will be
* cast to their corresponding PHP data type.
*
* @return $this
*/
public function enableResultsCasting()
{
$this->typeCastEnabled = true;
return $this;
}
/**
* Returns whether result casting is enabled/disabled.
*
* When enabled, the fields in the results returned by this Query will be
* casted to their corresponding PHP data type.
*
* When disabled, the fields will be returned as received from the database
* driver (which in most environments means they are being returned as
* strings), which can improve performance with larger datasets.
*
* @return bool
*/
public function isResultsCastingEnabled(): bool
{
return $this->typeCastEnabled;
}
/**
* Handles clearing iterator and cloning all expressions and value binders.
*
* @return void
*/
public function __clone()
{
parent::__clone();
$this->_results = null;
if ($this->_selectTypeMap !== null) {
$this->_selectTypeMap = clone $this->_selectTypeMap;
}
}
/**
* Returns an array that can be used to describe the internal state of this
* object.
*
* @return array<string, mixed>
*/
public function __debugInfo(): array
{
$return = parent::__debugInfo();
$return['decorators'] = count($this->_resultDecorators);
return $return;
}
/**
* Sets the connection role.
*
* @param string $role Connection role ('read' or 'write')
* @return $this
*/
public function setConnectionRole(string $role)
{
assert($role === Connection::ROLE_READ || $role === Connection::ROLE_WRITE);
$this->connectionRole = $role;
return $this;
}
/**
* Sets the connection role to read.
*
* @return $this
*/
public function useReadRole()
{
return $this->setConnectionRole(Connection::ROLE_READ);
}
/**
* Sets the connection role to write.
*
* @return $this
*/
public function useWriteRole()
{
return $this->setConnectionRole(Connection::ROLE_WRITE);
}
}
+150
View File
@@ -0,0 +1,150 @@
<?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
*/
namespace Cake\Database\Query;
use Cake\Database\Expression\ComparisonExpression;
use Cake\Database\Expression\QueryExpression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Closure;
/**
* This class is used to generate UPDATE queries for the relational database.
*/
class UpdateQuery extends Query
{
/**
* Type of this query.
*
* @var string
*/
protected string $_type = self::TYPE_UPDATE;
/**
* List of SQL parts that will be used to build this query.
*
* @var array<string, mixed>
*/
protected array $_parts = [
'comment' => null,
'with' => [],
'update' => [],
'optimizerHint' => [],
'modifier' => [],
'join' => [],
'set' => [],
'where' => null,
'order' => null,
'limit' => null,
'epilog' => null,
];
/**
* Create an update query.
*
* Can be combined with set() and where() methods to create update queries.
*
* @param \Cake\Database\ExpressionInterface|string $table The table you want to update.
* @return $this
*/
public function update(ExpressionInterface|string $table)
{
$this->_dirty();
$this->_parts['update'][0] = $table;
return $this;
}
/**
* Set one or many fields to update.
*
* ### Examples
*
* Passing a string:
*
* ```
* $query->update('articles')->set('title', 'The Title');
* ```
*
* Passing an array:
*
* ```
* $query->update('articles')->set(['title' => 'The Title'], ['title' => 'string']);
* ```
*
* Passing a callback:
*
* ```
* $query->update('articles')->set(function (ExpressionInterface $exp) {
* return $exp->eq('title', 'The title', 'string');
* });
* ```
*
* @param \Cake\Database\Expression\QueryExpression|\Closure|array|string $key The column name or array of keys
* + values to set. This can also be a QueryExpression containing a SQL fragment.
* It can also be a Closure, that is required to return an expression object.
* @param mixed $value The value to update $key to. Can be null if $key is an
* array or QueryExpression. When $key is an array, this parameter will be
* used as $types instead.
* @param array<string, string>|string $types The column types to treat data as.
* @return $this
*/
public function set(QueryExpression|Closure|array|string $key, mixed $value = null, array|string $types = [])
{
if (empty($this->_parts['set'])) {
$this->_parts['set'] = $this->expr()->setConjunction(',');
}
if ($key instanceof Closure) {
$exp = $this->expr()->setConjunction(',');
/** @var \Cake\Database\Expression\QueryExpression $setExpr */
$setExpr = $this->_parts['set'];
$setExpr->add($key($exp));
return $this;
}
if (is_array($key) && !isset($key[0])) {
$typeMap = $this->getTypeMap()->setTypes($value ?? []);
/** @var \Cake\Database\Expression\QueryExpression $setExpr */
$setExpr = $this->_parts['set'];
foreach ($key as $k => $v) {
$setExpr->add(new ComparisonExpression($k, $v, $typeMap->type($k)));
}
return $this;
}
if (is_array($key) || $key instanceof ExpressionInterface) {
$types = (array)$value;
/** @var \Cake\Database\Expression\QueryExpression $setExpr */
$setExpr = $this->_parts['set'];
$setExpr->add($key, $types);
return $this;
}
if (!is_string($types)) {
$types = null;
}
/** @var \Cake\Database\Expression\QueryExpression $setExpr */
$setExpr = $this->_parts['set'];
$setExpr->eq($key, $value, $types);
return $this;
}
}
+524
View File
@@ -0,0 +1,524 @@
<?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\Database\Exception\DatabaseException;
use Closure;
use Countable;
/**
* Responsible for compiling a Query object into its SQL representation
*
* @internal
*/
class QueryCompiler
{
/**
* List of sprintf templates that will be used for compiling the SQL for
* this query. There are some clauses that can be built as just as the
* direct concatenation of the internal parts, those are listed here.
*
* @var array<string, string>
*/
protected array $_templates = [
'delete' => 'DELETE',
'where' => ' WHERE %s',
'group' => ' GROUP BY %s ',
'having' => ' HAVING %s ',
'order' => ' %s',
'limit' => ' LIMIT %s',
'offset' => ' OFFSET %s',
'epilog' => ' %s',
'comment' => '/* %s */ ',
];
/**
* The list of query clauses to traverse for generating a SELECT statement
*
* @var array<string>
*/
protected array $_selectParts = [
'comment', 'with', 'select', 'from', 'join', 'where', 'group', 'having', 'window', 'order',
'limit', 'offset', 'union', 'epilog', 'intersect',
];
/**
* The list of query clauses to traverse for generating an UPDATE statement
*
* @var array<string>
*/
protected array $_updateParts = ['comment', 'with', 'update', 'set', 'where', 'epilog'];
/**
* The list of query clauses to traverse for generating a DELETE statement
*
* @var array<string>
*/
protected array $_deleteParts = ['comment', 'with', 'delete', 'optimizerHint', 'modifier', 'from', 'where',
'epilog'];
/**
* The list of query clauses to traverse for generating an INSERT statement
*
* @var array<string>
*/
protected array $_insertParts = ['comment', 'with', 'insert', 'values', 'epilog'];
/**
* Indicate whether aliases in SELECT clause need to be always quoted.
*
* @var bool
*/
protected bool $_quotedSelectAliases = false;
/**
* Returns the SQL representation of the provided query after generating
* the placeholders for the bound values using the provided generator
*
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholders
* @return string
*/
public function compile(Query $query, ValueBinder $binder): string
{
$sql = '';
$type = $query->type();
$query->traverseParts(
$this->_sqlCompiler($sql, $query, $binder),
$this->{"_{$type}Parts"},
);
// Propagate bound parameters from sub-queries if the
// placeholders can be found in the SQL statement. Only
// add new placeholders, as sub-queries may have been executed already.
if ($query->getValueBinder() !== $binder) {
$existing = $binder->bindings();
foreach ($query->getValueBinder()->bindings() as $binding) {
$placeholder = ':' . $binding['placeholder'];
if (!isset($existing[$placeholder]) && preg_match('/' . $placeholder . '(?:\W|$)/', $sql) > 0) {
$binder->bind($placeholder, $binding['value'], $binding['type']);
}
}
}
return $sql;
}
/**
* Returns a closure that can be used to compile a SQL string representation
* of this query.
*
* @param string $sql initial sql string to append to
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
* @return \Closure
*/
protected function _sqlCompiler(string &$sql, Query $query, ValueBinder $binder): Closure
{
return function ($part, $partName) use (&$sql, $query, $binder): void {
if (
$part === null ||
($part === []) ||
($part instanceof Countable && count($part) === 0)
) {
return;
}
if ($part instanceof ExpressionInterface) {
$part = [$part->sql($binder)];
}
if (isset($this->_templates[$partName])) {
$part = $this->_stringifyExpressions((array)$part, $binder);
$sql .= sprintf($this->_templates[$partName], implode(', ', $part));
return;
}
$sql .= $this->{'_build' . $partName . 'Part'}($part, $query, $binder);
};
}
/**
* Helper function used to build the string representation of a `WITH` clause,
* it constructs the CTE definitions list and generates the `RECURSIVE`
* keyword when required.
*
* @param array<\Cake\Database\Expression\CommonTableExpression> $parts List of CTEs to be transformed to string
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
* @return string
*/
protected function _buildWithPart(array $parts, Query $query, ValueBinder $binder): string
{
$recursive = false;
$expressions = [];
foreach ($parts as $cte) {
$recursive = $recursive || $cte->isRecursive();
$expressions[] = $cte->sql($binder);
}
$recursive = $recursive ? 'RECURSIVE ' : '';
return sprintf('WITH %s%s ', $recursive, implode(', ', $expressions));
}
/**
* Helper function used to build the string representation of a SELECT clause,
* it constructs the field list taking care of aliasing and
* converting expression objects to string. This function also constructs the
* DISTINCT clause for the query.
*
* @param array $parts list of fields to be transformed to string
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
* @return string
*/
protected function _buildSelectPart(array $parts, Query $query, ValueBinder $binder): string
{
$driver = $query->getDriver();
$select = 'SELECT%s%s %s%s';
if (
($query->clause('union') || $query->clause('intersect')) &&
$driver->supports(DriverFeatureEnum::SET_OPERATIONS_ORDER_BY)
) {
$select = '(SELECT%s%s %s%s';
}
$hint = $this->_buildOptimizerHintPart($query->clause('optimizerHint'), $query, $binder);
$modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder);
$quoteIdentifiers = $driver->isAutoQuotingEnabled() || $this->_quotedSelectAliases;
$normalized = [];
$parts = $this->_stringifyExpressions($parts, $binder);
foreach ($parts as $k => $p) {
if (!is_numeric($k)) {
$p .= ' AS ';
if ($quoteIdentifiers) {
$p .= $driver->quoteIdentifier($k);
} else {
$p .= $k;
}
}
$normalized[] = $p;
}
$distinct = $query->clause('distinct');
if ($distinct === true) {
$distinct = 'DISTINCT ';
} elseif (is_array($distinct)) {
$distinct = $this->_stringifyExpressions($distinct, $binder);
$distinct = sprintf('DISTINCT ON (%s) ', implode(', ', $distinct));
}
return sprintf($select, $hint, $modifiers, $distinct, implode(', ', $normalized));
}
/**
* Helper function used to build the string representation of a FROM clause,
* it constructs the tables list taking care of aliasing and
* converting expression objects to string.
*
* @param array $parts list of tables to be transformed to string
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
* @return string
*/
protected function _buildFromPart(array $parts, Query $query, ValueBinder $binder): string
{
$select = ' FROM %s';
$normalized = [];
$parts = $this->_stringifyExpressions($parts, $binder);
foreach ($parts as $k => $p) {
if (!is_numeric($k)) {
$p = $p . ' ' . $k;
}
$normalized[] = $p;
}
return sprintf($select, implode(', ', $normalized));
}
/**
* Helper function used to build the string representation of multiple JOIN clauses,
* it constructs the joins list taking care of aliasing and converting
* expression objects to string in both the table to be joined and the conditions
* to be used.
*
* @param array $parts list of joins to be transformed to string
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
* @return string
*/
protected function _buildJoinPart(array $parts, Query $query, ValueBinder $binder): string
{
$joins = '';
foreach ($parts as $join) {
if (!isset($join['table'])) {
throw new DatabaseException(sprintf(
'Could not compile join clause for alias `%s`. No table was specified. ' .
'Use the `table` key to define a table.',
$join['alias'],
));
}
if ($join['table'] instanceof ExpressionInterface) {
$join['table'] = '(' . $join['table']->sql($binder) . ')';
}
$joins .= sprintf(' %s JOIN %s %s', $join['type'], $join['table'], $join['alias']);
$condition = '';
if (isset($join['conditions']) && $join['conditions'] instanceof ExpressionInterface) {
$condition = $join['conditions']->sql($binder);
}
if ($condition === '') {
$joins .= ' ON 1 = 1';
} else {
$joins .= " ON {$condition}";
}
}
return $joins;
}
/**
* Helper function to build the string representation of a window clause.
*
* @param array $parts List of windows to be transformed to string
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
* @return string
*/
protected function _buildWindowPart(array $parts, Query $query, ValueBinder $binder): string
{
$windows = [];
foreach ($parts as $window) {
/** @var \Cake\Database\Expression\IdentifierExpression $expr */
$expr = $window['name'];
/** @var \Cake\Database\Expression\IdentifierExpression $windowExpr */
$windowExpr = $window['window'];
$windows[] = $expr->sql($binder) . ' AS (' . $windowExpr->sql($binder) . ')';
}
return ' WINDOW ' . implode(', ', $windows);
}
/**
* Helper function to generate SQL for SET expressions.
*
* @param array $parts List of keys and values to set.
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
* @return string
*/
protected function _buildSetPart(array $parts, Query $query, ValueBinder $binder): string
{
$set = [];
foreach ($parts as $part) {
if ($part instanceof ExpressionInterface) {
$part = $part->sql($binder);
}
if (str_starts_with($part, '(')) {
$part = substr($part, 1, -1);
}
$set[] = $part;
}
return ' SET ' . implode('', $set);
}
/**
* Builds the SQL string for all the `operation` clauses in this query, when dealing
* with query objects it will also transform them using their configured SQL
* dialect.
*
* @param string $operation
* @param array $parts
* @param \Cake\Database\Query $query
* @param \Cake\Database\ValueBinder $binder
* @return string
*/
protected function _buildSetOperationPart(
string $operation,
array $parts,
Query $query,
ValueBinder $binder,
): string {
$setOperationsOrderBy = $query
->getConnection()
->getDriver($query->getConnectionRole())
->supports(DriverFeatureEnum::SET_OPERATIONS_ORDER_BY);
$parts = array_map(function (array $p) use ($binder, $setOperationsOrderBy) {
/** @var \Cake\Database\Expression\IdentifierExpression $expr */
$expr = $p['query'];
$p['query'] = $expr->sql($binder);
$p['query'] = str_starts_with($p['query'], '(') ? trim($p['query'], '()') : $p['query'];
$prefix = $p['all'] ? 'ALL ' : '';
if ($setOperationsOrderBy) {
return "{$prefix}({$p['query']})";
}
return $prefix . $p['query'];
}, $parts);
if ($setOperationsOrderBy) {
return sprintf(")\n{$operation} %s", implode("\n{$operation} ", $parts));
}
return sprintf("\n{$operation} %s", implode("\n{$operation} ", $parts));
}
/**
* Builds the SQL string for all the INTERSECT clauses in this query, when dealing
* with query objects it will also transform them using their configured SQL
* dialect.
*
* @param array $parts list of queries to be operated with INTERSECT
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
* @return string
*/
protected function _buildIntersectPart(array $parts, Query $query, ValueBinder $binder): string
{
return $this->_buildSetOperationPart('INTERSECT', $parts, $query, $binder);
}
/**
* Builds the SQL string for all the UNION clauses in this query, when dealing
* with query objects it will also transform them using their configured SQL
* dialect.
*
* @param array $parts list of queries to be operated with UNION
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
* @return string
*/
protected function _buildUnionPart(array $parts, Query $query, ValueBinder $binder): string
{
return $this->_buildSetOperationPart('UNION', $parts, $query, $binder);
}
/**
* Builds the SQL fragment for INSERT INTO.
*
* @param array $parts The insert parts.
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
* @return string SQL fragment.
*/
protected function _buildInsertPart(array $parts, Query $query, ValueBinder $binder): string
{
if (!isset($parts[0])) {
throw new DatabaseException(
'Could not compile insert query. No table was specified. ' .
'Use `into()` to define a table.',
);
}
$table = $parts[0];
$columns = $this->_stringifyExpressions($parts[1], $binder);
$hint = $this->_buildOptimizerHintPart($query->clause('optimizerHint'), $query, $binder);
$modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder);
return sprintf('INSERT%s%s INTO %s (%s)', $hint, $modifiers, $table, implode(', ', $columns));
}
/**
* Builds the SQL fragment for INSERT INTO.
*
* @param array $parts The values parts.
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
* @return string SQL fragment.
*/
protected function _buildValuesPart(array $parts, Query $query, ValueBinder $binder): string
{
return implode('', $this->_stringifyExpressions($parts, $binder));
}
/**
* Builds the SQL fragment for UPDATE.
*
* @param array $parts The update parts.
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
* @return string SQL fragment.
*/
protected function _buildUpdatePart(array $parts, Query $query, ValueBinder $binder): string
{
$table = $this->_stringifyExpressions($parts, $binder);
$hint = $this->_buildOptimizerHintPart($query->clause('optimizerHint'), $query, $binder);
$modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder);
return sprintf('UPDATE%s%s %s', $hint, $modifiers, implode(',', $table));
}
/**
* Builds the optimizer hint comment part.
*
* @param list<string> $parts The optmizer hints
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
* @return string Optimizer hint comment
*/
protected function _buildOptimizerHintPart(array $parts, Query $query, ValueBinder $binder): string
{
if ($parts === [] || !$query->getDriver()->supports(DriverFeatureEnum::OPTIMIZER_HINT_COMMENT)) {
return '';
}
return sprintf(' /*+ %s */', implode(' ', $parts));
}
/**
* Builds the SQL modifier fragment
*
* @param array $parts The query modifier parts
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
* @return string SQL fragment.
*/
protected function _buildModifierPart(array $parts, Query $query, ValueBinder $binder): string
{
if ($parts === []) {
return '';
}
return ' ' . implode(' ', $this->_stringifyExpressions($parts, $binder, false));
}
/**
* Helper function used to covert ExpressionInterface objects inside an array
* into their string representation.
*
* @param array $expressions list of strings and ExpressionInterface objects
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
* @param bool $wrap Whether to wrap each expression object with parenthesis
* @return array
*/
protected function _stringifyExpressions(array $expressions, ValueBinder $binder, bool $wrap = true): array
{
$result = [];
foreach ($expressions as $k => $expression) {
if ($expression instanceof ExpressionInterface) {
$value = $expression->sql($binder);
$expression = $wrap ? '(' . $value . ')' : $value;
}
$result[$k] = $expression;
}
return $result;
}
}
+359
View File
@@ -0,0 +1,359 @@
[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/database.svg?style=flat-square)](https://packagist.org/packages/cakephp/database)
[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt)
# A flexible and lightweight Database Library for PHP
This library abstracts and provides help with most aspects of dealing with relational
databases such as keeping connections to the server, building queries,
preventing SQL injections, inspecting and altering schemas, and with debugging and
profiling queries sent to the database.
It adopts the API from the native PDO extension in PHP for familiarity, but solves many of the
inconsistencies PDO has, while also providing several features that extend PDO's capabilities.
A distinguishing factor of this library when compared to similar database connection packages,
is that it takes the concept of "data types" to its core. It lets you work with complex PHP objects
or structures that can be passed as query conditions or to be inserted in the database.
The typing system will intelligently convert the PHP structures when passing them to the database, and
convert them back when retrieving.
## Connecting to the database
This library is able to work with the following databases:
* MySQL
* Postgres
* SQLite
* Microsoft SQL Server (2008 and above)
The first thing you need to do when using this library is create a connection object.
Before performing any operations with the connection, you need to specify a driver
to use:
```php
use Cake\Database\Connection;
use Cake\Database\Driver\Mysql;
use Cake\Database\Driver\Sqlite;
$connection = new Connection([
'driver' => Mysql::class,
'database' => 'test',
'username' => 'root',
'password' => 'secret',
]);
$connection2 = new Connection([
'driver' => Sqlite::class,
'database' => '/path/to/file.db'
]);
```
Drivers are classes responsible for actually executing the commands to the database and
correctly building the SQL according to the database specific dialect.
### Connection options
This is a list of possible options that can be passed when creating a connection:
* `driver`: Driver class name
* `persistent`: Creates a persistent connection
* `host`: The server host
* `database`: The database name
* `username`: Login credential
* `password`: Connection secret
* `encoding`: The connection encoding (or charset)
* `timezone`: The connection timezone or time offset
## Using connections
After creating a connection, you can immediately interact with the database. You can choose
either to use the shorthand methods `execute()`, `insert()`, `update()`, `delete()` or use
one of `selectQuery()`, `updateQuery()`, `insertQuery()` or `deleteQuery()`
to get a query builder for particular type of query.
The easiest way of executing queries is by using the `execute()` method, it will return a
`Cake\Database\StatementInterface` that you can use to get the data back:
```php
$statement = $connection->execute('SELECT * FROM articles');
while($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
echo $row['title'] . PHP_EOL;
}
```
Binding values to parametrized arguments is also possible with the execute function:
```php
$statement = $connection->execute('SELECT * FROM articles WHERE id = :id', ['id' => 1], ['id' => 'integer']);
$results = $statement->fetch(\PDO::FETCH_ASSOC);
```
The third parameter is the types the passed values should be converted to when passed to the database. If
no types are passed, all arguments will be interpreted as a string.
Alternatively you can construct a statement manually and then fetch rows from it:
```php
$statement = $connection->prepare('SELECT * from articles WHERE id != :id');
$statement->bind(['id' => 1], ['id' => 'integer']);
$results = $statement->fetchAll(\PDO::FETCH_ASSOC);
```
The default types that are understood by this library and can be passed to the `bind()` function or to `execute()`
are:
* biginteger
* binary
* date
* float
* decimal
* integer
* time
* datetime
* timestamp
* uuid
More types can be added dynamically in a bit.
Statements can be reused by binding new values to the parameters in the query:
```php
$statement = $connection->prepare('SELECT * from articles WHERE id = :id');
$statement->bind(['id' => 1], ['id' => 'integer']);
$results = $statement->fetchAll(\PDO::FETCH_ASSOC);
$statement->bind(['id' => 1], ['id' => 'integer']);
$results = $statement->fetchAll(\PDO::FETCH_ASSOC);
```
### Updating Rows
Updating can be done using the `update()` function in the connection object. In the following
example we will update the title of the article with id = 1:
```php
$connection->update('articles', ['title' => 'New title'], ['id' => 1]);
```
The concept of data types is central to this library, so you can use the last parameter of the function
to specify what types should be used:
```php
$connection->update(
'articles',
['title' => 'New title'],
['created >=' => new DateTime('-3 day'), 'created <' => new DateTime('now')],
['created' => 'datetime']
);
```
The example above will execute the following SQL:
```sql
UPDATE articles SET title = 'New Title' WHERE created >= '2014-10-10 00:00:00' AND created < '2014-10-13 00:00:00';
```
More on creating complex where conditions or more complex update queries later.
### Deleting Rows
Similarly, the `delete()` method is used to delete rows from the database:
```php
$connection->delete('articles', ['created <' => DateTime('now')], ['created' => 'date']);
```
Will generate the following SQL
```sql
DELETE FROM articles where created < '2014-10-10'
```
### Inserting Rows
Rows can be inserted using the `insert()` method:
```php
$connection->insert(
'articles',
['title' => 'My Title', 'body' => 'Some paragraph', 'created' => new DateTime()],
['created' => 'datetime']
);
```
More complex updates, deletes and insert queries can be generated using the `Query` class.
## Query Builder
One of the goals of this library is to allow the generation of both simple and complex queries with
ease. The query builder can be accessed by getting a new instance of a query:
```php
$query = $connection->selectQuery();
```
### Selecting Fields
Adding fields to the `SELECT` clause:
```php
$query->select(['id', 'title', 'body']);
// Results in SELECT id AS pk, title AS aliased_title, body ...
$query->select(['pk' => 'id', 'aliased_title' => 'title', 'body']);
// Use a closure
$query->select(function ($query) {
return ['id', 'title', 'body'];
});
```
### Where Conditions
Generating conditions:
```php
// WHERE id = 1
$query->where(['id' => 1]);
// WHERE id > 1
$query->where(['id >' => 1]);
```
As you can see you can use any operator by placing it with a space after the field name.
Adding multiple conditions is easy as well:
```php
$query->where(['id >' => 1])->andWhere(['title' => 'My Title']);
// Equivalent to
$query->where(['id >' => 1, 'title' => 'My title']);
```
It is possible to generate `OR` conditions as well
```php
$query->where(['OR' => ['id >' => 1, 'title' => 'My title']]);
```
For even more complex conditions you can use closures and expression objects:
```php
$query->where(function (ExpressionInterface $exp) {
return $exp
->eq('author_id', 2)
->eq('published', true)
->notEq('spam', true)
->gt('view_count', 10);
});
```
Which results in:
```sql
SELECT * FROM articles
WHERE
author_id = 2
AND published = 1
AND spam != 1
AND view_count > 10
```
Combining expressions is also possible:
```php
$query->where(function (ExpressionInterface $exp) {
$orConditions = $exp->or(['author_id' => 2])
->eq('author_id', 5);
return $exp
->not($orConditions)
->lte('view_count', 10);
});
```
That generates:
```sql
SELECT *
FROM articles
WHERE
NOT (author_id = 2 OR author_id = 5)
AND view_count <= 10
```
When using the expression objects you can use the following methods to create conditions:
* `eq()` Creates an equality condition.
* `notEq()` Create an inequality condition
* `like()` Create a condition using the LIKE operator.
* `notLike()` Create a negated LIKE condition.
* `in()` Create a condition using IN.
* `notIn()` Create a negated condition using IN.
* `gt()` Create a > condition.
* `gte()` Create a >= condition.
* `lt()` Create a < condition.
* `lte()` Create a <= condition.
* `isNull()` Create an IS NULL condition.
* `isNotNull()` Create a negated IS NULL condition.
### Aggregates and SQL Functions
```php
// Results in SELECT COUNT(*) count FROM ...
$query->select(['count' => $query->func()->count('*')]);
```
A number of commonly used functions can be created with the func() method:
* `sum()` Calculate a sum. The arguments will be treated as literal values.
* `avg()` Calculate an average. The arguments will be treated as literal values.
* `min()` Calculate the min of a column. The arguments will be treated as literal values.
* `max()` Calculate the max of a column. The arguments will be treated as literal values.
* `count()` Calculate the count. The arguments will be treated as literal values.
* `concat()` Concatenate two values together. The arguments are treated as bound parameters unless marked as literal.
* `coalesce()` Coalesce values. The arguments are treated as bound parameters unless marked as literal.
* `dateDiff()` Get the difference between two dates/times. The arguments are treated as bound parameters unless marked as literal.
* `now()` Take either 'time' or 'date' as an argument allowing you to get either the current time, or current date.
When providing arguments for SQL functions, there are two kinds of parameters you can use, literal arguments and bound parameters. Literal
parameters allow you to reference columns or other SQL literals. Bound parameters can be used to safely add user data to SQL functions.
For example:
```php
$concat = $query->func()->concat([
'title' => 'literal',
' NEW'
]);
$query->select(['title' => $concat]);
```
The above generates:
```sql
SELECT CONCAT(title, :c0) ...;
```
### Other SQL Clauses
Read of all other SQL clauses that the builder is capable of generating in the [official API docs](https://api.cakephp.org/4.x/class-Cake.Database.Query.html)
### Getting Results out of a Query
Once youve made your query, youll want to retrieve rows from it. There are a few ways of doing this:
```php
// Iterate the query
foreach ($query as $row) {
// Do stuff.
}
// Get the statement and fetch all results
$results = $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
```
## Official API
You can read the official [official API docs](https://api.cakephp.org/5.x/namespace-Cake.Database.html) to learn more of what this library
has to offer.
@@ -0,0 +1,69 @@
<?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\Retry;
use Cake\Core\Retry\RetryStrategyInterface;
use Exception;
use PDOException;
/**
* Implements retry strategy based on db error codes and wait interval.
*
* @internal
*/
class ErrorCodeWaitStrategy implements RetryStrategyInterface
{
/**
* @var array<int>
*/
protected array $errorCodes;
/**
* @var int
*/
protected int $retryInterval;
/**
* @param array<int> $errorCodes DB-specific error codes that allow retrying
* @param int $retryInterval Seconds to wait before allowing next retry, 0 for no wait.
*/
public function __construct(array $errorCodes, int $retryInterval)
{
$this->errorCodes = $errorCodes;
$this->retryInterval = $retryInterval;
}
/**
* @inheritDoc
*/
public function shouldRetry(Exception $exception, int $retryCount): bool
{
if (
$exception instanceof PDOException &&
$exception->errorInfo &&
in_array($exception->errorInfo[1], $this->errorCodes)
) {
if ($this->retryInterval > 0) {
sleep($this->retryInterval);
}
return true;
}
return false;
}
}
@@ -0,0 +1,125 @@
<?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\Retry;
use Cake\Core\Retry\RetryStrategyInterface;
use Cake\Database\Connection;
use Exception;
/**
* Makes sure the connection to the database is alive before authorizing
* the retry of an action.
*
* @internal
*/
class ReconnectStrategy implements RetryStrategyInterface
{
/**
* The list of error strings to match when looking for a disconnection error.
*
* This is a static variable to enable opcache to inline the values.
*
* @var array<string>
*/
protected static array $causes = [
'gone away',
'Lost connection',
'Transaction() on null',
'closed the connection unexpectedly',
'closed unexpectedly',
'deadlock avoided',
'decryption failed or bad record mac',
'is dead or not enabled',
'no connection to the server',
'query_wait_timeout',
'reset by peer',
'terminate due to client_idle_limit',
'while sending',
'writing data to the connection',
];
/**
* The connection to check for validity
*
* @var \Cake\Database\Connection
*/
protected Connection $connection;
/**
* Creates the ReconnectStrategy object by storing a reference to the
* passed connection. This reference will be used to automatically
* reconnect to the server in case of failure.
*
* @param \Cake\Database\Connection $connection The connection to check
*/
public function __construct(Connection $connection)
{
$this->connection = $connection;
}
/**
* {@inheritDoc}
*
* Checks whether the exception was caused by a lost connection,
* and returns true if it was able to successfully reconnect.
*/
public function shouldRetry(Exception $exception, int $retryCount): bool
{
$message = $exception->getMessage();
foreach (static::$causes as $cause) {
if (str_contains($message, $cause)) {
return $this->reconnect();
}
}
return false;
}
/**
* Tries to re-establish the connection to the server, if it is safe to do so
*
* @return bool Whether the connection was re-established
*/
protected function reconnect(): bool
{
if ($this->connection->inTransaction()) {
// It is not safe to blindly reconnect in the middle of a transaction
return false;
}
try {
// Make sure we free any resources associated with the old connection
$this->connection->getDriver()->disconnect();
} catch (Exception) {
}
try {
$this->connection->getDriver()->connect();
$this->connection->getDriver()->log(
'connection={connection} [RECONNECT]',
['connection' => $this->connection->configName()],
);
return true;
} catch (Exception) {
// If there was an error connecting again, don't report it back,
// let the retry handler do it.
return false;
}
}
}
@@ -0,0 +1,146 @@
<?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\Schema;
use Psr\SimpleCache\CacheInterface;
/**
* Decorates a schema collection and adds caching
*/
class CachedCollection implements CollectionInterface
{
/**
* Cacher instance.
*
* @var \Psr\SimpleCache\CacheInterface
*/
protected CacheInterface $cacher;
/**
* The decorated schema collection
*
* @var \Cake\Database\Schema\CollectionInterface
*/
protected CollectionInterface $collection;
/**
* The cache key prefix
*
* @var string
*/
protected string $prefix;
/**
* Constructor.
*
* @param \Cake\Database\Schema\CollectionInterface $collection The collection to wrap.
* @param string $prefix The cache key prefix to use. Typically the connection name.
* @param \Psr\SimpleCache\CacheInterface $cacher Cacher instance.
*/
public function __construct(CollectionInterface $collection, string $prefix, CacheInterface $cacher)
{
$this->collection = $collection;
$this->prefix = $prefix;
$this->cacher = $cacher;
}
/**
* @inheritDoc
*/
public function listTablesWithoutViews(): array
{
return $this->collection->listTablesWithoutViews();
}
/**
* @inheritDoc
*/
public function listTables(): array
{
return $this->collection->listTables();
}
/**
* Get the column metadata for a table.
*
* The name can include a database schema name in the form 'schema.table'.
*
* Caching will be applied if `cacheMetadata` key is present in the Connection
* configuration options. Defaults to _cake_model_ when true.
*
* ### Options
*
* - `forceRefresh` - Set to true to force rebuilding the cached metadata.
* Defaults to false.
*
* @param string $name The name of the table to describe.
* @param array<string, mixed> $options The options to use, see above.
* @return \Cake\Database\Schema\TableSchemaInterface Object with column metadata.
* @throws \Cake\Database\Exception\DatabaseException when table cannot be described.
*/
public function describe(string $name, array $options = []): TableSchemaInterface
{
$options += ['forceRefresh' => false];
$cacheKey = $this->cacheKey($name);
if (!$options['forceRefresh']) {
$cached = $this->cacher->get($cacheKey);
if ($cached !== null) {
return $cached;
}
}
$table = $this->collection->describe($name, $options);
$this->cacher->set($cacheKey, $table);
return $table;
}
/**
* Get the cache key for a given name.
*
* @param string $name The name to get a cache key for.
* @return string The cache key.
*/
public function cacheKey(string $name): string
{
return $this->prefix . '_' . $name;
}
/**
* Set a cacher.
*
* @param \Psr\SimpleCache\CacheInterface $cacher Cacher object
* @return $this
*/
public function setCacher(CacheInterface $cacher)
{
$this->cacher = $cacher;
return $this;
}
/**
* Get a cacher.
*
* @return \Psr\SimpleCache\CacheInterface $cacher Cacher object
*/
public function getCacher(): CacheInterface
{
return $this->cacher;
}
}
@@ -0,0 +1,84 @@
<?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 5.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
use InvalidArgumentException;
/**
* Check constraint value object
*
* Models a check constraint.
*/
class CheckConstraint extends Constraint
{
protected string $type = self::CHECK;
/**
* Constructor
*
* @param string $name Constraint name.
* @param string $expression The check constraint expression (e.g., "age >= 18")
*/
public function __construct(
protected string $name,
protected string $expression,
) {
}
/**
* Set the check constraint expression.
*
* @param string $expression The SQL expression for the check constraint
* @return $this
* @throws \InvalidArgumentException
*/
public function setExpression(string $expression)
{
if (trim($expression) === '') {
throw new InvalidArgumentException('Check constraint expression cannot be empty');
}
$this->expression = trim($expression);
return $this;
}
/**
* Get the check constraint expression.
*
* @return string
*/
public function getExpression(): string
{
return $this->expression;
}
/**
* Converts a constraint to an array that is compatible
* with the constructor.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'name' => $this->name,
'type' => $this->type,
'expression' => $this->expression,
];
}
}
+99
View File
@@ -0,0 +1,99 @@
<?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\Schema;
use Cake\Database\Connection;
/**
* Represents a database schema collection
*
* Gives a simple high-level schema reflection API that can be
* decorated or extended with additional behavior like caching.
*
* @see \Cake\Database\Schema\SchemaDialect For lower level schema reflection API
*/
class Collection implements CollectionInterface
{
/**
* Connection object
*
* @var \Cake\Database\Connection
*/
protected Connection $_connection;
/**
* Schema dialect instance.
*
* @var \Cake\Database\Schema\SchemaDialect|null
*/
protected ?SchemaDialect $_dialect = null;
/**
* Constructor.
*
* @param \Cake\Database\Connection $connection The connection instance.
*/
public function __construct(Connection $connection)
{
$this->_connection = $connection;
}
/**
* Get the list of tables, excluding any views, available in the current connection.
*
* @return array<string> The list of tables in the connected database/schema.
*/
public function listTablesWithoutViews(): array
{
return $this->getDialect()->listTablesWithoutViews();
}
/**
* Get the list of tables and views available in the current connection.
*
* @return array<string> The list of tables and views in the connected database/schema.
*/
public function listTables(): array
{
return $this->getDialect()->listTables();
}
/**
* Get the column metadata for a table.
*
* The name can include a database schema name in the form 'schema.table'.
*
* @param string $name The name of the table to describe.
* @param array<string, mixed> $options Unused
* @return \Cake\Database\Schema\TableSchemaInterface Object with column metadata.
* @throws \Cake\Database\Exception\DatabaseException when table cannot be described.
*/
public function describe(string $name, array $options = []): TableSchemaInterface
{
return $this->getDialect()->describe($name);
}
/**
* Setups the schema dialect to be used for this collection.
*
* @return \Cake\Database\Schema\SchemaDialect
*/
protected function getDialect(): SchemaDialect
{
return $this->_dialect ??= $this->_connection->getWriteDriver()->schemaDialect();
}
}
@@ -0,0 +1,54 @@
<?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\Schema;
/**
* Represents a database schema collection
*
* Used to access information about the tables,
* and other data in a database.
*
* @method array<string> listTablesWithoutViews() Get the list of tables available in the current connection.
* This will exclude any views in the schema.
*/
interface CollectionInterface
{
/**
* Get the list of tables available in the current connection.
*
* @return array<string> The list of tables in the connected database/schema.
*/
public function listTables(): array;
/**
* Get the column metadata for a table.
*
* Caching will be applied if `cacheMetadata` key is present in the Connection
* configuration options. Defaults to _cake_model_ when true.
*
* ### Options
*
* - `forceRefresh` - Set to true to force rebuilding the cached metadata.
* Defaults to false.
*
* @param string $name The name of the table to describe.
* @param array<string, mixed> $options The options to use, see above.
* @return \Cake\Database\Schema\TableSchemaInterface Object with column metadata.
* @throws \Cake\Database\Exception\DatabaseException when table cannot be described.
*/
public function describe(string $name, array $options = []): TableSchemaInterface;
}
+595
View File
@@ -0,0 +1,595 @@
<?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)
* @copyright Copyright (c) Cake Software Foundation, Inc.
* (https://github.com/cakephp/migrations/tree/master/LICENSE.txt)
* @link https://cakephp.org CakePHP(tm) Project
* @since 5.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
use Cake\Database\TypeFactory;
use RuntimeException;
/**
* Schema metadata for a single column
*
* Used by `TableSchema` when reflecting schema or creating tables.
*/
class Column
{
/**
* Constructor.
*
* @param string $name Name of the column
* @param string $type Type of the column
* @param bool $null Whether the column allows null values
* @param mixed $default Default value for the column
* @param int|null $length Length of the column
* @param bool $identity Whether the column is an identity column
* @param string|null $generated Postgres identity option (always|default)
* @param int|null $precision Precision for decimal or float columns
* @param int|null $increment Increment for identity columns
* @param string|null $after Name of the column to add this column after
* @param string|null $onUpdate MySQL 'ON UPDATE' function
* @param string|null $comment Comment for the column
* @param bool|null $unsigned Whether the column is unsigned
* @param string|null $collate Collation for the column
* @param int|null $srid SRID for geometry fields
* @param string|null $baseType The basic schema type if the column type is a complex/custom type.
*/
public function __construct(
protected string $name,
protected string $type,
protected ?bool $null = null,
protected mixed $default = null,
protected ?int $length = null,
protected bool $identity = false,
protected ?string $generated = null,
protected ?int $precision = null,
protected ?int $increment = null,
protected ?string $after = null,
protected ?string $onUpdate = null,
protected ?string $comment = null,
protected ?bool $unsigned = null,
protected ?string $collate = null,
protected ?int $srid = null,
protected ?string $baseType = null,
) {
}
/**
* Sets the column name.
*
* @param string $name Name
* @return $this
*/
public function setName(string $name)
{
$this->name = $name;
return $this;
}
/**
* Gets the column name.
*
* @return string|null
*/
public function getName(): ?string
{
return $this->name;
}
/**
* Get the base type if defined. Will fallback to `type` if not set.
*
* Used to get the base type of a column when the column type is a complex/custom type.
*
* @return string|null
*/
public function getBaseType(): ?string
{
if (isset($this->baseType)) {
return $this->baseType;
}
$type = $this->type;
if (TypeFactory::getMapped($type)) {
$type = TypeFactory::build($type)->getBaseType();
}
return $this->baseType = $type;
}
/**
* Sets the base type of the column.
*
* Used to set the base type of a column when the column type is a complex/custom type.
*
* @param string|null $baseType Base type
* @return $this
*/
public function setBaseType(?string $baseType)
{
$this->baseType = $baseType;
return $this;
}
/**
* Sets the column type.
*
* Type names are not validated, as drivers and dialects may implement
* platform specific types that are not known by cakephp.
*
* Drivers are expected to handle unknown types gracefully.
*
* @param string $type Column type
* @return $this
*/
public function setType(string $type)
{
$this->type = $type;
return $this;
}
/**
* Gets the column type.
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Sets the column length.
*
* @param int|null $length Length
* @return $this
*/
public function setLength(?int $length)
{
$this->length = $length;
return $this;
}
/**
* Gets the column length.
*
* @return int|null
*/
public function getLength(): ?int
{
return $this->length;
}
/**
* Sets whether the column allows nulls.
*
* @param bool $null Null
* @return $this
*/
public function setNull(bool $null)
{
$this->null = $null;
return $this;
}
/**
* Gets whether the column allows nulls.
*
* @return bool|null
*/
public function getNull(): ?bool
{
return $this->null;
}
/**
* Does the column allow nulls?
*
* @return bool
*/
public function isNull(): bool
{
return $this->getNull() === true;
}
/**
* Sets the default column value.
*
* @param mixed $default Default
* @return $this
*/
public function setDefault(mixed $default)
{
$this->default = $default;
return $this;
}
/**
* Gets the default column value.
*
* @return mixed
*/
public function getDefault(): mixed
{
return $this->default;
}
/**
* Sets generated option for identity columns. Ignored otherwise.
*
* @param string|null $generated Generated option
* @return $this
*/
public function setGenerated(?string $generated)
{
$this->generated = $generated;
return $this;
}
/**
* Gets generated option for identity columns. Null otherwise
*
* @return string|null
*/
public function getGenerated(): ?string
{
return $this->generated;
}
/**
* Sets whether the column is an identity column.
*
* @param bool $identity Identity
* @return $this
*/
public function setIdentity(bool $identity)
{
$this->identity = $identity;
return $this;
}
/**
* Gets whether the column is an identity column.
*
* @return bool
*/
public function getIdentity(): bool
{
return $this->identity;
}
/**
* Is the column an identity column?
*
* @return bool
*/
public function isIdentity(): bool
{
return $this->getIdentity();
}
/**
* Sets the name of the column to add this column after.
*
* @param string $after After
* @return $this
*/
public function setAfter(string $after)
{
$this->after = $after;
return $this;
}
/**
* Returns the name of the column to add this column after.
*
* Used by MySQL and MariaDB in ALTER TABLE statements.
*
* @return string|null
*/
public function getAfter(): ?string
{
return $this->after;
}
/**
* Sets the 'ON UPDATE' mysql column function.
*
* Used by MySQL and MariaDB in ALTER TABLE statements.
*
* @param string $update On Update function
* @return $this
*/
public function setOnUpdate(string $update)
{
$this->onUpdate = $update;
return $this;
}
/**
* Returns the value of the ON UPDATE column function.
*
* @return string|null
*/
public function getOnUpdate(): ?string
{
return $this->onUpdate;
}
/**
* Sets the number precision for decimal or float column.
*
* For example `DECIMAL(5,2)`, 5 is the length and 2 is the precision,
* and the column could store value from -999.99 to 999.99.
*
* @param int|null $precision Number precision
* @return $this
*/
public function setPrecision(?int $precision)
{
$this->precision = $precision;
return $this;
}
/**
* Gets the number precision for decimal or float column.
*
* For example `DECIMAL(5,2)`, 5 is the length and 2 is the precision,
* and the column could store value from -999.99 to 999.99.
*
* @return int|null
*/
public function getPrecision(): ?int
{
return $this->precision;
}
/**
* Sets the column identity increment.
*
* @param int $increment Number increment
* @return $this
*/
public function setIncrement(int $increment)
{
$this->increment = $increment;
return $this;
}
/**
* Gets the column identity increment.
*
* @return int|null
*/
public function getIncrement(): ?int
{
return $this->increment;
}
/**
* Sets the column comment.
*
* @param string|null $comment Comment
* @return $this
*/
public function setComment(?string $comment)
{
$this->comment = $comment;
return $this;
}
/**
* Gets the column comment.
*
* @return string
*/
public function getComment(): ?string
{
return $this->comment;
}
/**
* Sets whether field should be unsigned.
*
* @param bool $unsigned Signed
* @return $this
*/
public function setUnsigned(bool $unsigned)
{
$this->unsigned = $unsigned;
return $this;
}
/**
* Gets whether field should be unsigned.
*
* @return bool|null
*/
public function getUnsigned(): ?bool
{
return $this->unsigned;
}
/**
* Should the column be signed?
*
* @return bool
*/
public function isSigned(): bool
{
return !$this->getUnsigned();
}
/**
* Should the column be unsigned?
*
* @return bool
*/
public function isUnsigned(): bool
{
return $this->getUnsigned() === true;
}
/**
* Sets the column collation.
*
* @param string $collation Collation
* @return $this
*/
public function setCollate(string $collation)
{
$this->collate = $collation;
return $this;
}
/**
* Gets the column collation.
*
* @return string|null
*/
public function getCollate(): ?string
{
return $this->collate;
}
/**
* Sets the column SRID for geometry fields.
*
* @param int $srid SRID
* @return $this
*/
public function setSrid(int $srid)
{
$this->srid = $srid;
return $this;
}
/**
* Gets the column SRID from geometry fields.
*
* @return int|null
*/
public function getSrid(): ?int
{
return $this->srid;
}
/**
* Gets all allowed options. Each option must have a corresponding `setFoo` method.
*
* @return array
*/
protected function getValidOptions(): array
{
return [
'name',
'length',
'precision',
'default',
'null',
'identity',
'after',
'onUpdate',
'comment',
'unsigned',
'type',
'properties',
'collate',
'srid',
'increment',
'generated',
];
}
/**
* Utility method that maps an array of column attributes to this object's methods.
*
* @param array<string, mixed> $attributes Attributes
* @throws \RuntimeException
* @return $this
*/
public function setAttributes(array $attributes)
{
$validOptions = $this->getValidOptions();
if (isset($attributes['identity']) && $attributes['identity'] && !isset($attributes['null'])) {
$attributes['null'] = false;
}
foreach ($attributes as $attribute => $value) {
if (!in_array($attribute, $validOptions, true)) {
throw new RuntimeException(sprintf('"%s" is not a valid column option.', $attribute));
}
$method = 'set' . ucfirst($attribute);
$this->$method($value);
}
return $this;
}
/**
* Convert an index into an array that is compatible with the Column constructor.
*
* @return array
*/
public function toArray(): array
{
$type = $this->getType();
$length = $this->getLength();
$precision = $this->getPrecision();
if ($precision !== null && $precision > 0) {
if ($type === TableSchemaInterface::TYPE_TIMESTAMP) {
$type = 'timestampfractional';
} elseif ($type === TableSchemaInterface::TYPE_DATETIME) {
$type = 'datetimefractional';
}
}
return [
'name' => $this->getName(),
'baseType' => $this->getBaseType(),
'type' => $type,
'length' => $length,
'null' => $this->getNull(),
'default' => $this->getDefault(),
'generated' => $this->getGenerated(),
'unsigned' => $this->getUnsigned(),
'onUpdate' => $this->getOnUpdate(),
'collate' => $this->getCollate(),
'precision' => $precision,
'srid' => $this->getSrid(),
'comment' => $this->getComment(),
'autoIncrement' => $this->getIdentity(),
'identity' => $this->getIdentity(),
];
}
}
+145
View File
@@ -0,0 +1,145 @@
<?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)
* @copyright Copyright (c) Cake Software Foundation, Inc.
* (https://github.com/cakephp/migrations/tree/master/LICENSE.txt)
* @link https://cakephp.org CakePHP(tm) Project
* @since 5.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
/**
* Constraint base class object
*
* Models a database constraint like a unique or primary key.
*/
class Constraint
{
/**
* @var string
*/
public const PRIMARY = TableSchema::CONSTRAINT_PRIMARY;
/**
* @var string
*/
public const UNIQUE = TableSchema::CONSTRAINT_UNIQUE;
/**
* @var string
*/
public const FOREIGN = TableSchema::CONSTRAINT_FOREIGN;
/**
* @var string
*/
public const CHECK = TableSchema::CONSTRAINT_CHECK;
/**
* Constructor
*
* @param string $name The name of the constraint.
* @param array<string> $columns The columns to constraint.
* @param string $type The type of constraint, e.g. 'unique', 'primary'.
*/
public function __construct(
protected string $name,
protected array $columns,
protected string $type,
) {
}
/**
* Sets the constraint columns.
*
* @param array<string>|string $columns Columns
* @return $this
*/
public function setColumns(string|array $columns)
{
$this->columns = (array)$columns;
return $this;
}
/**
* Gets the constraint columns.
*
* @return ?array<string>
*/
public function getColumns(): ?array
{
return $this->columns;
}
/**
* Sets the constraint type.
*
* @param string $type Type
* @return $this
*/
public function setType(string $type)
{
$this->type = $type;
return $this;
}
/**
* Gets the constraint type.
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Sets the constraint name.
*
* @param string $name Name
* @return $this
*/
public function setName(string $name)
{
$this->name = $name;
return $this;
}
/**
* Gets the constraint name.
*
* @return ?string
*/
public function getName(): ?string
{
return $this->name;
}
/**
* Converts a constraint to an array that is compatible
* with the constructor.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'name' => $this->name,
'type' => $this->type,
'columns' => $this->columns,
];
}
}
+267
View File
@@ -0,0 +1,267 @@
<?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)
* @copyright Copyright (c) Cake Software Foundation, Inc.
* (https://github.com/cakephp/migrations/tree/master/LICENSE.txt)
* @link https://cakephp.org CakePHP(tm) Project
* @since 5.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
use InvalidArgumentException;
/**
* ForeignKey metadata object
*
* Models a database foreign key constraint
*/
class ForeignKey extends Constraint
{
public const CASCADE = 'cascade';
public const RESTRICT = 'restrict';
public const SET_NULL = 'setNull';
public const NO_ACTION = 'noAction';
public const SET_DEFAULT = 'setDefault';
public const DEFERRED = 'DEFERRABLE INITIALLY DEFERRED';
public const IMMEDIATE = 'DEFERRABLE INITIALLY IMMEDIATE';
public const NOT_DEFERRED = 'NOT DEFERRABLE';
/**
* An allow list of valid actions
*
* @var array<string>
*/
protected array $validActions = [
self::CASCADE,
self::RESTRICT,
self::SET_NULL,
self::NO_ACTION,
self::SET_DEFAULT,
];
/**
* The action to take when the referenced row is deleted.
*/
protected ?string $delete = null;
/**
* The action to take when the referenced row is updated.
*/
protected ?string $update = null;
/**
* @var string|null
*/
protected ?string $deferrable = null;
/**
* Constructor
*
* @param string $name The name of the index.
* @param array<string> $columns The columns to index.
* @param ?string $referencedTable The columns to index.
* @param array<string> $referencedColumns The columns in $referencedTable that this key references.
* @param ?string $delete The action to take when the referenced row is deleted.
* @param ?string $update The action to take when the referenced row is updated.
*/
public function __construct(
protected string $name,
protected array $columns,
protected ?string $referencedTable = null,
protected array $referencedColumns = [],
?string $delete = null,
?string $update = null,
?string $deferrable = null,
) {
$this->type = self::FOREIGN;
$this->delete = $this->normalizeAction($delete ?? self::NO_ACTION);
$this->update = $this->normalizeAction($update ?? self::NO_ACTION);
if ($deferrable) {
$this->deferrable = $this->normalizeDeferrable($deferrable);
}
}
/**
* Sets the foreign key referenced table.
*
* @param string $table The table this KEY is pointing to
* @return $this
*/
public function setReferencedTable(string $table)
{
$this->referencedTable = $table;
return $this;
}
/**
* Gets the foreign key referenced table.
*
* @return ?string
*/
public function getReferencedTable(): ?string
{
return $this->referencedTable;
}
/**
* Sets the foreign key referenced columns.
*
* @param array<string>|string $referencedColumns Referenced columns
* @return $this
*/
public function setReferencedColumns(array|string $referencedColumns)
{
$referencedColumns = is_string($referencedColumns) ? [$referencedColumns] : $referencedColumns;
$this->referencedColumns = $referencedColumns;
return $this;
}
/**
* Gets the foreign key referenced columns.
*
* @return array<string>
*/
public function getReferencedColumns(): array
{
return $this->referencedColumns;
}
/**
* Converts the foreign key to an array that is compatible
* with the constructor.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'name' => $this->name,
'type' => $this->type,
'columns' => $this->columns,
'referencedTable' => $this->referencedTable,
'referencedColumns' => $this->referencedColumns,
'delete' => $this->delete,
'update' => $this->update,
'deferrable' => $this->deferrable,
];
}
/**
* Sets ON DELETE action for the foreign key.
*
* @param string $delete On Delete
* @return $this
*/
public function setDelete(string $delete)
{
$this->delete = $this->normalizeAction($delete);
return $this;
}
/**
* Gets ON DELETE action for the foreign key.
*
* @return string|null
*/
public function getDelete(): ?string
{
return $this->delete;
}
/**
* Gets ON UPDATE action for the foreign key.
*
* @return string|null
*/
public function getUpdate(): ?string
{
return $this->update;
}
/**
* Sets ON UPDATE action for the foreign key.
*
* @param string $update On Update
* @return $this
*/
public function setUpdate(string $update)
{
$this->update = $this->normalizeAction($update);
return $this;
}
/**
* From passed value checks if it's correct and fixes if needed
*
* @param string $action Action
* @throws \InvalidArgumentException
* @return string
*/
protected function normalizeAction(string $action): string
{
if (in_array($action, $this->validActions, true)) {
return $action;
}
throw new InvalidArgumentException('Unknown action passed: ' . $action);
}
/**
* Sets deferrable mode for the foreign key.
*
* @param string $deferrable Constraint
* @return $this
*/
public function setDeferrable(string $deferrable)
{
$this->deferrable = $this->normalizeDeferrable($deferrable);
return $this;
}
/**
* Gets deferrable mode for the foreign key.
*/
public function getDeferrable(): ?string
{
return $this->deferrable;
}
/**
* From passed value checks if it's correct and fixes if needed
*
* @param string $deferrable Deferrable
* @throws \InvalidArgumentException
* @return string
*/
protected function normalizeDeferrable(string $deferrable): string
{
$mapping = [
'DEFERRED' => ForeignKey::DEFERRED,
'IMMEDIATE' => ForeignKey::IMMEDIATE,
'NOT DEFERRED' => ForeignKey::NOT_DEFERRED,
ForeignKey::DEFERRED => ForeignKey::DEFERRED,
ForeignKey::IMMEDIATE => ForeignKey::IMMEDIATE,
ForeignKey::NOT_DEFERRED => ForeignKey::NOT_DEFERRED,
];
$normalized = strtoupper(str_replace('_', ' ', $deferrable));
if (array_key_exists($normalized, $mapping)) {
return $mapping[$normalized];
}
throw new InvalidArgumentException('Unknown deferrable passed: ' . $deferrable);
}
}
+273
View File
@@ -0,0 +1,273 @@
<?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)
* @copyright Copyright (c) Cake Software Foundation, Inc.
* (https://github.com/cakephp/migrations/tree/master/LICENSE.txt)
* @link https://cakephp.org CakePHP(tm) Project
* @since 5.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
use RuntimeException;
/**
* Index value object
*
* Models a database index and its attributes.
*/
class Index
{
// TODO change the direction of these.
/**
* @var string
*/
public const INDEX = 'index';
/**
* @var string
*/
public const FULLTEXT = 'fulltext';
/**
* Constructor
*
* @param string $name The name of the index.
* @param array<string> $columns The columns to index.
* @param string $type The type of index, e.g. 'index', 'fulltext'.
* @param array<string, int>|int|null $length The length of the index.
* @param array<string>|null $order The sort order of the index columns.
* @param array<string>|null $include The included columns for covering indexes.
* @param ?string $where The where clause for partial indexes.
*/
public function __construct(
protected string $name,
protected array $columns,
protected string $type = self::INDEX,
protected array|int|null $length = null,
protected ?array $order = null,
protected ?array $include = null,
protected ?string $where = null,
) {
}
/**
* Sets the index columns.
*
* @param array<string>|string $columns Columns
* @return $this
*/
public function setColumns(string|array $columns)
{
$this->columns = (array)$columns;
return $this;
}
/**
* Gets the index columns.
*
* @return ?array<string>
*/
public function getColumns(): ?array
{
return $this->columns;
}
/**
* Sets the index type.
*
* @param string $type Type
* @return $this
*/
public function setType(string $type)
{
$this->type = $type;
return $this;
}
/**
* Gets the index type.
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Sets the index name.
*
* @param string $name Name
* @return $this
*/
public function setName(string $name)
{
$this->name = $name;
return $this;
}
/**
* Gets the index name.
*
* @return ?string
*/
public function getName(): ?string
{
return $this->name;
}
/**
* Sets the index length.
*
* In MySQL indexes can have limit clauses to control the number of
* characters indexed in text and char columns.
*
* @param array<string, int>|int $length length value or array of length value
* @return $this
*/
public function setLength(int|array $length)
{
$this->length = $length;
return $this;
}
/**
* Gets the index length.
*
* Can be an array of column names and lengths under MySQL.
*
* @return array<string, int>|int|null
*/
public function getLength(): array|int|null
{
return $this->length;
}
/**
* Sets the index columns sort order.
*
* @param array<string> $order column name sort order key value pair
* @return $this
*/
public function setOrder(array $order)
{
$this->order = $order;
return $this;
}
/**
* Gets the index columns sort order.
*
* @return ?array<string>
*/
public function getOrder(): ?array
{
return $this->order;
}
/**
* Sets the index included columns for a 'covering index'.
*
* In postgres and sqlserver, indexes can define additional non-key
* columns to build 'covering indexes'. This feature allows you to
* further optimize well-crafted queries that leverage specific
* indexes by reading all data from the index.
*
* @param array<string> $includedColumns Columns
* @return $this
*/
public function setInclude(array $includedColumns)
{
$this->include = $includedColumns;
return $this;
}
/**
* Gets the index included columns.
*
* @return ?array<string>
*/
public function getInclude(): ?array
{
return $this->include;
}
/**
* Set the where clause for partial indexes.
*
* @param ?string $where The where clause for partial indexes.
* @return $this
*/
public function setWhere(?string $where)
{
$this->where = $where;
return $this;
}
/**
* Get the where clause for partial indexes.
*
* @return ?string
*/
public function getWhere(): ?string
{
return $this->where;
}
/**
* Utility method that maps an array of index options to this object's methods.
*
* @param array<string, mixed> $attributes Attributes to set.
* @throws \RuntimeException
* @return $this
*/
public function setAttributes(array $attributes)
{
// Valid Options
$validOptions = ['columns', 'type', 'name', 'length', 'order', 'include', 'where'];
foreach ($attributes as $attr => $value) {
if (!in_array($attr, $validOptions, true)) {
throw new RuntimeException(sprintf('"%s" is not a valid index option.', $attr));
}
$method = 'set' . ucfirst($attr);
$this->$method($value);
}
return $this;
}
/**
* Convert an index into an array that is compatible with the Index constructor.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'name' => $this->getName(),
'columns' => $this->getColumns(),
'type' => $this->getType(),
'length' => $this->getLength(),
'order' => $this->getOrder(),
'include' => $this->getInclude(),
'where' => $this->getWhere(),
];
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+760
View File
@@ -0,0 +1,760 @@
<?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\Schema;
use Cake\Database\Driver;
use Cake\Database\Exception\DatabaseException;
use Cake\Database\Exception\QueryException;
use Cake\Database\Type\ColumnSchemaAwareInterface;
use Cake\Database\TypeFactory;
use InvalidArgumentException;
use PDOException;
use function Cake\Core\deprecationWarning;
/**
* Base class for schema implementations.
*
* This class contains methods that are common across
* the various SQL dialects.
*
* Provides methods for performing schema reflection. Results
* will be in the form of structured arrays. The structure
* of each result will be documented in this class. Subclasses
* are free to include *additional* data that is not documented.
*
* @method array<mixed> listTablesWithoutViewsSql(array $config) Generate the SQL to list the tables, excluding all views.
*/
abstract class SchemaDialect
{
/**
* The driver instance being used.
*
* @var \Cake\Database\Driver
*/
protected Driver $_driver;
/**
* Constructor
*
* This constructor will connect the driver so that methods like columnSql() and others
* will fail when the driver has not been connected.
*
* @param \Cake\Database\Driver $driver The driver to use.
*/
public function __construct(Driver $driver)
{
$driver->connect();
$this->_driver = $driver;
}
/**
* Generate an ON clause for a foreign key.
*
* @param string $on The on clause
* @return string
*/
protected function _foreignOnClause(string $on): string
{
if ($on === TableSchema::ACTION_SET_NULL) {
return 'SET NULL';
}
if ($on === TableSchema::ACTION_SET_DEFAULT) {
return 'SET DEFAULT';
}
if ($on === TableSchema::ACTION_CASCADE) {
return 'CASCADE';
}
if ($on === TableSchema::ACTION_RESTRICT) {
return 'RESTRICT';
}
if ($on === TableSchema::ACTION_NO_ACTION) {
return 'NO ACTION';
}
throw new InvalidArgumentException('Invalid value for "on": ' . $on);
}
/**
* Convert string on clauses to the abstract ones.
*
* @param string $clause The on clause to convert.
* @return string
*/
protected function _convertOnClause(string $clause): string
{
if ($clause === 'CASCADE' || $clause === 'RESTRICT') {
return strtolower($clause);
}
if ($clause === 'NO ACTION') {
return TableSchema::ACTION_NO_ACTION;
}
return TableSchema::ACTION_SET_NULL;
}
/**
* Convert foreign key constraints references to a valid
* stringified list
*
* @param array<string>|string $references The referenced columns of a foreign key constraint statement
* @return string
*/
protected function _convertConstraintColumns(array|string $references): string
{
if (is_string($references)) {
return $this->_driver->quoteIdentifier($references);
}
return implode(', ', array_map(
$this->_driver->quoteIdentifier(...),
$references,
));
}
/**
* Tries to use a matching database type to generate the SQL
* fragment for a single column in a table.
*
* @param string $columnType The column type.
* @param \Cake\Database\Schema\TableSchemaInterface $schema The table schema instance the column is in.
* @param string $column The name of the column.
* @return string|null An SQL fragment, or `null` in case no corresponding type was found or the type didn't provide
* custom column SQL.
*/
protected function _getTypeSpecificColumnSql(
string $columnType,
TableSchemaInterface $schema,
string $column,
): ?string {
if (!TypeFactory::getMapped($columnType)) {
return null;
}
$type = TypeFactory::build($columnType);
if (!($type instanceof ColumnSchemaAwareInterface)) {
return null;
}
return $type->getColumnSql($schema, $column, $this->_driver);
}
/**
* Tries to use a matching database type to convert a SQL column
* definition to an abstract type definition.
*
* @param string $columnType The column type.
* @param array $definition The column definition.
* @return array<string, mixed>|null Array of column information, or `null`
* in case no corresponding type was found or the type didn't provide custom column information.
*/
protected function _applyTypeSpecificColumnConversion(string $columnType, array $definition): ?array
{
if (!TypeFactory::getMapped($columnType)) {
return null;
}
$type = TypeFactory::build($columnType);
if (!($type instanceof ColumnSchemaAwareInterface)) {
return null;
}
return $type->convertColumnDefinition($definition, $this->_driver);
}
/**
* Generate the SQL to drop a table.
*
* @param \Cake\Database\Schema\TableSchema $schema Schema instance
* @return array SQL statements to drop a table.
*/
public function dropTableSql(TableSchema $schema): array
{
$sql = sprintf(
'DROP TABLE %s',
$this->_driver->quoteIdentifier($schema->name()),
);
return [$sql];
}
/**
* Generate the SQL to list the tables.
*
* @param array<string, mixed> $config The connection configuration to use for
* getting tables from.
* @return array An array of (sql, params) to execute.
* @deprecated 5.2.0 Use `listTables()` instead.
*/
abstract public function listTablesSql(array $config): array;
/**
* Generate the SQL to describe a table.
*
* @param string $tableName The table name to get information on.
* @param array<string, mixed> $config The connection configuration.
* @return array An array of (sql, params) to execute.
* @deprecated 5.2.0 Use `describeColumns()` instead.
*/
abstract public function describeColumnSql(string $tableName, array $config): array;
/**
* Generate the SQL to describe the indexes in a table.
*
* @param string $tableName The table name to get information on.
* @param array<string, mixed> $config The connection configuration.
* @return array An array of (sql, params) to execute.
* @deprecated 5.2.0 Use `describeIndexes()` instead.
*/
abstract public function describeIndexSql(string $tableName, array $config): array;
/**
* Generate the SQL to describe the foreign keys in a table.
*
* @param string $tableName The table name to get information on.
* @param array<string, mixed> $config The connection configuration.
* @return array An array of (sql, params) to execute.
* @deprecated 5.2.0 Use `describeForeignKeys()` instead.
*/
abstract public function describeForeignKeySql(string $tableName, array $config): array;
/**
* Generate the SQL to describe table options
*
* @param string $tableName Table name.
* @param array<string, mixed> $config The connection configuration.
* @return array SQL statements to get options for a table.
* @deprecated 5.2.0 Use `describeOptions()` instead.
*/
public function describeOptionsSql(string $tableName, array $config): array
{
return ['', ''];
}
/**
* Convert field description results into abstract schema fields.
*
* @param \Cake\Database\Schema\TableSchema $schema The table object to append fields to.
* @param array $row The row data from `describeColumnSql`.
* @return void
* @deprecated 5.2.0 Use `describeColumns()` instead.
*/
abstract public function convertColumnDescription(TableSchema $schema, array $row): void;
/**
* Convert an index description results into abstract schema indexes or constraints.
*
* @param \Cake\Database\Schema\TableSchema $schema The table object to append
* an index or constraint to.
* @param array $row The row data from `describeIndexSql`.
* @return void
* @deprecated 5.2.0 Use `describeIndexes()` instead.
*/
abstract public function convertIndexDescription(TableSchema $schema, array $row): void;
/**
* Convert a foreign key description into constraints on the Table object.
*
* @param \Cake\Database\Schema\TableSchema $schema The table object to append
* a constraint to.
* @param array $row The row data from `describeForeignKeySql`.
* @return void
* @deprecated 5.2.0 Use `describeForeignKeys()` instead.
*/
abstract public function convertForeignKeyDescription(TableSchema $schema, array $row): void;
/**
* Convert options data into table options.
*
* @param \Cake\Database\Schema\TableSchema $schema Table instance.
* @param array $row The row of data.
* @return void
* @deprecated 5.2.0 Use `describeOptions()` instead.
*/
public function convertOptionsDescription(TableSchema $schema, array $row): void
{
}
/**
* Generate the SQL to create a table.
*
* @param \Cake\Database\Schema\TableSchema $schema Table instance.
* @param array<string> $columns The columns to go inside the table.
* @param array<string> $constraints The constraints for the table.
* @param array<string> $indexes The indexes for the table.
* @return array<string> SQL statements to create a table.
*/
abstract public function createTableSql(
TableSchema $schema,
array $columns,
array $constraints,
array $indexes,
): array;
/**
* Generate the SQL fragment for a single column in a table.
*
* @param \Cake\Database\Schema\TableSchema $schema The table instance the column is in.
* @param string $name The name of the column.
* @return string SQL fragment.
*/
abstract public function columnSql(TableSchema $schema, string $name): string;
/**
* Generate the SQL queries needed to add foreign key constraints to the table
*
* @param \Cake\Database\Schema\TableSchema $schema The table instance the foreign key constraints are.
* @return array SQL fragment.
*/
abstract public function addConstraintSql(TableSchema $schema): array;
/**
* Generate the SQL queries needed to drop foreign key constraints from the table
*
* @param \Cake\Database\Schema\TableSchema $schema The table instance the foreign key constraints are.
* @return array SQL fragment.
*/
abstract public function dropConstraintSql(TableSchema $schema): array;
/**
* Generate the SQL fragments for defining table constraints.
*
* @param \Cake\Database\Schema\TableSchema $schema The table instance the column is in.
* @param string $name The name of the column.
* @return string SQL fragment.
*/
abstract public function constraintSql(TableSchema $schema, string $name): string;
/**
* Generate the SQL fragment for a single index in a table.
*
* @param \Cake\Database\Schema\TableSchema $schema The table object the column is in.
* @param string $name The name of the column.
* @return string SQL fragment.
*/
abstract public function indexSql(TableSchema $schema, string $name): string;
/**
* Generate the SQL to truncate a table.
*
* @param \Cake\Database\Schema\TableSchema $schema Table instance.
* @return array SQL statements to truncate a table.
*/
abstract public function truncateTableSql(TableSchema $schema): array;
/**
* Create a SQL snippet for a column based on the array shape
* that `describeColumns()` creates.
*
* @param array $column The column metadata
* @return string Generated SQL fragment for a column
*/
public function columnDefinitionSql(array $column): string
{
deprecationWarning(
'5.2.0',
'SchemaDialect subclasses need to implement `columnDefinitionSql` before 6.0.0',
);
$table = new TableSchema('placeholder');
$table->addColumn($column['name'], $column);
return $this->columnSql($table, $column['name']);
}
/**
* Get the list of tables, excluding any views, available in the current connection.
*
* @return array<string> The list of tables in the connected database/schema.
*/
public function listTablesWithoutViews(): array
{
[$sql, $params] = $this->listTablesWithoutViewsSql($this->_driver->config());
$result = [];
$statement = $this->_driver->execute($sql, $params);
while ($row = $statement->fetch()) {
$result[] = $row[0];
}
return $result;
}
/**
* Get the list of tables and views available in the current connection.
*
* @param string|null $schema The schema to get the tables for. If null the default schema is used.
* @return array<string> The list of tables and views in the connected database/schema.
*/
public function listTables(?string $schema = null): array
{
$config = $this->_driver->config();
if ($schema !== null) {
$config['schema'] = $schema;
// Set database for MySQL
$config['database'] = $schema;
}
[$sql, $params] = $this->listTablesSql($config);
$result = [];
$statement = $this->_driver->execute($sql, $params);
while ($row = $statement->fetch()) {
$result[] = $row[0];
}
return $result;
}
/**
* Get the column metadata for a table.
*
* The name can include a database schema name in the form 'schema.table'.
*
* @param string $name The name of the table to describe.
* @return \Cake\Database\Schema\TableSchemaInterface Object with column metadata.
* @throws \Cake\Database\Exception\DatabaseException when table cannot be described.
*/
public function describe(string $name): TableSchemaInterface
{
$tableName = $name;
if (str_contains($name, '.')) {
$tableName = explode('.', $name)[1];
}
$table = $this->_driver->newTableSchema($tableName);
foreach ($this->describeColumns($name) as $column) {
$table->addColumn($column['name'], $column);
}
foreach ($this->describeIndexes($name) as $index) {
if (in_array($index['type'], [TableSchema::CONSTRAINT_UNIQUE, TableSchema::CONSTRAINT_PRIMARY])) {
$table->addConstraint($index['name'], $index);
} else {
$table->addIndex($index['name'], $index);
}
}
foreach ($this->describeForeignKeys($name) as $key) {
$table->addConstraint($key['name'], $key);
}
foreach ($this->describeCheckConstraints($name) as $key) {
$table->addConstraint($key['name'], $key);
}
$options = $this->describeOptions($name);
if ($options) {
$table->setOptions($options);
}
if ($table->columns() === []) {
throw new DatabaseException(sprintf('Cannot describe %s. It has 0 columns.', $name));
}
return $table;
}
/**
* Get a list of column metadata as a array
*
* Each item in the array will contain the following:
*
* - name : the name of the column.
* - type : the abstract type of the column.
* - length : the length of the column.
* - default : the default value of the column or null.
* - null : boolean indicating whether the column can be null.
* - comment : the column comment or null.
*
* Additionaly the `autoIncrement` key will be set for columns that are a primary key.
*
* @param string $tableName The name of the table to describe columns on.
* @return array
*/
public function describeColumns(string $tableName): array
{
deprecationWarning(
'5.2.0',
'SchemaDialect subclasses need to implement `describeColumns` before 6.0.0',
);
$config = $this->_driver->config();
if (str_contains($tableName, '.')) {
[$config['schema'], $tableName] = explode('.', $tableName);
}
/** @var \Cake\Database\Schema\TableSchema $table */
$table = $this->_driver->newTableSchema($tableName);
[$sql, $params] = $this->describeColumnSql($tableName, $config);
$statement = $this->_driver->execute($sql, $params);
foreach ($statement->fetchAll('assoc') as $row) {
$this->convertColumnDescription($table, $row);
}
$columns = [];
foreach ($table->columns() as $columnName) {
$column = $table->getColumn($columnName);
$column['name'] = $columnName;
$columns[] = $column;
}
return $columns;
}
/**
* Get a list of constraint metadata as a array
*
* Each item in the array will contain the following:
*
* - name : The name of the constraint
* - type : the type of the constraint. Generally `foreign`.
* - columns : the columns in the constraint on the.
* - references : A list of the table + all columns in the referenced table
* - update : The update action or null
* - delete : The delete action or null
*
* @param string $tableName The name of the table to describe foreign keys on.
* @return array
*/
public function describeForeignKeys(string $tableName): array
{
deprecationWarning(
'5.2.0',
'SchemaDialect subclasses need to implement `describeForeignKeys` before 6.0.0',
);
$config = $this->_driver->config();
if (str_contains($tableName, '.')) {
[$config['schema'], $tableName] = explode('.', $tableName);
}
/** @var \Cake\Database\Schema\TableSchema $table */
$table = $this->_driver->newTableSchema($tableName);
// Add the columns because TableSchema needs them.
foreach ($this->describeColumns($tableName) as $column) {
$table->addColumn($column['name'], $column);
}
[$sql, $params] = $this->describeForeignKeySql($tableName, $config);
$statement = $this->_driver->execute($sql, $params);
foreach ($statement->fetchAll('assoc') as $row) {
$this->convertForeignKeyDescription($table, $row);
}
$keys = [];
foreach ($table->constraints() as $name) {
$key = $table->getConstraint($name);
$key['name'] = $name;
$keys[] = $key;
}
return $keys;
}
/**
* Get a list of index metadata as a array
*
* Each item in the array will contain the following:
*
* - name : the name of the index.
* - type : the type of the index. One of `unique`, `index`, `primary`.
* - columns : the columns in the index.
* - length : the length of the index if applicable.
*
* @param string $tableName The name of the table to describe indexes on.
* @return array
*/
public function describeIndexes(string $tableName): array
{
deprecationWarning(
'5.2.0',
'SchemaDialect subclasses need to implement `describeIndexes` before 6.0.0',
);
$config = $this->_driver->config();
if (str_contains($tableName, '.')) {
[$config['schema'], $tableName] = explode('.', $tableName);
}
/** @var \Cake\Database\Schema\TableSchema $table */
$table = $this->_driver->newTableSchema($tableName);
// Add the columns because TableSchema needs them.
foreach ($this->describeColumns($tableName) as $column) {
$table->addColumn($column['name'], $column);
}
[$sql, $params] = $this->describeIndexSql($tableName, $config);
$statement = $this->_driver->execute($sql, $params);
foreach ($statement->fetchAll('assoc') as $row) {
$this->convertIndexDescription($table, $row);
}
$indexes = [];
foreach ($table->indexes() as $name) {
$index = $table->getIndex($name);
$index['name'] = $name;
$indexes[] = $index;
}
return $indexes;
}
/**
* Get platform specific options
*
* No keys are guaranteed to be present as they are database driver dependent.
*
* @param string $tableName The name of the table to describe options on.
* @return array
*/
public function describeOptions(string $tableName): array
{
deprecationWarning(
'5.2.0',
'SchemaDialect subclasses need to implement `describeOptions` before 6.0.0',
);
$config = $this->_driver->config();
if (str_contains($tableName, '.')) {
[$config['schema'], $tableName] = explode('.', $tableName);
}
/** @var \Cake\Database\Schema\TableSchema $table */
$table = $this->_driver->newTableSchema($tableName);
[$sql, $params] = $this->describeOptionsSql($tableName, $config);
if ($sql) {
$statement = $this->_driver->execute($sql, $params);
foreach ($statement->fetchAll('assoc') as $row) {
$this->convertOptionsDescription($table, $row);
}
}
return $table->getOptions();
}
/**
* Get a list of check constraint metadata as an array.
*
* Each item in the array will contain the following keys:
*
* - name - The name of the constraint.
* - expression - The check constraint expression as a SQL fragment.
*
* @param string $tableName The name of the table to describe options on.
* @return array
*/
public function describeCheckConstraints(string $tableName): array
{
return [];
}
/**
* Check if a table has a column with a given name.
*
* @param string $tableName The name of the table
* @param string $columnName The name of the column
* @return bool
*/
public function hasColumn(string $tableName, string $columnName): bool
{
try {
$columns = $this->describeColumns($tableName);
} catch (PDOException | DatabaseException) {
return false;
}
foreach ($columns as $column) {
if ($column['name'] === $columnName) {
return true;
}
}
return false;
}
/**
* Check if a table exists
*
* @param string $tableName The name of the table
* @param string|null $schema The schema look for table in. If null the default schema is used.
* @return bool
*/
public function hasTable(string $tableName, ?string $schema = null): bool
{
$tables = $this->listTables($schema);
return in_array($tableName, $tables, true);
}
/**
* Check if a table has an index with a given name.
*
* @param string $tableName The name of the table
* @param array<string> $columns The columns in the index. Specific
* ordering matters.
* @param string $name The name of the index to match on. Can be used alone,
* or with $columns to match indexes more precisely.
* @return bool
*/
public function hasIndex(string $tableName, array $columns = [], ?string $name = null): bool
{
try {
$indexes = $this->describeIndexes($tableName);
} catch (QueryException) {
return false;
}
$found = null;
foreach ($indexes as $index) {
if ($columns && $index['columns'] === $columns) {
$found = $index;
break;
}
if ($columns === [] && $name !== null) {
if ($index['name'] === $name) {
$found = $index;
break;
}
if (isset($index['constraint']) && $index['constraint'] === $name) {
$found = $index;
break;
}
}
}
// Both columns and name provided, both must match;
if ($columns && $found && $name !== null && $found['name'] !== $name) {
return false;
}
return $found !== null;
}
/**
* Check if a table has a foreign key with a given name.
*
* @param string $tableName The name of the table
* @param array<string> $columns The columns in the foriegn key. Specific
* ordering matters.
* @param string $name The name of the foreign key to match on. Can be used alone,
* or with $columns to match keys more precisely.
* @return bool
*/
public function hasForeignKey(string $tableName, array $columns = [], ?string $name = null): bool
{
try {
$keys = $this->describeForeignKeys($tableName);
} catch (QueryException) {
return false;
}
$found = null;
foreach ($keys as $key) {
if ($columns && $key['columns'] === $columns) {
$found = $key;
break;
}
if (!$columns && $name !== null && $key['name'] === $name) {
$found = $key;
break;
}
}
// Both columns and name provided, both must match;
if ($found !== null && $name !== null && $found['name'] !== $name) {
return false;
}
return $found !== null;
}
}
@@ -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.5.0
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
use Cake\Database\Connection;
/**
* An interface used by TableSchema objects.
*/
interface SqlGeneratorInterface
{
/**
* Generate the SQL to create the Table.
*
* Uses the connection to access the schema dialect
* to generate platform specific SQL.
*
* @param \Cake\Database\Connection $connection The connection to generate SQL for.
* @return array List of SQL statements to create the table and the
* required indexes.
*/
public function createSql(Connection $connection): array;
/**
* Generate the SQL to drop a table.
*
* Uses the connection to access the schema dialect to generate platform
* specific SQL.
*
* @param \Cake\Database\Connection $connection The connection to generate SQL for.
* @return array SQL to drop a table.
*/
public function dropSql(Connection $connection): array;
/**
* Generate the SQL statements to truncate a table
*
* @param \Cake\Database\Connection $connection The connection to generate SQL for.
* @return array SQL to truncate a table.
*/
public function truncateSql(Connection $connection): array;
/**
* Generate the SQL statements to add the constraints to the table
*
* @param \Cake\Database\Connection $connection The connection to generate SQL for.
* @return array SQL to add the constraints.
*/
public function addConstraintSql(Connection $connection): array;
/**
* Generate the SQL statements to drop the constraints to the table
*
* @param \Cake\Database\Connection $connection The connection to generate SQL for.
* @return array SQL to drop a table.
*/
public function dropConstraintSql(Connection $connection): array;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,971 @@
<?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\Schema;
/**
* Schema management/reflection features for SQLServer.
*
* @internal
*/
class SqlserverSchemaDialect extends SchemaDialect
{
/**
* @var string
*/
public const DEFAULT_SCHEMA_NAME = 'dbo';
/**
* Generate the SQL to list the tables and views.
*
* @param array<string, mixed> $config The connection configuration to use for
* getting tables from.
* @return array An array of (sql, params) to execute.
*/
public function listTablesSql(array $config): array
{
$sql = "SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = ?
AND (TABLE_TYPE = 'BASE TABLE' OR TABLE_TYPE = 'VIEW')
ORDER BY TABLE_NAME";
$schema = $config['schema'] ?? static::DEFAULT_SCHEMA_NAME;
return [$sql, [$schema]];
}
/**
* Generate the SQL to list the tables, excluding all views.
*
* @param array<string, mixed> $config The connection configuration to use for
* getting tables from.
* @return array<mixed> An array of (sql, params) to execute.
*/
public function listTablesWithoutViewsSql(array $config): array
{
$sql = "SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = ?
AND (TABLE_TYPE = 'BASE TABLE')
ORDER BY TABLE_NAME";
$schema = $config['schema'] ?? static::DEFAULT_SCHEMA_NAME;
return [$sql, [$schema]];
}
/**
* @inheritDoc
*/
public function describeColumnSql(string $tableName, array $config): array
{
$sql = $this->describeColumnQuery();
$schema = $config['schema'] ?? static::DEFAULT_SCHEMA_NAME;
return [$sql, [$tableName, $schema]];
}
/**
* Helper method for creating SQL to describe columns in a table.
*
* @return string SQL to reflect columns
*/
private function describeColumnQuery(): string
{
return 'SELECT DISTINCT
AC.column_id AS [column_id],
AC.name AS [name],
TY.name AS [type],
AC.max_length AS [char_length],
AC.precision AS [precision],
AC.scale AS [scale],
AC.is_identity AS [autoincrement],
AC.is_nullable AS [null],
OBJECT_DEFINITION(AC.default_object_id) AS [default],
AC.collation_name AS [collation_name],
EP.[value] AS [comment]
FROM sys.[objects] T
INNER JOIN sys.[schemas] S ON S.[schema_id] = T.[schema_id]
INNER JOIN sys.[all_columns] AC ON T.[object_id] = AC.[object_id]
INNER JOIN sys.[types] TY ON TY.[user_type_id] = AC.[user_type_id]
LEFT JOIN sys.[extended_properties] as EP
ON T.[object_id] = EP.[major_id]
AND AC.[column_id] = EP.[minor_id]
AND EP.[name] = \'MS_Description\'
WHERE T.[name] = ? AND S.[name] = ?
ORDER BY column_id';
}
/**
* Convert a column definition to the abstract types.
*
* The returned type will be a type that
* Cake\Database\TypeFactory can handle.
*
* @param string $col The column type
* @param int|null $length the column length
* @param int|null $precision The column precision
* @param int|null $scale The column scale
* @return array<string, mixed> Array of column information.
* @link https://technet.microsoft.com/en-us/library/ms187752.aspx
*/
protected function _convertColumn(
string $col,
?int $length = null,
?int $precision = null,
?int $scale = null,
): array {
$col = strtolower($col);
$type = $this->_applyTypeSpecificColumnConversion(
$col,
compact('length', 'precision', 'scale'),
);
if ($type !== null) {
return $type;
}
if (in_array($col, ['date', 'time'])) {
return ['type' => $col, 'length' => null];
}
if ($col === 'datetime') {
// datetime cannot parse more than 3 digits of precision and isn't accurate
return ['type' => TableSchemaInterface::TYPE_DATETIME, 'length' => null];
}
if (str_contains($col, 'datetime')) {
$typeName = TableSchemaInterface::TYPE_DATETIME;
if ($scale > 0) {
$typeName = TableSchemaInterface::TYPE_DATETIME_FRACTIONAL;
}
return ['type' => $typeName, 'length' => null, 'precision' => $scale];
}
if ($col === 'char') {
return ['type' => TableSchemaInterface::TYPE_CHAR, 'length' => $length];
}
if ($col === 'tinyint') {
return ['type' => TableSchemaInterface::TYPE_TINYINTEGER, 'length' => $precision ?: 3];
}
if ($col === 'smallint') {
return ['type' => TableSchemaInterface::TYPE_SMALLINTEGER, 'length' => $precision ?: 5];
}
if ($col === 'int' || $col === 'integer') {
return ['type' => TableSchemaInterface::TYPE_INTEGER, 'length' => $precision ?: 10];
}
if ($col === 'bigint') {
return ['type' => TableSchemaInterface::TYPE_BIGINTEGER, 'length' => $precision ?: 20];
}
if ($col === 'bit') {
return ['type' => TableSchemaInterface::TYPE_BOOLEAN, 'length' => null];
}
if (
str_contains($col, 'numeric') ||
str_contains($col, 'money') ||
str_contains($col, 'decimal')
) {
return ['type' => TableSchemaInterface::TYPE_DECIMAL, 'length' => $precision, 'precision' => $scale];
}
if ($col === 'real' || $col === 'float') {
return ['type' => TableSchemaInterface::TYPE_FLOAT, 'length' => null];
}
// SqlServer schema reflection returns double length for unicode
// columns because internally it uses UTF16/UCS2
if (in_array($col, ['nvarchar', 'nchar', 'ntext'], true)) {
$length /= 2;
}
if (str_contains($col, 'varchar') && $length < 0) {
return ['type' => TableSchemaInterface::TYPE_TEXT, 'length' => null];
}
if (str_contains($col, 'varchar')) {
return ['type' => TableSchemaInterface::TYPE_STRING, 'length' => $length ?: 255];
}
if (str_contains($col, 'char')) {
return ['type' => TableSchemaInterface::TYPE_CHAR, 'length' => $length];
}
if (str_contains($col, 'text')) {
return ['type' => TableSchemaInterface::TYPE_TEXT, 'length' => null];
}
if ($col === 'image' || str_contains($col, 'binary')) {
// -1 is the value for MAX which we treat as a 'long' binary
if ($length == -1) {
$length = TableSchema::LENGTH_LONG;
}
return ['type' => TableSchemaInterface::TYPE_BINARY, 'length' => $length];
}
if ($col === 'uniqueidentifier') {
return ['type' => TableSchemaInterface::TYPE_UUID];
}
if ($col === 'geometry') {
return ['type' => TableSchemaInterface::TYPE_GEOMETRY];
}
if ($col === 'geography') {
// SQLserver only has one generic geometry type that
// we map to point.
return ['type' => TableSchemaInterface::TYPE_POINT];
}
return ['type' => TableSchemaInterface::TYPE_STRING, 'length' => null];
}
/**
* @inheritDoc
*/
public function convertColumnDescription(TableSchema $schema, array $row): void
{
$field = $this->_convertColumn(
$row['type'],
$row['char_length'] !== null ? (int)$row['char_length'] : null,
$row['precision'] !== null ? (int)$row['precision'] : null,
$row['scale'] !== null ? (int)$row['scale'] : null,
);
if (!empty($row['autoincrement'])) {
$field['autoIncrement'] = true;
}
$field += [
'null' => $row['null'] === '1',
'default' => $this->_defaultValue($field['type'], $row['default']),
'collate' => $row['collation_name'],
];
$schema->addColumn($row['name'], $field);
}
/**
* Split a tablename into a tuple of schema, table
* If the table does not have a schema name included, the connection
* schema will be used.
*
* @param string $tableName The table name to split
* @return array A tuple of [schema, tablename]
*/
private function splitTablename(string $tableName): array
{
$config = $this->_driver->config();
$schema = $config['schema'] ?? static::DEFAULT_SCHEMA_NAME;
if (str_contains($tableName, '.')) {
return explode('.', $tableName);
}
return [$schema, $tableName];
}
/**
* @inheritDoc
*/
public function describeColumns(string $tableName): array
{
[$schema, $name] = $this->splitTablename($tableName);
$sql = $this->describeColumnQuery();
$statement = $this->_driver->execute($sql, [$name, $schema]);
$columns = [];
foreach ($statement->fetchAll('assoc') as $row) {
$field = $this->_convertColumn(
$row['type'],
$row['char_length'] !== null ? (int)$row['char_length'] : null,
$row['precision'] !== null ? (int)$row['precision'] : null,
$row['scale'] !== null ? (int)$row['scale'] : null,
);
if (!empty($row['autoincrement'])) {
$field['autoIncrement'] = true;
}
$field += [
'name' => $row['name'],
'null' => $row['null'] === '1',
'default' => $this->_defaultValue($field['type'], $row['default']),
'comment' => $row['comment'] ?? null,
'collate' => $row['collation_name'],
];
$columns[] = $field;
}
return $columns;
}
/**
* Manipulate the default value.
*
* Removes () wrapping default values, extracts strings from
* N'' wrappers and collation text and converts NULL strings.
*
* @param string $type The schema type
* @param string|null $default The default value.
* @return string|int|null
*/
protected function _defaultValue(string $type, ?string $default): string|int|null
{
if ($default === null) {
return null;
}
// remove () surrounding value (NULL) but leave () at the end of functions
// integers might have two ((0)) wrapping value
if (preg_match('/^\(+(.*?(\(\))?)\)+$/', $default, $matches)) {
$default = $matches[1];
}
if ($default === 'NULL') {
return null;
}
if ($type === TableSchemaInterface::TYPE_BOOLEAN) {
return (int)$default;
}
// Remove quotes
if (preg_match("/^\(?N?'(.*)'\)?/", $default, $matches)) {
return str_replace("''", "'", $matches[1]);
}
return $default;
}
/**
* @inheritDoc
*/
public function describeIndexSql(string $tableName, array $config): array
{
$sql = $this->describeIndexQuery();
$schema = $config['schema'] ?? static::DEFAULT_SCHEMA_NAME;
return [$sql, [$tableName, $schema]];
}
/**
* Get the query to describe indexes
*
* @return string
*/
private function describeIndexQuery(): string
{
return "SELECT
I.[name] AS [index_name],
IC.[index_column_id] AS [index_order],
AC.[name] AS [column_name],
I.[is_unique], I.[is_primary_key],
I.[is_unique_constraint],
IC.[is_included_column]
FROM sys.[tables] AS T
INNER JOIN sys.[schemas] S ON S.[schema_id] = T.[schema_id]
INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id]
INNER JOIN sys.[index_columns] IC ON I.[object_id] = IC.[object_id] AND I.[index_id] = IC.[index_id]
INNER JOIN sys.[all_columns] AC ON T.[object_id] = AC.[object_id] AND IC.[column_id] = AC.[column_id]
WHERE T.[is_ms_shipped] = 0 AND I.[type_desc] <> 'HEAP' AND T.[name] = ? AND S.[name] = ?
ORDER BY I.[index_id], IC.[index_column_id]";
}
/**
* @inheritDoc
*/
public function convertIndexDescription(TableSchema $schema, array $row): void
{
$type = TableSchema::INDEX_INDEX;
$name = $row['index_name'];
if ($row['is_primary_key']) {
$name = TableSchema::CONSTRAINT_PRIMARY;
$type = TableSchema::CONSTRAINT_PRIMARY;
}
if (($row['is_unique'] || $row['is_unique_constraint']) && $type === TableSchema::INDEX_INDEX) {
$type = TableSchema::CONSTRAINT_UNIQUE;
}
if ($type === TableSchema::INDEX_INDEX) {
$existing = $schema->getIndex($name);
} else {
$existing = $schema->getConstraint($name);
}
$columns = [$row['column_name']];
if ($existing) {
$columns = array_merge($existing['columns'], $columns);
}
if ($type === TableSchema::CONSTRAINT_PRIMARY || $type === TableSchema::CONSTRAINT_UNIQUE) {
$schema->addConstraint($name, [
'type' => $type,
'columns' => $columns,
]);
return;
}
$schema->addIndex($name, [
'type' => $type,
'columns' => $columns,
]);
}
/**
* @inheritDoc
*/
public function describeIndexes(string $tableName): array
{
[$schema, $name] = $this->splitTablename($tableName);
$sql = $this->describeIndexQuery();
$indexes = [];
$statement = $this->_driver->execute($sql, [$name, $schema]);
foreach ($statement->fetchAll('assoc') as $row) {
$type = TableSchema::INDEX_INDEX;
$name = $row['index_name'];
$constraint = null;
if ($row['is_primary_key']) {
$constraint = $name;
$name = TableSchema::CONSTRAINT_PRIMARY;
$type = TableSchema::CONSTRAINT_PRIMARY;
}
if (($row['is_unique'] || $row['is_unique_constraint']) && $type === TableSchema::INDEX_INDEX) {
$type = TableSchema::CONSTRAINT_UNIQUE;
}
if (!isset($indexes[$name])) {
$indexes[$name] = [
'name' => $name,
'type' => $type,
'columns' => [],
'length' => [],
];
}
if ($row['is_included_column']) {
$indexes[$name]['include'][] = $row['column_name'];
} else {
$indexes[$name]['columns'][] = $row['column_name'];
}
if ($constraint) {
$indexes[$name]['constraint'] = $constraint;
}
}
return array_values($indexes);
}
/**
* Get the query to describe foreign keys
*
* @return string
*/
private function describeForeignKeyQuery(): string
{
// phpcs:disable Generic.Files.LineLength
return 'SELECT FK.[name] AS [foreign_key_name],
FK.[delete_referential_action_desc] AS [delete_type],
FK.[update_referential_action_desc] AS [update_type],
C.name AS [column],
RT.name AS [reference_table],
RC.name AS [reference_column]
FROM sys.foreign_keys FK
INNER JOIN sys.foreign_key_columns FKC ON FKC.constraint_object_id = FK.object_id
INNER JOIN sys.tables T ON T.object_id = FKC.parent_object_id
INNER JOIN sys.tables RT ON RT.object_id = FKC.referenced_object_id
INNER JOIN sys.schemas S ON S.schema_id = T.schema_id AND S.schema_id = RT.schema_id
INNER JOIN sys.columns C ON C.column_id = FKC.parent_column_id AND C.object_id = FKC.parent_object_id
INNER JOIN sys.columns RC ON RC.column_id = FKC.referenced_column_id AND RC.object_id = FKC.referenced_object_id
WHERE FK.is_ms_shipped = 0 AND T.name = ? AND S.name = ?
ORDER BY FKC.constraint_column_id';
// phpcs:enable Generic.Files.LineLength
}
/**
* @inheritDoc
*/
public function describeForeignKeys(string $tableName): array
{
[$schema, $name] = $this->splitTablename($tableName);
$sql = $this->describeForeignKeyQuery();
$keys = [];
$statement = $this->_driver->execute($sql, [$name, $schema]);
foreach ($statement->fetchAll('assoc') as $row) {
$name = $row['foreign_key_name'];
if (!isset($keys[$name])) {
$keys[$name] = [
'name' => $name,
'type' => TableSchema::CONSTRAINT_FOREIGN,
'columns' => [],
'references' => [$row['reference_table'], []],
'update' => $this->_convertOnClause($row['update_type']),
'delete' => $this->_convertOnClause($row['delete_type']),
];
}
$keys[$name]['columns'][] = $row['column'];
$keys[$name]['references'][1][] = $row['reference_column'];
}
foreach ($keys as $id => $key) {
// references.1 is the referenced columns. Backwards compat
// requires a single column to be a string, but multiple to be an array.
if (count($key['references'][1]) === 1) {
$keys[$id]['references'][1] = $key['references'][1][0];
}
}
return array_values($keys);
}
/**
* @inheritDoc
*/
public function describeForeignKeySql(string $tableName, array $config): array
{
$sql = $this->describeForeignKeyQuery();
$schema = $config['schema'] ?? static::DEFAULT_SCHEMA_NAME;
return [$sql, [$tableName, $schema]];
}
/**
* @inheritDoc
*/
public function convertForeignKeyDescription(TableSchema $schema, array $row): void
{
$data = [
'type' => TableSchema::CONSTRAINT_FOREIGN,
'columns' => [$row['column']],
'references' => [$row['reference_table'], $row['reference_column']],
'update' => $this->_convertOnClause($row['update_type']),
'delete' => $this->_convertOnClause($row['delete_type']),
];
$name = $row['foreign_key_name'];
$schema->addConstraint($name, $data);
}
/**
* @inheritDoc
*/
public function describeOptions(string $tableName): array
{
return [];
}
/**
* @inheritDoc
*/
protected function _foreignOnClause(string $on): string
{
$parent = parent::_foreignOnClause($on);
return $parent === 'RESTRICT' ? parent::_foreignOnClause(TableSchema::ACTION_NO_ACTION) : $parent;
}
/**
* @inheritDoc
*/
protected function _convertOnClause(string $clause): string
{
return match ($clause) {
'NO_ACTION' => TableSchema::ACTION_NO_ACTION,
'CASCADE' => TableSchema::ACTION_CASCADE,
'SET_NULL' => TableSchema::ACTION_SET_NULL,
'SET_DEFAULT' => TableSchema::ACTION_SET_DEFAULT,
default => TableSchema::ACTION_SET_NULL,
};
}
/**
* @inheritDoc
*/
public function columnSql(TableSchema $schema, string $name): string
{
$data = $schema->getColumn($name);
assert($data !== null);
$data['name'] = $name;
$sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name);
if ($sql !== null) {
return $sql;
}
$autoIncrementTypes = [
TableSchemaInterface::TYPE_TINYINTEGER,
TableSchemaInterface::TYPE_SMALLINTEGER,
TableSchemaInterface::TYPE_INTEGER,
TableSchemaInterface::TYPE_BIGINTEGER,
];
$primaryKey = $schema->getPrimaryKey();
if (
in_array($data['type'], $autoIncrementTypes, true) &&
$primaryKey === [$name] &&
$name === 'id'
) {
$data['autoIncrement'] = true;
}
return $this->columnDefinitionSql($data);
}
/**
* @inheritDoc
*/
public function columnDefinitionSql(array $column): string
{
$name = $column['name'];
$column += [
'length' => null,
'precision' => null,
];
$out = $this->_driver->quoteIdentifier($name);
$typeMap = [
TableSchemaInterface::TYPE_TINYINTEGER => ' TINYINT',
TableSchemaInterface::TYPE_SMALLINTEGER => ' SMALLINT',
TableSchemaInterface::TYPE_INTEGER => ' INTEGER',
TableSchemaInterface::TYPE_BIGINTEGER => ' BIGINT',
TableSchemaInterface::TYPE_BINARY_UUID => ' UNIQUEIDENTIFIER',
TableSchemaInterface::TYPE_BOOLEAN => ' BIT',
TableSchemaInterface::TYPE_CHAR => ' NCHAR',
TableSchemaInterface::TYPE_STRING => ' NVARCHAR',
TableSchemaInterface::TYPE_FLOAT => ' FLOAT',
TableSchemaInterface::TYPE_DECIMAL => ' DECIMAL',
TableSchemaInterface::TYPE_DATE => ' DATE',
TableSchemaInterface::TYPE_TIME => ' TIME',
TableSchemaInterface::TYPE_DATETIME => ' DATETIME2',
TableSchemaInterface::TYPE_DATETIME_FRACTIONAL => ' DATETIME2',
TableSchemaInterface::TYPE_TIMESTAMP => ' DATETIME2',
TableSchemaInterface::TYPE_TIMESTAMP_FRACTIONAL => ' DATETIME2',
TableSchemaInterface::TYPE_TIMESTAMP_TIMEZONE => ' DATETIME2',
TableSchemaInterface::TYPE_UUID => ' UNIQUEIDENTIFIER',
TableSchemaInterface::TYPE_NATIVE_UUID => ' UNIQUEIDENTIFIER',
TableSchemaInterface::TYPE_JSON => ' NVARCHAR(MAX)',
TableSchemaInterface::TYPE_GEOMETRY => ' GEOMETRY',
TableSchemaInterface::TYPE_POINT => ' GEOGRAPHY',
TableSchemaInterface::TYPE_LINESTRING => ' GEOGRAPHY',
TableSchemaInterface::TYPE_POLYGON => ' GEOGRAPHY',
];
$foundType = false;
if (isset($typeMap[$column['type']])) {
$out .= $typeMap[$column['type']];
$foundType = true;
}
$hasLength = [
TableSchemaInterface::TYPE_CHAR,
TableSchemaInterface::TYPE_STRING,
TableSchemaInterface::TYPE_BINARY,
];
$autoIncrementTypes = [
TableSchemaInterface::TYPE_TINYINTEGER,
TableSchemaInterface::TYPE_SMALLINTEGER,
TableSchemaInterface::TYPE_INTEGER,
TableSchemaInterface::TYPE_BIGINTEGER,
];
$autoIncrement = (bool)($column['autoIncrement'] ?? false);
if (in_array($column['type'], $autoIncrementTypes, true) && $autoIncrement) {
$out .= ' IDENTITY(1, 1)';
$foundType = true;
unset($column['default']);
}
if ($column['type'] === TableSchemaInterface::TYPE_STRING && !isset($column['length'])) {
$column['length'] = TableSchema::LENGTH_TINY;
} elseif (
$column['type'] === TableSchemaInterface::TYPE_TEXT &&
$column['length'] !== TableSchema::LENGTH_TINY
) {
$out .= ' NVARCHAR(MAX)';
$foundType = true;
}
if ($column['type'] === TableSchemaInterface::TYPE_BINARY) {
if (
!isset($column['length'])
|| in_array($column['length'], [TableSchema::LENGTH_MEDIUM, TableSchema::LENGTH_LONG], true)
) {
$column['length'] = 'MAX';
}
if ($column['length'] === 1) {
$out .= ' BINARY';
} else {
$out .= ' VARBINARY';
}
$foundType = true;
}
if ($column['type'] === TableSchemaInterface::TYPE_TEXT && $column['length'] === TableSchema::LENGTH_TINY) {
$out .= ' NVARCHAR';
$hasLength[] = $column['type'];
$foundType = true;
}
if (!$foundType) {
$out .= ' ' . strtoupper($column['type']);
$hasLength[] = $column['type'];
}
if (in_array($column['type'], $hasLength, true) && isset($column['length'])) {
$out .= '(' . $column['length'] . ')';
}
$hasCollate = [
TableSchemaInterface::TYPE_TEXT,
TableSchemaInterface::TYPE_STRING,
TableSchemaInterface::TYPE_CHAR,
];
if (in_array($column['type'], $hasCollate, true) && isset($column['collate']) && $column['collate'] !== '') {
$out .= ' COLLATE ' . $column['collate'];
}
$precisionTypes = [
TableSchemaInterface::TYPE_FLOAT,
TableSchemaInterface::TYPE_DATETIME,
TableSchemaInterface::TYPE_DATETIME_FRACTIONAL,
TableSchemaInterface::TYPE_TIMESTAMP,
TableSchemaInterface::TYPE_TIMESTAMP_FRACTIONAL,
];
if (in_array($column['type'], $precisionTypes, true) && isset($column['precision'])) {
$out .= '(' . (int)$column['precision'] . ')';
}
if (
$column['type'] === TableSchemaInterface::TYPE_DECIMAL &&
(isset($column['length']) || isset($column['precision']))
) {
$out .= '(' . (int)$column['length'] . ',' . (int)$column['precision'] . ')';
}
if (isset($column['null']) && $column['null'] === false) {
$out .= ' NOT NULL';
}
$dateTimeTypes = [
TableSchemaInterface::TYPE_DATETIME,
TableSchemaInterface::TYPE_DATETIME_FRACTIONAL,
TableSchemaInterface::TYPE_TIMESTAMP,
TableSchemaInterface::TYPE_TIMESTAMP_FRACTIONAL,
];
$dateTimeDefaults = [
'current_timestamp',
'getdate()',
'getutcdate()',
'sysdatetime()',
'sysutcdatetime()',
'sysdatetimeoffset()',
];
if (
isset($column['default']) &&
in_array($column['type'], $dateTimeTypes, true) &&
is_string($column['default']) &&
in_array(strtolower($column['default']), $dateTimeDefaults, true)
) {
$out .= ' DEFAULT ' . strtoupper($column['default']);
} elseif (isset($column['default'])) {
$default = is_bool($column['default'])
? (int)$column['default']
: $this->_driver->schemaValue($column['default']);
$out .= ' DEFAULT ' . $default;
} elseif (isset($column['null']) && $column['null'] !== false) {
$out .= ' DEFAULT NULL';
}
return $out;
}
/**
* @inheritDoc
*/
public function addConstraintSql(TableSchema $schema): array
{
$sqlPattern = 'ALTER TABLE %s ADD %s;';
$sql = [];
foreach ($schema->constraints() as $name) {
$constraint = $schema->getConstraint($name);
assert($constraint !== null);
if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
$tableName = $this->_driver->quoteIdentifier($schema->name());
$sql[] = sprintf($sqlPattern, $tableName, $this->constraintSql($schema, $name));
}
}
return $sql;
}
/**
* @inheritDoc
*/
public function dropConstraintSql(TableSchema $schema): array
{
$sqlPattern = 'ALTER TABLE %s DROP CONSTRAINT %s;';
$sql = [];
foreach ($schema->constraints() as $name) {
$constraint = $schema->getConstraint($name);
assert($constraint !== null);
if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
$tableName = $this->_driver->quoteIdentifier($schema->name());
$constraintName = $this->_driver->quoteIdentifier($name);
$sql[] = sprintf($sqlPattern, $tableName, $constraintName);
}
}
return $sql;
}
/**
* @inheritDoc
*/
public function indexSql(TableSchema $schema, string $name): string
{
$index = $schema->index($name);
$columns = array_map(
$this->_driver->quoteIdentifier(...),
(array)$index->getColumns(),
);
$include = '';
$included = $index->getInclude();
if ($included !== null) {
$included = array_map(
$this->_driver->quoteIdentifier(...),
$included,
);
$include = sprintf(' INCLUDE (%s)', implode(', ', $included));
}
return sprintf(
'CREATE INDEX %s ON %s (%s)%s',
$this->_driver->quoteIdentifier($name),
$this->_driver->quoteIdentifier($schema->name()),
implode(', ', $columns),
$include,
);
}
/**
* @inheritDoc
*/
public function constraintSql(TableSchema $schema, string $name): string
{
$data = $schema->getConstraint($name);
assert($data !== null);
$out = 'CONSTRAINT ' . $this->_driver->quoteIdentifier($name);
if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) {
$out = 'PRIMARY KEY';
}
if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) {
$out .= ' UNIQUE';
}
return $this->_keySql($out, $data);
}
/**
* Helper method for generating key SQL snippets.
*
* @param string $prefix The key prefix
* @param array $data Key data.
* @return string
*/
protected function _keySql(string $prefix, array $data): string
{
$columns = array_map(
$this->_driver->quoteIdentifier(...),
$data['columns'],
);
if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) {
return $prefix . sprintf(
' FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s',
implode(', ', $columns),
$this->_driver->quoteIdentifier($data['references'][0]),
$this->_convertConstraintColumns($data['references'][1]),
$this->_foreignOnClause($data['update']),
$this->_foreignOnClause($data['delete']),
);
}
return $prefix . ' (' . implode(', ', $columns) . ')';
}
/**
* @inheritDoc
*/
public function createTableSql(TableSchema $schema, array $columns, array $constraints, array $indexes): array
{
$content = array_merge($columns, $constraints);
$content = implode(",\n", array_filter($content));
$tableName = $this->_driver->quoteIdentifier($schema->name());
$out = [];
$out[] = sprintf("CREATE TABLE %s (\n%s\n)", $tableName, $content);
foreach ($indexes as $index) {
$out[] = $index;
}
foreach ($schema->columns() as $name) {
$column = $schema->getColumn($name);
$comment = $column['comment'] ?? null;
if ($comment !== null) {
$out[] = $this->columnCommentSql($schema, $name, $comment);
}
}
return $out;
}
/**
* Generate the SQL to create a column comment.
*
* @param \Cake\Database\Schema\TableSchema $schema The table schema.
* @param string $name The column name.
* @param string $comment The column comment.
* @return string
*/
protected function columnCommentSql(TableSchema $schema, string $name, string $comment): string
{
$tableName = $this->_driver->quoteIdentifier($schema->name());
$columnName = $this->_driver->quoteIdentifier($name);
$comment = $this->_driver->schemaValue($comment);
return sprintf(
"EXEC sp_addextendedproperty N'MS_Description', %s, N'SCHEMA', N'dbo', N'TABLE', %s, N'COLUMN', %s;",
$comment,
$tableName,
$columnName,
);
}
/**
* @inheritDoc
*/
public function truncateTableSql(TableSchema $schema): array
{
$name = $this->_driver->quoteIdentifier($schema->name());
$queries = [
sprintf('DELETE FROM %s', $name),
];
// Restart identity sequences
$pk = $schema->getPrimaryKey();
if (count($pk) === 1) {
$column = $schema->getColumn($pk[0]);
assert($column !== null);
if (in_array($column['type'], ['integer', 'biginteger'])) {
$queries[] = sprintf(
"IF EXISTS (SELECT * FROM sys.identity_columns WHERE OBJECT_NAME(OBJECT_ID) = '%s' AND " .
"last_value IS NOT NULL) DBCC CHECKIDENT('%s', RESEED, 0)",
$schema->name(),
$schema->name(),
);
}
}
return $queries;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,381 @@
<?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.5.0
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
use Cake\Datasource\SchemaInterface;
/**
* An interface used by database TableSchema objects.
*
* @method \Cake\Database\Schema\Column column(string $name)
* @method \Cake\Database\Schema\Index index(string $name)
* @method \Cake\Database\Schema\Constraint constraint(string $name)
*/
interface TableSchemaInterface extends SchemaInterface
{
/**
* Binary column type
*
* @var string
*/
public const TYPE_BINARY = 'binary';
/**
* Binary UUID column type
*
* @var string
*/
public const TYPE_BINARY_UUID = 'binaryuuid';
/**
* Date column type
*
* @var string
*/
public const TYPE_DATE = 'date';
/**
* Datetime column type
*
* @var string
*/
public const TYPE_DATETIME = 'datetime';
/**
* Datetime with fractional seconds column type
*
* @var string
*/
public const TYPE_DATETIME_FRACTIONAL = 'datetimefractional';
/**
* Time column type
*
* @var string
*/
public const TYPE_TIME = 'time';
/**
* Year column type
*
* Currently only implemented in MySQL
*
* @var string
*/
public const TYPE_YEAR = 'year';
/**
* Timestamp column type
*
* @var string
*/
public const TYPE_TIMESTAMP = 'timestamp';
/**
* Timestamp with fractional seconds column type
*
* @var string
*/
public const TYPE_TIMESTAMP_FRACTIONAL = 'timestampfractional';
/**
* Timestamp with time zone column type
*
* @var string
*/
public const TYPE_TIMESTAMP_TIMEZONE = 'timestamptimezone';
/**
* Datetime interval. Only implemented in postgres.
*
* @var string
*/
public const TYPE_INTERVAL = 'interval';
/**
* JSON column type
*
* @var string
*/
public const TYPE_JSON = 'json';
/**
* String column type
*
* @var string
*/
public const TYPE_STRING = 'string';
/**
* Char column type
*
* @var string
*/
public const TYPE_CHAR = 'char';
/**
* Case-insensitive text column type.
*
* Only implemented in postgres
*
* @var string
*/
public const TYPE_CITEXT = 'citext';
/**
* Text column type
*
* @var string
*/
public const TYPE_TEXT = 'text';
/**
* Tiny Integer column type
*
* @var string
*/
public const TYPE_TINYINTEGER = 'tinyinteger';
/**
* Small Integer column type
*
* @var string
*/
public const TYPE_SMALLINTEGER = 'smallinteger';
/**
* Integer column type
*
* @var string
*/
public const TYPE_INTEGER = 'integer';
/**
* Big Integer column type
*
* @var string
*/
public const TYPE_BIGINTEGER = 'biginteger';
/**
* Float column type
*
* @var string
*/
public const TYPE_FLOAT = 'float';
/**
* Decimal column type
*
* @var string
*/
public const TYPE_DECIMAL = 'decimal';
/**
* Boolean column type
*
* @var string
*/
public const TYPE_BOOLEAN = 'boolean';
/**
* UUID column type
*
* @var string
*/
public const TYPE_UUID = 'uuid';
/**
* Native UUID column type
*
* @var string
*/
public const TYPE_NATIVE_UUID = 'nativeuuid';
/**
* Geometry column type
*
* @var string
*/
public const TYPE_GEOMETRY = 'geometry';
/**
* Point column type
*
* @var string
*/
public const TYPE_POINT = 'point';
/**
* Linestring column type
*
* @var string
*/
public const TYPE_LINESTRING = 'linestring';
/**
* Polgon column type
*
* @var string
*/
public const TYPE_POLYGON = 'polygon';
/**
* INET type. Only implemented in postgres.
*
* @var string
*/
public const TYPE_INET = 'inet';
/**
* CIDR type. Only implemented in postgres.
*
* @var string
*/
public const TYPE_CIDR = 'cidr';
/**
* Macaddr type. Only implemented in postgres.
*
* @var string
*/
public const TYPE_MACADDR = 'macaddr';
/**
* Geospatial column types
*
* @var array
*/
public const GEOSPATIAL_TYPES = [
self::TYPE_GEOMETRY,
self::TYPE_POINT,
self::TYPE_LINESTRING,
self::TYPE_POLYGON,
];
/**
* Check whether a table has an autoIncrement column defined.
*
* @return bool
*/
public function hasAutoincrement(): bool;
/**
* Sets whether the table is temporary in the database.
*
* @param bool $temporary Whether the table is to be temporary.
* @return $this
*/
public function setTemporary(bool $temporary);
/**
* Gets whether the table is temporary in the database.
*
* @return bool The current temporary setting.
*/
public function isTemporary(): bool;
/**
* Get the column(s) used for the primary key.
*
* @return array<string> Column name(s) for the primary key. An
* empty list will be returned when the table has no primary key.
*/
public function getPrimaryKey(): array;
/**
* Add an index.
*
* Used to add indexes, and full text indexes in platforms that support
* them.
*
* ### Attributes
*
* - `type` The type of index being added.
* - `columns` The columns in the index.
*
* @param string $name The name of the index.
* @param array<string, mixed>|string $attrs The attributes for the index.
* If string it will be used as `type`.
* @return $this
* @throws \Cake\Database\Exception\DatabaseException
*/
public function addIndex(string $name, array|string $attrs);
/**
* Read information about an index based on name.
*
* @param string $name The name of the index.
* @return array<string, mixed>|null Array of index data, or null
*/
public function getIndex(string $name): ?array;
/**
* Get the names of all the indexes in the table.
*
* @return array<string>
*/
public function indexes(): array;
/**
* Add a constraint.
*
* Used to add constraints to a table. For example primary keys, unique
* keys, check constraints and foreign keys.
*
* ### Attributes
*
* - `type` The type of constraint being added.
* - `columns` The columns in the index.
* - `references` The table, column a foreign key references.
* - `update` The behavior on update. Options are 'restrict', 'setNull', 'cascade', 'noAction'.
* - `delete` The behavior on delete. Options are 'restrict', 'setNull', 'cascade', 'noAction'.
* - `expression` The SQL expression for check constraints.
*
* The default for 'update' & 'delete' is 'cascade'.
*
* @param string $name The name of the constraint.
* @param array<string, mixed>|string $attrs The attributes for the constraint.
* If string it will be used as `type`.
* @return $this
* @throws \Cake\Database\Exception\DatabaseException
*/
public function addConstraint(string $name, array|string $attrs);
/**
* Read information about a constraint based on name.
*
* @param string $name The name of the constraint.
* @return array<string, mixed>|null Array of constraint data, or null
*/
public function getConstraint(string $name): ?array;
/**
* Remove a constraint.
*
* @param string $name Name of the constraint to remove
* @return $this
*/
public function dropConstraint(string $name);
/**
* Get the names of all the constraints in the table.
*
* @return array<string>
*/
public function constraints(): array;
}
+155
View File
@@ -0,0 +1,155 @@
<?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)
* @copyright Copyright (c) Cake Software Foundation, Inc.
* (https://github.com/cakephp/migrations/tree/master/LICENSE.txt)
* @link https://cakephp.org CakePHP(tm) Project
* @since 5.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
/**
* UniqueKey class
*
* Models a unique key constraint, and provides methods to set driver specific attributes.
*/
class UniqueKey extends Constraint
{
/**
* Constructor
*
* @param string $name The name of the constraint.
* @param array<string> $columns The columns to constraint.
* @param array<string, int>|null $length The length of the columns, if applicable.
*/
public function __construct(
protected string $name,
protected array $columns,
protected ?array $length = null,
) {
$this->type = self::UNIQUE;
}
/**
* Sets the constraint columns.
*
* @param array<string>|string $columns Columns
* @return $this
*/
public function setColumns(string|array $columns)
{
$this->columns = (array)$columns;
return $this;
}
/**
* Gets the constraint columns.
*
* @return ?array<string>
*/
public function getColumns(): ?array
{
return $this->columns;
}
/**
* Sets the constraint type.
*
* @param string $type Type
* @return $this
*/
public function setType(string $type)
{
$this->type = $type;
return $this;
}
/**
* Gets the constraint type.
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Sets the constraint name.
*
* @param string $name Name
* @return $this
*/
public function setName(string $name)
{
$this->name = $name;
return $this;
}
/**
* Gets the constraint name.
*
* @return ?string
*/
public function getName(): ?string
{
return $this->name;
}
/**
* Sets the constraint length.
*
* In MySQL unique constraints can have limit clauses to control the number of
* characters indexed in text and char columns.
*
* @param array<string, int> $length array of length values
* @return $this
*/
public function setLength(array $length)
{
$this->length = $length;
return $this;
}
/**
* Gets the constraint length.
*
* Can be an array of column names and lengths under MySQL.
*
* @return array<string, int>|null
*/
public function getLength(): ?array
{
return $this->length;
}
/**
* Converts a constraint to an array that is compatible
* with the constructor.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'name' => $this->name,
'type' => $this->type,
'columns' => $this->columns,
'length' => $this->length,
];
}
}
+112
View File
@@ -0,0 +1,112 @@
<?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\CachedCollection;
/**
* Schema Cache.
*
* This tool is intended to be used by deployment scripts so that you
* can prevent thundering herd effects on the metadata cache when new
* versions of your application are deployed, or when migrations
* requiring updated metadata are required.
*
* @link https://en.wikipedia.org/wiki/Thundering_herd_problem About the thundering herd problem
*/
class SchemaCache
{
/**
* Schema
*
* @var \Cake\Database\Schema\CachedCollection
*/
protected CachedCollection $_schema;
/**
* Constructor
*
* @param \Cake\Database\Connection $connection Connection name to get the schema for or a connection instance
*/
public function __construct(Connection $connection)
{
$this->_schema = $this->getSchema($connection);
}
/**
* Build metadata.
*
* @param string|null $name The name of the table to build cache data for.
* @return array<string> Returns a list build table caches
*/
public function build(?string $name = null): array
{
if ($name) {
$tables = [$name];
} else {
$tables = $this->_schema->listTables();
}
foreach ($tables as $table) {
$this->_schema->describe($table, ['forceRefresh' => true]);
}
return $tables;
}
/**
* Clear metadata.
*
* @param string|null $name The name of the table to clear cache data for.
* @return array<string> Returns a list of cleared table caches
*/
public function clear(?string $name = null): array
{
if ($name) {
$tables = [$name];
} else {
$tables = $this->_schema->listTables();
}
$cacher = $this->_schema->getCacher();
foreach ($tables as $table) {
$key = $this->_schema->cacheKey($table);
$cacher->delete($key);
}
return $tables;
}
/**
* Helper method to get the schema collection.
*
* @param \Cake\Database\Connection $connection Connection object
* @return \Cake\Database\Schema\CachedCollection
* @throws \RuntimeException If given connection object is not compatible with schema caching
*/
public function getSchema(Connection $connection): CachedCollection
{
$config = $connection->config();
if (empty($config['cacheMetadata'])) {
$connection->cacheMetadata(true);
}
/** @var \Cake\Database\Schema\CachedCollection */
return $connection->getSchemaCollection();
}
}
+165
View File
@@ -0,0 +1,165 @@
<?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\Database\Exception\DatabaseException;
use Cake\Database\Expression\FunctionExpression;
/**
* Responsible for compiling a Query object into its SQL representation
* for SQL Server
*
* @internal
*/
class SqlserverCompiler extends QueryCompiler
{
/**
* {@inheritDoc}
*
* @var array<string, string>
*/
protected array $_templates = [
'delete' => 'DELETE',
'where' => ' WHERE %s',
'group' => ' GROUP BY %s',
'order' => ' %s',
'offset' => ' OFFSET %s ROWS',
'epilog' => ' %s',
'comment' => '/* %s */ ',
];
/**
* {@inheritDoc}
*
* @var array<string>
*/
protected array $_selectParts = [
'comment', 'with', 'select', 'from', 'join', 'where', 'group', 'having', 'window', 'order',
'offset', 'limit', 'union', 'epilog', 'intersect',
];
/**
* Helper function used to build the string representation of a `WITH` clause,
* it constructs the CTE definitions list without generating the `RECURSIVE`
* keyword that is neither required nor valid.
*
* @param array<\Cake\Database\Expression\CommonTableExpression> $parts List of CTEs to be transformed to string
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
* @return string
*/
protected function _buildWithPart(array $parts, Query $query, ValueBinder $binder): string
{
$expressions = [];
foreach ($parts as $cte) {
$expressions[] = $cte->sql($binder);
}
return sprintf('WITH %s ', implode(', ', $expressions));
}
/**
* Generates the INSERT part of a SQL query
*
* To better handle concurrency and low transaction isolation levels,
* we also include an OUTPUT clause so we can ensure we get the inserted
* row's data back.
*
* @param array $parts The parts to build
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
* @return string
*/
protected function _buildInsertPart(array $parts, Query $query, ValueBinder $binder): string
{
if (!isset($parts[0])) {
throw new DatabaseException(
'Could not compile insert query. No table was specified. ' .
'Use `into()` to define a table.',
);
}
$table = $parts[0];
$columns = $this->_stringifyExpressions($parts[1], $binder);
$modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder);
return sprintf(
'INSERT%s INTO %s (%s) OUTPUT INSERTED.*',
$modifiers,
$table,
implode(', ', $columns),
);
}
/**
* Generates the LIMIT part of a SQL query
*
* @param int $limit the limit clause
* @param \Cake\Database\Query $query The query that is being compiled
* @return string
*/
protected function _buildLimitPart(int $limit, Query $query): string
{
if ($query->clause('offset') === null) {
return '';
}
return sprintf(' FETCH FIRST %d ROWS ONLY', $limit);
}
/**
* Helper function used to build the string representation of a HAVING clause,
* it constructs the field list taking care of aliasing and
* converting expression objects to string.
*
* @param array $parts list of fields to be transformed to string
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
* @return string
*/
protected function _buildHavingPart(array $parts, Query $query, ValueBinder $binder): string
{
$selectParts = $query->clause('select');
foreach ($selectParts as $selectKey => $selectPart) {
if (!$selectPart instanceof FunctionExpression) {
continue;
}
foreach ($parts as $k => $p) {
if (!is_string($p)) {
continue;
}
preg_match_all(
'/\b' . trim($selectKey, '[]') . '\b/i',
$p,
$matches,
);
if (empty($matches[0])) {
continue;
}
$parts[$k] = preg_replace(
['/\[|\]/', '/\b' . trim($selectKey, '[]') . '\b/i'],
['', $selectPart->sql($binder)],
$p,
);
}
}
return sprintf(' HAVING %s', implode(', ', $parts));
}
}
@@ -0,0 +1,65 @@
<?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\Statement;
/**
* Statement class meant to be used by an Sqlite driver
*
* @internal
*/
class SqliteStatement extends Statement
{
/**
* @var int|null
*/
protected ?int $affectedRows = null;
/**
* @inheritDoc
*/
public function execute(?array $params = null): bool
{
$this->affectedRows = null;
return parent::execute($params);
}
/**
* @inheritDoc
*/
public function rowCount(): int
{
if ($this->affectedRows !== null) {
return $this->affectedRows;
}
if (
$this->statement->queryString &&
preg_match('/^(?:DELETE|UPDATE|INSERT)/i', $this->statement->queryString)
) {
$changes = $this->_driver->prepare('SELECT CHANGES()');
$changes->execute();
$row = $changes->fetch();
$this->affectedRows = $row ? (int)$row[0] : 0;
} else {
$this->affectedRows = parent::rowCount();
}
return $this->affectedRows;
}
}
@@ -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\Statement;
use PDO;
/**
* Statement class meant to be used by an Sqlserver driver
*
* @internal
*/
class SqlserverStatement extends Statement
{
/**
* @inheritDoc
*/
protected function performBind(string|int $column, mixed $value, int $type): void
{
if ($type === PDO::PARAM_LOB) {
$this->statement->bindParam($column, $value, $type, 0, PDO::SQLSRV_ENCODING_BINARY);
} else {
parent::performBind($column, $value, $type);
}
}
}
+305
View File
@@ -0,0 +1,305 @@
<?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 5.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Statement;
use Cake\Database\Driver;
use Cake\Database\StatementInterface;
use Cake\Database\TypeFactory;
use Cake\Database\TypeInterface;
use Generator;
use InvalidArgumentException;
use PDO;
use PDOStatement;
class Statement implements StatementInterface
{
/**
* @var array<string, int>
*/
protected const MODE_NAME_MAP = [
self::FETCH_TYPE_ASSOC => PDO::FETCH_ASSOC,
self::FETCH_TYPE_NUM => PDO::FETCH_NUM,
self::FETCH_TYPE_OBJ => PDO::FETCH_OBJ,
];
/**
* @var \Cake\Database\Driver
*/
protected Driver $_driver;
/**
* Cached bound parameters used for logging
*
* @var array<mixed>
*/
protected array $params = [];
/**
* @param \PDOStatement $statement PDO statement
* @param \Cake\Database\Driver $driver Database driver
* @param array<\Closure> $resultDecorators Results decorators
*/
public function __construct(
protected PDOStatement $statement,
Driver $driver,
protected array $resultDecorators = [],
) {
$this->_driver = $driver;
}
/**
* @inheritDoc
*/
public function bind(array $params, array $types): void
{
if (!$params) {
return;
}
$anonymousParams = is_int(key($params));
$offset = 1;
foreach ($params as $index => $value) {
$type = $types[$index] ?? null;
if ($anonymousParams) {
$index += $offset;
}
$this->bindValue($index, $value, $type);
}
}
/**
* @inheritDoc
*/
public function bindValue(string|int $column, mixed $value, string|int|null $type = 'string'): void
{
$type ??= 'string';
if (!is_int($type)) {
[$value, $type] = $this->cast($value, $type);
}
$this->params[$column] = $value;
$this->performBind($column, $value, $type);
}
/**
* Converts a give value to a suitable database value based on type and
* return relevant internal statement type.
*
* @param mixed $value The value to cast.
* @param \Cake\Database\TypeInterface|string|int $type The type name or type instance to use.
* @return array List containing converted value and internal type.
* @phpstan-return array{0:mixed, 1:int}
*/
protected function cast(mixed $value, TypeInterface|string|int $type = 'string'): array
{
if (is_string($type)) {
$type = TypeFactory::build($type);
}
if ($type instanceof TypeInterface) {
$value = $type->toDatabase($value, $this->_driver);
$type = $type->toStatement($value, $this->_driver);
}
return [$value, $type];
}
/**
* @inheritDoc
*/
public function getBoundParams(): array
{
return $this->params;
}
/**
* @param string|int $column
* @param mixed $value
* @param int $type
* @return void
*/
protected function performBind(string|int $column, mixed $value, int $type): void
{
$this->statement->bindValue($column, $value, $type);
}
/**
* @inheritDoc
*/
public function execute(?array $params = null): bool
{
return $this->statement->execute($params);
}
/**
* @inheritDoc
*/
public function fetch(string|int $mode = PDO::FETCH_NUM): mixed
{
$mode = $this->convertMode($mode);
$row = $this->statement->fetch($mode);
if ($row === false) {
return false;
}
foreach ($this->resultDecorators as $decorator) {
$row = $decorator($row);
}
return $row;
}
/**
* @inheritDoc
*/
public function fetchAssoc(): array
{
return $this->fetch(PDO::FETCH_ASSOC) ?: [];
}
/**
* @inheritDoc
*/
public function fetchColumn(int $position): mixed
{
$row = $this->fetch(PDO::FETCH_NUM);
if ($row && isset($row[$position])) {
return $row[$position];
}
return false;
}
/**
* @inheritDoc
*/
public function fetchAll(string|int $mode = PDO::FETCH_NUM): array
{
$mode = $this->convertMode($mode);
$rows = $this->statement->fetchAll($mode);
foreach ($this->resultDecorators as $decorator) {
$rows = array_map($decorator, $rows);
}
return $rows;
}
/**
* Converts mode name to PDO constant.
*
* @param string|int $mode Mode name or PDO constant
* @return int
* @throws \InvalidArgumentException
*/
protected function convertMode(string|int $mode): int
{
if (is_int($mode)) {
// We don't try to validate the PDO constants
return $mode;
}
return static::MODE_NAME_MAP[$mode]
??
throw new InvalidArgumentException("Invalid fetch mode requested. Expected 'assoc', 'num' or 'obj'.");
}
/**
* @inheritDoc
*/
public function closeCursor(): void
{
$this->statement->closeCursor();
}
/**
* @inheritDoc
*/
public function rowCount(): int
{
return $this->statement->rowCount();
}
/**
* @inheritDoc
*/
public function columnCount(): int
{
return $this->statement->columnCount();
}
/**
* @inheritDoc
*/
public function errorCode(): string
{
return $this->statement->errorCode() ?: '';
}
/**
* @inheritDoc
*/
public function errorInfo(): array
{
return $this->statement->errorInfo();
}
/**
* @inheritDoc
*/
public function lastInsertId(?string $table = null, ?string $column = null): string|int
{
if ($column && $this->columnCount()) {
$row = $this->fetch(static::FETCH_TYPE_ASSOC);
if ($row && isset($row[$column])) {
return $row[$column];
}
}
return $this->_driver->lastInsertId($table);
}
/**
* Returns prepared query string stored in PDOStatement.
*
* @return string
*/
public function queryString(): string
{
return $this->statement->queryString;
}
/**
* Get the inner iterator
*
* @return \Generator
*/
public function getIterator(): Generator
{
$this->statement->setFetchMode(PDO::FETCH_ASSOC);
foreach ($this->statement as $row) {
foreach ($this->resultDecorators as $decorator) {
$row = $decorator($row);
}
yield $row;
}
$this->closeCursor();
}
}
+215
View File
@@ -0,0 +1,215 @@
<?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 IteratorAggregate;
use PDO;
/**
* @template-extends \IteratorAggregate<array>
*/
interface StatementInterface extends IteratorAggregate
{
/**
* Maps to PDO::FETCH_NUM.
*
* @var string
* @link https://www.php.net/manual/en/pdo.constants.php
*/
public const FETCH_TYPE_NUM = 'num';
/**
* Maps to PDO::FETCH_ASSOC.
*
* @var string
* @link https://www.php.net/manual/en/pdo.constants.php
*/
public const FETCH_TYPE_ASSOC = 'assoc';
/**
* Maps to PDO::FETCH_OBJ.
*
* @var string
* @link https://www.php.net/manual/en/pdo.constants.php
*/
public const FETCH_TYPE_OBJ = 'obj';
/**
* Assign a value to a positional or named variable in prepared query. If using
* positional variables you need to start with index one, if using named params then
* just use the name in any order.
*
* It is not allowed to combine positional and named variables in the same statement.
*
* ### Examples:
*
* ```
* $statement->bindValue(1, 'a title');
* $statement->bindValue('active', true, 'boolean');
* $statement->bindValue(5, new \DateTime(), 'date');
* ```
*
* @param string|int $column name or param position to be bound
* @param mixed $value The value to bind to variable in query
* @param string|int|null $type name of configured Type class
* @return void
*/
public function bindValue(string|int $column, mixed $value, string|int|null $type = 'string'): void;
/**
* Closes the cursor, enabling the statement to be executed again.
*
* This behaves the same as `PDOStatement::closeCursor()`.
*
* @return void
*/
public function closeCursor(): void;
/**
* Returns the number of columns in the result set.
*
* This behaves the same as `PDOStatement::columnCount()`.
*
* @return int
* @link https://php.net/manual/en/pdostatement.columncount.php
*/
public function columnCount(): int;
/**
* Fetch the SQLSTATE associated with the last operation on the statement handle.
*
* This behaves the same as `PDOStatement::errorCode()`.
*
* @return string
* @link https://www.php.net/manual/en/pdostatement.errorcode.php
*/
public function errorCode(): string;
/**
* Fetch extended error information associated with the last operation on the statement handle.
*
* This behaves the same as `PDOStatement::errorInfo()`.
*
* @return array
* @link https://www.php.net/manual/en/pdostatement.errorinfo.php
*/
public function errorInfo(): array;
/**
* Executes the statement by sending the SQL query to the database. It can optionally
* take an array or arguments to be bound to the query variables. Please note
* that binding parameters from this method will not perform any custom type conversion
* as it would normally happen when calling `bindValue`.
*
* @param array|null $params list of values to be bound to query
* @return bool true on success, false otherwise
*/
public function execute(?array $params = null): bool;
/**
* Fetches the next row from a result set
* and converts fields to types based on TypeMap.
*
* This behaves the same as `PDOStatement::fetch()`.
*
* @param string|int $mode PDO::FETCH_* constant or fetch mode name.
* Valid names are 'assoc', 'num' or 'obj'.
* @return mixed
* @throws \InvalidArgumentException
* @link https://www.php.net/manual/en/pdo.constants.php
*/
public function fetch(string|int $mode = PDO::FETCH_NUM): mixed;
/**
* Fetches the remaining rows from a result set
* and converts fields to types based on TypeMap.
*
* This behaves the same as `PDOStatement::fetchAll()`.
*
* @param string|int $mode PDO::FETCH_* constant or fetch mode name.
* Valid names are 'assoc', 'num' or 'obj'.
* @return array
* @throws \InvalidArgumentException
* @link https://www.php.net/manual/en/pdo.constants.php
*/
public function fetchAll(string|int $mode = PDO::FETCH_NUM): array;
/**
* Fetches the next row from a result set using PDO::FETCH_NUM
* and converts fields to types based on TypeMap.
*
* This behaves the same as `PDOStatement::fetch()` except only
* a specific column from the row is returned.
*
* @param int $position Column index in result row.
* @return mixed
*/
public function fetchColumn(int $position): mixed;
/**
* Fetches the next row from a result set using PDO::FETCH_ASSOC
* and converts fields to types based on TypeMap.
*
* This behaves the same as `PDOStatement::fetch()` except an
* empty array is returned instead of false.
*
* @return array
*/
public function fetchAssoc(): array;
/**
* Returns the number of rows affected by the last SQL statement.
*
* This behaves the same as `PDOStatement::rowCount()`.
*
* @return int
* @link https://www.php.net/manual/en/pdostatement.rowcount.php
*/
public function rowCount(): int;
/**
* Binds a set of values to statement object with corresponding type.
*
* @param array $params list of values to be bound
* @param array $types list of types to be used, keys should match those in $params
* @return void
*/
public function bind(array $params, array $types): void;
/**
* Returns the latest primary inserted using this statement.
*
* @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): string|int;
/**
* Returns prepared query string.
*
* @return string
*/
public function queryString(): string;
/**
* Get the bound params.
*
* @return array
*/
public function getBoundParams(): array;
}
+80
View File
@@ -0,0 +1,80 @@
<?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\Type;
use Cake\Database\Driver;
use Cake\Database\TypeInterface;
use PDO;
/**
* Base type class.
*/
abstract class BaseType implements TypeInterface
{
/**
* Identifier name for this type
*
* @var string|null
*/
protected ?string $_name = null;
/**
* Constructor
*
* @param string|null $name The name identifying this type
*/
public function __construct(?string $name = null)
{
$this->_name = $name;
}
/**
* @inheritDoc
*/
public function getName(): ?string
{
return $this->_name;
}
/**
* @inheritDoc
*/
public function getBaseType(): ?string
{
return $this->_name;
}
/**
* @inheritDoc
*/
public function toStatement(mixed $value, Driver $driver): int
{
if ($value === null) {
return PDO::PARAM_NULL;
}
return PDO::PARAM_STR;
}
/**
* @inheritDoc
*/
public function newId(): mixed
{
return null;
}
}
@@ -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.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Type;
use Cake\Database\Driver;
/**
* Denotes type objects capable of converting many values from their original
* database representation to php values.
*/
interface BatchCastingInterface
{
/**
* Returns an array of the values converted to the PHP representation of
* this type.
*
* @param array $values The original array of values containing the fields to be casted
* @param array<string> $fields The field keys to cast
* @param \Cake\Database\Driver $driver Object from which database preferences and configuration will be extracted.
* @return array<string, mixed>
*/
public function manyToPHP(array $values, array $fields, Driver $driver): array;
}
+88
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\Type;
use Cake\Core\Exception\CakeException;
use Cake\Database\Driver;
use PDO;
/**
* Binary type converter.
*
* Use to convert binary data between PHP and the database types.
*/
class BinaryType extends BaseType
{
/**
* Convert binary data into the database format.
*
* Binary data is not altered before being inserted into the database.
* As PDO will handle reading file handles.
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return resource|string
*/
public function toDatabase(mixed $value, Driver $driver): mixed
{
return $value;
}
/**
* Convert binary into resource handles
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return resource|null
* @throws \Cake\Core\Exception\CakeException
*/
public function toPHP(mixed $value, Driver $driver): mixed
{
if ($value === null) {
return null;
}
if (is_string($value)) {
return fopen('data:text/plain;base64,' . base64_encode($value), 'rb') ?: null;
}
if (is_resource($value)) {
return $value;
}
throw new CakeException(sprintf('Unable to convert `%s` into binary.', gettype($value)));
}
/**
* @inheritDoc
*/
public function toStatement(mixed $value, Driver $driver): int
{
return PDO::PARAM_LOB;
}
/**
* Marshals flat data into PHP objects.
*
* Most useful for converting request data into PHP objects
* that make sense for the rest of the ORM/Database layers.
*
* @param mixed $value The value to convert.
* @return mixed Converted value.
*/
public function marshal(mixed $value): mixed
{
return $value;
}
}
+143
View File
@@ -0,0 +1,143 @@
<?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\Type;
use Cake\Core\Exception\CakeException;
use Cake\Database\Driver;
use Cake\Utility\Text;
use PDO;
/**
* Binary UUID type converter.
*
* Use to convert binary uuid data between PHP and the database types.
*/
class BinaryUuidType extends BaseType
{
/**
* Convert binary uuid data into the database format.
*
* Binary data is not altered before being inserted into the database.
* As PDO will handle reading file handles.
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return mixed
*/
public function toDatabase(mixed $value, Driver $driver): mixed
{
if (!is_string($value)) {
return $value;
}
$length = strlen($value);
if ($length !== 36 && $length !== 32) {
return null;
}
return $this->convertStringToBinaryUuid($value);
}
/**
* Generate a new binary UUID
*
* @return string A new primary key value.
*/
public function newId(): string
{
return Text::uuid();
}
/**
* Convert binary uuid into resource handles
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return resource|string|null
* @throws \Cake\Core\Exception\CakeException
*/
public function toPHP(mixed $value, Driver $driver): mixed
{
if ($value === null) {
return null;
}
if (is_string($value)) {
return $this->convertBinaryUuidToString($value);
}
if (is_resource($value)) {
return $value;
}
throw new CakeException(sprintf('Unable to convert %s into binary uuid.', gettype($value)));
}
/**
* @inheritDoc
*/
public function toStatement(mixed $value, Driver $driver): int
{
return PDO::PARAM_LOB;
}
/**
* Marshals flat data into PHP objects.
*
* Most useful for converting request data into PHP objects
* that make sense for the rest of the ORM/Database layers.
*
* @param mixed $value The value to convert.
* @return mixed Converted value.
*/
public function marshal(mixed $value): mixed
{
return $value;
}
/**
* Converts a binary uuid to a string representation
*
* @param mixed $binary The value to convert.
* @return string Converted value.
*/
protected function convertBinaryUuidToString(mixed $binary): string
{
$string = unpack('H*', $binary);
assert($string !== false, 'Could not unpack uuid');
/** @var array<int, string> $string */
$string = preg_replace(
'/([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})/',
'$1-$2-$3-$4-$5',
$string,
);
return $string[1];
}
/**
* Converts a string UUID (36 or 32 char) to a binary representation.
*
* @param string $string The value to convert.
* @return string Converted value.
*/
protected function convertStringToBinaryUuid(string $string): string
{
$string = str_replace('-', '', $string);
return pack('H*', $string);
}
}
+122
View File
@@ -0,0 +1,122 @@
<?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.1.2
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Type;
use Cake\Database\Driver;
use InvalidArgumentException;
use PDO;
/**
* Bool type converter.
*
* Use to convert bool data between PHP and the database types.
*/
class BoolType extends BaseType implements BatchCastingInterface
{
/**
* Convert bool data into the database format.
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return bool|null
*/
public function toDatabase(mixed $value, Driver $driver): ?bool
{
if (in_array($value, [true, false, null], true)) {
return $value;
}
if (in_array($value, [1, 0, '1', '0'], true)) {
return (bool)$value;
}
throw new InvalidArgumentException(sprintf(
'Cannot convert value `%s` of type `%s` to bool',
print_r($value, true),
get_debug_type($value),
));
}
/**
* Convert bool values to PHP booleans
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return bool|null
*/
public function toPHP(mixed $value, Driver $driver): ?bool
{
if ($value === null || is_bool($value)) {
return $value;
}
if (!is_numeric($value)) {
return strtolower($value) === 'true';
}
return !empty($value);
}
/**
* @inheritDoc
*/
public function manyToPHP(array $values, array $fields, Driver $driver): array
{
foreach ($fields as $field) {
$value = $values[$field] ?? null;
if ($value === null || is_bool($value)) {
continue;
}
if (!is_numeric($value)) {
$values[$field] = strtolower($value) === 'true';
continue;
}
$values[$field] = !empty($value);
}
return $values;
}
/**
* @inheritDoc
*/
public function toStatement(mixed $value, Driver $driver): int
{
if ($value === null) {
return PDO::PARAM_NULL;
}
return PDO::PARAM_BOOL;
}
/**
* Marshals request data into PHP booleans.
*
* @param mixed $value The value to convert.
* @return bool|null Converted value.
*/
public function marshal(mixed $value): ?bool
{
if ($value === null || $value === '') {
return null;
}
return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
}
}
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Cake\Database\Type;
use Cake\Database\Driver;
use Cake\Database\Schema\TableSchemaInterface;
interface ColumnSchemaAwareInterface
{
/**
* Generate the SQL fragment for a single column in a table.
*
* @param \Cake\Database\Schema\TableSchemaInterface $schema The table schema instance the column is in.
* @param string $column The name of the column.
* @param \Cake\Database\Driver $driver The driver instance being used.
* @return string|null An SQL fragment, or `null` in case the column isn't processed by this type.
*/
public function getColumnSql(TableSchemaInterface $schema, string $column, Driver $driver): ?string;
/**
* Convert a SQL column definition to an abstract type definition.
*
* @param array $definition The column definition.
* @param \Cake\Database\Driver $driver The driver instance being used.
* @return array<string, mixed>|null Array of column information, or `null` in case the column isn't processed by this type.
*/
public function convertColumnDefinition(array $definition, Driver $driver): ?array;
}
@@ -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.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Type;
/**
* Extends DateTimeType with support for fractional seconds up to microseconds.
*/
class DateTimeFractionalType extends DateTimeType
{
/**
* @inheritDoc
*/
protected string $_format = 'Y-m-d H:i:s.u';
}
@@ -0,0 +1,47 @@
<?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\Type;
/**
* Extends DateTimeType with support for time zones.
*/
class DateTimeTimezoneType extends DateTimeType
{
/**
* @inheritDoc
*/
protected string $_format = 'Y-m-d H:i:s.uP';
/**
* {@inheritDoc}
*
* @var array<string>
*/
protected array $_marshalFormats = [
'Y-m-d H:i',
'Y-m-d H:i:s',
'Y-m-d H:i:sP',
'Y-m-d H:i:s.u',
'Y-m-d H:i:s.uP',
'Y-m-d\TH:i',
'Y-m-d\TH:i:s',
'Y-m-d\TH:i:sP',
'Y-m-d\TH:i:s.u',
'Y-m-d\TH:i:s.uP',
];
}
+474
View File
@@ -0,0 +1,474 @@
<?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\Type;
use Cake\Chronos\ChronosDate;
use Cake\Database\Driver;
use Cake\Database\Exception\DatabaseException;
use Cake\I18n\DateTime;
use DateTime as NativeDateTime;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use Exception;
use InvalidArgumentException;
use PDO;
/**
* Datetime type converter.
*
* Use to convert datetime instances to strings & back.
*/
class DateTimeType extends BaseType implements BatchCastingInterface
{
/**
* The DateTime format used when converting to string.
*
* @var string
*/
protected string $_format = 'Y-m-d H:i:s';
/**
* The DateTime formats allowed by `marshal()`.
*
* @var array<string>
*/
protected array $_marshalFormats = [
'Y-m-d H:i',
'Y-m-d H:i:s',
'Y-m-d H:i:s.u',
'Y-m-d\TH:i',
'Y-m-d\TH:i:s',
'Y-m-d\TH:i:sP',
'Y-m-d\TH:i:s.u',
'Y-m-d\TH:i:s.uP',
];
/**
* Whether `marshal()` should use locale-aware parser with `_localeMarshalFormat`.
*
* @var bool
*/
protected bool $_useLocaleMarshal = false;
/**
* The locale-aware format `marshal()` uses when `_useLocaleParser` is true.
*
* See `Cake\I18n\Time::parseDateTime()` for accepted formats.
*
* @var array|string|int|null
*/
protected array|string|int|null $_localeMarshalFormat = null;
/**
* The classname to use when creating objects.
*
* @var class-string<\Cake\I18n\DateTime>|class-string<\DateTimeImmutable>
*/
protected string $_className;
/**
* Database time zone.
*
* @var \DateTimeZone|null
*/
protected ?DateTimeZone $dbTimezone = null;
/**
* User time zone.
*
* @var \DateTimeZone|null
*/
protected ?DateTimeZone $userTimezone = null;
/**
* Default time zone.
*
* @var \DateTimeZone
*/
protected DateTimeZone $defaultTimezone;
/**
* Whether database time zone is kept when converting
*
* @var bool
*/
protected bool $keepDatabaseTimezone = false;
/**
* {@inheritDoc}
*
* @param string|null $name The name identifying this type
*/
public function __construct(?string $name = null)
{
parent::__construct($name);
$this->defaultTimezone = new DateTimeZone(date_default_timezone_get());
$this->_className = class_exists(DateTime::class) ? DateTime::class : DateTimeImmutable::class;
}
/**
* Convert DateTime instance into strings.
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return string|null
*/
public function toDatabase(mixed $value, Driver $driver): ?string
{
if ($value === null || is_string($value)) {
return $value;
}
if (is_int($value) || is_float($value)) {
$class = $this->_className;
$value = new $class('@' . $value);
}
if ($value instanceof ChronosDate) {
return $value->format($this->_format);
}
if (!$value instanceof DateTimeInterface) {
return null;
}
if (
$this->dbTimezone !== null
&& $this->dbTimezone->getName() !== $value->getTimezone()->getName()
) {
if (!$value instanceof DateTimeImmutable) {
$value = clone $value;
}
$value = $value->setTimezone($this->dbTimezone);
}
return $value->format($this->_format);
}
/**
* Set database timezone.
*
* This is the time zone used when converting database strings to DateTime
* instances and converting DateTime instances to database strings.
*
* @see DateTimeType::setKeepDatabaseTimezone
* @param \DateTimeZone|string|null $timezone Database timezone.
* @return $this
*/
public function setDatabaseTimezone(DateTimeZone|string|null $timezone)
{
if (is_string($timezone)) {
$timezone = new DateTimeZone($timezone);
}
$this->dbTimezone = $timezone;
return $this;
}
/**
* Set user timezone.
*
* This is the time zone used when marshaling strings to DateTime instances.
*
* @param \DateTimeZone|string|null $timezone User timezone.
* @return $this
*/
public function setUserTimezone(DateTimeZone|string|null $timezone)
{
if (is_string($timezone)) {
$timezone = new DateTimeZone($timezone);
}
$this->userTimezone = $timezone;
return $this;
}
/**
* {@inheritDoc}
*
* @param mixed $value Value to be converted to PHP equivalent
* @param \Cake\Database\Driver $driver Object from which database preferences and configuration will be extracted
* @return \Cake\I18n\DateTime|\DateTimeImmutable|null
*/
public function toPHP(mixed $value, Driver $driver): DateTime|DateTimeImmutable|null
{
if ($value === null) {
return null;
}
$class = $this->_className;
if (is_numeric($value)) {
$instance = new $class('@' . $value);
} elseif (str_starts_with($value, '0000-00-00')) {
return null;
} else {
$instance = new $class($value, $this->dbTimezone);
}
if (
!$this->keepDatabaseTimezone
&& $instance->getTimezone()
&& $instance->getTimezone()->getName() !== $this->defaultTimezone->getName()
) {
return $instance->setTimezone($this->defaultTimezone);
}
return $instance;
}
/**
* Set whether DateTime object created from database string is converted
* to default time zone.
*
* If your database date times are in a specific time zone that you want
* to keep in the DateTime instance then set this to true.
*
* When false, datetime timezones are converted to default time zone.
* This is default behavior.
*
* @param bool $keep If true, database time zone is kept when converting
* to DateTime instances.
* @return $this
*/
public function setKeepDatabaseTimezone(bool $keep)
{
$this->keepDatabaseTimezone = $keep;
return $this;
}
/**
* @inheritDoc
*/
public function manyToPHP(array $values, array $fields, Driver $driver): array
{
foreach ($fields as $field) {
if (!isset($values[$field])) {
continue;
}
$value = $values[$field];
$class = $this->_className;
if (is_int($value)) {
$instance = new $class('@' . $value);
} elseif (str_starts_with($value, '0000-00-00')) {
$values[$field] = null;
continue;
} else {
$instance = new $class($value, $this->dbTimezone);
}
if (
!$this->keepDatabaseTimezone
&& $instance->getTimezone()
&& $instance->getTimezone()->getName() !== $this->defaultTimezone->getName()
) {
$instance = $instance->setTimezone($this->defaultTimezone);
}
$values[$field] = $instance;
}
return $values;
}
/**
* Convert request data into a datetime object.
*
* @param mixed $value Request data
* @return \DateTimeInterface|null
*/
public function marshal(mixed $value): ?DateTimeInterface
{
if ($value instanceof DateTimeInterface) {
if ($value instanceof NativeDateTime) {
$value = clone $value;
}
/** @var \Datetime|\DateTimeImmutable $value */
return $value->setTimezone($this->defaultTimezone);
}
if ($value instanceof ChronosDate) {
return $value->toNative();
}
$class = $this->_className;
try {
if (is_int($value) || (is_string($value) && ctype_digit($value))) {
$dateTime = new $class('@' . $value);
return $dateTime->setTimezone($this->defaultTimezone);
}
if (is_string($value)) {
if ($this->_useLocaleMarshal) {
$dateTime = $this->_parseLocaleValue($value);
} else {
$dateTime = $this->_parseValue($value);
}
if ($dateTime) {
return $dateTime->setTimezone($this->defaultTimezone);
}
return $dateTime;
}
} catch (Exception) {
return null;
}
if (!is_array($value)) {
return null;
}
$value += [
'year' => null, 'month' => null, 'day' => null,
'hour' => 0, 'minute' => 0, 'second' => 0, 'microsecond' => 0,
];
if (
!is_numeric($value['year']) || !is_numeric($value['month']) || !is_numeric($value['day']) ||
!is_numeric($value['hour']) || !is_numeric($value['minute']) || !is_numeric($value['second']) ||
!is_numeric($value['microsecond'])
) {
return null;
}
if (isset($value['meridian']) && (int)$value['hour'] === 12) {
$value['hour'] = 0;
}
if (isset($value['meridian'])) {
$value['hour'] = strtolower($value['meridian']) === 'am' ? $value['hour'] : $value['hour'] + 12;
}
$format = sprintf(
'%d-%02d-%02d %02d:%02d:%02d.%06d',
$value['year'],
$value['month'],
$value['day'],
$value['hour'],
$value['minute'],
$value['second'],
$value['microsecond'],
);
$dateTime = new $class($format, $value['timezone'] ?? $this->userTimezone);
return $dateTime->setTimezone($this->defaultTimezone);
}
/**
* Sets whether to parse strings passed to `marshal()` using
* the locale-aware format set by `setLocaleFormat()`.
*
* @param bool $enable Whether to enable
* @return $this
*/
public function useLocaleParser(bool $enable = true)
{
if ($enable === false) {
$this->_useLocaleMarshal = $enable;
return $this;
}
if (is_a($this->_className, DateTime::class, true)) {
$this->_useLocaleMarshal = $enable;
return $this;
}
throw new DatabaseException(
sprintf('Cannot use locale parsing with the %s class', $this->_className),
);
}
/**
* Sets the locale-aware format used by `marshal()` when parsing strings.
*
* See `Cake\I18n\Time::parseDateTime()` for accepted formats.
*
* @param array|string $format The locale-aware format
* @see \Cake\I18n\Time::parseDateTime()
* @return $this
*/
public function setLocaleFormat(array|string $format)
{
$this->_localeMarshalFormat = $format;
return $this;
}
/**
* Get the classname used for building objects.
*
* @return class-string<\Cake\I18n\DateTime>|class-string<\DateTimeImmutable>
*/
public function getDateTimeClassName(): string
{
return $this->_className;
}
/**
* Converts a string into a DateTime object after parsing it using the locale
* aware parser with the format set by `setLocaleFormat()`.
*
* @param string $value The value to parse and convert to an object.
* @return \Cake\I18n\DateTime|null
*/
protected function _parseLocaleValue(string $value): ?DateTime
{
/** @var class-string<\Cake\I18n\DateTime> $class */
$class = $this->_className;
return $class::parseDateTime($value, $this->_localeMarshalFormat, $this->userTimezone);
}
/**
* Converts a string into a DateTime object after parsing it using the
* formats in `_marshalFormats`.
*
* @param string $value The value to parse and convert to an object.
* @return \Cake\I18n\DateTime|\DateTimeImmutable|null
*/
protected function _parseValue(string $value): DateTime|DateTimeImmutable|null
{
$class = $this->_className;
foreach ($this->_marshalFormats as $format) {
try {
$dateTime = $class::createFromFormat($format, $value, $this->userTimezone);
// Check for false in case DateTimeImmutable is used
if ($dateTime !== false) {
return $dateTime;
}
} catch (InvalidArgumentException) {
// Chronos wraps DateTimeImmutable::createFromFormat and throws
// exception if parse fails.
continue;
}
}
return null;
}
/**
* @inheritDoc
*/
public function toStatement(mixed $value, Driver $driver): int
{
return PDO::PARAM_STR;
}
}
+280
View File
@@ -0,0 +1,280 @@
<?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\Type;
use Cake\Chronos\ChronosDate;
use Cake\Database\Driver;
use Cake\Database\Exception\DatabaseException;
use Cake\I18n\Date;
use DateTimeInterface;
use Exception;
use InvalidArgumentException;
/**
* Class DateType
*/
class DateType extends BaseType implements BatchCastingInterface
{
/**
* @var string
*/
protected string $_format = 'Y-m-d';
/**
* @var array<string>
*/
protected array $_marshalFormats = [
'Y-m-d',
];
/**
* Whether `marshal()` should use locale-aware parser with `_localeMarshalFormat`.
*
* @var bool
*/
protected bool $_useLocaleMarshal = false;
/**
* The locale-aware format `marshal()` uses when `_useLocaleParser` is true.
*
* See `Cake\I18n\Date::parseDate()` for accepted formats.
*
* @var string|int|null
*/
protected string|int|null $_localeMarshalFormat = null;
/**
* The classname to use when creating objects.
*
* @var class-string<\Cake\Chronos\ChronosDate>
*/
protected string $_className;
/**
* @inheritDoc
*/
public function __construct(?string $name = null)
{
parent::__construct($name);
$this->_className = class_exists(Date::class) ? Date::class : ChronosDate::class;
}
/**
* Convert DateTime instance into strings.
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return string|null
*/
public function toDatabase(mixed $value, Driver $driver): ?string
{
if ($value === null || is_string($value)) {
return $value;
}
if (is_int($value)) {
$class = $this->_className;
$value = new $class('@' . $value);
}
assert(is_object($value) && method_exists($value, 'format'));
return $value->format($this->_format);
}
/**
* {@inheritDoc}
*
* @param mixed $value Value to be converted to PHP equivalent
* @param \Cake\Database\Driver $driver Object from which database preferences and configuration will be extracted
* @return \Cake\Chronos\ChronosDate|null
*/
public function toPHP(mixed $value, Driver $driver): ?ChronosDate
{
if ($value === null) {
return null;
}
$class = $this->_className;
if (is_int($value)) {
$instance = new $class('@' . $value);
} elseif (str_starts_with($value, '0000-00-00')) {
return null;
} else {
$instance = new $class($value);
}
return $instance;
}
/**
* @inheritDoc
*/
public function manyToPHP(array $values, array $fields, Driver $driver): array
{
foreach ($fields as $field) {
if (!isset($values[$field])) {
continue;
}
$value = $values[$field];
$class = $this->_className;
if (is_int($value)) {
$instance = new $class('@' . $value);
} elseif (str_starts_with($value, '0000-00-00')) {
$values[$field] = null;
continue;
} else {
$instance = new $class($value);
}
$values[$field] = $instance;
}
return $values;
}
/**
* Convert request data into a datetime object.
*
* @param mixed $value Request data
* @return \Cake\Chronos\ChronosDate|null
*/
public function marshal(mixed $value): ?ChronosDate
{
if ($value instanceof $this->_className) {
return $value;
}
if ($value instanceof DateTimeInterface || $value instanceof ChronosDate) {
return new $this->_className($value->format($this->_format));
}
$class = $this->_className;
try {
if (is_int($value) || (is_string($value) && ctype_digit($value))) {
return new $class('@' . $value);
}
if (is_string($value)) {
if ($this->_useLocaleMarshal) {
return $this->_parseLocaleValue($value);
}
return $this->_parseValue($value);
}
} catch (Exception) {
return null;
}
if (
!is_array($value) ||
!isset($value['year'], $value['month'], $value['day']) ||
!is_numeric($value['year']) || !is_numeric($value['month']) || !is_numeric($value['day'])
) {
return null;
}
$format = sprintf('%d-%02d-%02d', $value['year'], $value['month'], $value['day']);
return new $class($format);
}
/**
* Sets whether to parse strings passed to `marshal()` using
* the locale-aware format set by `setLocaleFormat()`.
*
* @param bool $enable Whether to enable
* @return $this
*/
public function useLocaleParser(bool $enable = true)
{
if ($enable === false) {
$this->_useLocaleMarshal = $enable;
return $this;
}
if (is_a($this->_className, Date::class, true)) {
$this->_useLocaleMarshal = $enable;
return $this;
}
throw new DatabaseException(
sprintf('Cannot use locale parsing with %s', $this->_className),
);
}
/**
* Sets the locale-aware format used by `marshal()` when parsing strings.
*
* See `Cake\I18n\Date::parseDate()` for accepted formats.
*
* @param string|int $format The locale-aware format
* @see \Cake\I18n\Date::parseDate()
* @return $this
*/
public function setLocaleFormat(string|int $format)
{
$this->_localeMarshalFormat = $format;
return $this;
}
/**
* Get the classname used for building objects.
*
* @return class-string<\Cake\Chronos\ChronosDate>
*/
public function getDateClassName(): string
{
return $this->_className;
}
/**
* @param string $value
* @return \Cake\I18n\Date|null
*/
protected function _parseLocaleValue(string $value): ?Date
{
/** @var class-string<\Cake\I18n\Date> $class */
$class = $this->_className;
return $class::parseDate($value, $this->_localeMarshalFormat);
}
/**
* Converts a string into a DateTime object after parsing it using the
* formats in `_marshalFormats`.
*
* @param string $value The value to parse and convert to an object.
* @return \Cake\Chronos\ChronosDate|null
*/
protected function _parseValue(string $value): ?ChronosDate
{
$class = $this->_className;
foreach ($this->_marshalFormats as $format) {
try {
return $class::createFromFormat($format, $value);
} catch (InvalidArgumentException) {
continue;
}
}
return null;
}
}
+186
View File
@@ -0,0 +1,186 @@
<?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.3.4
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Type;
use Cake\Database\Driver;
use Cake\Database\Exception\DatabaseException;
use Cake\I18n\Number;
use InvalidArgumentException;
use PDO;
use Stringable;
/**
* Decimal type converter.
*
* Use to convert decimal data between PHP and the database types.
*/
class DecimalType extends BaseType implements BatchCastingInterface
{
/**
* The class to use for representing number objects
*
* @var class-string<\Cake\I18n\Number>|string
*/
public static string $numberClass = Number::class;
/**
* Whether numbers should be parsed using a locale aware parser
* when marshaling string inputs.
*
* @var bool
*/
protected bool $_useLocaleParser = false;
/**
* Convert decimal strings into the database format.
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return string|float|int|null
* @throws \InvalidArgumentException
*/
public function toDatabase(mixed $value, Driver $driver): string|float|int|null
{
if ($value === null || $value === '') {
return null;
}
if (is_numeric($value)) {
return $value;
}
if ($value instanceof Stringable) {
$str = (string)$value;
if (is_numeric($str)) {
return $str;
}
}
throw new InvalidArgumentException(sprintf(
'Cannot convert value `%s` of type `%s` to a decimal',
print_r($value, true),
get_debug_type($value),
));
}
/**
* {@inheritDoc}
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return string|null
*/
public function toPHP(mixed $value, Driver $driver): ?string
{
if ($value === null) {
return null;
}
return (string)$value;
}
/**
* @inheritDoc
*/
public function manyToPHP(array $values, array $fields, Driver $driver): array
{
foreach ($fields as $field) {
if (!isset($values[$field])) {
continue;
}
$values[$field] = (string)$values[$field];
}
return $values;
}
/**
* @inheritDoc
*/
public function toStatement(mixed $value, Driver $driver): int
{
return PDO::PARAM_STR;
}
/**
* Marshalls request data into decimal strings.
*
* @param mixed $value The value to convert.
* @return string|null Converted value.
*/
public function marshal(mixed $value): ?string
{
if ($value === null || $value === '') {
return null;
}
if (is_string($value) && $this->_useLocaleParser) {
return $this->_parseValue($value);
}
if (is_numeric($value)) {
return (string)$value;
}
if (is_string($value) && preg_match('/^[0-9,. ]+$/', $value)) {
return $value;
}
return null;
}
/**
* Sets whether to parse numbers passed to the marshal() function
* by using a locale aware parser.
*
* @param bool $enable Whether to enable
* @return $this
* @throws \Cake\Database\Exception\DatabaseException
*/
public function useLocaleParser(bool $enable = true)
{
if ($enable === false) {
$this->_useLocaleParser = $enable;
return $this;
}
if (
static::$numberClass === Number::class ||
is_subclass_of(static::$numberClass, Number::class)
) {
$this->_useLocaleParser = $enable;
return $this;
}
throw new DatabaseException(
sprintf('Cannot use locale parsing with the %s class', static::$numberClass),
);
}
/**
* Converts localized string into a decimal string after parsing it using
* the locale aware parser.
*
* @param string $value The value to parse and convert to an float.
* @return string
*/
protected function _parseValue(string $value): string
{
$class = static::$numberClass;
return (string)$class::parseFloat($value);
}
}
@@ -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 5.0.3
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Type;
/**
* An interface used to clarify that an enum has a label() method instead of having to use
* `name` property.
*/
interface EnumLabelInterface
{
/**
* Label to return as string.
*
* @return string
*/
public function label(): string;
}
+227
View File
@@ -0,0 +1,227 @@
<?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 5.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Type;
use BackedEnum;
use Cake\Database\Driver;
use Cake\Database\Exception\DatabaseException;
use Cake\Database\TypeFactory;
use Cake\Utility\Text;
use InvalidArgumentException;
use PDO;
use ReflectionEnum;
use ReflectionException;
use TypeError;
use ValueError;
/**
* Enum type converter.
*
* Use to convert string data between PHP and the database types.
*/
class EnumType extends BaseType
{
/**
* The type of the enum which is either string or int
*
* @var string
*/
protected string $backingType;
/**
* The enum classname which is associated to the type instance
*
* @var class-string<\BackedEnum>
*/
protected string $enumClassName;
/**
* @param string $name The name identifying this type
* @param class-string<\BackedEnum> $enumClassName The associated enum class name
*/
public function __construct(
string $name,
string $enumClassName,
) {
parent::__construct($name);
$this->enumClassName = $enumClassName;
try {
$reflectionEnum = new ReflectionEnum($enumClassName);
} catch (ReflectionException $e) {
throw new DatabaseException(sprintf(
'Unable to use `%s` for type `%s`. %s.',
$enumClassName,
$name,
$e->getMessage(),
));
}
$namedType = $reflectionEnum->getBackingType();
if ($namedType == null) {
throw new DatabaseException(
sprintf('Unable to use enum `%s` for type `%s`, must be a backed enum.', $enumClassName, $name),
);
}
$this->backingType = (string)$namedType;
}
/**
* Convert enum instances into the database format.
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return string|int|null
* @throws \InvalidArgumentException When the given value is not a valid value for the associated enum
*/
public function toDatabase(mixed $value, Driver $driver): string|int|null
{
if ($value === null) {
return null;
}
if ($value instanceof $this->enumClassName) {
return $value->value;
}
if ($this->backingType === 'int' && is_string($value)) {
$intVal = filter_var($value, FILTER_VALIDATE_INT);
if ($intVal !== false) {
$value = $intVal;
}
}
try {
return $this->enumClassName::from($value)->value;
} catch (ValueError | TypeError $exception) {
if ($exception instanceof TypeError) {
throw new InvalidArgumentException(sprintf(
'Given value `%s` of type `%s` does not match associated `%s` backed enum in `%s`',
print_r($value, true),
get_debug_type($value),
$this->backingType,
$this->enumClassName,
));
}
throw new InvalidArgumentException(sprintf(
'`%s` is not a valid value for `%s`',
$value,
$this->enumClassName,
));
}
}
/**
* Transform DB value to backed enum instance
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return \BackedEnum|null
*/
public function toPHP(mixed $value, Driver $driver): ?BackedEnum
{
if ($value === null) {
return null;
}
if ($this->backingType === 'int' && is_string($value)) {
$intVal = filter_var($value, FILTER_VALIDATE_INT);
if ($intVal !== false) {
$value = $intVal;
}
}
return $this->enumClassName::from($value);
}
/**
* @inheritDoc
*/
public function toStatement(mixed $value, Driver $driver): int
{
if ($this->backingType === 'int') {
return PDO::PARAM_INT;
}
return PDO::PARAM_STR;
}
/**
* Marshals request data
*
* @param mixed $value The value to convert.
* @return \BackedEnum|null Converted value.
*/
public function marshal(mixed $value): ?BackedEnum
{
if ($value === null) {
return null;
}
if ($value instanceof $this->enumClassName) {
return $value;
}
if ($this->backingType === 'int') {
if ($value === '') {
return null;
}
if (is_numeric($value)) {
$value = (int)$value;
}
}
try {
return $this->enumClassName::from($value);
} catch (ValueError | TypeError) {
return null;
}
}
/**
* Create an `EnumType` that is paired with the provided `$enumClassName`.
*
* ### Usage
*
* ```
* // In a table class
* $this->getSchema()->setColumnType('status', EnumType::from(StatusEnum::class));
* ```
*
* @param class-string<\BackedEnum> $enumClassName The enum class name
* @return string
*/
public static function from(string $enumClassName): string
{
$typeName = 'enum-' . strtolower(Text::slug($enumClassName));
$instance = new EnumType($typeName, $enumClassName);
TypeFactory::set($typeName, $instance);
return $typeName;
}
/**
* @return class-string<\BackedEnum>
*/
public function getEnumClassName(): string
{
return $this->enumClassName;
}
}
@@ -0,0 +1,80 @@
<?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.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Type;
use Cake\Database\TypeFactory;
/**
* Offers a method to convert values to ExpressionInterface objects
* if the type they should be converted to implements ExpressionTypeInterface
*/
trait ExpressionTypeCasterTrait
{
/**
* Conditionally converts the passed value to an ExpressionInterface object
* if the type class implements the ExpressionTypeInterface. Otherwise,
* returns the value unmodified.
*
* @param mixed $value The value to convert to ExpressionInterface
* @param string|null $type The type name
* @return mixed
*/
protected function _castToExpression(mixed $value, ?string $type = null): mixed
{
if ($type === null) {
return $value;
}
$baseType = str_replace('[]', '', $type);
$converter = TypeFactory::build($baseType);
if (!$converter instanceof ExpressionTypeInterface) {
return $value;
}
$multi = $type !== $baseType;
if ($multi) {
/** @var \Cake\Database\Type\ExpressionTypeInterface&\Cake\Database\TypeInterface $converter */
return array_map($converter->toExpression(...), $value);
}
return $converter->toExpression($value);
}
/**
* Returns an array with the types that require values to
* be casted to expressions, out of the list of type names
* passed as parameter.
*
* @param array $types List of type names
* @return array
*/
protected function _requiresToExpressionCasting(array $types): array
{
$result = [];
$types = array_filter($types);
foreach ($types as $k => $type) {
$object = TypeFactory::build($type);
if ($object instanceof ExpressionTypeInterface) {
$result[$k] = $object;
}
}
return $result;
}
}
@@ -0,0 +1,36 @@
<?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.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Type;
use Cake\Database\ExpressionInterface;
/**
* An interface used by Type objects to signal whether the value should
* be converted to an ExpressionInterface instead of a string when sent
* to the database.
*/
interface ExpressionTypeInterface
{
/**
* Returns an ExpressionInterface object for the given value that can
* be used in queries.
*
* @param mixed $value The value to be converted to an expression
* @return \Cake\Database\ExpressionInterface
*/
public function toExpression(mixed $value): ExpressionInterface;
}
+166
View File
@@ -0,0 +1,166 @@
<?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\Type;
use Cake\Database\Driver;
use Cake\Database\Exception\DatabaseException;
use Cake\I18n\Number;
use PDO;
/**
* Float type converter.
*
* Use to convert float/decimal data between PHP and the database types.
*/
class FloatType extends BaseType implements BatchCastingInterface
{
/**
* The class to use for representing number objects
*
* @var string
*/
public static string $numberClass = Number::class;
/**
* Whether numbers should be parsed using a locale aware parser
* when marshaling string inputs.
*
* @var bool
*/
protected bool $_useLocaleParser = false;
/**
* Convert integer data into the database format.
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return float|null
*/
public function toDatabase(mixed $value, Driver $driver): ?float
{
if ($value === null || $value === '') {
return null;
}
return (float)$value;
}
/**
* {@inheritDoc}
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return float|null
*/
public function toPHP(mixed $value, Driver $driver): ?float
{
if ($value === null) {
return null;
}
return (float)$value;
}
/**
* @inheritDoc
*/
public function manyToPHP(array $values, array $fields, Driver $driver): array
{
foreach ($fields as $field) {
if (!isset($values[$field])) {
continue;
}
$values[$field] = (float)$values[$field];
}
return $values;
}
/**
* @inheritDoc
*/
public function toStatement(mixed $value, Driver $driver): int
{
return PDO::PARAM_STR;
}
/**
* Marshals request data into PHP floats.
*
* @param mixed $value The value to convert.
* @return string|float|null Converted value.
*/
public function marshal(mixed $value): string|float|null
{
if ($value === null || $value === '') {
return null;
}
if (is_string($value) && $this->_useLocaleParser) {
return $this->_parseValue($value);
}
if (is_numeric($value)) {
return (float)$value;
}
if (is_string($value) && preg_match('/^[0-9,. ]+$/', $value)) {
return $value;
}
return null;
}
/**
* Sets whether to parse numbers passed to the marshal() function
* by using a locale aware parser.
*
* @param bool $enable Whether to enable
* @return $this
*/
public function useLocaleParser(bool $enable = true)
{
if ($enable === false) {
$this->_useLocaleParser = $enable;
return $this;
}
if (
static::$numberClass === Number::class ||
is_subclass_of(static::$numberClass, Number::class)
) {
$this->_useLocaleParser = $enable;
return $this;
}
throw new DatabaseException(
sprintf('Cannot use locale parsing with the %s class', static::$numberClass),
);
}
/**
* Converts a string into a float point after parsing it using the locale
* aware parser.
*
* @param string $value The value to parse and convert to an float.
* @return float
*/
protected function _parseValue(string $value): float
{
$class = static::$numberClass;
return $class::parseFloat($value);
}
}
+122
View File
@@ -0,0 +1,122 @@
<?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\Type;
use Cake\Database\Driver;
use InvalidArgumentException;
use PDO;
/**
* Integer type converter.
*
* Use to convert integer data between PHP and the database types.
*/
class IntegerType extends BaseType implements BatchCastingInterface
{
/**
* Checks if the value is not a numeric value
*
* @throws \InvalidArgumentException
* @param mixed $value Value to check
* @return void
*/
protected function checkNumeric(mixed $value): void
{
if (!is_numeric($value) && !is_bool($value)) {
throw new InvalidArgumentException(sprintf(
'Cannot convert value `%s` of type `%s` to int',
print_r($value, true),
get_debug_type($value),
));
}
}
/**
* Convert integer data into the database format.
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return int|null
*/
public function toDatabase(mixed $value, Driver $driver): ?int
{
if ($value === null || $value === '') {
return null;
}
$this->checkNumeric($value);
return (int)$value;
}
/**
* {@inheritDoc}
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return int|null
*/
public function toPHP(mixed $value, Driver $driver): ?int
{
if ($value === null) {
return null;
}
return (int)$value;
}
/**
* @inheritDoc
*/
public function manyToPHP(array $values, array $fields, Driver $driver): array
{
foreach ($fields as $field) {
if (!isset($values[$field])) {
continue;
}
$this->checkNumeric($values[$field]);
$values[$field] = (int)$values[$field];
}
return $values;
}
/**
* @inheritDoc
*/
public function toStatement(mixed $value, Driver $driver): int
{
return PDO::PARAM_INT;
}
/**
* Marshals request data into PHP integers.
*
* @param mixed $value The value to convert.
* @return int|null Converted value.
*/
public function marshal(mixed $value): ?int
{
if ($value === '' || !is_numeric($value)) {
return null;
}
return (int)$value;
}
}
+143
View File
@@ -0,0 +1,143 @@
<?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.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Type;
use Cake\Database\Driver;
use InvalidArgumentException;
use PDO;
/**
* JSON type converter.
*
* Used to convert JSON data between PHP and the database types.
*/
class JsonType extends BaseType implements BatchCastingInterface
{
/**
* @var int
*/
protected int $_encodingOptions = 0;
/**
* Flags for json_decode()
*
* @var int
*/
protected int $_decodingOptions = JSON_OBJECT_AS_ARRAY;
/**
* Convert a value data into a JSON string
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return string|null
* @throws \InvalidArgumentException
* @throws \JsonException
*/
public function toDatabase(mixed $value, Driver $driver): ?string
{
if (is_resource($value)) {
throw new InvalidArgumentException('Cannot convert a resource value to JSON');
}
if ($value === null) {
return null;
}
return json_encode($value, JSON_THROW_ON_ERROR | $this->_encodingOptions);
}
/**
* {@inheritDoc}
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return mixed
*/
public function toPHP(mixed $value, Driver $driver): mixed
{
if (!is_string($value)) {
return null;
}
return json_decode($value, flags: $this->_decodingOptions);
}
/**
* @inheritDoc
*/
public function manyToPHP(array $values, array $fields, Driver $driver): array
{
foreach ($fields as $field) {
if (!isset($values[$field])) {
continue;
}
$values[$field] = json_decode($values[$field], flags: $this->_decodingOptions);
}
return $values;
}
/**
* @inheritDoc
*/
public function toStatement(mixed $value, Driver $driver): int
{
return PDO::PARAM_STR;
}
/**
* Marshals request data into a JSON compatible structure.
*
* @param mixed $value The value to convert.
* @return mixed Converted value.
*/
public function marshal(mixed $value): mixed
{
return $value;
}
/**
* Set json_encode options.
*
* @param int $options Encoding flags. Use JSON_* flags. Set `0` to reset.
* @return $this
* @see https://www.php.net/manual/en/function.json-encode.php
*/
public function setEncodingOptions(int $options)
{
$this->_encodingOptions = $options;
return $this;
}
/**
* Set json_decode() options.
*
* By default, the value is `JSON_OBJECT_AS_ARRAY`.
*
* @param int $options Decoding flags. Use JSON_* flags. Set `0` to reset.
* @return $this
*/
public function setDecodingOptions(int $options)
{
$this->_decodingOptions = $options;
return $this;
}
}
@@ -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 3.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Type;
/**
* An interface used by Type objects to signal whether the casting
* is actually required.
*/
interface OptionalConvertInterface
{
/**
* Returns whether the cast to PHP is required to be invoked, since
* it is not a identity function.
*
* @return bool
*/
public function requiresToPhpCast(): bool;
}
+107
View File
@@ -0,0 +1,107 @@
<?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.1.2
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Type;
use Cake\Database\Driver;
use InvalidArgumentException;
use PDO;
use Stringable;
/**
* String type converter.
*
* Use to convert string data between PHP and the database types.
*/
class StringType extends BaseType implements OptionalConvertInterface
{
/**
* Convert string data into the database format.
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return string|null
*/
public function toDatabase(mixed $value, Driver $driver): ?string
{
if ($value === null || is_string($value)) {
return $value;
}
if ($value instanceof Stringable) {
return (string)$value;
}
if (is_scalar($value)) {
return (string)$value;
}
throw new InvalidArgumentException(sprintf(
'Cannot convert value `%s` of type `%s` to string',
print_r($value, true),
get_debug_type($value),
));
}
/**
* Convert string values to PHP strings.
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return string|null
*/
public function toPHP(mixed $value, Driver $driver): ?string
{
if ($value === null) {
return null;
}
return (string)$value;
}
/**
* @inheritDoc
*/
public function toStatement(mixed $value, Driver $driver): int
{
return PDO::PARAM_STR;
}
/**
* Marshals request data into PHP strings.
*
* @param mixed $value The value to convert.
* @return string|null Converted value.
*/
public function marshal(mixed $value): ?string
{
if ($value === null || is_array($value)) {
return null;
}
return (string)$value;
}
/**
* {@inheritDoc}
*
* @return bool False as database results are returned already as strings
*/
public function requiresToPhpCast(): bool
{
return false;
}
}
+260
View File
@@ -0,0 +1,260 @@
<?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\Type;
use Cake\Chronos\ChronosTime;
use Cake\Core\Exception\CakeException;
use Cake\Database\Driver;
use Cake\I18n\Time;
use DateTimeInterface;
use InvalidArgumentException;
/**
* Time type converter.
*
* Use to convert time instances to strings and back.
*/
class TimeType extends BaseType implements BatchCastingInterface
{
/**
* The PHP Time format used when converting to string.
*
* @var string
*/
protected string $_format = 'H:i:s';
/**
* Whether `marshal()` should use locale-aware parser with `_localeMarshalFormat`.
*
* @var bool
*/
protected bool $_useLocaleMarshal = false;
/**
* The locale-aware format `marshal()` uses when `_useLocaleParser` is true.
*
* See `Cake\I18n\Time::parseTime()` for accepted formats.
*
* @var string|int|null
*/
protected string|int|null $_localeMarshalFormat = null;
/**
* The classname to use when creating objects.
*
* @var class-string<\Cake\Chronos\ChronosTime>
*/
protected string $_className;
/**
* Constructor
*
* @param string|null $name The name identifying this type.
* @param class-string<\Cake\Chronos\ChronosTime>|null $className Class name for time representation.
*/
public function __construct(?string $name = null, ?string $className = null)
{
parent::__construct($name);
if ($className === null) {
$className = class_exists(Time::class) ? Time::class : ChronosTime::class;
}
$this->_className = $className;
}
/**
* Convert request data into a datetime object.
*
* @param mixed $value Request data
* @return \Cake\Chronos\ChronosTime|null
*/
public function marshal(mixed $value): ?ChronosTime
{
if ($value instanceof $this->_className) {
return $value;
}
if ($value instanceof DateTimeInterface || $value instanceof ChronosTime) {
return new $this->_className($value->format($this->_format));
}
if (is_string($value)) {
if ($this->_useLocaleMarshal) {
return $this->_parseLocalTimeValue($value);
}
return $this->_parseTimeValue($value);
}
if (!is_array($value)) {
return null;
}
$value += ['hour' => null, 'minute' => null, 'second' => 0, 'microsecond' => 0];
if (
!is_numeric($value['hour']) || !is_numeric($value['minute']) || !is_numeric($value['second']) ||
!is_numeric($value['microsecond'])
) {
return null;
}
if (isset($value['meridian']) && (int)$value['hour'] === 12) {
$value['hour'] = 0;
}
if (isset($value['meridian'])) {
$value['hour'] = strtolower($value['meridian']) === 'am' ? $value['hour'] : $value['hour'] + 12;
}
$format = sprintf(
'%02d:%02d:%02d.%06d',
$value['hour'],
$value['minute'],
$value['second'],
$value['microsecond'],
);
return new $this->_className($format);
}
/**
* @inheritDoc
*/
public function manyToPHP(array $values, array $fields, Driver $driver): array
{
foreach ($fields as $field) {
if (!isset($values[$field])) {
continue;
}
$value = $values[$field];
$instance = new $this->_className($value);
$values[$field] = $instance;
}
return $values;
}
/**
* Convert time data into the database time format.
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return mixed
*/
public function toDatabase(mixed $value, Driver $driver): mixed
{
if ($value === null || is_string($value)) {
return $value;
}
assert(method_exists($value, 'format'));
return $value->format($this->_format);
}
/**
* Convert time values to PHP time instances
*
* @param mixed $value The value to convert.
* @param \Cake\Database\Driver $driver The driver instance to convert with.
* @return \Cake\Chronos\ChronosTime|null
*/
public function toPHP(mixed $value, Driver $driver): ?ChronosTime
{
if ($value === null) {
return null;
}
return new $this->_className($value);
}
/**
* Get the classname used for building objects.
*
* @return class-string<\Cake\Chronos\ChronosTime>
*/
public function getTimeClassName(): string
{
return $this->_className;
}
/**
* Converts a string into a Time object
*
* @param string $value The value to parse and convert to an object.
* @return \Cake\Chronos\ChronosTime|null
*/
protected function _parseTimeValue(string $value): ?ChronosTime
{
try {
return $this->_className::parse($value);
} catch (InvalidArgumentException) {
return null;
}
}
/**
* Converts a string into a Time object after parsing it using the locale
* aware parser with the format set by `setLocaleFormat()`.
*
* @param string $value The value to parse and convert to an object.
* @return \Cake\Chronos\ChronosTime|null
*/
protected function _parseLocalTimeValue(string $value): ?ChronosTime
{
assert(is_a($this->_className, Time::class, true));
return $this->_className::parseTime($value, $this->_localeMarshalFormat);
}
/**
* Sets whether to parse strings passed to `marshal()` using
* the locale-aware format set by `setLocaleFormat()`.
*
* @param bool $enable Whether to enable
* @return $this
*/
public function useLocaleParser(bool $enable = true)
{
if (
$enable &&
($this->_className !== Time::class && !is_subclass_of($this->_className, Time::class))
) {
throw new CakeException('You must install the `cakephp/i18n` package to use locale aware parsing.');
}
$this->_useLocaleMarshal = $enable;
return $this;
}
/**
* Sets the locale-aware format used by `marshal()` when parsing strings.
*
* See `Cake\I18n\Time::parseTime()` for accepted formats.
*
* @param string|int|null $format The locale-aware format
* @see \Cake\I18n\Time::parseTime()
* @return $this
*/
public function setLocaleFormat(string|int|null $format)
{
$this->_localeMarshalFormat = $format;
return $this;
}
}
+67
View File
@@ -0,0 +1,67 @@
<?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\Type;
use Cake\Database\Driver;
use Cake\Utility\Text;
/**
* Provides behavior for the UUID type
*/
class UuidType extends StringType
{
/**
* Casts given value from a PHP type to one acceptable by database
*
* @param mixed $value value to be converted to database equivalent
* @param \Cake\Database\Driver $driver object from which database preferences and configuration will be extracted
* @return string|null
*/
public function toDatabase(mixed $value, Driver $driver): ?string
{
if (in_array($value, [null, '', false], true)) {
return null;
}
return parent::toDatabase($value, $driver);
}
/**
* Generate a new UUID
*
* @return string A new primary key value.
*/
public function newId(): string
{
return Text::uuid();
}
/**
* Marshals request data into a PHP string
*
* @param mixed $value The value to convert.
* @return string|null Converted value.
*/
public function marshal(mixed $value): ?string
{
if ($value === null || $value === '' || is_array($value)) {
return null;
}
return (string)$value;
}
}
+189
View File
@@ -0,0 +1,189 @@
<?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;
/**
* Factory for building database type classes.
*/
class TypeFactory
{
/**
* List of supported database types. A human-readable
* identifier is used as key and a complete namespaced class name as value
* representing the class that will do actual type conversions.
*
* @var array<string, string>
* @phpstan-var array<string, class-string<\Cake\Database\TypeInterface>>
*/
protected static array $_types = [
'biginteger' => Type\IntegerType::class,
'binary' => Type\BinaryType::class,
'binaryuuid' => Type\BinaryUuidType::class,
'boolean' => Type\BoolType::class,
'char' => Type\StringType::class,
'cidr' => Type\StringType::class,
'citext' => Type\StringType::class,
'date' => Type\DateType::class,
'datetime' => Type\DateTimeType::class,
'datetimefractional' => Type\DateTimeFractionalType::class,
'decimal' => Type\DecimalType::class,
'float' => Type\FloatType::class,
'geometry' => Type\StringType::class,
'integer' => Type\IntegerType::class,
'inet' => Type\StringType::class,
'json' => Type\JsonType::class,
'linestring' => Type\StringType::class,
'macaddr' => Type\StringType::class,
'nativeuuid' => Type\UuidType::class,
'point' => Type\StringType::class,
'polygon' => Type\StringType::class,
'smallinteger' => Type\IntegerType::class,
'string' => Type\StringType::class,
'text' => Type\StringType::class,
'time' => Type\TimeType::class,
'timestamp' => Type\DateTimeType::class,
'timestampfractional' => Type\DateTimeFractionalType::class,
'timestamptimezone' => Type\DateTimeTimezoneType::class,
'tinyinteger' => Type\IntegerType::class,
'uuid' => Type\UuidType::class,
'year' => Type\IntegerType::class,
];
/**
* Contains a map of type object instances to be reused if needed.
*
* @var array<\Cake\Database\TypeInterface>
*/
protected static array $_builtTypes = [];
/**
* Returns a Type object capable of converting a type identified by name.
*
* @param string $name type identifier
* @return \Cake\Database\TypeInterface
*/
public static function build(string $name): TypeInterface
{
if (isset(static::$_builtTypes[$name])) {
return static::$_builtTypes[$name];
}
if (!isset(static::$_types[$name])) {
return static::$_builtTypes[$name] = new static::$_types['string']($name);
}
return static::$_builtTypes[$name] = new static::$_types[$name]($name);
}
/**
* Returns an arrays with all the mapped type objects, indexed by name.
*
* @return array<\Cake\Database\TypeInterface>
*/
public static function buildAll(): array
{
foreach (static::$_types as $name => $type) {
static::$_builtTypes[$name] ??= static::build($name);
}
return static::$_builtTypes;
}
/**
* Set TypeInterface instance capable of converting a type identified by $name
*
* @param string $name The type identifier you want to set.
* @param \Cake\Database\TypeInterface $instance The type instance you want to set.
* @return void
*/
public static function set(string $name, TypeInterface $instance): void
{
static::$_builtTypes[$name] = $instance;
}
/**
* Registers a new type identifier and maps it to a fully namespaced classname.
*
* @param string $type Name of type to map.
* @param string $className The classname to register.
* @return void
* @phpstan-param class-string<\Cake\Database\TypeInterface> $className
*/
public static function map(string $type, string $className): void
{
static::$_types[$type] = $className;
unset(static::$_builtTypes[$type]);
}
/**
* Set type to classname mapping.
*
* @param array<string, string> $map List of types to be mapped.
* @return void
* @phpstan-param array<string, class-string<\Cake\Database\TypeInterface>> $map
*/
public static function setMap(array $map): void
{
static::$_types = $map;
static::$_builtTypes = [];
}
/**
* Get the type mapping array.
*
* Deprecated 5.3.0: Argument $type has been deprecated.
* Use getMap() without arguments to get the full map, or getMapped($type) to get a specific type mapping.
*
* @param string|null $type Type name to get mapped class for or null to get map array.
* @return array<string, class-string<\Cake\Database\TypeInterface>>|string|null Configured class name for given $type or map array.
*/
public static function getMap(?string $type = null): array|string|null
{
if ($type === null) {
return static::$_types;
}
trigger_error(
'Calling getMap() with a type argument is deprecated. Use getMapped() instead.',
E_USER_DEPRECATED,
);
return static::$_types[$type] ?? null;
}
/**
* Get mapped class name for a specific type.
*
* @param string $type Type name to get mapped class for.
* @return string|null Configured class name for given $type or null if not found.
* @phpstan-return class-string<\Cake\Database\TypeInterface>|null
*/
public static function getMapped(string $type): ?string
{
return static::$_types[$type] ?? null;
}
/**
* Clears out all created instances and mapped types classes, useful for testing
*
* @return void
*/
public static function clear(): void
{
static::$_types = [];
static::$_builtTypes = [];
}
}
+91
View File
@@ -0,0 +1,91 @@
<?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.2.14
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database;
/**
* Encapsulates all conversion functions for values coming from a database into PHP and
* going from PHP into a database.
*/
interface TypeInterface
{
/**
* Casts given value from a PHP type to one acceptable by a database.
*
* @param mixed $value Value to be converted to a database equivalent.
* @param \Cake\Database\Driver $driver Object from which database preferences and configuration will be extracted.
* @return mixed Given PHP type casted to one acceptable by a database.
*/
public function toDatabase(mixed $value, Driver $driver): mixed;
/**
* Casts given value from a database type to a PHP equivalent.
*
* @param mixed $value Value to be converted to PHP equivalent
* @param \Cake\Database\Driver $driver Object from which database preferences and configuration will be extracted
* @return mixed Given value casted from a database to a PHP equivalent.
*/
public function toPHP(mixed $value, Driver $driver): mixed;
/**
* Get the binding type to use in a PDO statement.
*
* @param mixed $value The value being bound.
* @param \Cake\Database\Driver $driver Object from which database preferences and configuration will be extracted.
* @return int One of PDO::PARAM_* constants.
*/
public function toStatement(mixed $value, Driver $driver): int;
/**
* Marshals flat data into PHP objects.
*
* Most useful for converting request data into PHP objects,
* that make sense for the rest of the ORM/Database layers.
*
* @param mixed $value The value to convert.
* @return mixed Converted value.
*/
public function marshal(mixed $value): mixed;
/**
* Returns the base type name that this class is inheriting.
*
* This is useful when extending base type for adding extra functionality,
* but still want the rest of the framework to use the same assumptions it would
* do about the base type it inherits from.
*
* @return string|null The base type name that this class is inheriting.
*/
public function getBaseType(): ?string;
/**
* Returns type identifier name for this object.
*
* @return string|null The type identifier name for this object.
*/
public function getName(): ?string;
/**
* Generate a new primary key value for a given type.
*
* This method can be used by types to create new primary key values
* when entities are inserted.
*
* @return mixed A new primary key value.
* @see \Cake\Database\Type\UuidType
*/
public function newId(): mixed;
}

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