This commit is contained in:
2024-11-05 12:10:06 +08:00
commit 6b687feee2
3995 changed files with 418583 additions and 0 deletions
+269
View File
@@ -0,0 +1,269 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 1.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
/**
* App is responsible for resource location, and path management.
*
* ### Adding paths
*
* Additional paths for Templates and Plugins are configured with Configure now. See config/app.php for an
* example. The `App.paths.plugins` and `App.paths.templates` variables are used to configure paths for plugins
* and templates respectively. All class based resources should be mapped using your application's autoloader.
*
* ### Inspecting loaded paths
*
* You can inspect the currently loaded paths using `App::classPath('Controller')` for example to see loaded
* controller paths.
*
* It is also possible to inspect paths for plugin classes, for instance, to get
* the path to a plugin's helpers you would call `App::classPath('View/Helper', 'MyPlugin')`
*
* ### Locating plugins
*
* Plugins can be located with App as well. Using Plugin::path('DebugKit') for example, will
* give you the full path to the DebugKit plugin.
*
* @link https://book.cakephp.org/4/en/core-libraries/app.html
*/
class App
{
/**
* Return the class name namespaced. This method checks if the class is defined on the
* application/plugin, otherwise try to load from the CakePHP core
*
* @param string $class Class name
* @param string $type Type of class
* @param string $suffix Class name suffix
* @return string|null Namespaced class name, null if the class is not found.
* @psalm-return class-string|null
*/
public static function className(string $class, string $type = '', string $suffix = ''): ?string
{
if (strpos($class, '\\') !== false) {
return class_exists($class) ? $class : null;
}
[$plugin, $name] = pluginSplit($class);
$fullname = '\\' . str_replace('/', '\\', $type . '\\' . $name) . $suffix;
$base = $plugin ?: Configure::read('App.namespace');
if ($base !== null) {
$base = str_replace('/', '\\', rtrim($base, '\\'));
if (static::_classExistsInBase($fullname, $base)) {
/** @var class-string */
return $base . $fullname;
}
}
if ($plugin || !static::_classExistsInBase($fullname, 'Cake')) {
return null;
}
/** @var class-string */
return 'Cake' . $fullname;
}
/**
* Returns the plugin split name of a class
*
* Examples:
*
* ```
* App::shortName(
* 'SomeVendor\SomePlugin\Controller\Component\TestComponent',
* 'Controller/Component',
* 'Component'
* )
* ```
*
* Returns: SomeVendor/SomePlugin.Test
*
* ```
* App::shortName(
* 'SomeVendor\SomePlugin\Controller\Component\Subfolder\TestComponent',
* 'Controller/Component',
* 'Component'
* )
* ```
*
* Returns: SomeVendor/SomePlugin.Subfolder/Test
*
* ```
* App::shortName(
* 'Cake\Controller\Component\AuthComponent',
* 'Controller/Component',
* 'Component'
* )
* ```
*
* Returns: Auth
*
* @param string $class Class name
* @param string $type Type of class
* @param string $suffix Class name suffix
* @return string Plugin split name of class
*/
public static function shortName(string $class, string $type, string $suffix = ''): string
{
$class = str_replace('\\', '/', $class);
$type = '/' . $type . '/';
$pos = strrpos($class, $type);
if ($pos === false) {
return $class;
}
$pluginName = (string)substr($class, 0, $pos);
$name = (string)substr($class, $pos + strlen($type));
if ($suffix) {
$name = (string)substr($name, 0, -strlen($suffix));
}
$nonPluginNamespaces = [
'Cake',
str_replace('\\', '/', (string)Configure::read('App.namespace')),
];
if (in_array($pluginName, $nonPluginNamespaces, true)) {
return $name;
}
return $pluginName . '.' . $name;
}
/**
* _classExistsInBase
*
* Test isolation wrapper
*
* @param string $name Class name.
* @param string $namespace Namespace.
* @return bool
*/
protected static function _classExistsInBase(string $name, string $namespace): bool
{
return class_exists($namespace . $name);
}
/**
* Used to read information stored path.
*
* The 1st character of $type argument should be lower cased and will return the
* value of `App.paths.$type` config.
*
* Default types:
* - plugins
* - templates
* - locales
*
* Example:
*
* ```
* App::path('plugins');
* ```
*
* Will return the value of `App.paths.plugins` config.
*
* Deprecated: 4.0 App::path() is deprecated for class path (inside src/ directory).
* Use \Cake\Core\App::classPath() instead or directly the method on \Cake\Core\Plugin class.
*
* @param string $type Type of path
* @param string|null $plugin Plugin name
* @return array<string>
* @link https://book.cakephp.org/4/en/core-libraries/app.html#finding-paths-to-namespaces
*/
public static function path(string $type, ?string $plugin = null): array
{
if ($plugin === null && $type[0] === strtolower($type[0])) {
return (array)Configure::read('App.paths.' . $type);
}
if ($type === 'templates') {
/** @psalm-suppress PossiblyNullArgument */
return [Plugin::templatePath($plugin)];
}
if ($type === 'locales') {
/** @psalm-suppress PossiblyNullArgument */
return [Plugin::path($plugin) . 'resources' . DIRECTORY_SEPARATOR . 'locales' . DIRECTORY_SEPARATOR];
}
deprecationWarning(
'App::path() is deprecated for class path.'
. ' Use \Cake\Core\App::classPath() or \Cake\Core\Plugin::classPath() instead.'
);
return static::classPath($type, $plugin);
}
/**
* Gets the path to a class type in the application or a plugin.
*
* Example:
*
* ```
* App::classPath('Model/Table');
* ```
*
* Will return the path for tables - e.g. `src/Model/Table/`.
*
* ```
* App::classPath('Model/Table', 'My/Plugin');
* ```
*
* Will return the plugin based path for those.
*
* @param string $type Package type.
* @param string|null $plugin Plugin name.
* @return array<string>
*/
public static function classPath(string $type, ?string $plugin = null): array
{
if ($plugin !== null) {
return [
Plugin::classPath($plugin) . $type . DIRECTORY_SEPARATOR,
];
}
return [APP . $type . DIRECTORY_SEPARATOR];
}
/**
* Returns the full path to a package inside the CakePHP core
*
* Usage:
*
* ```
* App::core('Cache/Engine');
* ```
*
* Will return the full path to the cache engines package.
*
* @param string $type Package type.
* @return array<string> Full path to package
*/
public static function core(string $type): array
{
if ($type === 'templates') {
return [CORE_PATH . 'templates' . DIRECTORY_SEPARATOR];
}
return [CAKE . str_replace('/', DIRECTORY_SEPARATOR, $type) . DIRECTORY_SEPARATOR];
}
}
+305
View File
@@ -0,0 +1,305 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Console\CommandCollection;
use Cake\Http\MiddlewareQueue;
use Cake\Routing\RouteBuilder;
use Closure;
use InvalidArgumentException;
use ReflectionClass;
/**
* Base Plugin Class
*
* Every plugin should extend from this class or implement the interfaces and
* include a plugin class in its src root folder.
*/
class BasePlugin implements PluginInterface
{
/**
* Do bootstrapping or not
*
* @var bool
*/
protected $bootstrapEnabled = true;
/**
* Console middleware
*
* @var bool
*/
protected $consoleEnabled = true;
/**
* Enable middleware
*
* @var bool
*/
protected $middlewareEnabled = true;
/**
* Register container services
*
* @var bool
*/
protected $servicesEnabled = true;
/**
* Load routes or not
*
* @var bool
*/
protected $routesEnabled = true;
/**
* The path to this plugin.
*
* @var string
*/
protected $path;
/**
* The class path for this plugin.
*
* @var string
*/
protected $classPath;
/**
* The config path for this plugin.
*
* @var string
*/
protected $configPath;
/**
* The templates path for this plugin.
*
* @var string
*/
protected $templatePath;
/**
* The name of this plugin
*
* @var string
*/
protected $name;
/**
* Constructor
*
* @param array<string, mixed> $options Options
*/
public function __construct(array $options = [])
{
foreach (static::VALID_HOOKS as $key) {
if (isset($options[$key])) {
$this->{"{$key}Enabled"} = (bool)$options[$key];
}
}
foreach (['name', 'path', 'classPath', 'configPath', 'templatePath'] as $path) {
if (isset($options[$path])) {
$this->{$path} = $options[$path];
}
}
$this->initialize();
}
/**
* Initialization hook called from constructor.
*
* @return void
*/
public function initialize(): void
{
}
/**
* @inheritDoc
*/
public function getName(): string
{
if ($this->name) {
return $this->name;
}
$parts = explode('\\', static::class);
array_pop($parts);
$this->name = implode('/', $parts);
return $this->name;
}
/**
* @inheritDoc
*/
public function getPath(): string
{
if ($this->path) {
return $this->path;
}
$reflection = new ReflectionClass($this);
$path = dirname($reflection->getFileName());
// Trim off src
if (substr($path, -3) === 'src') {
$path = substr($path, 0, -3);
}
$this->path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
return $this->path;
}
/**
* @inheritDoc
*/
public function getConfigPath(): string
{
if ($this->configPath) {
return $this->configPath;
}
$path = $this->getPath();
return $path . 'config' . DIRECTORY_SEPARATOR;
}
/**
* @inheritDoc
*/
public function getClassPath(): string
{
if ($this->classPath) {
return $this->classPath;
}
$path = $this->getPath();
return $path . 'src' . DIRECTORY_SEPARATOR;
}
/**
* @inheritDoc
*/
public function getTemplatePath(): string
{
if ($this->templatePath) {
return $this->templatePath;
}
$path = $this->getPath();
return $this->templatePath = $path . 'templates' . DIRECTORY_SEPARATOR;
}
/**
* @inheritDoc
*/
public function enable(string $hook)
{
$this->checkHook($hook);
$this->{"{$hook}Enabled"} = true;
return $this;
}
/**
* @inheritDoc
*/
public function disable(string $hook)
{
$this->checkHook($hook);
$this->{"{$hook}Enabled"} = false;
return $this;
}
/**
* @inheritDoc
*/
public function isEnabled(string $hook): bool
{
$this->checkHook($hook);
return $this->{"{$hook}Enabled"} === true;
}
/**
* Check if a hook name is valid
*
* @param string $hook The hook name to check
* @throws \InvalidArgumentException on invalid hooks
* @return void
*/
protected function checkHook(string $hook): void
{
if (!in_array($hook, static::VALID_HOOKS, true)) {
throw new InvalidArgumentException(
"`$hook` is not a valid hook name. Must be one of " . implode(', ', static::VALID_HOOKS)
);
}
}
/**
* @inheritDoc
*/
public function routes(RouteBuilder $routes): void
{
$path = $this->getConfigPath() . 'routes.php';
if (is_file($path)) {
$return = require $path;
if ($return instanceof Closure) {
$return($routes);
}
}
}
/**
* @inheritDoc
*/
public function bootstrap(PluginApplicationInterface $app): void
{
$bootstrap = $this->getConfigPath() . 'bootstrap.php';
if (is_file($bootstrap)) {
require $bootstrap;
}
}
/**
* @inheritDoc
*/
public function console(CommandCollection $commands): CommandCollection
{
return $commands->addMany($commands->discoverPlugin($this->getName()));
}
/**
* @inheritDoc
*/
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
return $middlewareQueue;
}
/**
* Register container services for this plugin.
*
* @param \Cake\Core\ContainerInterface $container The container to add services to.
* @return void
*/
public function services(ContainerInterface $container): void
{
}
}
+139
View File
@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
/**
* ClassLoader
*
* @deprecated 4.0.3 Use composer to generate autoload files instead.
*/
class ClassLoader
{
/**
* An associative array where the key is a namespace prefix and the value
* is an array of base directories for classes in that namespace.
*
* @var array<string, array>
*/
protected $_prefixes = [];
/**
* Register loader with SPL autoloader stack.
*
* @return void
*/
public function register(): void
{
/** @var callable $callable */
$callable = [$this, 'loadClass'];
spl_autoload_register($callable);
}
/**
* Adds a base directory for a namespace prefix.
*
* @param string $prefix The namespace prefix.
* @param string $baseDir A base directory for class files in the
* namespace.
* @param bool $prepend If true, prepend the base directory to the stack
* instead of appending it; this causes it to be searched first rather
* than last.
* @return void
*/
public function addNamespace(string $prefix, string $baseDir, bool $prepend = false): void
{
$prefix = trim($prefix, '\\') . '\\';
$baseDir = rtrim($baseDir, '/') . DIRECTORY_SEPARATOR;
$baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . '/';
$this->_prefixes[$prefix] = $this->_prefixes[$prefix] ?? [];
if ($prepend) {
array_unshift($this->_prefixes[$prefix], $baseDir);
} else {
$this->_prefixes[$prefix][] = $baseDir;
}
}
/**
* Loads the class file for a given class name.
*
* @param string $class The fully-qualified class name.
* @return string|false The mapped file name on success, or boolean false on
* failure.
*/
public function loadClass(string $class)
{
$prefix = $class;
while (($pos = strrpos($prefix, '\\')) !== false) {
$prefix = substr($class, 0, $pos + 1);
$relativeClass = substr($class, $pos + 1);
$mappedFile = $this->_loadMappedFile($prefix, $relativeClass);
if ($mappedFile) {
return $mappedFile;
}
$prefix = rtrim($prefix, '\\');
}
return false;
}
/**
* Load the mapped file for a namespace prefix and relative class.
*
* @param string $prefix The namespace prefix.
* @param string $relativeClass The relative class name.
* @return string|false Boolean false if no mapped file can be loaded, or the
* name of the mapped file that was loaded.
*/
protected function _loadMappedFile(string $prefix, string $relativeClass)
{
if (!isset($this->_prefixes[$prefix])) {
return false;
}
foreach ($this->_prefixes[$prefix] as $baseDir) {
$file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php';
if ($this->_requireFile($file)) {
return $file;
}
}
return false;
}
/**
* If a file exists, require it from the file system.
*
* @param string $file The file to require.
* @return bool True if the file exists, false if not.
*/
protected function _requireFile(string $file): bool
{
if (file_exists($file)) {
require $file;
return true;
}
return false;
}
}
+498
View File
@@ -0,0 +1,498 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 1.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Cache\Cache;
use Cake\Core\Configure\ConfigEngineInterface;
use Cake\Core\Configure\Engine\PhpConfig;
use Cake\Core\Exception\CakeException;
use Cake\Utility\Hash;
use RuntimeException;
/**
* Configuration class. Used for managing runtime configuration information.
*
* Provides features for reading and writing to the runtime configuration, as well
* as methods for loading additional configuration files or storing runtime configuration
* for future use.
*
* @link https://book.cakephp.org/4/en/development/configuration.html
*/
class Configure
{
/**
* Array of values currently stored in Configure.
*
* @var array<string, mixed>
*/
protected static $_values = [
'debug' => false,
];
/**
* Configured engine classes, used to load config files from resources
*
* @see \Cake\Core\Configure::load()
* @var array<\Cake\Core\Configure\ConfigEngineInterface>
*/
protected static $_engines = [];
/**
* Flag to track whether ini_set exists.
*
* @var bool|null
*/
protected static $_hasIniSet;
/**
* Used to store a dynamic variable in Configure.
*
* Usage:
* ```
* Configure::write('One.key1', 'value of the Configure::One[key1]');
* Configure::write(['One.key1' => 'value of the Configure::One[key1]']);
* Configure::write('One', [
* 'key1' => 'value of the Configure::One[key1]',
* 'key2' => 'value of the Configure::One[key2]'
* ]);
*
* Configure::write([
* 'One.key1' => 'value of the Configure::One[key1]',
* 'One.key2' => 'value of the Configure::One[key2]'
* ]);
* ```
*
* @param array<string, mixed>|string $config The key to write, can be a dot notation value.
* Alternatively can be an array containing key(s) and value(s).
* @param mixed $value Value to set for the given key.
* @return void
* @link https://book.cakephp.org/4/en/development/configuration.html#writing-configuration-data
*/
public static function write($config, $value = null): void
{
if (!is_array($config)) {
$config = [$config => $value];
}
foreach ($config as $name => $valueToInsert) {
static::$_values = Hash::insert(static::$_values, $name, $valueToInsert);
}
if (isset($config['debug'])) {
if (static::$_hasIniSet === null) {
static::$_hasIniSet = function_exists('ini_set');
}
if (static::$_hasIniSet) {
ini_set('display_errors', $config['debug'] ? '1' : '0');
}
}
}
/**
* Used to read information stored in Configure. It's not
* possible to store `null` values in Configure.
*
* Usage:
* ```
* Configure::read('Name'); will return all values for Name
* Configure::read('Name.key'); will return only the value of Configure::Name[key]
* ```
*
* @param string|null $var Variable to obtain. Use '.' to access array elements.
* @param mixed $default The return value when the configure does not exist
* @return mixed Value stored in configure, or null.
* @link https://book.cakephp.org/4/en/development/configuration.html#reading-configuration-data
*/
public static function read(?string $var = null, $default = null)
{
if ($var === null) {
return static::$_values;
}
return Hash::get(static::$_values, $var, $default);
}
/**
* Returns true if given variable is set in Configure.
*
* @param string $var Variable name to check for
* @return bool True if variable is there
*/
public static function check(string $var): bool
{
if (empty($var)) {
return false;
}
return static::read($var) !== null;
}
/**
* Used to get information stored in Configure. It's not
* possible to store `null` values in Configure.
*
* Acts as a wrapper around Configure::read() and Configure::check().
* The configure key/value pair fetched via this method is expected to exist.
* In case it does not an exception will be thrown.
*
* Usage:
* ```
* Configure::readOrFail('Name'); will return all values for Name
* Configure::readOrFail('Name.key'); will return only the value of Configure::Name[key]
* ```
*
* @param string $var Variable to obtain. Use '.' to access array elements.
* @return mixed Value stored in configure.
* @throws \RuntimeException if the requested configuration is not set.
* @link https://book.cakephp.org/4/en/development/configuration.html#reading-configuration-data
*/
public static function readOrFail(string $var)
{
if (!static::check($var)) {
throw new RuntimeException(sprintf('Expected configuration key "%s" not found.', $var));
}
return static::read($var);
}
/**
* Used to delete a variable from Configure.
*
* Usage:
* ```
* Configure::delete('Name'); will delete the entire Configure::Name
* Configure::delete('Name.key'); will delete only the Configure::Name[key]
* ```
*
* @param string $var the var to be deleted
* @return void
* @link https://book.cakephp.org/4/en/development/configuration.html#deleting-configuration-data
*/
public static function delete(string $var): void
{
static::$_values = Hash::remove(static::$_values, $var);
}
/**
* Used to consume information stored in Configure. It's not
* possible to store `null` values in Configure.
*
* Acts as a wrapper around Configure::consume() and Configure::check().
* The configure key/value pair consumed via this method is expected to exist.
* In case it does not an exception will be thrown.
*
* @param string $var Variable to consume. Use '.' to access array elements.
* @return mixed Value stored in configure.
* @throws \RuntimeException if the requested configuration is not set.
* @since 3.6.0
*/
public static function consumeOrFail(string $var)
{
if (!static::check($var)) {
throw new RuntimeException(sprintf('Expected configuration key "%s" not found.', $var));
}
return static::consume($var);
}
/**
* Used to read and delete a variable from Configure.
*
* This is primarily used during bootstrapping to move configuration data
* out of configure into the various other classes in CakePHP.
*
* @param string $var The key to read and remove.
* @return array|string|null
*/
public static function consume(string $var)
{
if (strpos($var, '.') === false) {
if (!isset(static::$_values[$var])) {
return null;
}
$value = static::$_values[$var];
unset(static::$_values[$var]);
return $value;
}
$value = Hash::get(static::$_values, $var);
static::delete($var);
return $value;
}
/**
* Add a new engine to Configure. Engines allow you to read configuration
* files in various formats/storage locations. CakePHP comes with two built-in engines
* PhpConfig and IniConfig. You can also implement your own engine classes in your application.
*
* To add a new engine to Configure:
*
* ```
* Configure::config('ini', new IniConfig());
* ```
*
* @param string $name The name of the engine being configured. This alias is used later to
* read values from a specific engine.
* @param \Cake\Core\Configure\ConfigEngineInterface $engine The engine to append.
* @return void
*/
public static function config(string $name, ConfigEngineInterface $engine): void
{
static::$_engines[$name] = $engine;
}
/**
* Returns true if the Engine objects is configured.
*
* @param string $name Engine name.
* @return bool
*/
public static function isConfigured(string $name): bool
{
return isset(static::$_engines[$name]);
}
/**
* Gets the names of the configured Engine objects.
*
* @return array<string>
*/
public static function configured(): array
{
$engines = array_keys(static::$_engines);
return array_map(function ($key) {
return (string)$key;
}, $engines);
}
/**
* Remove a configured engine. This will unset the engine
* and make any future attempts to use it cause an Exception.
*
* @param string $name Name of the engine to drop.
* @return bool Success
*/
public static function drop(string $name): bool
{
if (!isset(static::$_engines[$name])) {
return false;
}
unset(static::$_engines[$name]);
return true;
}
/**
* Loads stored configuration information from a resource. You can add
* config file resource engines with `Configure::config()`.
*
* Loaded configuration information will be merged with the current
* runtime configuration. You can load configuration files from plugins
* by preceding the filename with the plugin name.
*
* `Configure::load('Users.user', 'default')`
*
* Would load the 'user' config file using the default config engine. You can load
* app config files by giving the name of the resource you want loaded.
*
* ```
* Configure::load('setup', 'default');
* ```
*
* If using `default` config and no engine has been configured for it yet,
* one will be automatically created using PhpConfig
*
* @param string $key name of configuration resource to load.
* @param string $config Name of the configured engine to use to read the resource identified by $key.
* @param bool $merge if config files should be merged instead of simply overridden
* @return bool True if load successful.
* @throws \Cake\Core\Exception\CakeException if the $config engine is not found
* @link https://book.cakephp.org/4/en/development/configuration.html#reading-and-writing-configuration-files
*/
public static function load(string $key, string $config = 'default', bool $merge = true): bool
{
$engine = static::_getEngine($config);
if (!$engine) {
throw new CakeException(
sprintf(
'Config %s engine not found when attempting to load %s.',
$config,
$key
)
);
}
$values = $engine->read($key);
if ($merge) {
$values = Hash::merge(static::$_values, $values);
}
static::write($values);
return true;
}
/**
* Dump data currently in Configure into $key. The serialization format
* is decided by the config engine attached as $config. For example, if the
* 'default' adapter is a PhpConfig, the generated file will be a PHP
* configuration file loadable by the PhpConfig.
*
* ### Usage
*
* Given that the 'default' engine is an instance of PhpConfig.
* Save all data in Configure to the file `my_config.php`:
*
* ```
* Configure::dump('my_config', 'default');
* ```
*
* Save only the error handling configuration:
*
* ```
* Configure::dump('error', 'default', ['Error', 'Exception'];
* ```
*
* @param string $key The identifier to create in the config adapter.
* This could be a filename or a cache key depending on the adapter being used.
* @param string $config The name of the configured adapter to dump data with.
* @param array<string> $keys The name of the top-level keys you want to dump.
* This allows you save only some data stored in Configure.
* @return bool Success
* @throws \Cake\Core\Exception\CakeException if the adapter does not implement a `dump` method.
*/
public static function dump(string $key, string $config = 'default', array $keys = []): bool
{
$engine = static::_getEngine($config);
if (!$engine) {
throw new CakeException(sprintf('There is no "%s" config engine.', $config));
}
$values = static::$_values;
if (!empty($keys)) {
$values = array_intersect_key($values, array_flip($keys));
}
return $engine->dump($key, $values);
}
/**
* Get the configured engine. Internally used by `Configure::load()` and `Configure::dump()`
* Will create new PhpConfig for default if not configured yet.
*
* @param string $config The name of the configured adapter
* @return \Cake\Core\Configure\ConfigEngineInterface|null Engine instance or null
*/
protected static function _getEngine(string $config): ?ConfigEngineInterface
{
if (!isset(static::$_engines[$config])) {
if ($config !== 'default') {
return null;
}
static::config($config, new PhpConfig());
}
return static::$_engines[$config];
}
/**
* Used to determine the current version of CakePHP.
*
* Usage
* ```
* Configure::version();
* ```
*
* @return string Current version of CakePHP
*/
public static function version(): string
{
$version = static::read('Cake.version');
if ($version !== null) {
return $version;
}
$path = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'config/config.php';
if (is_file($path)) {
$config = require $path;
static::write($config);
return static::read('Cake.version');
}
return 'unknown';
}
/**
* Used to write runtime configuration into Cache. Stored runtime configuration can be
* restored using `Configure::restore()`. These methods can be used to enable configuration managers
* frontends, or other GUI type interfaces for configuration.
*
* @param string $name The storage name for the saved configuration.
* @param string $cacheConfig The cache configuration to save into. Defaults to 'default'
* @param array|null $data Either an array of data to store, or leave empty to store all values.
* @return bool Success
* @throws \RuntimeException
*/
public static function store(string $name, string $cacheConfig = 'default', ?array $data = null): bool
{
if ($data === null) {
$data = static::$_values;
}
if (!class_exists(Cache::class)) {
throw new RuntimeException('You must install cakephp/cache to use Configure::store()');
}
return Cache::write($name, $data, $cacheConfig);
}
/**
* Restores configuration data stored in the Cache into configure. Restored
* values will overwrite existing ones.
*
* @param string $name Name of the stored config file to load.
* @param string $cacheConfig Name of the Cache configuration to read from.
* @return bool Success.
* @throws \RuntimeException
*/
public static function restore(string $name, string $cacheConfig = 'default'): bool
{
if (!class_exists(Cache::class)) {
throw new RuntimeException('You must install cakephp/cache to use Configure::restore()');
}
$values = Cache::read($name, $cacheConfig);
if ($values) {
static::write($values);
return true;
}
return false;
}
/**
* Clear all values stored in Configure.
*
* @return void
*/
public static function clear(): void
{
static::$_values = [];
}
}
+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 1.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Configure;
/**
* An interface for creating objects compatible with Configure::load()
*/
interface ConfigEngineInterface
{
/**
* Read a configuration file/storage key
*
* This method is used for reading configuration information from sources.
* These sources can either be static resources like files, or dynamic ones like
* a database, or other datasource.
*
* @param string $key Key to read.
* @return array An array of data to merge into the runtime configuration
*/
public function read(string $key): array;
/**
* Dumps the configure data into the storage key/file of the given `$key`.
*
* @param string $key The identifier to write to.
* @param array $data The data to dump.
* @return bool True on success or false on failure.
*/
public function dump(string $key, array $data): bool;
}
+203
View File
@@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 2.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Configure\Engine;
use Cake\Core\Configure\ConfigEngineInterface;
use Cake\Core\Configure\FileConfigTrait;
use Cake\Utility\Hash;
/**
* Ini file configuration engine.
*
* Since IniConfig uses parse_ini_file underneath, you should be aware that this
* class shares the same behavior, especially with regards to boolean and null values.
*
* In addition to the native `parse_ini_file` features, IniConfig also allows you
* to create nested array structures through usage of `.` delimited names. This allows
* you to create nested arrays structures in an ini config file. For example:
*
* `db.password = secret` would turn into `['db' => ['password' => 'secret']]`
*
* You can nest properties as deeply as needed using `.`'s. In addition to using `.` you
* can use standard ini section notation to create nested structures:
*
* ```
* [section]
* key = value
* ```
*
* Once loaded into Configure, the above would be accessed using:
*
* `Configure::read('section.key');
*
* You can also use `.` separated values in section names to create more deeply
* nested structures.
*
* IniConfig also manipulates how the special ini values of
* 'yes', 'no', 'on', 'off', 'null' are handled. These values will be
* converted to their boolean equivalents.
*
* @see https://secure.php.net/parse_ini_file
*/
class IniConfig implements ConfigEngineInterface
{
use FileConfigTrait;
/**
* File extension.
*
* @var string
*/
protected $_extension = '.ini';
/**
* The section to read, if null all sections will be read.
*
* @var string|null
*/
protected $_section;
/**
* Build and construct a new ini file parser. The parser can be used to read
* ini files that are on the filesystem.
*
* @param string|null $path Path to load ini config files from. Defaults to CONFIG.
* @param string|null $section Only get one section, leave null to parse and fetch
* all sections in the ini file.
*/
public function __construct(?string $path = null, ?string $section = null)
{
if ($path === null) {
$path = CONFIG;
}
$this->_path = $path;
$this->_section = $section;
}
/**
* Read an ini file and return the results as an array.
*
* @param string $key The identifier to read from. If the key has a . it will be treated
* as a plugin prefix. The chosen file must be on the engine's path.
* @return array Parsed configuration values.
* @throws \Cake\Core\Exception\CakeException when files don't exist.
* Or when files contain '..' as this could lead to abusive reads.
*/
public function read(string $key): array
{
$file = $this->_getFilePath($key, true);
$contents = parse_ini_file($file, true);
if ($this->_section && isset($contents[$this->_section])) {
$values = $this->_parseNestedValues($contents[$this->_section]);
} else {
$values = [];
foreach ($contents as $section => $attribs) {
if (is_array($attribs)) {
$values[$section] = $this->_parseNestedValues($attribs);
} else {
$parse = $this->_parseNestedValues([$attribs]);
$values[$section] = array_shift($parse);
}
}
}
return $values;
}
/**
* parses nested values out of keys.
*
* @param array $values Values to be exploded.
* @return array Array of values exploded
*/
protected function _parseNestedValues(array $values): array
{
foreach ($values as $key => $value) {
if ($value === '1') {
$value = true;
}
if ($value === '') {
$value = false;
}
unset($values[$key]);
if (strpos((string)$key, '.') !== false) {
$values = Hash::insert($values, $key, $value);
} else {
$values[$key] = $value;
}
}
return $values;
}
/**
* Dumps the state of Configure data into an ini formatted string.
*
* @param string $key The identifier to write to. If the key has a . it will be treated
* as a plugin prefix.
* @param array $data The data to convert to ini file.
* @return bool Success.
*/
public function dump(string $key, array $data): bool
{
$result = [];
foreach ($data as $k => $value) {
$isSection = false;
/** @psalm-suppress InvalidArrayAccess */
if ($k[0] !== '[') {
$result[] = "[$k]";
$isSection = true;
}
if (is_array($value)) {
$kValues = Hash::flatten($value, '.');
foreach ($kValues as $k2 => $v) {
$result[] = "$k2 = " . $this->_value($v);
}
}
if ($isSection) {
$result[] = '';
}
}
$contents = trim(implode("\n", $result));
$filename = $this->_getFilePath($key);
return file_put_contents($filename, $contents) > 0;
}
/**
* Converts a value into the ini equivalent
*
* @param mixed $value Value to export.
* @return string String value for ini file.
*/
protected function _value($value): string
{
if ($value === null) {
return 'null';
}
if ($value === true) {
return 'true';
}
if ($value === false) {
return 'false';
}
return (string)$value;
}
}
+115
View File
@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Configure\Engine;
use Cake\Core\Configure\ConfigEngineInterface;
use Cake\Core\Configure\FileConfigTrait;
use Cake\Core\Exception\CakeException;
/**
* JSON engine allows Configure to load configuration values from
* files containing JSON strings.
*
* An example JSON file would look like::
*
* ```
* {
* "debug": false,
* "App": {
* "namespace": "MyApp"
* },
* "Security": {
* "salt": "its-secret"
* }
* }
* ```
*/
class JsonConfig implements ConfigEngineInterface
{
use FileConfigTrait;
/**
* File extension.
*
* @var string
*/
protected $_extension = '.json';
/**
* Constructor for JSON Config file reading.
*
* @param string|null $path The path to read config files from. Defaults to CONFIG.
*/
public function __construct(?string $path = null)
{
if ($path === null) {
$path = CONFIG;
}
$this->_path = $path;
}
/**
* Read a config file and return its contents.
*
* Files with `.` in the name will be treated as values in plugins. Instead of
* reading from the initialized path, plugin keys will be located using Plugin::path().
*
* @param string $key The identifier to read from. If the key has a . it will be treated
* as a plugin prefix.
* @return array Parsed configuration values.
* @throws \Cake\Core\Exception\CakeException When files don't exist or when
* files contain '..' (as this could lead to abusive reads) or when there
* is an error parsing the JSON string.
*/
public function read(string $key): array
{
$file = $this->_getFilePath($key, true);
$values = json_decode(file_get_contents($file), true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new CakeException(sprintf(
'Error parsing JSON string fetched from config file "%s.json": %s',
$key,
json_last_error_msg()
));
}
if (!is_array($values)) {
throw new CakeException(sprintf(
'Decoding JSON config file "%s.json" did not return an array',
$key
));
}
return $values;
}
/**
* Converts the provided $data into a JSON string that can be used saved
* into a file and loaded later.
*
* @param string $key The identifier to write to. If the key has a . it will
* be treated as a plugin prefix.
* @param array $data Data to dump.
* @return bool Success
*/
public function dump(string $key, array $data): bool
{
$filename = $this->_getFilePath($key);
return file_put_contents($filename, json_encode($data, JSON_PRETTY_PRINT)) > 0;
}
}
+114
View File
@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 2.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Configure\Engine;
use Cake\Core\Configure\ConfigEngineInterface;
use Cake\Core\Configure\FileConfigTrait;
use Cake\Core\Exception\CakeException;
/**
* PHP engine allows Configure to load configuration values from
* files containing simple PHP arrays.
*
* Files compatible with PhpConfig should return an array that
* contains all the configuration data contained in the file.
*
* An example configuration file would look like::
*
* ```
* <?php
* return [
* 'debug' => false,
* 'Security' => [
* 'salt' => 'its-secret'
* ],
* 'App' => [
* 'namespace' => 'App'
* ]
* ];
* ```
*
* @see \Cake\Core\Configure::load() for how to load custom configuration files.
*/
class PhpConfig implements ConfigEngineInterface
{
use FileConfigTrait;
/**
* File extension.
*
* @var string
*/
protected $_extension = '.php';
/**
* Constructor for PHP Config file reading.
*
* @param string|null $path The path to read config files from. Defaults to CONFIG.
*/
public function __construct(?string $path = null)
{
if ($path === null) {
$path = CONFIG;
}
$this->_path = $path;
}
/**
* Read a config file and return its contents.
*
* Files with `.` in the name will be treated as values in plugins. Instead of
* reading from the initialized path, plugin keys will be located using Plugin::path().
*
* @param string $key The identifier to read from. If the key has a . it will be treated
* as a plugin prefix.
* @return array Parsed configuration values.
* @throws \Cake\Core\Exception\CakeException when files don't exist or they don't contain `$config`.
* Or when files contain '..' as this could lead to abusive reads.
*/
public function read(string $key): array
{
$file = $this->_getFilePath($key, true);
$config = null;
$return = include $file;
if (is_array($return)) {
return $return;
}
throw new CakeException(sprintf('Config file "%s" did not return an array', $key . '.php'));
}
/**
* Converts the provided $data into a string of PHP code that can
* be used saved into a file and loaded later.
*
* @param string $key The identifier to write to. If the key has a . it will be treated
* as a plugin prefix.
* @param array $data Data to dump.
* @return bool Success
*/
public function dump(string $key, array $data): bool
{
$contents = '<?php' . "\n" . 'return ' . var_export($data, true) . ';';
$filename = $this->_getFilePath($key);
return file_put_contents($filename, $contents) > 0;
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Configure;
use Cake\Core\Exception\CakeException;
use Cake\Core\Plugin;
use function Cake\Core\pluginSplit;
/**
* Trait providing utility methods for file based config engines.
*/
trait FileConfigTrait
{
/**
* The path this engine finds files on.
*
* @var string
*/
protected $_path = '';
/**
* Get file path
*
* @param string $key The identifier to write to. If the key has a . it will be treated
* as a plugin prefix.
* @param bool $checkExists Whether to check if file exists. Defaults to false.
* @return string Full file path
* @throws \Cake\Core\Exception\CakeException When files don't exist or when
* files contain '..' as this could lead to abusive reads.
*/
protected function _getFilePath(string $key, bool $checkExists = false): string
{
if (strpos($key, '..') !== false) {
throw new CakeException('Cannot load/dump configuration files with ../ in them.');
}
[$plugin, $key] = pluginSplit($key);
if ($plugin) {
$file = Plugin::configPath($plugin) . $key;
} else {
$file = $this->_path . $key;
}
$file .= $this->_extension;
if (!$checkExists || is_file($file)) {
return $file;
}
$realPath = realpath($file);
if ($realPath !== false && is_file($realPath)) {
return $realPath;
}
throw new CakeException(sprintf('Could not load configuration file: %s', $file));
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.5.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Console\CommandCollection;
/**
* An interface defining the methods that the
* console runner depend on.
*/
interface ConsoleApplicationInterface
{
/**
* Load all the application configuration and bootstrap logic.
*
* Override this method to add additional bootstrap logic for your application.
*
* @return void
*/
public function bootstrap(): void;
/**
* Define the console commands for an application.
*
* @param \Cake\Console\CommandCollection $commands The CommandCollection to add commands into.
* @return \Cake\Console\CommandCollection The updated collection.
*/
public function console(CommandCollection $commands): CommandCollection;
}
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use League\Container\Container as LeagueContainer;
/**
* Dependency Injection container
*
* Based on the container out of League\Container
*/
class Container extends LeagueContainer implements ContainerInterface
{
}
+45
View File
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
/**
* Interface for applications that configure and use a dependency injection container.
*/
interface ContainerApplicationInterface
{
/**
* Register services to the container
*
* Registered services can have instances fetched out of the container
* using `get()`. Dependencies and parameters will be resolved based
* on service definitions.
*
* @param \Cake\Core\ContainerInterface $container The container to add services to
* @return void
*/
public function services(ContainerInterface $container): void;
/**
* Create a new container and register services.
*
* This will `register()` services provided by both the application
* and any plugins if the application has plugin support.
*
* @return \Cake\Core\ContainerInterface A populated container
*/
public function getContainer(): ContainerInterface;
}
+32
View File
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use League\Container\DefinitionContainerInterface;
/**
* Interface for the Dependency Injection Container in CakePHP applications
*
* This interface extends the PSR-11 container interface and adds
* methods to add services and service providers to the container.
*
* The methods defined in this interface use the conventions provided
* by league/container as that is the library that CakePHP uses.
*/
interface ContainerInterface extends DefinitionContainerInterface
{
}
+156
View File
@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Utility\Inflector;
/**
* Provides methods that allow other classes access to conventions based inflections.
*/
trait ConventionsTrait
{
/**
* Creates a fixture name
*
* @param string $name Model class name
* @return string Singular model key
*/
protected function _fixtureName(string $name): string
{
return Inflector::camelize($name);
}
/**
* Creates the proper entity name (singular) for the specified name
*
* @param string $name Name
* @return string Camelized and plural model name
*/
protected function _entityName(string $name): string
{
return Inflector::singularize(Inflector::camelize($name));
}
/**
* Creates the proper underscored model key for associations
*
* If the input contains a dot, assume that the right side is the real table name.
*
* @param string $name Model class name
* @return string Singular model key
*/
protected function _modelKey(string $name): string
{
[, $name] = pluginSplit($name);
return Inflector::underscore(Inflector::singularize($name)) . '_id';
}
/**
* Creates the proper model name from a foreign key
*
* @param string $key Foreign key
* @return string Model name
*/
protected function _modelNameFromKey(string $key): string
{
$key = str_replace('_id', '', $key);
return Inflector::camelize(Inflector::pluralize($key));
}
/**
* Creates the singular name for use in views.
*
* @param string $name Name to use
* @return string Variable name
*/
protected function _singularName(string $name): string
{
return Inflector::variable(Inflector::singularize($name));
}
/**
* Creates the plural variable name for views
*
* @param string $name Name to use
* @return string Plural name for views
*/
protected function _variableName(string $name): string
{
return Inflector::variable($name);
}
/**
* Creates the singular human name used in views
*
* @param string $name Controller name
* @return string Singular human name
*/
protected function _singularHumanName(string $name): string
{
return Inflector::humanize(Inflector::underscore(Inflector::singularize($name)));
}
/**
* Creates a camelized version of $name
*
* @param string $name name
* @return string Camelized name
*/
protected function _camelize(string $name): string
{
return Inflector::camelize($name);
}
/**
* Creates the plural human name used in views
*
* @param string $name Controller name
* @return string Plural human name
*/
protected function _pluralHumanName(string $name): string
{
return Inflector::humanize(Inflector::underscore($name));
}
/**
* Find the correct path for a plugin. Scans $pluginPaths for the plugin you want.
*
* @param string $pluginName Name of the plugin you want ie. DebugKit
* @return string path path to the correct plugin.
*/
protected function _pluginPath(string $pluginName): string
{
if (Plugin::isLoaded($pluginName)) {
return Plugin::path($pluginName);
}
return current(App::path('plugins')) . $pluginName . DIRECTORY_SEPARATOR;
}
/**
* Return plugin's namespace
*
* @param string $pluginName Plugin name
* @return string Plugin's namespace
*/
protected function _pluginNamespace(string $pluginName): string
{
return str_replace('/', '\\', $pluginName);
}
}
+123
View File
@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Exception;
use RuntimeException;
use Throwable;
use function Cake\Core\deprecationWarning;
/**
* Base class that all CakePHP Exceptions extend.
*
* @method int getCode() Gets the Exception code.
*/
class CakeException extends RuntimeException
{
/**
* Array of attributes that are passed in from the constructor, and
* made available in the view when a development error is displayed.
*
* @var array
*/
protected $_attributes = [];
/**
* Template string that has attributes sprintf()'ed into it.
*
* @var string
*/
protected $_messageTemplate = '';
/**
* Array of headers to be passed to {@link \Cake\Http\Response::withHeader()}
*
* @var array|null
*/
protected $_responseHeaders;
/**
* Default exception code
*
* @var int
*/
protected $_defaultCode = 0;
/**
* Constructor.
*
* Allows you to create exceptions that are treated as framework errors and disabled
* when debug mode is off.
*
* @param array|string $message Either the string of the error message, or an array of attributes
* that are made available in the view, and sprintf()'d into Exception::$_messageTemplate
* @param int|null $code The error code
* @param \Throwable|null $previous the previous exception.
*/
public function __construct($message = '', ?int $code = null, ?Throwable $previous = null)
{
if (is_array($message)) {
$this->_attributes = $message;
$message = vsprintf($this->_messageTemplate, $message);
}
parent::__construct($message, $code ?? $this->_defaultCode, $previous);
}
/**
* Get the passed in attributes
*
* @return array
*/
public function getAttributes(): array
{
return $this->_attributes;
}
/**
* Get/set the response header to be used
*
* See also {@link \Cake\Http\Response::withHeader()}
*
* @param array|string|null $header A single header string or an associative
* array of "header name" => "header value"
* @param string|null $value The header value.
* @return array|null
* @deprecated 4.2.0 Use `HttpException::setHeaders()` instead. Response headers
* should be set for HttpException only.
*/
public function responseHeader($header = null, $value = null): ?array
{
if ($header === null) {
return $this->_responseHeaders;
}
deprecationWarning(
'Setting HTTP response headers from Exception directly is deprecated. ' .
'If your exceptions extend Exception, they must now extend HttpException. ' .
'You should only set HTTP headers on HttpException instances via the `setHeaders()` method.'
);
if (is_array($header)) {
return $this->_responseHeaders = $header;
}
return $this->_responseHeaders = [$header => $value];
}
}
// phpcs:disable
class_alias(
'Cake\Core\Exception\CakeException',
'Cake\Core\Exception\Exception'
);
// phpcs:enable
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
use function Cake\Core\deprecationWarning;
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
deprecationWarning(
'Since 4.2.0: Cake\Core\Exception\Exception is deprecated.' .
'Use Cake\Core\Exception\CakeException instead.'
);
class_exists('Cake\Core\Exception\CakeException');
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Exception;
/**
* Exception raised when a plugin could not be found
*/
class MissingPluginException extends CakeException
{
/**
* @inheritDoc
*/
protected $_messageTemplate = 'Plugin %s could not be found.';
}
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.5.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Http\MiddlewareQueue;
use Psr\Http\Server\RequestHandlerInterface;
/**
* An interface defining the methods that the
* http server depend on.
*/
interface HttpApplicationInterface extends RequestHandlerInterface
{
/**
* Load all the application configuration and bootstrap logic.
*
* Override this method to add additional bootstrap logic for your application.
*
* @return void
*/
public function bootstrap(): void;
/**
* Define the HTTP middleware layers for an application.
*
* @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to set in your App Class
* @return \Cake\Http\MiddlewareQueue
*/
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue;
}
+312
View File
@@ -0,0 +1,312 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Core\Exception\CakeException;
use Cake\Utility\Hash;
use InvalidArgumentException;
/**
* A trait for reading and writing instance config
*
* Implementing objects are expected to declare a `$_defaultConfig` property.
*/
trait InstanceConfigTrait
{
/**
* Runtime config
*
* @var array<string, mixed>
*/
protected $_config = [];
/**
* Whether the config property has already been configured with defaults
*
* @var bool
*/
protected $_configInitialized = false;
/**
* Sets the config.
*
* ### Usage
*
* Setting a specific value:
*
* ```
* $this->setConfig('key', $value);
* ```
*
* Setting a nested value:
*
* ```
* $this->setConfig('some.nested.key', $value);
* ```
*
* Updating multiple config settings at the same time:
*
* ```
* $this->setConfig(['one' => 'value', 'another' => 'value']);
* ```
*
* @param array<string, mixed>|string $key The key to set, or a complete array of configs.
* @param mixed|null $value The value to set.
* @param bool $merge Whether to recursively merge or overwrite existing config, defaults to true.
* @return $this
* @throws \Cake\Core\Exception\CakeException When trying to set a key that is invalid.
*/
public function setConfig($key, $value = null, $merge = true)
{
if (!$this->_configInitialized) {
$this->_config = $this->_defaultConfig;
$this->_configInitialized = true;
}
$this->_configWrite($key, $value, $merge);
return $this;
}
/**
* Returns the config.
*
* ### Usage
*
* Reading the whole config:
*
* ```
* $this->getConfig();
* ```
*
* Reading a specific value:
*
* ```
* $this->getConfig('key');
* ```
*
* Reading a nested value:
*
* ```
* $this->getConfig('some.nested.key');
* ```
*
* Reading with default value:
*
* ```
* $this->getConfig('some-key', 'default-value');
* ```
*
* @param string|null $key The key to get or null for the whole config.
* @param mixed $default The return value when the key does not exist.
* @return mixed Configuration data at the named key or null if the key does not exist.
*/
public function getConfig(?string $key = null, $default = null)
{
if (!$this->_configInitialized) {
$this->_config = $this->_defaultConfig;
$this->_configInitialized = true;
}
$return = $this->_configRead($key);
return $return ?? $default;
}
/**
* Returns the config for this specific key.
*
* The config value for this key must exist, it can never be null.
*
* @param string $key The key to get.
* @return mixed Configuration data at the named key
* @throws \InvalidArgumentException
*/
public function getConfigOrFail(string $key)
{
$config = $this->getConfig($key);
if ($config === null) {
throw new InvalidArgumentException(sprintf('Expected configuration `%s` not found.', $key));
}
return $config;
}
/**
* Merge provided config with existing config. Unlike `config()` which does
* a recursive merge for nested keys, this method does a simple merge.
*
* Setting a specific value:
*
* ```
* $this->configShallow('key', $value);
* ```
*
* Setting a nested value:
*
* ```
* $this->configShallow('some.nested.key', $value);
* ```
*
* Updating multiple config settings at the same time:
*
* ```
* $this->configShallow(['one' => 'value', 'another' => 'value']);
* ```
*
* @param array<string, mixed>|string $key The key to set, or a complete array of configs.
* @param mixed|null $value The value to set.
* @return $this
*/
public function configShallow($key, $value = null)
{
if (!$this->_configInitialized) {
$this->_config = $this->_defaultConfig;
$this->_configInitialized = true;
}
$this->_configWrite($key, $value, 'shallow');
return $this;
}
/**
* Reads a config key.
*
* @param string|null $key Key to read.
* @return mixed
*/
protected function _configRead(?string $key)
{
if ($key === null) {
return $this->_config;
}
if (strpos($key, '.') === false) {
return $this->_config[$key] ?? null;
}
$return = $this->_config;
foreach (explode('.', $key) as $k) {
if (!is_array($return) || !isset($return[$k])) {
$return = null;
break;
}
$return = $return[$k];
}
return $return;
}
/**
* Writes a config key.
*
* @param array<string, mixed>|string $key Key to write to.
* @param mixed $value Value to write.
* @param string|bool $merge True to merge recursively, 'shallow' for simple merge,
* false to overwrite, defaults to false.
* @return void
* @throws \Cake\Core\Exception\CakeException if attempting to clobber existing config
*/
protected function _configWrite($key, $value, $merge = false): void
{
if (is_string($key) && $value === null) {
$this->_configDelete($key);
return;
}
if ($merge) {
$update = is_array($key) ? $key : [$key => $value];
if ($merge === 'shallow') {
$this->_config = array_merge($this->_config, Hash::expand($update));
} else {
$this->_config = Hash::merge($this->_config, Hash::expand($update));
}
return;
}
if (is_array($key)) {
foreach ($key as $k => $val) {
$this->_configWrite($k, $val);
}
return;
}
if (strpos($key, '.') === false) {
$this->_config[$key] = $value;
return;
}
$update = &$this->_config;
$stack = explode('.', $key);
foreach ($stack as $k) {
if (!is_array($update)) {
throw new CakeException(sprintf('Cannot set %s value', $key));
}
$update[$k] = $update[$k] ?? [];
$update = &$update[$k];
}
$update = $value;
}
/**
* Deletes a single config key.
*
* @param string $key Key to delete.
* @return void
* @throws \Cake\Core\Exception\CakeException if attempting to clobber existing config
*/
protected function _configDelete(string $key): void
{
if (strpos($key, '.') === false) {
unset($this->_config[$key]);
return;
}
$update = &$this->_config;
$stack = explode('.', $key);
$length = count($stack);
foreach ($stack as $i => $k) {
if (!is_array($update)) {
throw new CakeException(sprintf('Cannot unset %s value', $key));
}
if (!isset($update[$k])) {
break;
}
if ($i === $length - 1) {
unset($update[$k]);
break;
}
$update = &$update[$k];
}
}
}
+22
View File
@@ -0,0 +1,22 @@
The MIT License (MIT)
CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org)
Copyright (c) 2005-2020, Cake Software Foundation, Inc. (https://cakefoundation.org)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+416
View File
@@ -0,0 +1,416 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use ArrayIterator;
use Cake\Event\EventDispatcherInterface;
use Cake\Event\EventListenerInterface;
use Countable;
use IteratorAggregate;
use RuntimeException;
use Traversable;
/**
* Acts as a registry/factory for objects.
*
* Provides registry & factory functionality for object types. Used
* as a super class for various composition based re-use features in CakePHP.
*
* Each subclass needs to implement the various abstract methods to complete
* the template method load().
*
* The ObjectRegistry is EventManager aware, but each extending class will need to use
* \Cake\Event\EventDispatcherTrait to attach and detach on set and bind
*
* @see \Cake\Controller\ComponentRegistry
* @see \Cake\View\HelperRegistry
* @see \Cake\Console\TaskRegistry
* @template TObject of object
* @template-implements \IteratorAggregate<string, TObject>
*/
abstract class ObjectRegistry implements Countable, IteratorAggregate
{
/**
* Map of loaded objects.
*
* @var array<object>
* @psalm-var array<array-key, TObject>
*/
protected $_loaded = [];
/**
* Loads/constructs an object instance.
*
* Will return the instance in the registry if it already exists.
* If a subclass provides event support, you can use `$config['enabled'] = false`
* to exclude constructed objects from being registered for events.
*
* Using {@link \Cake\Controller\Component::$components} as an example. You can alias
* an object by setting the 'className' key, i.e.,
*
* ```
* protected $components = [
* 'Email' => [
* 'className' => 'App\Controller\Component\AliasedEmailComponent'
* ];
* ];
* ```
*
* All calls to the `Email` component would use `AliasedEmail` instead.
*
* @param string $name The name/class of the object to load.
* @param array<string, mixed> $config Additional settings to use when loading the object.
* @return mixed
* @psalm-return TObject
* @throws \Exception If the class cannot be found.
*/
public function load(string $name, array $config = [])
{
if (isset($config['className'])) {
$objName = $name;
$name = $config['className'];
} else {
[, $objName] = pluginSplit($name);
}
$loaded = isset($this->_loaded[$objName]);
if ($loaded && !empty($config)) {
$this->_checkDuplicate($objName, $config);
}
if ($loaded) {
return $this->_loaded[$objName];
}
$className = $name;
if (is_string($name)) {
$className = $this->_resolveClassName($name);
if ($className === null) {
[$plugin, $name] = pluginSplit($name);
$this->_throwMissingClassError($name, $plugin);
}
}
/**
* @psalm-var TObject $instance
* @psalm-suppress PossiblyNullArgument
**/
$instance = $this->_create($className, $objName, $config);
$this->_loaded[$objName] = $instance;
return $instance;
}
/**
* Check for duplicate object loading.
*
* If a duplicate is being loaded and has different configuration, that is
* bad and an exception will be raised.
*
* An exception is raised, as replacing the object will not update any
* references other objects may have. Additionally, simply updating the runtime
* configuration is not a good option as we may be missing important constructor
* logic dependent on the configuration.
*
* @param string $name The name of the alias in the registry.
* @param array<string, mixed> $config The config data for the new instance.
* @return void
* @throws \RuntimeException When a duplicate is found.
*/
protected function _checkDuplicate(string $name, array $config): void
{
$existing = $this->_loaded[$name];
$msg = sprintf('The "%s" alias has already been loaded.', $name);
$hasConfig = method_exists($existing, 'getConfig');
if (!$hasConfig) {
throw new RuntimeException($msg);
}
if (empty($config)) {
return;
}
$existingConfig = $existing->getConfig();
unset($config['enabled'], $existingConfig['enabled']);
$failure = null;
foreach ($config as $key => $value) {
if (!array_key_exists($key, $existingConfig)) {
$failure = " The `{$key}` was not defined in the previous configuration data.";
break;
}
if (isset($existingConfig[$key]) && $existingConfig[$key] !== $value) {
$failure = sprintf(
' The `%s` key has a value of `%s` but previously had a value of `%s`',
$key,
json_encode($value),
json_encode($existingConfig[$key])
);
break;
}
}
if ($failure) {
throw new RuntimeException($msg . $failure);
}
}
/**
* Should resolve the classname for a given object type.
*
* @param string $class The class to resolve.
* @return string|null The resolved name or null for failure.
* @psalm-return class-string|null
*/
abstract protected function _resolveClassName(string $class): ?string;
/**
* Throw an exception when the requested object name is missing.
*
* @param string $class The class that is missing.
* @param string|null $plugin The plugin $class is missing from.
* @return void
* @throws \Exception
*/
abstract protected function _throwMissingClassError(string $class, ?string $plugin): void;
/**
* Create an instance of a given classname.
*
* This method should construct and do any other initialization logic
* required.
*
* @param object|string $class The class to build.
* @param string $alias The alias of the object.
* @param array<string, mixed> $config The Configuration settings for construction
* @return object
* @psalm-param TObject|string $class
* @psalm-return TObject
*/
abstract protected function _create($class, string $alias, array $config);
/**
* Get the list of loaded objects.
*
* @return array<string> List of object names.
*/
public function loaded(): array
{
return array_keys($this->_loaded);
}
/**
* Check whether a given object is loaded.
*
* @param string $name The object name to check for.
* @return bool True is object is loaded else false.
*/
public function has(string $name): bool
{
return isset($this->_loaded[$name]);
}
/**
* Get loaded object instance.
*
* @param string $name Name of object.
* @return object Object instance.
* @throws \RuntimeException If not loaded or found.
* @psalm-return TObject
*/
public function get(string $name)
{
if (!isset($this->_loaded[$name])) {
throw new RuntimeException(sprintf('Unknown object "%s"', $name));
}
return $this->_loaded[$name];
}
/**
* Provide public read access to the loaded objects
*
* @param string $name Name of property to read
* @return object|null
* @psalm-return TObject|null
*/
public function __get(string $name)
{
return $this->_loaded[$name] ?? null;
}
/**
* Provide isset access to _loaded
*
* @param string $name Name of object being checked.
* @return bool
*/
public function __isset(string $name): bool
{
return $this->has($name);
}
/**
* Sets an object.
*
* @param string $name Name of a property to set.
* @param object $object Object to set.
* @psalm-param TObject $object
* @return void
*/
public function __set(string $name, $object): void
{
$this->set($name, $object);
}
/**
* Unsets an object.
*
* @param string $name Name of a property to unset.
* @return void
*/
public function __unset(string $name): void
{
$this->unload($name);
}
/**
* Normalizes an object array, creates an array that makes lazy loading
* easier
*
* @param array $objects Array of child objects to normalize.
* @return array<string, array> Array of normalized objects.
*/
public function normalizeArray(array $objects): array
{
$normal = [];
foreach ($objects as $i => $objectName) {
$config = [];
if (!is_int($i)) {
$config = (array)$objectName;
$objectName = $i;
}
[, $name] = pluginSplit($objectName);
if (isset($config['class'])) {
$normal[$name] = $config + ['config' => []];
} else {
$normal[$name] = ['class' => $objectName, 'config' => $config];
}
}
return $normal;
}
/**
* Clear loaded instances in the registry.
*
* If the registry subclass has an event manager, the objects will be detached from events as well.
*
* @return $this
*/
public function reset()
{
foreach (array_keys($this->_loaded) as $name) {
$this->unload((string)$name);
}
return $this;
}
/**
* Set an object directly into the registry by name.
*
* If this collection implements events, the passed object will
* be attached into the event manager
*
* @param string $name The name of the object to set in the registry.
* @param object $object instance to store in the registry
* @return $this
* @psalm-param TObject $object
*/
public function set(string $name, object $object)
{
[, $objName] = pluginSplit($name);
// Just call unload if the object was loaded before
if (array_key_exists($name, $this->_loaded)) {
$this->unload($name);
}
if ($this instanceof EventDispatcherInterface && $object instanceof EventListenerInterface) {
$this->getEventManager()->on($object);
}
$this->_loaded[$objName] = $object;
return $this;
}
/**
* Remove an object from the registry.
*
* If this registry has an event manager, the object will be detached from any events as well.
*
* @param string $name The name of the object to remove from the registry.
* @return $this
*/
public function unload(string $name)
{
if (empty($this->_loaded[$name])) {
[$plugin, $name] = pluginSplit($name);
$this->_throwMissingClassError($name, $plugin);
}
$object = $this->_loaded[$name];
if ($this instanceof EventDispatcherInterface && $object instanceof EventListenerInterface) {
$this->getEventManager()->off($object);
}
unset($this->_loaded[$name]);
return $this;
}
/**
* Returns an array iterator.
*
* @return \Traversable
* @psalm-return \Traversable<string, TObject>
*/
public function getIterator(): Traversable
{
return new ArrayIterator($this->_loaded);
}
/**
* Returns the number of loaded objects.
*
* @return int
*/
public function count(): int
{
return count($this->_loaded);
}
/**
* Debug friendly object properties.
*
* @return array<string, mixed>
*/
public function __debugInfo(): array
{
$properties = get_object_vars($this);
if (isset($properties['_loaded'])) {
$properties['_loaded'] = array_keys($properties['_loaded']);
}
return $properties;
}
}
+136
View File
@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 2.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
/**
* Plugin is used to load and locate plugins.
*
* It also can retrieve plugin paths and load their bootstrap and routes files.
*
* @link https://book.cakephp.org/4/en/plugins.html
*/
class Plugin
{
/**
* Holds a list of all loaded plugins and their configuration
*
* @var \Cake\Core\PluginCollection|null
*/
protected static $plugins;
/**
* Returns the filesystem path for a plugin
*
* @param string $name name of the plugin in CamelCase format
* @return string path to the plugin folder
* @throws \Cake\Core\Exception\MissingPluginException If the folder for plugin was not found
* or plugin has not been loaded.
*/
public static function path(string $name): string
{
$plugin = static::getCollection()->get($name);
return $plugin->getPath();
}
/**
* Returns the filesystem path for plugin's folder containing class files.
*
* @param string $name name of the plugin in CamelCase format.
* @return string Path to the plugin folder containing class files.
* @throws \Cake\Core\Exception\MissingPluginException If plugin has not been loaded.
*/
public static function classPath(string $name): string
{
$plugin = static::getCollection()->get($name);
return $plugin->getClassPath();
}
/**
* Returns the filesystem path for plugin's folder containing config files.
*
* @param string $name name of the plugin in CamelCase format.
* @return string Path to the plugin folder containing config files.
* @throws \Cake\Core\Exception\MissingPluginException If plugin has not been loaded.
*/
public static function configPath(string $name): string
{
$plugin = static::getCollection()->get($name);
return $plugin->getConfigPath();
}
/**
* Returns the filesystem path for plugin's folder containing template files.
*
* @param string $name name of the plugin in CamelCase format.
* @return string Path to the plugin folder containing template files.
* @throws \Cake\Core\Exception\MissingPluginException If plugin has not been loaded.
*/
public static function templatePath(string $name): string
{
$plugin = static::getCollection()->get($name);
return $plugin->getTemplatePath();
}
/**
* Returns true if the plugin $plugin is already loaded.
*
* @param string $plugin Plugin name.
* @return bool
* @since 3.7.0
*/
public static function isLoaded(string $plugin): bool
{
return static::getCollection()->has($plugin);
}
/**
* Return a list of loaded plugins.
*
* @return array<string> A list of plugins that have been loaded
*/
public static function loaded(): array
{
$names = [];
foreach (static::getCollection() as $plugin) {
$names[] = $plugin->getName();
}
sort($names);
return $names;
}
/**
* Get the shared plugin collection.
*
* This method should generally not be used during application
* runtime as plugins should be set during Application startup.
*
* @return \Cake\Core\PluginCollection
*/
public static function getCollection(): PluginCollection
{
if (!isset(static::$plugins)) {
static::$plugins = new PluginCollection();
}
return static::$plugins;
}
}
+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 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Console\CommandCollection;
use Cake\Event\EventDispatcherInterface;
use Cake\Http\MiddlewareQueue;
use Cake\Routing\RouteBuilder;
/**
* Interface for Applications that leverage plugins & events.
*
* Events can be bound to the application event manager during
* the application's bootstrap and plugin bootstrap.
*/
interface PluginApplicationInterface extends EventDispatcherInterface
{
/**
* Add a plugin to the loaded plugin set.
*
* If the named plugin does not exist, or does not define a Plugin class, an
* instance of `Cake\Core\BasePlugin` will be used. This generated class will have
* all plugin hooks enabled.
*
* @param \Cake\Core\PluginInterface|string $name The plugin name or plugin object.
* @param array<string, mixed> $config The configuration data for the plugin if using a string for $name
* @return $this
*/
public function addPlugin($name, array $config = []);
/**
* Run bootstrap logic for loaded plugins.
*
* @return void
*/
public function pluginBootstrap(): void;
/**
* Run routes hooks for loaded plugins
*
* @param \Cake\Routing\RouteBuilder $routes The route builder to use.
* @return \Cake\Routing\RouteBuilder
*/
public function pluginRoutes(RouteBuilder $routes): RouteBuilder;
/**
* Run middleware hooks for plugins
*
* @param \Cake\Http\MiddlewareQueue $middleware The MiddlewareQueue to use.
* @return \Cake\Http\MiddlewareQueue
*/
public function pluginMiddleware(MiddlewareQueue $middleware): MiddlewareQueue;
/**
* Run console hooks for plugins
*
* @param \Cake\Console\CommandCollection $commands The CommandCollection to use.
* @return \Cake\Console\CommandCollection
*/
public function pluginConsole(CommandCollection $commands): CommandCollection;
}
+363
View File
@@ -0,0 +1,363 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Core\Exception\CakeException;
use Cake\Core\Exception\MissingPluginException;
use Countable;
use Generator;
use InvalidArgumentException;
use Iterator;
/**
* Plugin Collection
*
* Holds onto plugin objects loaded into an application, and
* provides methods for iterating, and finding plugins based
* on criteria.
*
* This class implements the Iterator interface to allow plugins
* to be iterated, handling the situation where a plugin's hook
* method (usually bootstrap) loads another plugin during iteration.
*
* While its implementation supported nested iteration it does not
* support using `continue` or `break` inside loops.
*
* @template-implements \Iterator<string, \Cake\Core\PluginInterface>
*/
class PluginCollection implements Iterator, Countable
{
/**
* Plugin list
*
* @var array<\Cake\Core\PluginInterface>
*/
protected $plugins = [];
/**
* Names of plugins
*
* @var array<string>
*/
protected $names = [];
/**
* Iterator position stack.
*
* @var array<int>
*/
protected $positions = [];
/**
* Loop depth
*
* @var int
*/
protected $loopDepth = -1;
/**
* Constructor
*
* @param array<\Cake\Core\PluginInterface> $plugins The map of plugins to add to the collection.
*/
public function __construct(array $plugins = [])
{
foreach ($plugins as $plugin) {
$this->add($plugin);
}
$this->loadConfig();
}
/**
* Load the path information stored in vendor/cakephp-plugins.php
*
* This file is generated by the cakephp/plugin-installer package and used
* to locate plugins on the filesystem as applications can use `extra.plugin-paths`
* in their composer.json file to move plugin outside of vendor/
*
* @internal
* @return void
*/
protected function loadConfig(): void
{
if (Configure::check('plugins')) {
return;
}
$vendorFile = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'cakephp-plugins.php';
if (!is_file($vendorFile)) {
$vendorFile = dirname(dirname(dirname(dirname(__DIR__)))) . DIRECTORY_SEPARATOR . 'cakephp-plugins.php';
if (!is_file($vendorFile)) {
Configure::write(['plugins' => []]);
return;
}
}
$config = require $vendorFile;
Configure::write($config);
}
/**
* Locate a plugin path by looking at configuration data.
*
* This will use the `plugins` Configure key, and fallback to enumerating `App::path('plugins')`
*
* This method is not part of the official public API as plugins with
* no plugin class are being phased out.
*
* @param string $name The plugin name to locate a path for.
* @return string
* @throws \Cake\Core\Exception\MissingPluginException when a plugin path cannot be resolved.
* @internal
*/
public function findPath(string $name): string
{
// Ensure plugin config is loaded each time. This is necessary primarily
// for testing because the Configure::clear() call in TestCase::tearDown()
// wipes out all configuration including plugin paths config.
$this->loadConfig();
$path = Configure::read('plugins.' . $name);
if ($path) {
return $path;
}
$pluginPath = str_replace('/', DIRECTORY_SEPARATOR, $name);
$paths = App::path('plugins');
foreach ($paths as $path) {
if (is_dir($path . $pluginPath)) {
return $path . $pluginPath . DIRECTORY_SEPARATOR;
}
}
throw new MissingPluginException(['plugin' => $name]);
}
/**
* Add a plugin to the collection
*
* Plugins will be keyed by their names.
*
* @param \Cake\Core\PluginInterface $plugin The plugin to load.
* @return $this
*/
public function add(PluginInterface $plugin)
{
$name = $plugin->getName();
$this->plugins[$name] = $plugin;
$this->names = array_keys($this->plugins);
return $this;
}
/**
* Remove a plugin from the collection if it exists.
*
* @param string $name The named plugin.
* @return $this
*/
public function remove(string $name)
{
unset($this->plugins[$name]);
$this->names = array_keys($this->plugins);
return $this;
}
/**
* Remove all plugins from the collection
*
* @return $this
*/
public function clear()
{
$this->plugins = [];
$this->names = [];
$this->positions = [];
$this->loopDepth = -1;
return $this;
}
/**
* Check whether the named plugin exists in the collection.
*
* @param string $name The named plugin.
* @return bool
*/
public function has(string $name): bool
{
return isset($this->plugins[$name]);
}
/**
* Get the a plugin by name.
*
* If a plugin isn't already loaded it will be autoloaded on first access
* and that plugins loaded this way may miss some hook methods.
*
* @param string $name The plugin to get.
* @return \Cake\Core\PluginInterface The plugin.
* @throws \Cake\Core\Exception\MissingPluginException when unknown plugins are fetched.
*/
public function get(string $name): PluginInterface
{
if ($this->has($name)) {
return $this->plugins[$name];
}
$plugin = $this->create($name);
$this->add($plugin);
return $plugin;
}
/**
* Create a plugin instance from a name/classname and configuration.
*
* @param string $name The plugin name or classname
* @param array<string, mixed> $config Configuration options for the plugin.
* @return \Cake\Core\PluginInterface
* @throws \Cake\Core\Exception\MissingPluginException When plugin instance could not be created.
*/
public function create(string $name, array $config = []): PluginInterface
{
if ($name === '') {
throw new CakeException('Cannot create a plugin with empty name');
}
if (strpos($name, '\\') !== false) {
/** @var \Cake\Core\PluginInterface */
return new $name($config);
}
$config += ['name' => $name];
$namespace = str_replace('/', '\\', $name);
$className = $namespace . '\\' . 'Plugin';
// Check for [Vendor/]Foo/Plugin class
if (!class_exists($className)) {
$pos = strpos($name, '/');
if ($pos === false) {
$className = $namespace . '\\' . $name . 'Plugin';
} else {
$className = $namespace . '\\' . substr($name, $pos + 1) . 'Plugin';
}
// Check for [Vendor/]Foo/FooPlugin
if (!class_exists($className)) {
$className = BasePlugin::class;
if (empty($config['path'])) {
$config['path'] = $this->findPath($name);
}
}
}
/** @var class-string<\Cake\Core\PluginInterface> $className */
return new $className($config);
}
/**
* Implementation of Countable.
*
* Get the number of plugins in the collection.
*
* @return int
*/
public function count(): int
{
return count($this->plugins);
}
/**
* Part of Iterator Interface
*
* @return void
*/
public function next(): void
{
$this->positions[$this->loopDepth]++;
}
/**
* Part of Iterator Interface
*
* @return string
*/
public function key(): string
{
return $this->names[$this->positions[$this->loopDepth]];
}
/**
* Part of Iterator Interface
*
* @return \Cake\Core\PluginInterface
*/
public function current(): PluginInterface
{
$position = $this->positions[$this->loopDepth];
$name = $this->names[$position];
return $this->plugins[$name];
}
/**
* Part of Iterator Interface
*
* @return void
*/
public function rewind(): void
{
$this->positions[] = 0;
$this->loopDepth += 1;
}
/**
* Part of Iterator Interface
*
* @return bool
*/
public function valid(): bool
{
$valid = isset($this->names[$this->positions[$this->loopDepth]]);
if (!$valid) {
array_pop($this->positions);
$this->loopDepth -= 1;
}
return $valid;
}
/**
* Filter the plugins to those with the named hook enabled.
*
* @param string $hook The hook to filter plugins by
* @return \Generator<\Cake\Core\PluginInterface> A generator containing matching plugins.
* @throws \InvalidArgumentException on invalid hooks
*/
public function with(string $hook): Generator
{
if (!in_array($hook, PluginInterface::VALID_HOOKS, true)) {
throw new InvalidArgumentException("The `{$hook}` hook is not a known plugin hook.");
}
foreach ($this as $plugin) {
if ($plugin->isEnabled($hook)) {
yield $plugin;
}
}
}
}
+135
View File
@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (https://cakefoundation.org)
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use Cake\Console\CommandCollection;
use Cake\Http\MiddlewareQueue;
use Cake\Routing\RouteBuilder;
/**
* Plugin Interface
*
* @method void services(\Cake\Core\ContainerInterface $container) Register plugin services to
* the application's container
*/
interface PluginInterface
{
/**
* List of valid hooks.
*
* @var array<string>
*/
public const VALID_HOOKS = ['bootstrap', 'console', 'middleware', 'routes', 'services'];
/**
* Get the name of this plugin.
*
* @return string
*/
public function getName(): string;
/**
* Get the filesystem path to this plugin
*
* @return string
*/
public function getPath(): string;
/**
* Get the filesystem path to configuration for this plugin
*
* @return string
*/
public function getConfigPath(): string;
/**
* Get the filesystem path to configuration for this plugin
*
* @return string
*/
public function getClassPath(): string;
/**
* Get the filesystem path to templates for this plugin
*
* @return string
*/
public function getTemplatePath(): string;
/**
* Load all the application configuration and bootstrap logic.
*
* The default implementation of this method will include the `config/bootstrap.php` in the plugin if it exist. You
* can override this method to replace that behavior.
*
* The host application is provided as an argument. This allows you to load additional
* plugin dependencies, or attach events.
*
* @param \Cake\Core\PluginApplicationInterface $app The host application
* @return void
*/
public function bootstrap(PluginApplicationInterface $app): void;
/**
* Add console commands for the plugin.
*
* @param \Cake\Console\CommandCollection $commands The command collection to update
* @return \Cake\Console\CommandCollection
*/
public function console(CommandCollection $commands): CommandCollection;
/**
* Add middleware for the plugin.
*
* @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to update.
* @return \Cake\Http\MiddlewareQueue
*/
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue;
/**
* Add routes for the plugin.
*
* The default implementation of this method will include the `config/routes.php` in the plugin if it exists. You
* can override this method to replace that behavior.
*
* @param \Cake\Routing\RouteBuilder $routes The route builder to update.
* @return void
*/
public function routes(RouteBuilder $routes): void;
/**
* Disables the named hook
*
* @param string $hook The hook to disable
* @return $this
*/
public function disable(string $hook);
/**
* Enables the named hook
*
* @param string $hook The hook to disable
* @return $this
*/
public function enable(string $hook);
/**
* Check if the named hook is enabled
*
* @param string $hook The hook to check
* @return bool
*/
public function isEnabled(string $hook): bool;
}
+37
View File
@@ -0,0 +1,37 @@
[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/core.svg?style=flat-square)](https://packagist.org/packages/cakephp/core)
[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt)
# CakePHP Core Classes
A set of classes used for configuration files reading and storing.
This repository contains the classes that are used as glue for creating the CakePHP framework.
## Usage
You can use the `Configure` class to store arbitrary configuration data:
```php
use Cake\Core\Configure;
use Cake\Core\Configure\Engine\PhpConfig;
Configure::write('Company.name','Pizza, Inc.');
Configure::read('Company.name'); // Returns: 'Pizza, Inc.'
```
It also possible to load configuration from external files:
```php
Configure::config('default', new PhpConfig('/path/to/config/folder'));
Configure::load('app', 'default', false);
Configure::load('other_config', 'default');
```
And write the configuration back into files:
```php
Configure::dump('my_config', 'default');
```
## Documentation
Please make sure you check the [official documentation](https://book.cakephp.org/4/en/development/configuration.html)
+94
View File
@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Retry;
use Exception;
/**
* Allows any action to be retried in case of an exception.
*
* This class can be parametrized with a strategy, which will be followed
* to determine whether the action should be retried.
*/
class CommandRetry
{
/**
* The strategy to follow should the executed action fail.
*
* @var \Cake\Core\Retry\RetryStrategyInterface
*/
protected $strategy;
/**
* @var int
*/
protected $maxRetries;
/**
* @var int
*/
protected $numRetries;
/**
* Creates the CommandRetry object with the given strategy and retry count
*
* @param \Cake\Core\Retry\RetryStrategyInterface $strategy The strategy to follow should the action fail
* @param int $maxRetries The maximum number of retry attempts allowed
*/
public function __construct(RetryStrategyInterface $strategy, int $maxRetries = 1)
{
$this->strategy = $strategy;
$this->maxRetries = $maxRetries;
}
/**
* The number of retries to perform in case of failure
*
* @param callable $action The callable action to execute with a retry strategy
* @return mixed The return value of the passed action callable
* @throws \Exception
*/
public function run(callable $action)
{
$this->numRetries = 0;
while (true) {
try {
return $action();
} catch (Exception $e) {
if (
$this->numRetries < $this->maxRetries &&
$this->strategy->shouldRetry($e, $this->numRetries)
) {
$this->numRetries++;
continue;
}
throw $e;
}
}
}
/**
* Returns the last number of retry attemps.
*
* @return int
*/
public function getRetries(): int
{
return $this->numRetries;
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Retry;
use Exception;
/**
* Used to instruct a CommandRetry object on whether a retry
* for an action should be performed
*/
interface RetryStrategyInterface
{
/**
* Returns true if the action can be retried, false otherwise.
*
* @param \Exception $exception The exception that caused the action to fail
* @param int $retryCount The number of times action has been retried
* @return bool Whether it is OK to retry the action
*/
public function shouldRetry(Exception $exception, int $retryCount): bool;
}
+50
View File
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
/**
* Read-only wrapper for configuration data
*
* Intended for use with {@link \Cake\Core\Container} as
* a typehintable way for services to have application
* configuration injected as arrays cannot be typehinted.
*/
class ServiceConfig
{
/**
* Read a configuration key
*
* @param string $path The path to read.
* @param mixed $default The default value to use if $path does not exist.
* @return mixed The configuration data or $default value.
*/
public function get(string $path, $default = null)
{
return Configure::read($path, $default);
}
/**
* Check if $path exists and has a non-null value.
*
* @param string $path The path to check.
* @return bool True if the configuration data exists.
*/
public function has(string $path): bool
{
return Configure::check($path);
}
}
+132
View File
@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use League\Container\DefinitionContainerInterface;
use League\Container\ServiceProvider\AbstractServiceProvider;
use League\Container\ServiceProvider\BootableServiceProviderInterface;
use RuntimeException;
/**
* Container ServiceProvider
*
* Service provider bundle related services together helping
* to organize your application's dependencies. They also help
* improve performance of applications with many services by
* allowing service registration to be deferred until services are needed.
*/
abstract class ServiceProvider extends AbstractServiceProvider implements BootableServiceProviderInterface
{
/**
* List of ids of services this provider provides.
*
* @var array<string>
* @see ServiceProvider::provides()
*/
protected $provides = [];
/**
* Get the container.
*
* This method's actual return type and documented return type differ
* because PHP 7.2 doesn't support return type narrowing.
*
* @return \Cake\Core\ContainerInterface
*/
public function getContainer(): DefinitionContainerInterface
{
$container = parent::getContainer();
if (!($container instanceof ContainerInterface)) {
$message = sprintf(
'Unexpected container type. Expected `%s` got `%s` instead.',
ContainerInterface::class,
getTypeName($container)
);
throw new RuntimeException($message);
}
return $container;
}
/**
* Delegate to the bootstrap() method
*
* This method wraps the league/container function so users
* only need to use the CakePHP bootstrap() interface.
*
* @return void
*/
public function boot(): void
{
$this->bootstrap($this->getContainer());
}
/**
* Bootstrap hook for ServiceProviders
*
* This hook should be implemented if your service provider
* needs to register additional service providers, load configuration
* files or do any other work when the service provider is added to the
* container.
*
* @param \Cake\Core\ContainerInterface $container The container to add services to.
* @return void
*/
public function bootstrap(ContainerInterface $container): void
{
}
/**
* Call the abstract services() method.
*
* This method primarily exists as a shim between the interface
* that league/container has and the one we want to offer in CakePHP.
*
* @return void
*/
public function register(): void
{
$this->services($this->getContainer());
}
/**
* The provides method is a way to let the container know that a service
* is provided by this service provider.
*
* Every service that is registered via this service provider must have an
* alias added to this array or it will be ignored.
*
* @param string $id Identifier.
* @return bool
*/
public function provides(string $id): bool
{
return in_array($id, $this->provides, true);
}
/**
* Register the services in a provider.
*
* All services registered in this method should also be included in the $provides
* property so that services can be located.
*
* @param \Cake\Core\ContainerInterface $container The container to add services to.
* @return void
*/
abstract public function services(ContainerInterface $container): void;
}
+325
View File
@@ -0,0 +1,325 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core;
use BadMethodCallException;
use InvalidArgumentException;
use LogicException;
/**
* A trait that provides a set of static methods to manage configuration
* for classes that provide an adapter facade or need to have sets of
* configuration data registered and manipulated.
*
* Implementing objects are expected to declare a static `$_dsnClassMap` property.
*/
trait StaticConfigTrait
{
/**
* Configuration sets.
*
* @var array<string, mixed>
*/
protected static $_config = [];
/**
* This method can be used to define configuration adapters for an application.
*
* To change an adapter's configuration at runtime, first drop the adapter and then
* reconfigure it.
*
* Adapters will not be constructed until the first operation is done.
*
* ### Usage
*
* Assuming that the class' name is `Cache` the following scenarios
* are supported:
*
* Setting a cache engine up.
*
* ```
* Cache::setConfig('default', $settings);
* ```
*
* Injecting a constructed adapter in:
*
* ```
* Cache::setConfig('default', $instance);
* ```
*
* Configure multiple adapters at once:
*
* ```
* Cache::setConfig($arrayOfConfig);
* ```
*
* @param array<string, mixed>|string $key The name of the configuration, or an array of multiple configs.
* @param mixed $config Configuration value. Generally an array of name => configuration data for adapter.
* @throws \BadMethodCallException When trying to modify an existing config.
* @throws \LogicException When trying to store an invalid structured config array.
* @return void
*/
public static function setConfig($key, $config = null): void
{
if ($config === null) {
if (!is_array($key)) {
throw new LogicException('If config is null, key must be an array.');
}
foreach ($key as $name => $settings) {
static::setConfig($name, $settings);
}
return;
}
if (isset(static::$_config[$key])) {
/** @psalm-suppress PossiblyInvalidArgument */
throw new BadMethodCallException(sprintf('Cannot reconfigure existing key "%s"', $key));
}
if (is_object($config)) {
$config = ['className' => $config];
}
if (is_array($config) && isset($config['url'])) {
$parsed = static::parseDsn($config['url']);
unset($config['url']);
$config = $parsed + $config;
}
if (isset($config['engine']) && empty($config['className'])) {
$config['className'] = $config['engine'];
unset($config['engine']);
}
/** @psalm-suppress InvalidPropertyAssignmentValue */
static::$_config[$key] = $config;
}
/**
* Reads existing configuration.
*
* @param string $key The name of the configuration.
* @return mixed|null Configuration data at the named key or null if the key does not exist.
*/
public static function getConfig(string $key)
{
return static::$_config[$key] ?? null;
}
/**
* Reads existing configuration for a specific key.
*
* The config value for this key must exist, it can never be null.
*
* @param string $key The name of the configuration.
* @return mixed Configuration data at the named key.
* @throws \InvalidArgumentException If value does not exist.
*/
public static function getConfigOrFail(string $key)
{
if (!isset(static::$_config[$key])) {
throw new InvalidArgumentException(sprintf('Expected configuration `%s` not found.', $key));
}
return static::$_config[$key];
}
/**
* Drops a constructed adapter.
*
* If you wish to modify an existing configuration, you should drop it,
* change configuration and then re-add it.
*
* If the implementing objects supports a `$_registry` object the named configuration
* will also be unloaded from the registry.
*
* @param string $config An existing configuration you wish to remove.
* @return bool Success of the removal, returns false when the config does not exist.
*/
public static function drop(string $config): bool
{
if (!isset(static::$_config[$config])) {
return false;
}
/** @psalm-suppress RedundantPropertyInitializationCheck */
if (isset(static::$_registry)) {
static::$_registry->unload($config);
}
unset(static::$_config[$config]);
return true;
}
/**
* Returns an array containing the named configurations
*
* @return array<string> Array of configurations.
*/
public static function configured(): array
{
$configurations = array_keys(static::$_config);
return array_map(function ($key) {
return (string)$key;
}, $configurations);
}
/**
* Parses a DSN into a valid connection configuration
*
* This method allows setting a DSN using formatting similar to that used by PEAR::DB.
* The following is an example of its usage:
*
* ```
* $dsn = 'mysql://user:pass@localhost/database?';
* $config = ConnectionManager::parseDsn($dsn);
*
* $dsn = 'Cake\Log\Engine\FileLog://?types=notice,info,debug&file=debug&path=LOGS';
* $config = Log::parseDsn($dsn);
*
* $dsn = 'smtp://user:secret@localhost:25?timeout=30&client=null&tls=null';
* $config = Email::parseDsn($dsn);
*
* $dsn = 'file:///?className=\My\Cache\Engine\FileEngine';
* $config = Cache::parseDsn($dsn);
*
* $dsn = 'File://?prefix=myapp_cake_core_&serialize=true&duration=+2 minutes&path=/tmp/persistent/';
* $config = Cache::parseDsn($dsn);
* ```
*
* For all classes, the value of `scheme` is set as the value of both the `className`
* unless they have been otherwise specified.
*
* Note that querystring arguments are also parsed and set as values in the returned configuration.
*
* @param string $dsn The DSN string to convert to a configuration array
* @return array<string, mixed> The configuration array to be stored after parsing the DSN
* @throws \InvalidArgumentException If not passed a string, or passed an invalid string
*/
public static function parseDsn(string $dsn): array
{
if (empty($dsn)) {
return [];
}
$pattern = <<<'REGEXP'
{
^
(?P<_scheme>
(?P<scheme>[\w\\\\]+)://
)
(?P<_username>
(?P<username>.*?)
(?P<_password>
:(?P<password>.*?)
)?
@
)?
(?P<_host>
(?P<host>[^?#/:@]+)
(?P<_port>
:(?P<port>\d+)
)?
)?
(?P<_path>
(?P<path>/[^?#]*)
)?
(?P<_query>
\?(?P<query>[^#]*)
)?
(?P<_fragment>
\#(?P<fragment>.*)
)?
$
}x
REGEXP;
preg_match($pattern, $dsn, $parsed);
if (!$parsed) {
throw new InvalidArgumentException("The DSN string '{$dsn}' could not be parsed.");
}
$exists = [];
foreach ($parsed as $k => $v) {
if (is_int($k)) {
unset($parsed[$k]);
} elseif (strpos($k, '_') === 0) {
$exists[substr($k, 1)] = ($v !== '');
unset($parsed[$k]);
} elseif ($v === '' && !$exists[$k]) {
unset($parsed[$k]);
}
}
$query = '';
if (isset($parsed['query'])) {
$query = $parsed['query'];
unset($parsed['query']);
}
parse_str($query, $queryArgs);
foreach ($queryArgs as $key => $value) {
if ($value === 'true') {
$queryArgs[$key] = true;
} elseif ($value === 'false') {
$queryArgs[$key] = false;
} elseif ($value === 'null') {
$queryArgs[$key] = null;
}
}
$parsed = $queryArgs + $parsed;
if (empty($parsed['className'])) {
$classMap = static::getDsnClassMap();
$parsed['className'] = $parsed['scheme'];
if (isset($classMap[$parsed['scheme']])) {
/** @psalm-suppress PossiblyNullArrayOffset */
$parsed['className'] = $classMap[$parsed['scheme']];
}
}
return $parsed;
}
/**
* Updates the DSN class map for this class.
*
* @param array<string, string> $map Additions/edits to the class map to apply.
* @return void
* @psalm-param array<string, class-string> $map
*/
public static function setDsnClassMap(array $map): void
{
static::$_dsnClassMap = $map + static::$_dsnClassMap;
}
/**
* Returns the DSN class map for this class.
*
* @return array<string, string>
* @psalm-return array<string, class-string>
*/
public static function getDsnClassMap(): array
{
return static::$_dsnClassMap;
}
}
+181
View File
@@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @since 4.2.0
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\TestSuite;
use Cake\Core\Configure;
use Cake\Core\ContainerInterface;
use Cake\Event\EventInterface;
use Closure;
use League\Container\Exception\NotFoundException;
use LogicException;
/**
* A set of methods used for defining container services
* in test cases.
*
* This trait leverages the `Application.buildContainer` event
* to inject the mocked services into the container that the
* application uses.
*/
trait ContainerStubTrait
{
/**
* The customized application class name.
*
* @psalm-var class-string<\Cake\Core\HttpApplicationInterface>|class-string<\Cake\Core\ConsoleApplicationInterface>|null
* @var string|null
*/
protected $_appClass;
/**
* The customized application constructor arguments.
*
* @var array|null
*/
protected $_appArgs;
/**
* The collection of container services.
*
* @var array
*/
private $containerServices = [];
/**
* Configure the application class to use in integration tests.
*
* @param string $class The application class name.
* @param array|null $constructorArgs The constructor arguments for your application class.
* @return void
* @psalm-param class-string<\Cake\Core\HttpApplicationInterface>|class-string<\Cake\Core\ConsoleApplicationInterface> $class
*/
public function configApplication(string $class, ?array $constructorArgs): void
{
$this->_appClass = $class;
$this->_appArgs = $constructorArgs;
}
/**
* Create an application instance.
*
* Uses the configuration set in `configApplication()`.
*
* @return \Cake\Core\HttpApplicationInterface|\Cake\Core\ConsoleApplicationInterface
*/
protected function createApp()
{
if ($this->_appClass) {
$appClass = $this->_appClass;
} else {
/** @psalm-var class-string<\Cake\Http\BaseApplication> */
$appClass = Configure::read('App.namespace') . '\Application';
}
if (!class_exists($appClass)) {
throw new LogicException("Cannot load `{$appClass}` for use in integration testing.");
}
$appArgs = $this->_appArgs ?: [CONFIG];
$app = new $appClass(...$appArgs);
if (!empty($this->containerServices) && method_exists($app, 'getEventManager')) {
$app->getEventManager()->on('Application.buildContainer', [$this, 'modifyContainer']);
}
return $app;
}
/**
* Add a mocked service to the container.
*
* When the container is created the provided classname
* will be mapped to the factory function. The factory
* function will be used to create mocked services.
*
* @param string $class The class or interface you want to define.
* @param \Closure $factory The factory function for mocked services.
* @return $this
*/
public function mockService(string $class, Closure $factory)
{
$this->containerServices[$class] = $factory;
return $this;
}
/**
* Remove a mocked service to the container.
*
* @param string $class The class or interface you want to remove.
* @return $this
*/
public function removeMockService(string $class)
{
unset($this->containerServices[$class]);
return $this;
}
/**
* Wrap the application's container with one containing mocks.
*
* If any mocked services are defined, the application's container
* will be replaced with one containing mocks. The original
* container will be set as a delegate to the mock container.
*
* @param \Cake\Event\EventInterface $event The event
* @param \Cake\Core\ContainerInterface $container The container to wrap.
* @return \Cake\Core\ContainerInterface|null
*/
public function modifyContainer(EventInterface $event, ContainerInterface $container): ?ContainerInterface
{
if (empty($this->containerServices)) {
return null;
}
foreach ($this->containerServices as $key => $factory) {
if ($container->has($key)) {
try {
$container->extend($key)->setConcrete($factory);
} catch (NotFoundException $e) {
$container->add($key, $factory);
}
} else {
$container->add($key, $factory);
}
}
return $container;
}
/**
* Clears any mocks that were defined and cleans
* up application class configuration.
*
* @after
* @return void
*/
public function cleanupContainer(): void
{
$this->_appArgs = null;
$this->_appClass = null;
$this->containerServices = [];
}
}
// phpcs:disable
class_alias(
'Cake\Core\TestSuite\ContainerStubTrait',
'Cake\TestSuite\ContainerStubTrait'
);
// phpcs:enable
+44
View File
@@ -0,0 +1,44 @@
{
"name": "cakephp/core",
"description": "CakePHP Framework Core classes",
"type": "library",
"keywords": [
"cakephp",
"framework",
"core"
],
"homepage": "https://cakephp.org",
"license": "MIT",
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/core/graphs/contributors"
}
],
"support": {
"issues": "https://github.com/cakephp/cakephp/issues",
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"source": "https://github.com/cakephp/core"
},
"require": {
"php": ">=7.4.0",
"cakephp/utility": "^4.0"
},
"provide": {
"psr/container-implementation": "^1.0 || ^2.0"
},
"suggest": {
"cakephp/event": "To use PluginApplicationInterface or plugin applications.",
"cakephp/cache": "To use Configure::store() and restore().",
"league/container": "To use Container and ServiceProvider classes"
},
"autoload": {
"psr-4": {
"Cake\\Core\\": "."
},
"files": [
"functions.php"
]
}
}
+340
View File
@@ -0,0 +1,340 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.5.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
// phpcs:disable PSR1.Files.SideEffects
namespace Cake\Core;
if (!function_exists('Cake\Core\h')) {
/**
* Convenience method for htmlspecialchars.
*
* @param mixed $text Text to wrap through htmlspecialchars. Also works with arrays, and objects.
* Arrays will be mapped and have all their elements escaped. Objects will be string cast if they
* implement a `__toString` method. Otherwise, the class name will be used.
* Other scalar types will be returned unchanged.
* @param bool $double Encode existing html entities.
* @param string|null $charset Character set to use when escaping.
* Defaults to config value in `mb_internal_encoding()` or 'UTF-8'.
* @return mixed Wrapped text.
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#h
*/
function h($text, bool $double = true, ?string $charset = null)
{
if (is_string($text)) {
//optimize for strings
} elseif (is_array($text)) {
$texts = [];
foreach ($text as $k => $t) {
$texts[$k] = h($t, $double, $charset);
}
return $texts;
} elseif (is_object($text)) {
if (method_exists($text, '__toString')) {
$text = $text->__toString();
} else {
$text = '(object)' . get_class($text);
}
} elseif ($text === null || is_scalar($text)) {
return $text;
}
static $defaultCharset = false;
if ($defaultCharset === false) {
$defaultCharset = mb_internal_encoding() ?: 'UTF-8';
}
return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, $charset ?: $defaultCharset, $double);
}
}
if (!function_exists('Cake\Core\pluginSplit')) {
/**
* Splits a dot syntax plugin name into its plugin and class name.
* If $name does not have a dot, then index 0 will be null.
*
* Commonly used like
* ```
* list($plugin, $name) = pluginSplit($name);
* ```
*
* @param string $name The name you want to plugin split.
* @param bool $dotAppend Set to true if you want the plugin to have a '.' appended to it.
* @param string|null $plugin Optional default plugin to use if no plugin is found. Defaults to null.
* @return array Array with 2 indexes. 0 => plugin name, 1 => class name.
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#pluginSplit
* @psalm-return array{string|null, string}
*/
function pluginSplit(string $name, bool $dotAppend = false, ?string $plugin = null): array
{
if (strpos($name, '.') !== false) {
$parts = explode('.', $name, 2);
if ($dotAppend) {
$parts[0] .= '.';
}
/** @psalm-var array{string, string} */
return $parts;
}
return [$plugin, $name];
}
}
if (!function_exists('Cake\Core\namespaceSplit')) {
/**
* Split the namespace from the classname.
*
* Commonly used like `list($namespace, $className) = namespaceSplit($class);`.
*
* @param string $class The full class name, ie `Cake\Core\App`.
* @return array<string> Array with 2 indexes. 0 => namespace, 1 => classname.
*/
function namespaceSplit(string $class): array
{
$pos = strrpos($class, '\\');
if ($pos === false) {
return ['', $class];
}
return [substr($class, 0, $pos), substr($class, $pos + 1)];
}
}
if (!function_exists('Cake\Core\pr')) {
/**
* print_r() convenience function.
*
* In terminals this will act similar to using print_r() directly, when not run on CLI
* print_r() will also wrap `<pre>` tags around the output of given variable. Similar to debug().
*
* This function returns the same variable that was passed.
*
* @param mixed $var Variable to print out.
* @return mixed the same $var that was passed to this function
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#pr
* @see debug()
*/
function pr($var)
{
if (!Configure::read('debug')) {
return $var;
}
$template = PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg' ? '<pre class="pr">%s</pre>' : "\n%s\n\n";
printf($template, trim(print_r($var, true)));
return $var;
}
}
if (!function_exists('Cake\Core\pj')) {
/**
* JSON pretty print convenience function.
*
* In terminals this will act similar to using json_encode() with JSON_PRETTY_PRINT directly, when not run on CLI
* will also wrap `<pre>` tags around the output of given variable. Similar to pr().
*
* This function returns the same variable that was passed.
*
* @param mixed $var Variable to print out.
* @return mixed the same $var that was passed to this function
* @see pr()
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#pj
*/
function pj($var)
{
if (!Configure::read('debug')) {
return $var;
}
$template = PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg' ? '<pre class="pj">%s</pre>' : "\n%s\n\n";
printf($template, trim(json_encode($var, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)));
return $var;
}
}
if (!function_exists('Cake\Core\env')) {
/**
* Gets an environment variable from available sources, and provides emulation
* for unsupported or inconsistent environment variables (i.e. DOCUMENT_ROOT on
* IIS, or SCRIPT_NAME in CGI mode). Also exposes some additional custom
* environment information.
*
* @param string $key Environment variable name.
* @param string|bool|null $default Specify a default value in case the environment variable is not defined.
* @return string|bool|null Environment variable setting.
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#env
*/
function env(string $key, $default = null)
{
if ($key === 'HTTPS') {
if (isset($_SERVER['HTTPS'])) {
return !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
}
return strpos((string)env('SCRIPT_URI'), 'https://') === 0;
}
if ($key === 'SCRIPT_NAME' && env('CGI_MODE') && isset($_ENV['SCRIPT_URL'])) {
$key = 'SCRIPT_URL';
}
/** @var string|null $val */
$val = $_SERVER[$key] ?? $_ENV[$key] ?? null;
if ($val == null && getenv($key) !== false) {
/** @var string|false $val */
$val = getenv($key);
}
if ($key === 'REMOTE_ADDR' && $val === env('SERVER_ADDR')) {
$addr = env('HTTP_PC_REMOTE_ADDR');
if ($addr !== null) {
$val = $addr;
}
}
if ($val !== null) {
return $val;
}
switch ($key) {
case 'DOCUMENT_ROOT':
$name = (string)env('SCRIPT_NAME');
$filename = (string)env('SCRIPT_FILENAME');
$offset = 0;
if (!strpos($name, '.php')) {
$offset = 4;
}
return substr($filename, 0, -(strlen($name) + $offset));
case 'PHP_SELF':
return str_replace((string)env('DOCUMENT_ROOT'), '', (string)env('SCRIPT_FILENAME'));
case 'CGI_MODE':
return PHP_SAPI === 'cgi';
}
return $default;
}
}
if (!function_exists('Cake\Core\triggerWarning')) {
/**
* Triggers an E_USER_WARNING.
*
* @param string $message The warning message.
* @return void
*/
function triggerWarning(string $message): void
{
$trace = debug_backtrace();
if (isset($trace[1])) {
$frame = $trace[1];
$frame += ['file' => '[internal]', 'line' => '??'];
$message = sprintf(
'%s - %s, line: %s',
$message,
$frame['file'],
$frame['line']
);
}
trigger_error($message, E_USER_WARNING);
}
}
if (!function_exists('Cake\Core\deprecationWarning')) {
/**
* Helper method for outputting deprecation warnings
*
* @param string $message The message to output as a deprecation warning.
* @param int $stackFrame The stack frame to include in the error. Defaults to 1
* as that should point to application/plugin code.
* @return void
*/
function deprecationWarning(string $message, int $stackFrame = 1): void
{
if (!(error_reporting() & E_USER_DEPRECATED)) {
return;
}
$trace = debug_backtrace();
if (isset($trace[$stackFrame])) {
$frame = $trace[$stackFrame];
$frame += ['file' => '[internal]', 'line' => '??'];
// Assuming we're installed in vendor/cakephp/cakephp/src/Core/functions.php
$root = dirname(__DIR__, 5);
if (defined('ROOT')) {
$root = ROOT;
}
$relative = str_replace(
DIRECTORY_SEPARATOR,
'/',
substr($frame['file'], strlen($root) + 1)
);
$patterns = (array)Configure::read('Error.ignoredDeprecationPaths');
foreach ($patterns as $pattern) {
$pattern = str_replace(DIRECTORY_SEPARATOR, '/', $pattern);
if (fnmatch($pattern, $relative)) {
return;
}
}
$message = sprintf(
"%s\n%s, line: %s\n" . 'You can disable all deprecation warnings by setting `Error.errorLevel` to ' .
'`E_ALL & ~E_USER_DEPRECATED`. Adding `%s` to `Error.ignoredDeprecationPaths` ' .
'in your `config/app.php` config will mute deprecations from that file only.',
$message,
$frame['file'],
$frame['line'],
$relative
);
}
static $errors = [];
$checksum = md5($message);
$duplicate = (bool)Configure::read('Error.allowDuplicateDeprecations', false);
if (isset($errors[$checksum]) && !$duplicate) {
return;
}
if (!$duplicate) {
$errors[$checksum] = true;
}
trigger_error($message, E_USER_DEPRECATED);
}
}
if (!function_exists('Cake\Core\getTypeName')) {
/**
* Returns the objects class or var type of it's not an object
*
* @param mixed $var Variable to check
* @return string Returns the class name or variable type
*/
function getTypeName($var): string
{
return is_object($var) ? get_class($var) : gettype($var);
}
}
/**
* Include global functions.
*/
if (!getenv('CAKE_DISABLE_GLOBAL_FUNCS')) {
include 'functions_global.php';
}
+190
View File
@@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
use function Cake\Core\deprecationWarning as cakeDeprecationWarning;
use function Cake\Core\env as cakeEnv;
use function Cake\Core\getTypeName as cakeGetTypeName;
use function Cake\Core\h as cakeH;
use function Cake\Core\namespaceSplit as cakeNamespaceSplit;
use function Cake\Core\pj as cakePj;
use function Cake\Core\pluginSplit as cakePluginSplit;
use function Cake\Core\pr as cakePr;
use function Cake\Core\triggerWarning as cakeTriggerWarning;
if (!defined('DS')) {
/**
* Defines DS as short form of DIRECTORY_SEPARATOR.
*/
define('DS', DIRECTORY_SEPARATOR);
}
if (!function_exists('h')) {
/**
* Convenience method for htmlspecialchars.
*
* @param mixed $text Text to wrap through htmlspecialchars. Also works with arrays, and objects.
* Arrays will be mapped and have all their elements escaped. Objects will be string cast if they
* implement a `__toString` method. Otherwise, the class name will be used.
* Other scalar types will be returned unchanged.
* @param bool $double Encode existing html entities.
* @param string|null $charset Character set to use when escaping.
* Defaults to config value in `mb_internal_encoding()` or 'UTF-8'.
* @return mixed Wrapped text.
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#h
*/
function h($text, bool $double = true, ?string $charset = null)
{
return cakeH($text, $double, $charset);
}
}
if (!function_exists('pluginSplit')) {
/**
* Splits a dot syntax plugin name into its plugin and class name.
* If $name does not have a dot, then index 0 will be null.
*
* Commonly used like
* ```
* list($plugin, $name) = pluginSplit($name);
* ```
*
* @param string $name The name you want to plugin split.
* @param bool $dotAppend Set to true if you want the plugin to have a '.' appended to it.
* @param string|null $plugin Optional default plugin to use if no plugin is found. Defaults to null.
* @return array Array with 2 indexes. 0 => plugin name, 1 => class name.
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#pluginSplit
* @psalm-return array{string|null, string}
*/
function pluginSplit(string $name, bool $dotAppend = false, ?string $plugin = null): array
{
return cakePluginSplit($name, $dotAppend, $plugin);
}
}
if (!function_exists('namespaceSplit')) {
/**
* Split the namespace from the classname.
*
* Commonly used like `list($namespace, $className) = namespaceSplit($class);`.
*
* @param string $class The full class name, ie `Cake\Core\App`.
* @return array<string> Array with 2 indexes. 0 => namespace, 1 => classname.
*/
function namespaceSplit(string $class): array
{
return cakeNamespaceSplit($class);
}
}
if (!function_exists('pr')) {
/**
* print_r() convenience function.
*
* In terminals this will act similar to using print_r() directly, when not run on CLI
* print_r() will also wrap `<pre>` tags around the output of given variable. Similar to debug().
*
* This function returns the same variable that was passed.
*
* @param mixed $var Variable to print out.
* @return mixed the same $var that was passed to this function
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#pr
* @see debug()
*/
function pr($var)
{
return cakePr($var);
}
}
if (!function_exists('pj')) {
/**
* JSON pretty print convenience function.
*
* In terminals this will act similar to using json_encode() with JSON_PRETTY_PRINT directly, when not run on CLI
* will also wrap `<pre>` tags around the output of given variable. Similar to pr().
*
* This function returns the same variable that was passed.
*
* @param mixed $var Variable to print out.
* @return mixed the same $var that was passed to this function
* @see pr()
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#pj
*/
function pj($var)
{
return cakePj($var);
}
}
if (!function_exists('env')) {
/**
* Gets an environment variable from available sources, and provides emulation
* for unsupported or inconsistent environment variables (i.e. DOCUMENT_ROOT on
* IIS, or SCRIPT_NAME in CGI mode). Also exposes some additional custom
* environment information.
*
* @param string $key Environment variable name.
* @param string|bool|null $default Specify a default value in case the environment variable is not defined.
* @return string|bool|null Environment variable setting.
* @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#env
*/
function env(string $key, $default = null)
{
return cakeEnv($key, $default);
}
}
if (!function_exists('triggerWarning')) {
/**
* Triggers an E_USER_WARNING.
*
* @param string $message The warning message.
* @return void
*/
function triggerWarning(string $message): void
{
cakeTriggerWarning($message);
}
}
if (!function_exists('deprecationWarning')) {
/**
* Helper method for outputting deprecation warnings
*
* @param string $message The message to output as a deprecation warning.
* @param int $stackFrame The stack frame to include in the error. Defaults to 1
* as that should point to application/plugin code.
* @return void
*/
function deprecationWarning(string $message, int $stackFrame = 1): void
{
cakeDeprecationWarning($message, $stackFrame + 1);
}
}
if (!function_exists('getTypeName')) {
/**
* Returns the objects class or var type of it's not an object
*
* @param mixed $var Variable to check
* @return string Returns the class name or variable type
*/
function getTypeName($var): string
{
return cakeGetTypeName($var);
}
}
File diff suppressed because it is too large Load Diff
+49
View File
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database;
use Cake\Datasource\ConnectionInterface;
/**
* Defines the interface for a fixture that needs to manage constraints.
*
* If an implementation of `Cake\Datasource\FixtureInterface` also implements
* this interface, the FixtureManager will use these methods to manage
* a fixtures constraints.
*/
interface ConstraintsInterface
{
/**
* Build and execute SQL queries necessary to create the constraints for the
* fixture
*
* @param \Cake\Datasource\ConnectionInterface $connection An instance of the database
* into which the constraints will be created.
* @return bool on success or if there are no constraints to create, or false on failure
*/
public function createConstraints(ConnectionInterface $connection): bool;
/**
* Build and execute SQL queries necessary to drop the constraints for the
* fixture
*
* @param \Cake\Datasource\ConnectionInterface $connection An instance of the database
* into which the constraints will be dropped.
* @return bool on success or if there are no constraints to drop, or false on failure
*/
public function dropConstraints(ConnectionInterface $connection): bool;
}
+559
View File
@@ -0,0 +1,559 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database;
use Cake\Core\App;
use Cake\Core\Retry\CommandRetry;
use Cake\Database\Exception\MissingConnectionException;
use Cake\Database\Retry\ErrorCodeWaitStrategy;
use Cake\Database\Schema\SchemaDialect;
use Cake\Database\Schema\TableSchema;
use Cake\Database\Statement\PDOStatement;
use Closure;
use InvalidArgumentException;
use PDO;
use PDOException;
use function Cake\Core\deprecationWarning;
/**
* Represents a database driver containing all specificities for
* a database engine including its SQL dialect.
*/
abstract class Driver implements DriverInterface
{
/**
* @var int|null Maximum alias length or null if no limit
*/
protected const MAX_ALIAS_LENGTH = null;
/**
* @var array<int> DB-specific error codes that allow connect retry
*/
protected const RETRY_ERROR_CODES = [];
/**
* Instance of PDO.
*
* @var \PDO
*/
protected $_connection;
/**
* Configuration data.
*
* @var array<string, mixed>
*/
protected $_config;
/**
* Base configuration that is merged into the user
* supplied configuration data.
*
* @var array<string, mixed>
*/
protected $_baseConfig = [];
/**
* Indicates whether the driver is doing automatic identifier quoting
* for all queries
*
* @var bool
*/
protected $_autoQuoting = false;
/**
* The server version
*
* @var string|null
*/
protected $_version;
/**
* The last number of connection retry attempts.
*
* @var int
*/
protected $connectRetries = 0;
/**
* Constructor
*
* @param array<string, mixed> $config The configuration for the driver.
* @throws \InvalidArgumentException
*/
public function __construct(array $config = [])
{
if (empty($config['username']) && !empty($config['login'])) {
throw new InvalidArgumentException(
'Please pass "username" instead of "login" for connecting to the database'
);
}
$config += $this->_baseConfig;
$this->_config = $config;
if (!empty($config['quoteIdentifiers'])) {
$this->enableAutoQuoting();
}
}
/**
* Get the configuration data used to create the driver.
*
* @return array<string, mixed>
*/
public function config(): array
{
return $this->_config;
}
/**
* Establishes a connection to the database server
*
* @param string $dsn A Driver-specific PDO-DSN
* @param array<string, mixed> $config configuration to be used for creating connection
* @return bool true on success
*/
protected function _connect(string $dsn, array $config): bool
{
$action = function () use ($dsn, $config) {
$this->setConnection(new PDO(
$dsn,
$config['username'] ?: null,
$config['password'] ?: null,
$config['flags']
));
};
$retry = new CommandRetry(new ErrorCodeWaitStrategy(static::RETRY_ERROR_CODES, 5), 4);
try {
$retry->run($action);
} catch (PDOException $e) {
throw new MissingConnectionException(
[
'driver' => App::shortName(static::class, 'Database/Driver'),
'reason' => $e->getMessage(),
],
null,
$e
);
} finally {
$this->connectRetries = $retry->getRetries();
}
return true;
}
/**
* @inheritDoc
*/
abstract public function connect(): bool;
/**
* @inheritDoc
*/
public function disconnect(): void
{
/** @psalm-suppress PossiblyNullPropertyAssignmentValue */
$this->_connection = null;
$this->_version = null;
}
/**
* Returns connected server version.
*
* @return string
*/
public function version(): string
{
if ($this->_version === null) {
$this->connect();
$this->_version = (string)$this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION);
}
return $this->_version;
}
/**
* Get the internal PDO connection instance.
*
* @return \PDO
*/
public function getConnection()
{
if ($this->_connection === null) {
throw new MissingConnectionException([
'driver' => App::shortName(static::class, 'Database/Driver'),
'reason' => 'Unknown',
]);
}
return $this->_connection;
}
/**
* Set the internal PDO connection instance.
*
* @param \PDO $connection PDO instance.
* @return $this
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function setConnection($connection)
{
$this->_connection = $connection;
return $this;
}
/**
* @inheritDoc
*/
abstract public function enabled(): bool;
/**
* @inheritDoc
*/
public function prepare($query): StatementInterface
{
$this->connect();
$statement = $this->_connection->prepare($query instanceof Query ? $query->sql() : $query);
return new PDOStatement($statement, $this);
}
/**
* @inheritDoc
*/
public function beginTransaction(): bool
{
$this->connect();
if ($this->_connection->inTransaction()) {
return true;
}
return $this->_connection->beginTransaction();
}
/**
* @inheritDoc
*/
public function commitTransaction(): bool
{
$this->connect();
if (!$this->_connection->inTransaction()) {
return false;
}
return $this->_connection->commit();
}
/**
* @inheritDoc
*/
public function rollbackTransaction(): bool
{
$this->connect();
if (!$this->_connection->inTransaction()) {
return false;
}
return $this->_connection->rollBack();
}
/**
* Returns whether a transaction is active for connection.
*
* @return bool
*/
public function inTransaction(): bool
{
$this->connect();
return $this->_connection->inTransaction();
}
/**
* @inheritDoc
*/
public function supportsSavePoints(): bool
{
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
return $this->supports(static::FEATURE_SAVEPOINT);
}
/**
* Returns true if the server supports common table expressions.
*
* @return bool
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_QUOTE)` instead
*/
public function supportsCTEs(): bool
{
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
return $this->supports(static::FEATURE_CTE);
}
/**
* @inheritDoc
*/
public function quote($value, $type = PDO::PARAM_STR): string
{
$this->connect();
return $this->_connection->quote((string)$value, $type);
}
/**
* Checks if the driver supports quoting, as PDO_ODBC does not support it.
*
* @return bool
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_QUOTE)` instead
*/
public function supportsQuoting(): bool
{
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
return $this->supports(static::FEATURE_QUOTE);
}
/**
* @inheritDoc
*/
abstract public function queryTranslator(string $type): Closure;
/**
* @inheritDoc
*/
abstract public function schemaDialect(): SchemaDialect;
/**
* @inheritDoc
*/
abstract public function quoteIdentifier(string $identifier): string;
/**
* @inheritDoc
*/
public function schemaValue($value): string
{
if ($value === null) {
return 'NULL';
}
if ($value === false) {
return 'FALSE';
}
if ($value === true) {
return 'TRUE';
}
if (is_float($value)) {
return str_replace(',', '.', (string)$value);
}
/** @psalm-suppress InvalidArgument */
if (
(
is_int($value) ||
$value === '0'
) ||
(
is_numeric($value) &&
strpos($value, ',') === false &&
substr($value, 0, 1) !== '0' &&
strpos($value, 'e') === false
)
) {
return (string)$value;
}
return $this->_connection->quote((string)$value, PDO::PARAM_STR);
}
/**
* @inheritDoc
*/
public function schema(): string
{
return $this->_config['schema'];
}
/**
* @inheritDoc
*/
public function lastInsertId(?string $table = null, ?string $column = null)
{
$this->connect();
if ($this->_connection instanceof PDO) {
return $this->_connection->lastInsertId($table);
}
return $this->_connection->lastInsertId($table);
}
/**
* @inheritDoc
*/
public function isConnected(): bool
{
if ($this->_connection === null) {
$connected = false;
} else {
try {
$connected = (bool)$this->_connection->query('SELECT 1');
} catch (PDOException $e) {
$connected = false;
}
}
return $connected;
}
/**
* @inheritDoc
*/
public function enableAutoQuoting(bool $enable = true)
{
$this->_autoQuoting = $enable;
return $this;
}
/**
* @inheritDoc
*/
public function disableAutoQuoting()
{
$this->_autoQuoting = false;
return $this;
}
/**
* @inheritDoc
*/
public function isAutoQuotingEnabled(): bool
{
return $this->_autoQuoting;
}
/**
* Returns whether the driver supports the feature.
*
* Defaults to true for FEATURE_QUOTE and FEATURE_SAVEPOINT.
*
* @param string $feature Driver feature name
* @return bool
*/
public function supports(string $feature): bool
{
switch ($feature) {
case static::FEATURE_DISABLE_CONSTRAINT_WITHOUT_TRANSACTION:
case static::FEATURE_QUOTE:
case static::FEATURE_SAVEPOINT:
return true;
}
return false;
}
/**
* @inheritDoc
*/
public function compileQuery(Query $query, ValueBinder $binder): array
{
$processor = $this->newCompiler();
$translator = $this->queryTranslator($query->type());
$query = $translator($query);
return [$query, $processor->compile($query, $binder)];
}
/**
* @inheritDoc
*/
public function newCompiler(): QueryCompiler
{
return new QueryCompiler();
}
/**
* @inheritDoc
*/
public function newTableSchema(string $table, array $columns = []): TableSchema
{
$className = TableSchema::class;
if (isset($this->_config['tableSchema'])) {
/** @var class-string<\Cake\Database\Schema\TableSchema> $className */
$className = $this->_config['tableSchema'];
}
return new $className($table, $columns);
}
/**
* Returns the maximum alias length allowed.
* This can be different from the maximum identifier length for columns.
*
* @return int|null Maximum alias length or null if no limit
*/
public function getMaxAliasLength(): ?int
{
return static::MAX_ALIAS_LENGTH;
}
/**
* Returns the number of connection retry attempts made.
*
* @return int
*/
public function getConnectRetries(): int
{
return $this->connectRetries;
}
/**
* Returns the connection role this driver performs.
*
* @return string
*/
public function getRole(): string
{
return $this->_config['_role'] ?? Connection::ROLE_WRITE;
}
/**
* Destructor
*/
public function __destruct()
{
/** @psalm-suppress PossiblyNullPropertyAssignmentValue */
$this->_connection = null;
}
/**
* Returns an array that can be used to describe the internal state of this
* object.
*
* @return array<string, mixed>
*/
public function __debugInfo(): array
{
return [
'connected' => $this->_connection !== null,
'role' => $this->getRole(),
];
}
}
+345
View File
@@ -0,0 +1,345 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Driver;
use Cake\Database\Driver;
use Cake\Database\Query;
use Cake\Database\Schema\MysqlSchemaDialect;
use Cake\Database\Schema\SchemaDialect;
use Cake\Database\Statement\MysqlStatement;
use Cake\Database\StatementInterface;
use PDO;
use function Cake\Core\deprecationWarning;
/**
* MySQL Driver
*/
class Mysql extends Driver
{
use SqlDialectTrait;
/**
* @inheritDoc
*/
protected const MAX_ALIAS_LENGTH = 256;
/**
* Server type MySQL
*
* @var string
*/
protected const SERVER_TYPE_MYSQL = 'mysql';
/**
* Server type MariaDB
*
* @var string
*/
protected const SERVER_TYPE_MARIADB = 'mariadb';
/**
* Base configuration settings for MySQL driver
*
* @var array<string, mixed>
*/
protected $_baseConfig = [
'persistent' => true,
'host' => 'localhost',
'username' => 'root',
'password' => '',
'database' => 'cake',
'port' => '3306',
'flags' => [],
'encoding' => 'utf8mb4',
'timezone' => null,
'init' => [],
];
/**
* The schema dialect for this driver
*
* @var \Cake\Database\Schema\MysqlSchemaDialect|null
*/
protected $_schemaDialect;
/**
* String used to start a database identifier quoting to make it safe
*
* @var string
*/
protected $_startQuote = '`';
/**
* String used to end a database identifier quoting to make it safe
*
* @var string
*/
protected $_endQuote = '`';
/**
* Server type.
*
* If the underlying server is MariaDB, its value will get set to `'mariadb'`
* after `version()` method is called.
*
* @var string
*/
protected $serverType = self::SERVER_TYPE_MYSQL;
/**
* Mapping of feature to db server version for feature availability checks.
*
* @var array<string, array<string, string>>
*/
protected $featureVersions = [
'mysql' => [
'json' => '5.7.0',
'cte' => '8.0.0',
'window' => '8.0.0',
],
'mariadb' => [
'json' => '10.2.7',
'cte' => '10.2.1',
'window' => '10.2.0',
],
];
/**
* Establishes a connection to the database server
*
* @return bool true on success
*/
public function connect(): bool
{
if ($this->_connection) {
return true;
}
$config = $this->_config;
if ($config['timezone'] === 'UTC') {
$config['timezone'] = '+0:00';
}
if (!empty($config['timezone'])) {
$config['init'][] = sprintf("SET time_zone = '%s'", $config['timezone']);
}
$config['flags'] += [
PDO::ATTR_PERSISTENT => $config['persistent'],
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
if (!empty($config['ssl_key']) && !empty($config['ssl_cert'])) {
$config['flags'][PDO::MYSQL_ATTR_SSL_KEY] = $config['ssl_key'];
$config['flags'][PDO::MYSQL_ATTR_SSL_CERT] = $config['ssl_cert'];
}
if (!empty($config['ssl_ca'])) {
$config['flags'][PDO::MYSQL_ATTR_SSL_CA] = $config['ssl_ca'];
}
if (empty($config['unix_socket'])) {
$dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']}";
} else {
$dsn = "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}";
}
if (!empty($config['encoding'])) {
$dsn .= ";charset={$config['encoding']}";
}
$this->_connect($dsn, $config);
if (!empty($config['init'])) {
$connection = $this->getConnection();
foreach ((array)$config['init'] as $command) {
$connection->exec($command);
}
}
return true;
}
/**
* Returns whether php is able to use this driver for connecting to database
*
* @return bool true if it is valid to use this driver
*/
public function enabled(): bool
{
return in_array('mysql', PDO::getAvailableDrivers(), true);
}
/**
* Prepares a sql statement to be executed
*
* @param \Cake\Database\Query|string $query The query to prepare.
* @return \Cake\Database\StatementInterface
*/
public function prepare($query): StatementInterface
{
$this->connect();
$isObject = $query instanceof Query;
/**
* @psalm-suppress PossiblyInvalidMethodCall
* @psalm-suppress PossiblyInvalidArgument
*/
$statement = $this->_connection->prepare($isObject ? $query->sql() : $query);
$result = new MysqlStatement($statement, $this);
/** @psalm-suppress PossiblyInvalidMethodCall */
if ($isObject && $query->isBufferedResultsEnabled() === false) {
$result->bufferResults(false);
}
return $result;
}
/**
* @inheritDoc
*/
public function schemaDialect(): SchemaDialect
{
if ($this->_schemaDialect === null) {
$this->_schemaDialect = new MysqlSchemaDialect($this);
}
return $this->_schemaDialect;
}
/**
* @inheritDoc
*/
public function schema(): string
{
return $this->_config['database'];
}
/**
* @inheritDoc
*/
public function disableForeignKeySQL(): string
{
return 'SET foreign_key_checks = 0';
}
/**
* @inheritDoc
*/
public function enableForeignKeySQL(): string
{
return 'SET foreign_key_checks = 1';
}
/**
* @inheritDoc
*/
public function supports(string $feature): bool
{
switch ($feature) {
case static::FEATURE_CTE:
case static::FEATURE_JSON:
case static::FEATURE_WINDOW:
return version_compare(
$this->version(),
$this->featureVersions[$this->serverType][$feature],
'>='
);
}
return parent::supports($feature);
}
/**
* @inheritDoc
*/
public function supportsDynamicConstraints(): bool
{
return true;
}
/**
* Returns true if the connected server is MariaDB.
*
* @return bool
*/
public function isMariadb(): bool
{
$this->version();
return $this->serverType === static::SERVER_TYPE_MARIADB;
}
/**
* Returns connected server version.
*
* @return string
*/
public function version(): string
{
if ($this->_version === null) {
$this->connect();
$this->_version = (string)$this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION);
if (strpos($this->_version, 'MariaDB') !== false) {
$this->serverType = static::SERVER_TYPE_MARIADB;
preg_match('/^(?:5\.5\.5-)?(\d+\.\d+\.\d+.*-MariaDB[^:]*)/', $this->_version, $matches);
$this->_version = $matches[1];
}
}
return $this->_version;
}
/**
* Returns true if the server supports common table expressions.
*
* @return bool
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_CTE)` instead
*/
public function supportsCTEs(): bool
{
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
return $this->supports(static::FEATURE_CTE);
}
/**
* Returns true if the server supports native JSON columns
*
* @return bool
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_JSON)` instead
*/
public function supportsNativeJson(): bool
{
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
return $this->supports(static::FEATURE_JSON);
}
/**
* Returns true if the connected server supports window functions.
*
* @return bool
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_WINDOW)` instead
*/
public function supportsWindowFunctions(): bool
{
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
return $this->supports(static::FEATURE_WINDOW);
}
}
+348
View File
@@ -0,0 +1,348 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Driver;
use Cake\Database\Driver;
use Cake\Database\Expression\FunctionExpression;
use Cake\Database\Expression\IdentifierExpression;
use Cake\Database\Expression\StringExpression;
use Cake\Database\PostgresCompiler;
use Cake\Database\Query;
use Cake\Database\QueryCompiler;
use Cake\Database\Schema\PostgresSchemaDialect;
use Cake\Database\Schema\SchemaDialect;
use PDO;
/**
* Class Postgres
*/
class Postgres extends Driver
{
use SqlDialectTrait;
/**
* @inheritDoc
*/
protected const MAX_ALIAS_LENGTH = 63;
/**
* Base configuration settings for Postgres driver
*
* @var array<string, mixed>
*/
protected $_baseConfig = [
'persistent' => true,
'host' => 'localhost',
'username' => 'root',
'password' => '',
'database' => 'cake',
'schema' => 'public',
'port' => 5432,
'encoding' => 'utf8',
'timezone' => null,
'flags' => [],
'init' => [],
];
/**
* The schema dialect class for this driver
*
* @var \Cake\Database\Schema\PostgresSchemaDialect|null
*/
protected $_schemaDialect;
/**
* String used to start a database identifier quoting to make it safe
*
* @var string
*/
protected $_startQuote = '"';
/**
* String used to end a database identifier quoting to make it safe
*
* @var string
*/
protected $_endQuote = '"';
/**
* Establishes a connection to the database server
*
* @return bool true on success
*/
public function connect(): bool
{
if ($this->_connection) {
return true;
}
$config = $this->_config;
$config['flags'] += [
PDO::ATTR_PERSISTENT => $config['persistent'],
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
if (empty($config['unix_socket'])) {
$dsn = "pgsql:host={$config['host']};port={$config['port']};dbname={$config['database']}";
} else {
$dsn = "pgsql:dbname={$config['database']}";
}
$this->_connect($dsn, $config);
$this->_connection = $connection = $this->getConnection();
if (!empty($config['encoding'])) {
$this->setEncoding($config['encoding']);
}
if (!empty($config['schema'])) {
$this->setSchema($config['schema']);
}
if (!empty($config['timezone'])) {
$config['init'][] = sprintf('SET timezone = %s', $connection->quote($config['timezone']));
}
foreach ($config['init'] as $command) {
$connection->exec($command);
}
return true;
}
/**
* Returns whether php is able to use this driver for connecting to database
*
* @return bool true if it is valid to use this driver
*/
public function enabled(): bool
{
return in_array('pgsql', PDO::getAvailableDrivers(), true);
}
/**
* @inheritDoc
*/
public function schemaDialect(): SchemaDialect
{
if ($this->_schemaDialect === null) {
$this->_schemaDialect = new PostgresSchemaDialect($this);
}
return $this->_schemaDialect;
}
/**
* Sets connection encoding
*
* @param string $encoding The encoding to use.
* @return void
*/
public function setEncoding(string $encoding): void
{
$this->connect();
$this->_connection->exec('SET NAMES ' . $this->_connection->quote($encoding));
}
/**
* Sets connection default schema, if any relation defined in a query is not fully qualified
* postgres will fallback to looking the relation into defined default schema
*
* @param string $schema The schema names to set `search_path` to.
* @return void
*/
public function setSchema(string $schema): void
{
$this->connect();
$this->_connection->exec('SET search_path TO ' . $this->_connection->quote($schema));
}
/**
* @inheritDoc
*/
public function disableForeignKeySQL(): string
{
return 'SET CONSTRAINTS ALL DEFERRED';
}
/**
* @inheritDoc
*/
public function enableForeignKeySQL(): string
{
return 'SET CONSTRAINTS ALL IMMEDIATE';
}
/**
* @inheritDoc
*/
public function supports(string $feature): bool
{
switch ($feature) {
case static::FEATURE_CTE:
case static::FEATURE_JSON:
case static::FEATURE_TRUNCATE_WITH_CONSTRAINTS:
case static::FEATURE_WINDOW:
return true;
case static::FEATURE_DISABLE_CONSTRAINT_WITHOUT_TRANSACTION:
return false;
}
return parent::supports($feature);
}
/**
* @inheritDoc
*/
public function supportsDynamicConstraints(): bool
{
return true;
}
/**
* @inheritDoc
*/
protected function _transformDistinct(Query $query): Query
{
return $query;
}
/**
* @inheritDoc
*/
protected function _insertQueryTranslator(Query $query): Query
{
if (!$query->clause('epilog')) {
$query->epilog('RETURNING *');
}
return $query;
}
/**
* @inheritDoc
*/
protected function _expressionTranslators(): array
{
return [
IdentifierExpression::class => '_transformIdentifierExpression',
FunctionExpression::class => '_transformFunctionExpression',
StringExpression::class => '_transformStringExpression',
];
}
/**
* Changes identifer expression into postgresql format.
*
* @param \Cake\Database\Expression\IdentifierExpression $expression The expression to tranform.
* @return void
*/
protected function _transformIdentifierExpression(IdentifierExpression $expression): void
{
$collation = $expression->getCollation();
if ($collation) {
// use trim() to work around expression being transformed multiple times
$expression->setCollation('"' . trim($collation, '"') . '"');
}
}
/**
* Receives a FunctionExpression and changes it so that it conforms to this
* SQL dialect.
*
* @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert
* to postgres SQL.
* @return void
*/
protected function _transformFunctionExpression(FunctionExpression $expression): void
{
switch ($expression->getName()) {
case 'CONCAT':
// CONCAT function is expressed as exp1 || exp2
$expression->setName('')->setConjunction(' ||');
break;
case 'DATEDIFF':
$expression
->setName('')
->setConjunction('-')
->iterateParts(function ($p) {
if (is_string($p)) {
$p = ['value' => [$p => 'literal'], 'type' => null];
} else {
$p['value'] = [$p['value']];
}
return new FunctionExpression('DATE', $p['value'], [$p['type']]);
});
break;
case 'CURRENT_DATE':
$time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']);
$expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'date' => 'literal']);
break;
case 'CURRENT_TIME':
$time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']);
$expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'time' => 'literal']);
break;
case 'NOW':
$expression->setName('LOCALTIMESTAMP')->add([' 0 ' => 'literal']);
break;
case 'RAND':
$expression->setName('RANDOM');
break;
case 'DATE_ADD':
$expression
->setName('')
->setConjunction(' + INTERVAL')
->iterateParts(function ($p, $key) {
if ($key === 1) {
$p = sprintf("'%s'", $p);
}
return $p;
});
break;
case 'DAYOFWEEK':
$expression
->setName('EXTRACT')
->setConjunction(' ')
->add(['DOW FROM' => 'literal'], [], true)
->add([') + (1' => 'literal']); // Postgres starts on index 0 but Sunday should be 1
break;
}
}
/**
* Changes string expression into postgresql format.
*
* @param \Cake\Database\Expression\StringExpression $expression The string expression to tranform.
* @return void
*/
protected function _transformStringExpression(StringExpression $expression): void
{
// use trim() to work around expression being transformed multiple times
$expression->setCollation('"' . trim($expression->getCollation(), '"') . '"');
}
/**
* {@inheritDoc}
*
* @return \Cake\Database\PostgresCompiler
*/
public function newCompiler(): QueryCompiler
{
return new PostgresCompiler();
}
}
+315
View File
@@ -0,0 +1,315 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Driver;
use Cake\Database\Expression\ComparisonExpression;
use Cake\Database\Expression\IdentifierExpression;
use Cake\Database\IdentifierQuoter;
use Cake\Database\Query;
use Closure;
use RuntimeException;
/**
* Sql dialect trait
*
* @internal
*/
trait SqlDialectTrait
{
/**
* Quotes a database identifier (a column name, table name, etc..) to
* be used safely in queries without the risk of using reserved words
*
* @param string $identifier The identifier to quote.
* @return string
*/
public function quoteIdentifier(string $identifier): string
{
$identifier = trim($identifier);
if ($identifier === '*' || $identifier === '') {
return $identifier;
}
// string
if (preg_match('/^[\w-]+$/u', $identifier)) {
return $this->_startQuote . $identifier . $this->_endQuote;
}
// string.string
if (preg_match('/^[\w-]+\.[^ \*]*$/u', $identifier)) {
$items = explode('.', $identifier);
return $this->_startQuote . implode($this->_endQuote . '.' . $this->_startQuote, $items) . $this->_endQuote;
}
// string.*
if (preg_match('/^[\w-]+\.\*$/u', $identifier)) {
return $this->_startQuote . str_replace('.*', $this->_endQuote . '.*', $identifier);
}
// Functions
if (preg_match('/^([\w-]+)\((.*)\)$/', $identifier, $matches)) {
return $matches[1] . '(' . $this->quoteIdentifier($matches[2]) . ')';
}
// Alias.field AS thing
if (preg_match('/^([\w-]+(\.[\w\s-]+|\(.*\))*)\s+AS\s*([\w-]+)$/ui', $identifier, $matches)) {
return $this->quoteIdentifier($matches[1]) . ' AS ' . $this->quoteIdentifier($matches[3]);
}
// string.string with spaces
if (preg_match('/^([\w-]+\.[\w][\w\s-]*[\w])(.*)/u', $identifier, $matches)) {
$items = explode('.', $matches[1]);
$field = implode($this->_endQuote . '.' . $this->_startQuote, $items);
return $this->_startQuote . $field . $this->_endQuote . $matches[2];
}
if (preg_match('/^[\w\s-]*[\w-]+/u', $identifier)) {
return $this->_startQuote . $identifier . $this->_endQuote;
}
return $identifier;
}
/**
* Returns a callable function that will be used to transform a passed Query object.
* This function, in turn, will return an instance of a Query object that has been
* transformed to accommodate any specificities of the SQL dialect in use.
*
* @param string $type the type of query to be transformed
* (select, insert, update, delete)
* @return \Closure
*/
public function queryTranslator(string $type): Closure
{
return function ($query) use ($type) {
if ($this->isAutoQuotingEnabled()) {
$query = (new IdentifierQuoter($this))->quote($query);
}
/** @var \Cake\ORM\Query $query */
$query = $this->{'_' . $type . 'QueryTranslator'}($query);
$translators = $this->_expressionTranslators();
if (!$translators) {
return $query;
}
$query->traverseExpressions(function ($expression) use ($translators, $query): void {
foreach ($translators as $class => $method) {
if ($expression instanceof $class) {
$this->{$method}($expression, $query);
}
}
});
return $query;
};
}
/**
* Returns an associative array of methods that will transform Expression
* objects to conform with the specific SQL dialect. Keys are class names
* and values a method in this class.
*
* @psalm-return array<class-string, string>
* @return array<string>
*/
protected function _expressionTranslators(): array
{
return [];
}
/**
* Apply translation steps to select queries.
*
* @param \Cake\Database\Query $query The query to translate
* @return \Cake\Database\Query The modified query
*/
protected function _selectQueryTranslator(Query $query): Query
{
return $this->_transformDistinct($query);
}
/**
* Returns the passed query after rewriting the DISTINCT clause, so that drivers
* that do not support the "ON" part can provide the actual way it should be done
*
* @param \Cake\Database\Query $query The query to be transformed
* @return \Cake\Database\Query
*/
protected function _transformDistinct(Query $query): Query
{
if (is_array($query->clause('distinct'))) {
$query->group($query->clause('distinct'), true);
$query->distinct(false);
}
return $query;
}
/**
* Apply translation steps to delete queries.
*
* Chops out aliases on delete query conditions as most database dialects do not
* support aliases in delete queries. This also removes aliases
* in table names as they frequently don't work either.
*
* We are intentionally not supporting deletes with joins as they have even poorer support.
*
* @param \Cake\Database\Query $query The query to translate
* @return \Cake\Database\Query The modified query
*/
protected function _deleteQueryTranslator(Query $query): Query
{
$hadAlias = false;
$tables = [];
foreach ($query->clause('from') as $alias => $table) {
if (is_string($alias)) {
$hadAlias = true;
}
$tables[] = $table;
}
if ($hadAlias) {
$query->from($tables, true);
}
if (!$hadAlias) {
return $query;
}
return $this->_removeAliasesFromConditions($query);
}
/**
* Apply translation steps to update queries.
*
* Chops out aliases on update query conditions as not all database dialects do support
* aliases in update queries.
*
* Just like for delete queries, joins are currently not supported for update queries.
*
* @param \Cake\Database\Query $query The query to translate
* @return \Cake\Database\Query The modified query
*/
protected function _updateQueryTranslator(Query $query): Query
{
return $this->_removeAliasesFromConditions($query);
}
/**
* Removes aliases from the `WHERE` clause of a query.
*
* @param \Cake\Database\Query $query The query to process.
* @return \Cake\Database\Query The modified query.
* @throws \RuntimeException In case the processed query contains any joins, as removing
* aliases from the conditions can break references to the joined tables.
*/
protected function _removeAliasesFromConditions(Query $query): Query
{
if ($query->clause('join')) {
throw new RuntimeException(
'Aliases are being removed from conditions for UPDATE/DELETE queries, ' .
'this can break references to joined tables.'
);
}
$conditions = $query->clause('where');
if ($conditions) {
$conditions->traverse(function ($expression) {
if ($expression instanceof ComparisonExpression) {
$field = $expression->getField();
if (
is_string($field) &&
strpos($field, '.') !== false
) {
[, $unaliasedField] = explode('.', $field, 2);
$expression->setField($unaliasedField);
}
return $expression;
}
if ($expression instanceof IdentifierExpression) {
$identifier = $expression->getIdentifier();
if (strpos($identifier, '.') !== false) {
[, $unaliasedIdentifier] = explode('.', $identifier, 2);
$expression->setIdentifier($unaliasedIdentifier);
}
return $expression;
}
return $expression;
});
}
return $query;
}
/**
* Apply translation steps to insert queries.
*
* @param \Cake\Database\Query $query The query to translate
* @return \Cake\Database\Query The modified query
*/
protected function _insertQueryTranslator(Query $query): Query
{
return $query;
}
/**
* Returns a SQL snippet for creating a new transaction savepoint
*
* @param string|int $name save point name
* @return string
*/
public function savePointSQL($name): string
{
return 'SAVEPOINT LEVEL' . $name;
}
/**
* Returns a SQL snippet for releasing a previously created save point
*
* @param string|int $name save point name
* @return string
*/
public function releaseSavePointSQL($name): string
{
return 'RELEASE SAVEPOINT LEVEL' . $name;
}
/**
* Returns a SQL snippet for rollbacking a previously created save point
*
* @param string|int $name save point name
* @return string
*/
public function rollbackSavePointSQL($name): string
{
return 'ROLLBACK TO SAVEPOINT LEVEL' . $name;
}
}
// phpcs:disable
class_alias(
'Cake\Database\Driver\SqlDialectTrait',
'Cake\Database\SqlDialectTrait'
);
// phpcs:enable
+385
View File
@@ -0,0 +1,385 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Driver;
use Cake\Database\Driver;
use Cake\Database\Expression\FunctionExpression;
use Cake\Database\Expression\TupleComparison;
use Cake\Database\Query;
use Cake\Database\QueryCompiler;
use Cake\Database\Schema\SchemaDialect;
use Cake\Database\Schema\SqliteSchemaDialect;
use Cake\Database\SqliteCompiler;
use Cake\Database\Statement\PDOStatement;
use Cake\Database\Statement\SqliteStatement;
use Cake\Database\StatementInterface;
use InvalidArgumentException;
use PDO;
use RuntimeException;
use function Cake\Core\deprecationWarning;
/**
* Class Sqlite
*/
class Sqlite extends Driver
{
use SqlDialectTrait;
use TupleComparisonTranslatorTrait;
/**
* Base configuration settings for Sqlite driver
*
* - `mask` The mask used for created database
*
* @var array<string, mixed>
*/
protected $_baseConfig = [
'persistent' => false,
'username' => null,
'password' => null,
'database' => ':memory:',
'encoding' => 'utf8',
'mask' => 0644,
'cache' => null,
'mode' => null,
'flags' => [],
'init' => [],
];
/**
* The schema dialect class for this driver
*
* @var \Cake\Database\Schema\SqliteSchemaDialect|null
*/
protected $_schemaDialect;
/**
* Whether the connected server supports window functions.
*
* @var bool|null
*/
protected $_supportsWindowFunctions;
/**
* String used to start a database identifier quoting to make it safe
*
* @var string
*/
protected $_startQuote = '"';
/**
* String used to end a database identifier quoting to make it safe
*
* @var string
*/
protected $_endQuote = '"';
/**
* Mapping of date parts.
*
* @var array<string, string>
*/
protected $_dateParts = [
'day' => 'd',
'hour' => 'H',
'month' => 'm',
'minute' => 'M',
'second' => 'S',
'week' => 'W',
'year' => 'Y',
];
/**
* Mapping of feature to db server version for feature availability checks.
*
* @var array<string, string>
*/
protected $featureVersions = [
'cte' => '3.8.3',
'window' => '3.28.0',
];
/**
* Establishes a connection to the database server
*
* @return bool true on success
*/
public function connect(): bool
{
if ($this->_connection) {
return true;
}
$config = $this->_config;
$config['flags'] += [
PDO::ATTR_PERSISTENT => $config['persistent'],
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
if (!is_string($config['database']) || $config['database'] === '') {
$name = $config['name'] ?? 'unknown';
throw new InvalidArgumentException(
"The `database` key for the `{$name}` SQLite connection needs to be a non-empty string."
);
}
$chmodFile = false;
if ($config['database'] !== ':memory:' && $config['mode'] !== 'memory') {
$chmodFile = !file_exists($config['database']);
}
$params = [];
if ($config['cache']) {
$params[] = 'cache=' . $config['cache'];
}
if ($config['mode']) {
$params[] = 'mode=' . $config['mode'];
}
if ($params) {
if (PHP_VERSION_ID < 80100) {
throw new RuntimeException('SQLite URI support requires PHP 8.1.');
}
$dsn = 'sqlite:file:' . $config['database'] . '?' . implode('&', $params);
} else {
$dsn = 'sqlite:' . $config['database'];
}
$this->_connect($dsn, $config);
if ($chmodFile) {
// phpcs:disable
@chmod($config['database'], $config['mask']);
// phpcs:enable
}
if (!empty($config['init'])) {
foreach ((array)$config['init'] as $command) {
$this->getConnection()->exec($command);
}
}
return true;
}
/**
* Returns whether php is able to use this driver for connecting to database
*
* @return bool true if it is valid to use this driver
*/
public function enabled(): bool
{
return in_array('sqlite', PDO::getAvailableDrivers(), true);
}
/**
* Prepares a sql statement to be executed
*
* @param \Cake\Database\Query|string $query The query to prepare.
* @return \Cake\Database\StatementInterface
*/
public function prepare($query): StatementInterface
{
$this->connect();
$isObject = $query instanceof Query;
/**
* @psalm-suppress PossiblyInvalidMethodCall
* @psalm-suppress PossiblyInvalidArgument
*/
$statement = $this->_connection->prepare($isObject ? $query->sql() : $query);
$result = new SqliteStatement(new PDOStatement($statement, $this), $this);
/** @psalm-suppress PossiblyInvalidMethodCall */
if ($isObject && $query->isBufferedResultsEnabled() === false) {
$result->bufferResults(false);
}
return $result;
}
/**
* @inheritDoc
*/
public function disableForeignKeySQL(): string
{
return 'PRAGMA foreign_keys = OFF';
}
/**
* @inheritDoc
*/
public function enableForeignKeySQL(): string
{
return 'PRAGMA foreign_keys = ON';
}
/**
* @inheritDoc
*/
public function supports(string $feature): bool
{
switch ($feature) {
case static::FEATURE_CTE:
case static::FEATURE_WINDOW:
return version_compare(
$this->version(),
$this->featureVersions[$feature],
'>='
);
case static::FEATURE_TRUNCATE_WITH_CONSTRAINTS:
return true;
}
return parent::supports($feature);
}
/**
* @inheritDoc
*/
public function supportsDynamicConstraints(): bool
{
return false;
}
/**
* @inheritDoc
*/
public function schemaDialect(): SchemaDialect
{
if ($this->_schemaDialect === null) {
$this->_schemaDialect = new SqliteSchemaDialect($this);
}
return $this->_schemaDialect;
}
/**
* @inheritDoc
*/
public function newCompiler(): QueryCompiler
{
return new SqliteCompiler();
}
/**
* @inheritDoc
*/
protected function _expressionTranslators(): array
{
return [
FunctionExpression::class => '_transformFunctionExpression',
TupleComparison::class => '_transformTupleComparison',
];
}
/**
* Receives a FunctionExpression and changes it so that it conforms to this
* SQL dialect.
*
* @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert to TSQL.
* @return void
*/
protected function _transformFunctionExpression(FunctionExpression $expression): void
{
switch ($expression->getName()) {
case 'CONCAT':
// CONCAT function is expressed as exp1 || exp2
$expression->setName('')->setConjunction(' ||');
break;
case 'DATEDIFF':
$expression
->setName('ROUND')
->setConjunction('-')
->iterateParts(function ($p) {
return new FunctionExpression('JULIANDAY', [$p['value']], [$p['type']]);
});
break;
case 'NOW':
$expression->setName('DATETIME')->add(["'now'" => 'literal']);
break;
case 'RAND':
$expression
->setName('ABS')
->add(['RANDOM() % 1' => 'literal'], [], true);
break;
case 'CURRENT_DATE':
$expression->setName('DATE')->add(["'now'" => 'literal']);
break;
case 'CURRENT_TIME':
$expression->setName('TIME')->add(["'now'" => 'literal']);
break;
case 'EXTRACT':
$expression
->setName('STRFTIME')
->setConjunction(' ,')
->iterateParts(function ($p, $key) {
if ($key === 0) {
$value = rtrim(strtolower($p), 's');
if (isset($this->_dateParts[$value])) {
$p = ['value' => '%' . $this->_dateParts[$value], 'type' => null];
}
}
return $p;
});
break;
case 'DATE_ADD':
$expression
->setName('DATE')
->setConjunction(',')
->iterateParts(function ($p, $key) {
if ($key === 1) {
$p = ['value' => $p, 'type' => null];
}
return $p;
});
break;
case 'DAYOFWEEK':
$expression
->setName('STRFTIME')
->setConjunction(' ')
->add(["'%w', " => 'literal'], [], true)
->add([') + (1' => 'literal']); // Sqlite starts on index 0 but Sunday should be 1
break;
}
}
/**
* Returns true if the server supports common table expressions.
*
* @return bool
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_CTE)` instead
*/
public function supportsCTEs(): bool
{
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
return $this->supports(static::FEATURE_CTE);
}
/**
* Returns true if the connected server supports window functions.
*
* @return bool
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_WINDOW)` instead
*/
public function supportsWindowFunctions(): bool
{
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
return $this->supports(static::FEATURE_WINDOW);
}
}
+569
View File
@@ -0,0 +1,569 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Driver;
use Cake\Database\Driver;
use Cake\Database\Expression\FunctionExpression;
use Cake\Database\Expression\OrderByExpression;
use Cake\Database\Expression\OrderClauseExpression;
use Cake\Database\Expression\TupleComparison;
use Cake\Database\Expression\UnaryExpression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\QueryCompiler;
use Cake\Database\Schema\SchemaDialect;
use Cake\Database\Schema\SqlserverSchemaDialect;
use Cake\Database\SqlserverCompiler;
use Cake\Database\Statement\SqlserverStatement;
use Cake\Database\StatementInterface;
use InvalidArgumentException;
use PDO;
/**
* SQLServer driver.
*/
class Sqlserver extends Driver
{
use SqlDialectTrait;
use TupleComparisonTranslatorTrait;
/**
* @inheritDoc
*/
protected const MAX_ALIAS_LENGTH = 128;
/**
* @inheritDoc
*/
protected const RETRY_ERROR_CODES = [
40613, // Azure Sql Database paused
];
/**
* Base configuration settings for Sqlserver driver
*
* @var array<string, mixed>
*/
protected $_baseConfig = [
'host' => 'localhost\SQLEXPRESS',
'username' => '',
'password' => '',
'database' => 'cake',
'port' => '',
// PDO::SQLSRV_ENCODING_UTF8
'encoding' => 65001,
'flags' => [],
'init' => [],
'settings' => [],
'attributes' => [],
'app' => null,
'connectionPooling' => null,
'failoverPartner' => null,
'loginTimeout' => null,
'multiSubnetFailover' => null,
'encrypt' => null,
'trustServerCertificate' => null,
];
/**
* The schema dialect class for this driver
*
* @var \Cake\Database\Schema\SqlserverSchemaDialect|null
*/
protected $_schemaDialect;
/**
* String used to start a database identifier quoting to make it safe
*
* @var string
*/
protected $_startQuote = '[';
/**
* String used to end a database identifier quoting to make it safe
*
* @var string
*/
protected $_endQuote = ']';
/**
* Establishes a connection to the database server.
*
* Please note that the PDO::ATTR_PERSISTENT attribute is not supported by
* the SQL Server PHP PDO drivers. As a result you cannot use the
* persistent config option when connecting to a SQL Server (for more
* information see: https://github.com/Microsoft/msphpsql/issues/65).
*
* @throws \InvalidArgumentException if an unsupported setting is in the driver config
* @return bool true on success
*/
public function connect(): bool
{
if ($this->_connection) {
return true;
}
$config = $this->_config;
if (isset($config['persistent']) && $config['persistent']) {
throw new InvalidArgumentException(
'Config setting "persistent" cannot be set to true, '
. 'as the Sqlserver PDO driver does not support PDO::ATTR_PERSISTENT'
);
}
$config['flags'] += [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
if (!empty($config['encoding'])) {
$config['flags'][PDO::SQLSRV_ATTR_ENCODING] = $config['encoding'];
}
$port = '';
if ($config['port']) {
$port = ',' . $config['port'];
}
$dsn = "sqlsrv:Server={$config['host']}{$port};Database={$config['database']};MultipleActiveResultSets=false";
if ($config['app'] !== null) {
$dsn .= ";APP={$config['app']}";
}
if ($config['connectionPooling'] !== null) {
$dsn .= ";ConnectionPooling={$config['connectionPooling']}";
}
if ($config['failoverPartner'] !== null) {
$dsn .= ";Failover_Partner={$config['failoverPartner']}";
}
if ($config['loginTimeout'] !== null) {
$dsn .= ";LoginTimeout={$config['loginTimeout']}";
}
if ($config['multiSubnetFailover'] !== null) {
$dsn .= ";MultiSubnetFailover={$config['multiSubnetFailover']}";
}
if ($config['encrypt'] !== null) {
$dsn .= ";Encrypt={$config['encrypt']}";
}
if ($config['trustServerCertificate'] !== null) {
$dsn .= ";TrustServerCertificate={$config['trustServerCertificate']}";
}
$this->_connect($dsn, $config);
$connection = $this->getConnection();
if (!empty($config['init'])) {
foreach ((array)$config['init'] as $command) {
$connection->exec($command);
}
}
if (!empty($config['settings']) && is_array($config['settings'])) {
foreach ($config['settings'] as $key => $value) {
$connection->exec("SET {$key} {$value}");
}
}
if (!empty($config['attributes']) && is_array($config['attributes'])) {
foreach ($config['attributes'] as $key => $value) {
$connection->setAttribute($key, $value);
}
}
return true;
}
/**
* Returns whether PHP is able to use this driver for connecting to database
*
* @return bool true if it is valid to use this driver
*/
public function enabled(): bool
{
return in_array('sqlsrv', PDO::getAvailableDrivers(), true);
}
/**
* Prepares a sql statement to be executed
*
* @param \Cake\Database\Query|string $query The query to prepare.
* @return \Cake\Database\StatementInterface
*/
public function prepare($query): StatementInterface
{
$this->connect();
$sql = $query;
$options = [
PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL,
PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED,
];
if ($query instanceof Query) {
$sql = $query->sql();
if (count($query->getValueBinder()->bindings()) > 2100) {
throw new InvalidArgumentException(
'Exceeded maximum number of parameters (2100) for prepared statements in Sql Server. ' .
'This is probably due to a very large WHERE IN () clause which generates a parameter ' .
'for each value in the array. ' .
'If using an Association, try changing the `strategy` from select to subquery.'
);
}
if (!$query->isBufferedResultsEnabled()) {
$options = [];
}
}
/** @psalm-suppress PossiblyInvalidArgument */
$statement = $this->_connection->prepare($sql, $options);
return new SqlserverStatement($statement, $this);
}
/**
* @inheritDoc
*/
public function savePointSQL($name): string
{
return 'SAVE TRANSACTION t' . $name;
}
/**
* @inheritDoc
*/
public function releaseSavePointSQL($name): string
{
// SQLServer has no release save point operation.
return '';
}
/**
* @inheritDoc
*/
public function rollbackSavePointSQL($name): string
{
return 'ROLLBACK TRANSACTION t' . $name;
}
/**
* @inheritDoc
*/
public function disableForeignKeySQL(): string
{
return 'EXEC sp_MSforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT all"';
}
/**
* @inheritDoc
*/
public function enableForeignKeySQL(): string
{
return 'EXEC sp_MSforeachtable "ALTER TABLE ? WITH CHECK CHECK CONSTRAINT all"';
}
/**
* @inheritDoc
*/
public function supports(string $feature): bool
{
switch ($feature) {
case static::FEATURE_CTE:
case static::FEATURE_TRUNCATE_WITH_CONSTRAINTS:
case static::FEATURE_WINDOW:
return true;
case static::FEATURE_QUOTE:
$this->connect();
return $this->_connection->getAttribute(PDO::ATTR_DRIVER_NAME) !== 'odbc';
}
return parent::supports($feature);
}
/**
* @inheritDoc
*/
public function supportsDynamicConstraints(): bool
{
return true;
}
/**
* @inheritDoc
*/
public function schemaDialect(): SchemaDialect
{
if ($this->_schemaDialect === null) {
$this->_schemaDialect = new SqlserverSchemaDialect($this);
}
return $this->_schemaDialect;
}
/**
* {@inheritDoc}
*
* @return \Cake\Database\SqlserverCompiler
*/
public function newCompiler(): QueryCompiler
{
return new SqlserverCompiler();
}
/**
* @inheritDoc
*/
protected function _selectQueryTranslator(Query $query): Query
{
$limit = $query->clause('limit');
$offset = $query->clause('offset');
if ($limit && $offset === null) {
$query->modifier(['_auto_top_' => sprintf('TOP %d', $limit)]);
}
if ($offset !== null && !$query->clause('order')) {
$query->order($query->newExpr()->add('(SELECT NULL)'));
}
if ($this->version() < 11 && $offset !== null) {
return $this->_pagingSubquery($query, $limit, $offset);
}
return $this->_transformDistinct($query);
}
/**
* Generate a paging subquery for older versions of SQLserver.
*
* Prior to SQLServer 2012 there was no equivalent to LIMIT OFFSET, so a subquery must
* be used.
*
* @param \Cake\Database\Query $original The query to wrap in a subquery.
* @param int|null $limit The number of rows to fetch.
* @param int|null $offset The number of rows to offset.
* @return \Cake\Database\Query Modified query object.
*/
protected function _pagingSubquery(Query $original, ?int $limit, ?int $offset): Query
{
$field = '_cake_paging_._cake_page_rownum_';
if ($original->clause('order')) {
// SQL server does not support column aliases in OVER clauses. But
// the only practical way to specify the use of calculated columns
// is with their alias. So substitute the select SQL in place of
// any column aliases for those entries in the order clause.
$select = $original->clause('select');
$order = new OrderByExpression();
$original
->clause('order')
->iterateParts(function ($direction, $orderBy) use ($select, $order) {
$key = $orderBy;
if (
isset($select[$orderBy]) &&
$select[$orderBy] instanceof ExpressionInterface
) {
$order->add(new OrderClauseExpression($select[$orderBy], $direction));
} else {
$order->add([$key => $direction]);
}
// Leave original order clause unchanged.
return $orderBy;
});
} else {
$order = new OrderByExpression('(SELECT NULL)');
}
$query = clone $original;
$query->select([
'_cake_page_rownum_' => new UnaryExpression('ROW_NUMBER() OVER', $order),
])->limit(null)
->offset(null)
->order([], true);
$outer = new Query($query->getConnection());
$outer->select('*')
->from(['_cake_paging_' => $query]);
if ($offset) {
$outer->where(["$field > " . $offset]);
}
if ($limit) {
$value = (int)$offset + $limit;
$outer->where(["$field <= $value"]);
}
// Decorate the original query as that is what the
// end developer will be calling execute() on originally.
$original->decorateResults(function ($row) {
if (isset($row['_cake_page_rownum_'])) {
unset($row['_cake_page_rownum_']);
}
return $row;
});
return $outer;
}
/**
* @inheritDoc
*/
protected function _transformDistinct(Query $query): Query
{
if (!is_array($query->clause('distinct'))) {
return $query;
}
$original = $query;
$query = clone $original;
$distinct = $query->clause('distinct');
$query->distinct(false);
$order = new OrderByExpression($distinct);
$query
->select(function ($q) use ($distinct, $order) {
$over = $q->newExpr('ROW_NUMBER() OVER')
->add('(PARTITION BY')
->add($q->newExpr()->add($distinct)->setConjunction(','))
->add($order)
->add(')')
->setConjunction(' ');
return [
'_cake_distinct_pivot_' => $over,
];
})
->limit(null)
->offset(null)
->order([], true);
$outer = new Query($query->getConnection());
$outer->select('*')
->from(['_cake_distinct_' => $query])
->where(['_cake_distinct_pivot_' => 1]);
// Decorate the original query as that is what the
// end developer will be calling execute() on originally.
$original->decorateResults(function ($row) {
if (isset($row['_cake_distinct_pivot_'])) {
unset($row['_cake_distinct_pivot_']);
}
return $row;
});
return $outer;
}
/**
* @inheritDoc
*/
protected function _expressionTranslators(): array
{
return [
FunctionExpression::class => '_transformFunctionExpression',
TupleComparison::class => '_transformTupleComparison',
];
}
/**
* Receives a FunctionExpression and changes it so that it conforms to this
* SQL dialect.
*
* @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert to TSQL.
* @return void
*/
protected function _transformFunctionExpression(FunctionExpression $expression): void
{
switch ($expression->getName()) {
case 'CONCAT':
// CONCAT function is expressed as exp1 + exp2
$expression->setName('')->setConjunction(' +');
break;
case 'DATEDIFF':
/** @var bool $hasDay */
$hasDay = false;
$visitor = function ($value) use (&$hasDay) {
if ($value === 'day') {
$hasDay = true;
}
return $value;
};
$expression->iterateParts($visitor);
if (!$hasDay) {
$expression->add(['day' => 'literal'], [], true);
}
break;
case 'CURRENT_DATE':
$time = new FunctionExpression('GETUTCDATE');
$expression->setName('CONVERT')->add(['date' => 'literal', $time]);
break;
case 'CURRENT_TIME':
$time = new FunctionExpression('GETUTCDATE');
$expression->setName('CONVERT')->add(['time' => 'literal', $time]);
break;
case 'NOW':
$expression->setName('GETUTCDATE');
break;
case 'EXTRACT':
$expression->setName('DATEPART')->setConjunction(' ,');
break;
case 'DATE_ADD':
$params = [];
$visitor = function ($p, $key) use (&$params) {
if ($key === 0) {
$params[2] = $p;
} else {
$valueUnit = explode(' ', $p);
$params[0] = rtrim($valueUnit[1], 's');
$params[1] = $valueUnit[0];
}
return $p;
};
$manipulator = function ($p, $key) use (&$params) {
return $params[$key];
};
$expression
->setName('DATEADD')
->setConjunction(',')
->iterateParts($visitor)
->iterateParts($manipulator)
->add([$params[2] => 'literal']);
break;
case 'DAYOFWEEK':
$expression
->setName('DATEPART')
->setConjunction(' ')
->add(['weekday, ' => 'literal'], [], true);
break;
case 'SUBSTR':
$expression->setName('SUBSTRING');
if (count($expression) < 4) {
$params = [];
$expression
->iterateParts(function ($p) use (&$params) {
return $params[] = $p;
})
->add([new FunctionExpression('LEN', [$params[0]]), ['string']]);
}
break;
}
}
}
@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Driver;
use Cake\Database\Expression\IdentifierExpression;
use Cake\Database\Expression\QueryExpression;
use Cake\Database\Expression\TupleComparison;
use Cake\Database\Query;
use RuntimeException;
/**
* Provides a translator method for tuple comparisons
*
* @internal
*/
trait TupleComparisonTranslatorTrait
{
/**
* Receives a TupleExpression and changes it so that it conforms to this
* SQL dialect.
*
* It transforms expressions looking like '(a, b) IN ((c, d), (e, f))' into an
* equivalent expression of the form '((a = c) AND (b = d)) OR ((a = e) AND (b = f))'.
*
* It can also transform transform expressions where the right hand side is a query
* selecting the same amount of columns as the elements in the left hand side of
* the expression:
*
* (a, b) IN (SELECT c, d FROM a_table) is transformed into
*
* 1 = (SELECT 1 FROM a_table WHERE (a = c) AND (b = d))
*
* @param \Cake\Database\Expression\TupleComparison $expression The expression to transform
* @param \Cake\Database\Query $query The query to update.
* @return void
*/
protected function _transformTupleComparison(TupleComparison $expression, Query $query): void
{
$fields = $expression->getField();
if (!is_array($fields)) {
return;
}
$operator = strtoupper($expression->getOperator());
if (!in_array($operator, ['IN', '='])) {
throw new RuntimeException(
sprintf(
'Tuple comparison transform only supports the `IN` and `=` operators, `%s` given.',
$operator
)
);
}
$value = $expression->getValue();
$true = new QueryExpression('1');
if ($value instanceof Query) {
$selected = array_values($value->clause('select'));
foreach ($fields as $i => $field) {
$value->andWhere([$field => new IdentifierExpression($selected[$i])]);
}
$value->select($true, true);
$expression->setField($true);
$expression->setOperator('=');
return;
}
$type = $expression->getType();
if ($type) {
/** @var array<string, string> $typeMap */
$typeMap = array_combine($fields, $type) ?: [];
} else {
$typeMap = [];
}
$surrogate = $query->getConnection()
->selectQuery($true);
if (!is_array(current($value))) {
$value = [$value];
}
$conditions = ['OR' => []];
foreach ($value as $tuple) {
$item = [];
foreach (array_values($tuple) as $i => $value2) {
$item[] = [$fields[$i] => $value2];
}
$conditions['OR'][] = $item;
}
$surrogate->where($conditions, $typeMap);
$expression->setField($true);
$expression->setValue($surrogate);
$expression->setOperator('=');
}
}
+336
View File
@@ -0,0 +1,336 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database;
use Cake\Database\Schema\SchemaDialect;
use Cake\Database\Schema\TableSchema;
use Closure;
/**
* Interface for database driver.
*
* @method int|null getMaxAliasLength() Returns the maximum alias length allowed.
* @method int getConnectRetries() Returns the number of connection retry attempts made.
* @method bool supports(string $feature) Checks whether a feature is supported by the driver.
* @method bool inTransaction() Returns whether a transaction is active.
* @method array config() Get the configuration data used to create the driver.
* @method string getRole() Returns the connection role this driver prforms.
*/
interface DriverInterface
{
/**
* Common Table Expressions (with clause) support.
*
* @var string
*/
public const FEATURE_CTE = 'cte';
/**
* Disabling constraints without being in transaction support.
*
* @var string
*/
public const FEATURE_DISABLE_CONSTRAINT_WITHOUT_TRANSACTION = 'disable-constraint-without-transaction';
/**
* Native JSON data type support.
*
* @var string
*/
public const FEATURE_JSON = 'json';
/**
* PDO::quote() support.
*
* @var string
*/
public const FEATURE_QUOTE = 'quote';
/**
* Transaction savepoint support.
*
* @var string
*/
public const FEATURE_SAVEPOINT = 'savepoint';
/**
* Truncate with foreign keys attached support.
*
* @var string
*/
public const FEATURE_TRUNCATE_WITH_CONSTRAINTS = 'truncate-with-constraints';
/**
* Window function support (all or partial clauses).
*
* @var string
*/
public const FEATURE_WINDOW = 'window';
/**
* Establishes a connection to the database server.
*
* @throws \Cake\Database\Exception\MissingConnectionException If database connection could not be established.
* @return bool True on success, false on failure.
*/
public function connect(): bool;
/**
* Disconnects from database server.
*
* @return void
*/
public function disconnect(): void;
/**
* Returns correct connection resource or object that is internally used.
*
* @return object Connection object used internally.
*/
public function getConnection();
/**
* Set the internal connection object.
*
* @param object $connection The connection instance.
* @return $this
*/
public function setConnection($connection);
/**
* Returns whether php is able to use this driver for connecting to database.
*
* @return bool True if it is valid to use this driver.
*/
public function enabled(): bool;
/**
* Prepares a sql statement to be executed.
*
* @param \Cake\Database\Query|string $query The query to turn into a prepared statement.
* @return \Cake\Database\StatementInterface
*/
public function prepare($query): StatementInterface;
/**
* Starts a transaction.
*
* @return bool True on success, false otherwise.
*/
public function beginTransaction(): bool;
/**
* Commits a transaction.
*
* @return bool True on success, false otherwise.
*/
public function commitTransaction(): bool;
/**
* Rollbacks a transaction.
*
* @return bool True on success, false otherwise.
*/
public function rollbackTransaction(): bool;
/**
* Get the SQL for releasing a save point.
*
* @param string|int $name Save point name or id
* @return string
*/
public function releaseSavePointSQL($name): string;
/**
* Get the SQL for creating a save point.
*
* @param string|int $name Save point name or id
* @return string
*/
public function savePointSQL($name): string;
/**
* Get the SQL for rollingback a save point.
*
* @param string|int $name Save point name or id
* @return string
*/
public function rollbackSavePointSQL($name): string;
/**
* Get the SQL for disabling foreign keys.
*
* @return string
*/
public function disableForeignKeySQL(): string;
/**
* Get the SQL for enabling foreign keys.
*
* @return string
*/
public function enableForeignKeySQL(): string;
/**
* Returns whether the driver supports adding or dropping constraints
* to already created tables.
*
* @return bool True if driver supports dynamic constraints.
* @deprecated 4.3.0 Fixtures no longer dynamically drop and create constraints.
*/
public function supportsDynamicConstraints(): bool;
/**
* Returns whether this driver supports save points for nested transactions.
*
* @return bool True if save points are supported, false otherwise.
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_SAVEPOINT)` instead
*/
public function supportsSavePoints(): bool;
/**
* Returns a value in a safe representation to be used in a query string
*
* @param mixed $value The value to quote.
* @param int $type Must be one of the \PDO::PARAM_* constants
* @return string
*/
public function quote($value, $type): string;
/**
* Checks if the driver supports quoting.
*
* @return bool
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_QUOTE)` instead
*/
public function supportsQuoting(): bool;
/**
* Returns a callable function that will be used to transform a passed Query object.
* This function, in turn, will return an instance of a Query object that has been
* transformed to accommodate any specificities of the SQL dialect in use.
*
* @param string $type The type of query to be transformed
* (select, insert, update, delete).
* @return \Closure
*/
public function queryTranslator(string $type): Closure;
/**
* Get the schema dialect.
*
* Used by {@link \Cake\Database\Schema} package to reflect schema and
* generate schema.
*
* If all the tables that use this Driver specify their
* own schemas, then this may return null.
*
* @return \Cake\Database\Schema\SchemaDialect
*/
public function schemaDialect(): SchemaDialect;
/**
* Quotes a database identifier (a column name, table name, etc..) to
* be used safely in queries without the risk of using reserved words.
*
* @param string $identifier The identifier expression to quote.
* @return string
*/
public function quoteIdentifier(string $identifier): string;
/**
* Escapes values for use in schema definitions.
*
* @param mixed $value The value to escape.
* @return string String for use in schema definitions.
*/
public function schemaValue($value): string;
/**
* Returns the schema name that's being used.
*
* @return string
*/
public function schema(): string;
/**
* Returns last id generated for a table or sequence in database.
*
* @param string|null $table table name or sequence to get last insert value from.
* @param string|null $column the name of the column representing the primary key.
* @return string|int
*/
public function lastInsertId(?string $table = null, ?string $column = null);
/**
* Checks whether the driver is connected.
*
* @return bool
*/
public function isConnected(): bool;
/**
* Sets whether this driver should automatically quote identifiers
* in queries.
*
* @param bool $enable Whether to enable auto quoting
* @return $this
*/
public function enableAutoQuoting(bool $enable = true);
/**
* Disable auto quoting of identifiers in queries.
*
* @return $this
*/
public function disableAutoQuoting();
/**
* Returns whether this driver should automatically quote identifiers
* in queries.
*
* @return bool
*/
public function isAutoQuotingEnabled(): bool;
/**
* Transforms the passed query to this Driver's dialect and returns an instance
* of the transformed query and the full compiled SQL string.
*
* @param \Cake\Database\Query $query The query to compile.
* @param \Cake\Database\ValueBinder $binder The value binder to use.
* @return array containing 2 entries. The first entity is the transformed query
* and the second one the compiled SQL.
*/
public function compileQuery(Query $query, ValueBinder $binder): array;
/**
* Returns an instance of a QueryCompiler.
*
* @return \Cake\Database\QueryCompiler
*/
public function newCompiler(): QueryCompiler;
/**
* Constructs new TableSchema.
*
* @param string $table The table name.
* @param array $columns The list of columns for the schema.
* @return \Cake\Database\Schema\TableSchema
*/
public function newTableSchema(string $table, array $columns = []): TableSchema;
}
+10
View File
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
use function Cake\Core\deprecationWarning;
deprecationWarning(
'Since 4.2.0: Cake\Database\Exception is deprecated. ' .
'Use Cake\Database\Exception\DatabaseException instead.'
);
class_exists('Cake\Database\Exception\DatabaseException');
+37
View File
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Exception;
use Cake\Core\Exception\CakeException;
/**
* Exception for the database package.
*/
class DatabaseException extends CakeException
{
/**
* @inheritDoc
*/
protected $_messageTemplate = '%s';
}
// phpcs:disable
class_alias(
'Cake\Database\Exception\DatabaseException',
'Cake\Database\Exception'
);
// phpcs:enable
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Exception;
use Cake\Core\Exception\CakeException;
/**
* Class MissingConnectionException
*/
class MissingConnectionException extends CakeException
{
/**
* @inheritDoc
*/
protected $_messageTemplate = 'Connection to %s could not be established: %s';
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Exception;
use Cake\Core\Exception\CakeException;
/**
* Class MissingDriverException
*/
class MissingDriverException extends CakeException
{
/**
* @inheritDoc
*/
protected $_messageTemplate = 'Could not find driver `%s` for connection `%s`.';
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Exception;
use Cake\Core\Exception\CakeException;
/**
* Class MissingExtensionException
*/
class MissingExtensionException extends CakeException
{
/**
* @inheritDoc
*/
// phpcs:ignore Generic.Files.LineLength
protected $_messageTemplate = 'Database driver %s cannot be used due to a missing PHP extension or unmet dependency. Requested by connection "%s"';
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.4.3
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Exception;
use Cake\Core\Exception\CakeException;
use Throwable;
/**
* Class NestedTransactionRollbackException
*/
class NestedTransactionRollbackException extends CakeException
{
/**
* Constructor
*
* @param string|null $message If no message is given a default meesage will be used.
* @param int|null $code Status code, defaults to 500.
* @param \Throwable|null $previous the previous exception.
*/
public function __construct(?string $message = null, ?int $code = 500, ?Throwable $previous = null)
{
if ($message === null) {
$message = 'Cannot commit transaction - rollback() has been already called in the nested transaction';
}
parent::__construct($message, $code, $previous);
}
}
@@ -0,0 +1,253 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ValueBinder;
use Closure;
/**
* This represents an SQL aggregate function expression in an SQL statement.
* Calls can be constructed by passing the name of the function and a list of params.
* For security reasons, all params passed are quoted by default unless
* explicitly told otherwise.
*/
class AggregateExpression extends FunctionExpression implements WindowInterface
{
/**
* @var \Cake\Database\Expression\QueryExpression
*/
protected $filter;
/**
* @var \Cake\Database\Expression\WindowExpression
*/
protected $window;
/**
* Adds conditions to the FILTER clause. The conditions are the same format as
* `Query::where()`.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions The conditions to filter on.
* @param array<string, string> $types Associative array of type names used to bind values to query
* @return $this
* @see \Cake\Database\Query::where()
*/
public function filter($conditions, array $types = [])
{
if ($this->filter === null) {
$this->filter = new QueryExpression();
}
if ($conditions instanceof Closure) {
$conditions = $conditions(new QueryExpression());
}
$this->filter->add($conditions, $types);
return $this;
}
/**
* Adds an empty `OVER()` window expression or a named window epression.
*
* @param string|null $name Window name
* @return $this
*/
public function over(?string $name = null)
{
if ($this->window === null) {
$this->window = new WindowExpression();
}
if ($name) {
// Set name manually in case this was chained from FunctionsBuilder wrapper
$this->window->name($name);
}
return $this;
}
/**
* @inheritDoc
*/
public function partition($partitions)
{
$this->over();
$this->window->partition($partitions);
return $this;
}
/**
* @inheritDoc
*/
public function order($fields)
{
$this->over();
$this->window->order($fields);
return $this;
}
/**
* @inheritDoc
*/
public function range($start, $end = 0)
{
$this->over();
$this->window->range($start, $end);
return $this;
}
/**
* @inheritDoc
*/
public function rows(?int $start, ?int $end = 0)
{
$this->over();
$this->window->rows($start, $end);
return $this;
}
/**
* @inheritDoc
*/
public function groups(?int $start, ?int $end = 0)
{
$this->over();
$this->window->groups($start, $end);
return $this;
}
/**
* @inheritDoc
*/
public function frame(
string $type,
$startOffset,
string $startDirection,
$endOffset,
string $endDirection
) {
$this->over();
$this->window->frame($type, $startOffset, $startDirection, $endOffset, $endDirection);
return $this;
}
/**
* @inheritDoc
*/
public function excludeCurrent()
{
$this->over();
$this->window->excludeCurrent();
return $this;
}
/**
* @inheritDoc
*/
public function excludeGroup()
{
$this->over();
$this->window->excludeGroup();
return $this;
}
/**
* @inheritDoc
*/
public function excludeTies()
{
$this->over();
$this->window->excludeTies();
return $this;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$sql = parent::sql($binder);
if ($this->filter !== null) {
$sql .= ' FILTER (WHERE ' . $this->filter->sql($binder) . ')';
}
if ($this->window !== null) {
if ($this->window->isNamedOnly()) {
$sql .= ' OVER ' . $this->window->sql($binder);
} else {
$sql .= ' OVER (' . $this->window->sql($binder) . ')';
}
}
return $sql;
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
parent::traverse($callback);
if ($this->filter !== null) {
$callback($this->filter);
$this->filter->traverse($callback);
}
if ($this->window !== null) {
$callback($this->window);
$this->window->traverse($callback);
}
return $this;
}
/**
* @inheritDoc
*/
public function count(): int
{
$count = parent::count();
if ($this->window !== null) {
$count = $count + 1;
}
return $count;
}
/**
* Clone this object and its subtree of expressions.
*
* @return void
*/
public function __clone()
{
parent::__clone();
if ($this->filter !== null) {
$this->filter = clone $this->filter;
}
if ($this->window !== null) {
$this->window = clone $this->window;
}
}
}
+144
View File
@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Type\ExpressionTypeCasterTrait;
use Cake\Database\ValueBinder;
use Closure;
/**
* An expression object that represents a SQL BETWEEN snippet
*/
class BetweenExpression implements ExpressionInterface, FieldInterface
{
use ExpressionTypeCasterTrait;
use FieldTrait;
/**
* The first value in the expression
*
* @var mixed
*/
protected $_from;
/**
* The second value in the expression
*
* @var mixed
*/
protected $_to;
/**
* The data type for the from and to arguments
*
* @var mixed
*/
protected $_type;
/**
* Constructor
*
* @param \Cake\Database\ExpressionInterface|string $field The field name to compare for values inbetween the range.
* @param mixed $from The initial value of the range.
* @param mixed $to The ending value in the comparison range.
* @param string|null $type The data type name to bind the values with.
*/
public function __construct($field, $from, $to, $type = null)
{
if ($type !== null) {
$from = $this->_castToExpression($from, $type);
$to = $this->_castToExpression($to, $type);
}
$this->_field = $field;
$this->_from = $from;
$this->_to = $to;
$this->_type = $type;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$parts = [
'from' => $this->_from,
'to' => $this->_to,
];
/** @var \Cake\Database\ExpressionInterface|string $field */
$field = $this->_field;
if ($field instanceof ExpressionInterface) {
$field = $field->sql($binder);
}
foreach ($parts as $name => $part) {
if ($part instanceof ExpressionInterface) {
$parts[$name] = $part->sql($binder);
continue;
}
$parts[$name] = $this->_bindValue($part, $binder, $this->_type);
}
return sprintf('%s BETWEEN %s AND %s', $field, $parts['from'], $parts['to']);
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
foreach ([$this->_field, $this->_from, $this->_to] as $part) {
if ($part instanceof ExpressionInterface) {
$callback($part);
}
}
return $this;
}
/**
* Registers a value in the placeholder generator and returns the generated placeholder
*
* @param mixed $value The value to bind
* @param \Cake\Database\ValueBinder $binder The value binder to use
* @param string $type The type of $value
* @return string generated placeholder
*/
protected function _bindValue($value, $binder, $type): string
{
$placeholder = $binder->placeholder('c');
$binder->bind($placeholder, $value, $type);
return $placeholder;
}
/**
* Do a deep clone of this expression.
*
* @return void
*/
public function __clone()
{
foreach (['_field', '_from', '_to'] as $part) {
if ($this->{$part} instanceof ExpressionInterface) {
$this->{$part} = clone $this->{$part};
}
}
}
}
+251
View File
@@ -0,0 +1,251 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Type\ExpressionTypeCasterTrait;
use Cake\Database\ValueBinder;
use Closure;
/**
* This class represents a SQL Case statement
*
* @deprecated 4.3.0 Use QueryExpression::case() or CaseStatementExpression instead
*/
class CaseExpression implements ExpressionInterface
{
use ExpressionTypeCasterTrait;
/**
* A list of strings or other expression objects that represent the conditions of
* the case statement. For example one key of the array might look like "sum > :value"
*
* @var array
*/
protected $_conditions = [];
/**
* Values that are associated with the conditions in the $_conditions array.
* Each value represents the 'true' value for the condition with the corresponding key.
*
* @var array
*/
protected $_values = [];
/**
* The `ELSE` value for the case statement. If null then no `ELSE` will be included.
*
* @var \Cake\Database\ExpressionInterface|array|string|null
*/
protected $_elseValue;
/**
* Constructs the case expression
*
* @param \Cake\Database\ExpressionInterface|array $conditions The conditions to test. Must be a ExpressionInterface
* instance, or an array of ExpressionInterface instances.
* @param \Cake\Database\ExpressionInterface|array $values Associative array of values to be associated with the
* conditions passed in $conditions. If there are more $values than $conditions,
* the last $value is used as the `ELSE` value.
* @param array<string> $types Associative array of types to be associated with the values
* passed in $values
*/
public function __construct($conditions = [], $values = [], $types = [])
{
$conditions = is_array($conditions) ? $conditions : [$conditions];
$values = is_array($values) ? $values : [$values];
$types = is_array($types) ? $types : [$types];
if (!empty($conditions)) {
$this->add($conditions, $values, $types);
}
if (count($values) > count($conditions)) {
end($values);
$key = key($values);
$this->elseValue($values[$key], $types[$key] ?? null);
}
}
/**
* Adds one or more conditions and their respective true values to the case object.
* Conditions must be a one dimensional array or a QueryExpression.
* The trueValues must be a similar structure, but may contain a string value.
*
* @param \Cake\Database\ExpressionInterface|array $conditions Must be a ExpressionInterface instance,
* or an array of ExpressionInterface instances.
* @param \Cake\Database\ExpressionInterface|array $values Associative array of values of each condition
* @param array<string> $types Associative array of types to be associated with the values
* @return $this
*/
public function add($conditions = [], $values = [], $types = [])
{
$conditions = is_array($conditions) ? $conditions : [$conditions];
$values = is_array($values) ? $values : [$values];
$types = is_array($types) ? $types : [$types];
$this->_addExpressions($conditions, $values, $types);
return $this;
}
/**
* Iterates over the passed in conditions and ensures that there is a matching true value for each.
* If no matching true value, then it is defaulted to '1'.
*
* @param array $conditions Array of ExpressionInterface instances.
* @param array<mixed> $values Associative array of values of each condition
* @param array<string> $types Associative array of types to be associated with the values
* @return void
*/
protected function _addExpressions(array $conditions, array $values, array $types): void
{
$rawValues = array_values($values);
$keyValues = array_keys($values);
foreach ($conditions as $k => $c) {
$numericKey = is_numeric($k);
if ($numericKey && empty($c)) {
continue;
}
if (!$c instanceof ExpressionInterface) {
continue;
}
$this->_conditions[] = $c;
$value = $rawValues[$k] ?? 1;
if ($value === 'literal') {
$value = $keyValues[$k];
$this->_values[] = $value;
continue;
}
if ($value === 'identifier') {
/** @var string $identifier */
$identifier = $keyValues[$k];
$value = new IdentifierExpression($identifier);
$this->_values[] = $value;
continue;
}
$type = $types[$k] ?? null;
if ($type !== null && !$value instanceof ExpressionInterface) {
$value = $this->_castToExpression($value, $type);
}
if ($value instanceof ExpressionInterface) {
$this->_values[] = $value;
continue;
}
$this->_values[] = ['value' => $value, 'type' => $type];
}
}
/**
* Sets the default value
*
* @param \Cake\Database\ExpressionInterface|array|string|null $value Value to set
* @param string|null $type Type of value
* @return void
*/
public function elseValue($value = null, ?string $type = null): void
{
if (is_array($value)) {
end($value);
$value = key($value);
}
if ($value !== null && !$value instanceof ExpressionInterface) {
$value = $this->_castToExpression($value, $type);
}
if (!$value instanceof ExpressionInterface) {
$value = ['value' => $value, 'type' => $type];
}
$this->_elseValue = $value;
}
/**
* Compiles the relevant parts into sql
*
* @param \Cake\Database\ExpressionInterface|array|string $part The part to compile
* @param \Cake\Database\ValueBinder $binder Sql generator
* @return string
*/
protected function _compile($part, ValueBinder $binder): string
{
if ($part instanceof ExpressionInterface) {
$part = $part->sql($binder);
} elseif (is_array($part)) {
$placeholder = $binder->placeholder('param');
$binder->bind($placeholder, $part['value'], $part['type']);
$part = $placeholder;
}
return $part;
}
/**
* Converts the Node into a SQL string fragment.
*
* @param \Cake\Database\ValueBinder $binder Placeholder generator object
* @return string
*/
public function sql(ValueBinder $binder): string
{
$parts = [];
$parts[] = 'CASE';
foreach ($this->_conditions as $k => $part) {
$value = $this->_values[$k];
$parts[] = 'WHEN ' . $this->_compile($part, $binder) . ' THEN ' . $this->_compile($value, $binder);
}
if ($this->_elseValue !== null) {
$parts[] = 'ELSE';
$parts[] = $this->_compile($this->_elseValue, $binder);
}
$parts[] = 'END';
return implode(' ', $parts);
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
foreach (['_conditions', '_values'] as $part) {
foreach ($this->{$part} as $c) {
if ($c instanceof ExpressionInterface) {
$callback($c);
$c->traverse($callback);
}
}
}
if ($this->_elseValue instanceof ExpressionInterface) {
$callback($this->_elseValue);
$this->_elseValue->traverse($callback);
}
return $this;
}
}
@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Chronos\ChronosDate;
use Cake\Chronos\MutableDate;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\TypedResultInterface;
use Cake\Database\ValueBinder;
use DateTimeInterface;
/**
* Trait that holds shared functionality for case related expressions.
*
* @property \Cake\Database\TypeMap $_typeMap The type map to use when using an array of conditions for the `WHEN`
* value.
* @internal
*/
trait CaseExpressionTrait
{
/**
* Infers the abstract type for the given value.
*
* @param mixed $value The value for which to infer the type.
* @return string|null The abstract type, or `null` if it could not be inferred.
*/
protected function inferType($value): ?string
{
$type = null;
if (is_string($value)) {
$type = 'string';
} elseif (is_int($value)) {
$type = 'integer';
} elseif (is_float($value)) {
$type = 'float';
} elseif (is_bool($value)) {
$type = 'boolean';
} elseif (
$value instanceof ChronosDate ||
$value instanceof MutableDate
) {
$type = 'date';
} elseif ($value instanceof DateTimeInterface) {
$type = 'datetime';
} elseif (
is_object($value) &&
method_exists($value, '__toString')
) {
$type = 'string';
} elseif (
$this->_typeMap !== null &&
$value instanceof IdentifierExpression
) {
$type = $this->_typeMap->type($value->getIdentifier());
} elseif ($value instanceof TypedResultInterface) {
$type = $value->getReturnType();
}
return $type;
}
/**
* Compiles a nullable value to SQL.
*
* @param \Cake\Database\ValueBinder $binder The value binder to use.
* @param \Cake\Database\ExpressionInterface|object|scalar|null $value The value to compile.
* @param string|null $type The value type.
* @return string
*/
protected function compileNullableValue(ValueBinder $binder, $value, ?string $type = null): string
{
if (
$type !== null &&
!($value instanceof ExpressionInterface)
) {
$value = $this->_castToExpression($value, $type);
}
if ($value === null) {
$value = 'NULL';
} elseif ($value instanceof Query) {
$value = sprintf('(%s)', $value->sql($binder));
} elseif ($value instanceof ExpressionInterface) {
$value = $value->sql($binder);
} else {
$placeholder = $binder->placeholder('c');
$binder->bind($placeholder, $value, $type);
$value = $placeholder;
}
return $value;
}
}
@@ -0,0 +1,597 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Type\ExpressionTypeCasterTrait;
use Cake\Database\TypedResultInterface;
use Cake\Database\TypeMapTrait;
use Cake\Database\ValueBinder;
use Closure;
use InvalidArgumentException;
use LogicException;
use function Cake\Core\getTypeName;
/**
* Represents a SQL case statement with a fluid API
*/
class CaseStatementExpression implements ExpressionInterface, TypedResultInterface
{
use CaseExpressionTrait;
use ExpressionTypeCasterTrait;
use TypeMapTrait;
/**
* The names of the clauses that are valid for use with the
* `clause()` method.
*
* @var array<string>
*/
protected $validClauseNames = [
'value',
'when',
'else',
];
/**
* Whether this is a simple case expression.
*
* @var bool
*/
protected $isSimpleVariant = false;
/**
* The case value.
*
* @var \Cake\Database\ExpressionInterface|object|scalar|null
*/
protected $value = null;
/**
* The case value type.
*
* @var string|null
*/
protected $valueType = null;
/**
* The `WHEN ... THEN ...` expressions.
*
* @var array<\Cake\Database\Expression\WhenThenExpression>
*/
protected $when = [];
/**
* Buffer that holds values and types for use with `then()`.
*
* @var array|null
*/
protected $whenBuffer = null;
/**
* The else part result value.
*
* @var \Cake\Database\ExpressionInterface|object|scalar|null
*/
protected $else = null;
/**
* The else part result type.
*
* @var string|null
*/
protected $elseType = null;
/**
* The return type.
*
* @var string|null
*/
protected $returnType = null;
/**
* Constructor.
*
* When a value is set, the syntax generated is
* `CASE case_value WHEN when_value ... END` (simple case),
* where the `when_value`'s are compared against the
* `case_value`.
*
* When no value is set, the syntax generated is
* `CASE WHEN when_conditions ... END` (searched case),
* where the conditions hold the comparisons.
*
* Note that `null` is a valid case value, and thus should
* only be passed if you actually want to create the simple
* case expression variant!
*
* @param \Cake\Database\ExpressionInterface|object|scalar|null $value The case value.
* @param string|null $type The case value type. If no type is provided, the type will be tried to be inferred
* from the value.
*/
public function __construct($value = null, ?string $type = null)
{
if (func_num_args() > 0) {
if (
$value !== null &&
!is_scalar($value) &&
!(is_object($value) && !($value instanceof Closure))
) {
throw new InvalidArgumentException(sprintf(
'The `$value` argument must be either `null`, a scalar value, an object, ' .
'or an instance of `\%s`, `%s` given.',
ExpressionInterface::class,
getTypeName($value)
));
}
$this->value = $value;
if (
$value !== null &&
$type === null &&
!($value instanceof ExpressionInterface)
) {
$type = $this->inferType($value);
}
$this->valueType = $type;
$this->isSimpleVariant = true;
}
}
/**
* Sets the `WHEN` value for a `WHEN ... THEN ...` expression, or a
* self-contained expression that holds both the value for `WHEN`
* and the value for `THEN`.
*
* ### Order based syntax
*
* When passing a value other than a self-contained
* `\Cake\Database\Expression\WhenThenExpression`,
* instance, the `WHEN ... THEN ...` statement must be closed off with
* a call to `then()` before invoking `when()` again or `else()`:
*
* ```
* $queryExpression
* ->case($query->identifier('Table.column'))
* ->when(true)
* ->then('Yes')
* ->when(false)
* ->then('No')
* ->else('Maybe');
* ```
*
* ### Self-contained expressions
*
* When passing an instance of `\Cake\Database\Expression\WhenThenExpression`,
* being it directly, or via a callable, then there is no need to close
* using `then()` on this object, instead the statement will be closed
* on the `\Cake\Database\Expression\WhenThenExpression`
* object using
* `\Cake\Database\Expression\WhenThenExpression::then()`.
*
* Callables will receive an instance of `\Cake\Database\Expression\WhenThenExpression`,
* and must return one, being it the same object, or a custom one:
*
* ```
* $queryExpression
* ->case()
* ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
* return $whenThen
* ->when(['Table.column' => true])
* ->then('Yes');
* })
* ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
* return $whenThen
* ->when(['Table.column' => false])
* ->then('No');
* })
* ->else('Maybe');
* ```
*
* ### Type handling
*
* The types provided via the `$type` argument will be merged with the
* type map set for this expression. When using callables for `$when`,
* the `\Cake\Database\Expression\WhenThenExpression`
* instance received by the callables will inherit that type map, however
* the types passed here will _not_ be merged in case of using callables,
* instead the types must be passed in
* `\Cake\Database\Expression\WhenThenExpression::when()`:
*
* ```
* $queryExpression
* ->case()
* ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
* return $whenThen
* ->when(['unmapped_column' => true], ['unmapped_column' => 'bool'])
* ->then('Yes');
* })
* ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
* return $whenThen
* ->when(['unmapped_column' => false], ['unmapped_column' => 'bool'])
* ->then('No');
* })
* ->else('Maybe');
* ```
*
* ### User data safety
*
* When passing user data, be aware that allowing a user defined array
* to be passed, is a potential SQL injection vulnerability, as it
* allows for raw SQL to slip in!
*
* The following is _unsafe_ usage that must be avoided:
*
* ```
* $case
* ->when($userData)
* ```
*
* A safe variant for the above would be to define a single type for
* the value:
*
* ```
* $case
* ->when($userData, 'integer')
* ```
*
* This way an exception would be triggered when an array is passed for
* the value, thus preventing raw SQL from slipping in, and all other
* types of values would be forced to be bound as an integer.
*
* Another way to safely pass user data is when using a conditions
* array, and passing user data only on the value side of the array
* entries, which will cause them to be bound:
*
* ```
* $case
* ->when([
* 'Table.column' => $userData,
* ])
* ```
*
* Lastly, data can also be bound manually:
*
* ```
* $query
* ->select([
* 'val' => $query->newExpr()
* ->case()
* ->when($query->newExpr(':userData'))
* ->then(123)
* ])
* ->bind(':userData', $userData, 'integer')
* ```
*
* @param \Cake\Database\ExpressionInterface|\Closure|object|array|scalar $when The `WHEN` value. When using an
* array of conditions, it must be compatible with `\Cake\Database\Query::where()`. Note that this argument is
* _not_ completely safe for use with user data, as a user supplied array would allow for raw SQL to slip in! If
* you plan to use user data, either pass a single type for the `$type` argument (which forces the `$when` value to
* be a non-array, and then always binds the data), use a conditions array where the user data is only passed on
* the value side of the array entries, or custom bindings!
* @param array<string, string>|string|null $type The when value type. Either an associative array when using array style
* conditions, or else a string. If no type is provided, the type will be tried to be inferred from the value.
* @return $this
* @throws \LogicException In case this a closing `then()` call is required before calling this method.
* @throws \LogicException In case the callable doesn't return an instance of
* `\Cake\Database\Expression\WhenThenExpression`.
*/
public function when($when, $type = null)
{
if ($this->whenBuffer !== null) {
throw new LogicException('Cannot call `when()` between `when()` and `then()`.');
}
if ($when instanceof Closure) {
$when = $when(new WhenThenExpression($this->getTypeMap()));
if (!($when instanceof WhenThenExpression)) {
throw new LogicException(sprintf(
'`when()` callables must return an instance of `\%s`, `%s` given.',
WhenThenExpression::class,
getTypeName($when)
));
}
}
if ($when instanceof WhenThenExpression) {
$this->when[] = $when;
} else {
$this->whenBuffer = ['when' => $when, 'type' => $type];
}
return $this;
}
/**
* Sets the `THEN` result value for the last `WHEN ... THEN ...`
* statement that was opened using `when()`.
*
* ### Order based syntax
*
* This method can only be invoked in case `when()` was previously
* used with a value other than a closure or an instance of
* `\Cake\Database\Expression\WhenThenExpression`:
*
* ```
* $case
* ->when(['Table.column' => true])
* ->then('Yes')
* ->when(['Table.column' => false])
* ->then('No')
* ->else('Maybe');
* ```
*
* The following would all fail with an exception:
*
* ```
* $case
* ->when(['Table.column' => true])
* ->when(['Table.column' => false])
* // ...
* ```
*
* ```
* $case
* ->when(['Table.column' => true])
* ->else('Maybe')
* // ...
* ```
*
* ```
* $case
* ->then('Yes')
* // ...
* ```
*
* ```
* $case
* ->when(['Table.column' => true])
* ->then('Yes')
* ->then('No')
* // ...
* ```
*
* @param \Cake\Database\ExpressionInterface|object|scalar|null $result The result value.
* @param string|null $type The result type. If no type is provided, the type will be tried to be inferred from the
* value.
* @return $this
* @throws \LogicException In case `when()` wasn't previously called with a value other than a closure or an
* instance of `\Cake\Database\Expression\WhenThenExpression`.
*/
public function then($result, ?string $type = null)
{
if ($this->whenBuffer === null) {
throw new LogicException('Cannot call `then()` before `when()`.');
}
$whenThen = (new WhenThenExpression($this->getTypeMap()))
->when($this->whenBuffer['when'], $this->whenBuffer['type'])
->then($result, $type);
$this->whenBuffer = null;
$this->when[] = $whenThen;
return $this;
}
/**
* Sets the `ELSE` result value.
*
* @param \Cake\Database\ExpressionInterface|object|scalar|null $result The result value.
* @param string|null $type The result type. If no type is provided, the type will be tried to be inferred from the
* value.
* @return $this
* @throws \LogicException In case a closing `then()` call is required before calling this method.
* @throws \InvalidArgumentException In case the `$result` argument is neither a scalar value, nor an object, an
* instance of `\Cake\Database\ExpressionInterface`, or `null`.
*/
public function else($result, ?string $type = null)
{
if ($this->whenBuffer !== null) {
throw new LogicException('Cannot call `else()` between `when()` and `then()`.');
}
if (
$result !== null &&
!is_scalar($result) &&
!(is_object($result) && !($result instanceof Closure))
) {
throw new InvalidArgumentException(sprintf(
'The `$result` argument must be either `null`, a scalar value, an object, ' .
'or an instance of `\%s`, `%s` given.',
ExpressionInterface::class,
getTypeName($result)
));
}
if ($type === null) {
$type = $this->inferType($result);
}
$this->else = $result;
$this->elseType = $type;
return $this;
}
/**
* Returns the abstract type that this expression will return.
*
* If no type has been explicitly set via `setReturnType()`, this
* method will try to obtain the type from the result types of the
* `then()` and `else() `calls. All types must be identical in order
* for this to work, otherwise the type will default to `string`.
*
* @return string
* @see CaseStatementExpression::then()
*/
public function getReturnType(): string
{
if ($this->returnType !== null) {
return $this->returnType;
}
$types = [];
foreach ($this->when as $when) {
$type = $when->getResultType();
if ($type !== null) {
$types[] = $type;
}
}
if ($this->elseType !== null) {
$types[] = $this->elseType;
}
$types = array_unique($types);
if (count($types) === 1) {
return $types[0];
}
return 'string';
}
/**
* Sets the abstract type that this expression will return.
*
* If no type is being explicitly set via this method, then the
* `getReturnType()` method will try to infer the type from the
* result types of the `then()` and `else() `calls.
*
* @param string $type The type name to use.
* @return $this
*/
public function setReturnType(string $type)
{
$this->returnType = $type;
return $this;
}
/**
* Returns the available data for the given clause.
*
* ### Available clauses
*
* The following clause names are available:
*
* * `value`: The case value for a `CASE case_value WHEN ...` expression.
* * `when`: An array of `WHEN ... THEN ...` expressions.
* * `else`: The `ELSE` result value.
*
* @param string $clause The name of the clause to obtain.
* @return \Cake\Database\ExpressionInterface|object|array<\Cake\Database\Expression\WhenThenExpression>|scalar|null
* @throws \InvalidArgumentException In case the given clause name is invalid.
*/
public function clause(string $clause)
{
if (!in_array($clause, $this->validClauseNames, true)) {
throw new InvalidArgumentException(
sprintf(
'The `$clause` argument must be one of `%s`, the given value `%s` is invalid.',
implode('`, `', $this->validClauseNames),
$clause
)
);
}
return $this->{$clause};
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
if ($this->whenBuffer !== null) {
throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.');
}
if (empty($this->when)) {
throw new LogicException('Case expression must have at least one when statement.');
}
$value = '';
if ($this->isSimpleVariant) {
$value = $this->compileNullableValue($binder, $this->value, $this->valueType) . ' ';
}
$whenThenExpressions = [];
foreach ($this->when as $whenThen) {
$whenThenExpressions[] = $whenThen->sql($binder);
}
$whenThen = implode(' ', $whenThenExpressions);
$else = $this->compileNullableValue($binder, $this->else, $this->elseType);
return "CASE {$value}{$whenThen} ELSE $else END";
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
if ($this->whenBuffer !== null) {
throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.');
}
if ($this->value instanceof ExpressionInterface) {
$callback($this->value);
$this->value->traverse($callback);
}
foreach ($this->when as $when) {
$callback($when);
$when->traverse($callback);
}
if ($this->else instanceof ExpressionInterface) {
$callback($this->else);
$this->else->traverse($callback);
}
return $this;
}
/**
* Clones the inner expression objects.
*
* @return void
*/
public function __clone()
{
if ($this->whenBuffer !== null) {
throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.');
}
if ($this->value instanceof ExpressionInterface) {
$this->value = clone $this->value;
}
foreach ($this->when as $key => $when) {
$this->when[$key] = clone $this->when[$key];
}
if ($this->else instanceof ExpressionInterface) {
$this->else = clone $this->else;
}
}
}
@@ -0,0 +1,239 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use Closure;
use RuntimeException;
/**
* An expression that represents a common table expression definition.
*/
class CommonTableExpression implements ExpressionInterface
{
/**
* The CTE name.
*
* @var \Cake\Database\Expression\IdentifierExpression
*/
protected $name;
/**
* The field names to use for the CTE.
*
* @var array<\Cake\Database\Expression\IdentifierExpression>
*/
protected $fields = [];
/**
* The CTE query definition.
*
* @var \Cake\Database\ExpressionInterface|null
*/
protected $query;
/**
* Whether the CTE is materialized or not materialized.
*
* @var string|null
*/
protected $materialized = null;
/**
* Whether the CTE is recursive.
*
* @var bool
*/
protected $recursive = false;
/**
* Constructor.
*
* @param string $name The CTE name.
* @param \Cake\Database\ExpressionInterface|\Closure $query CTE query
*/
public function __construct(string $name = '', $query = null)
{
$this->name = new IdentifierExpression($name);
if ($query) {
$this->query($query);
}
}
/**
* Sets the name of this CTE.
*
* This is the named you used to reference the expression
* in select, insert, etc queries.
*
* @param string $name The CTE name.
* @return $this
*/
public function name(string $name)
{
$this->name = new IdentifierExpression($name);
return $this;
}
/**
* Sets the query for this CTE.
*
* @param \Cake\Database\ExpressionInterface|\Closure $query CTE query
* @return $this
*/
public function query($query)
{
if ($query instanceof Closure) {
$query = $query();
if (!($query instanceof ExpressionInterface)) {
throw new RuntimeException(
'You must return an `ExpressionInterface` from a Closure passed to `query()`.'
);
}
}
$this->query = $query;
return $this;
}
/**
* Adds one or more fields (arguments) to the CTE.
*
* @param \Cake\Database\Expression\IdentifierExpression|array<\Cake\Database\Expression\IdentifierExpression>|array<string>|string $fields Field names
* @return $this
*/
public function field($fields)
{
$fields = (array)$fields;
foreach ($fields as &$field) {
if (!($field instanceof IdentifierExpression)) {
$field = new IdentifierExpression($field);
}
}
$this->fields = array_merge($this->fields, $fields);
return $this;
}
/**
* Sets this CTE as materialized.
*
* @return $this
*/
public function materialized()
{
$this->materialized = 'MATERIALIZED';
return $this;
}
/**
* Sets this CTE as not materialized.
*
* @return $this
*/
public function notMaterialized()
{
$this->materialized = 'NOT MATERIALIZED';
return $this;
}
/**
* Gets whether this CTE is recursive.
*
* @return bool
*/
public function isRecursive(): bool
{
return $this->recursive;
}
/**
* Sets this CTE as recursive.
*
* @return $this
*/
public function recursive()
{
$this->recursive = true;
return $this;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$fields = '';
if ($this->fields) {
$expressions = array_map(function (IdentifierExpression $e) use ($binder) {
return $e->sql($binder);
}, $this->fields);
$fields = sprintf('(%s)', implode(', ', $expressions));
}
$suffix = $this->materialized ? $this->materialized . ' ' : '';
return sprintf(
'%s%s AS %s(%s)',
$this->name->sql($binder),
$fields,
$suffix,
$this->query ? $this->query->sql($binder) : ''
);
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
$callback($this->name);
foreach ($this->fields as $field) {
$callback($field);
$field->traverse($callback);
}
if ($this->query) {
$callback($this->query);
$this->query->traverse($callback);
}
return $this;
}
/**
* Clones the inner expression objects.
*
* @return void
*/
public function __clone()
{
$this->name = clone $this->name;
if ($this->query) {
$this->query = clone $this->query;
}
foreach ($this->fields as $key => $field) {
$this->fields[$key] = clone $field;
}
}
}
+7
View File
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
use function Cake\Core\deprecationWarning;
deprecationWarning('Since 4.1.0: `Comparison` deprecated. Use `ComparisonExpression` instead.');
class_exists('Cake\Database\Expression\ComparisonExpression');
@@ -0,0 +1,324 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\Exception\DatabaseException;
use Cake\Database\ExpressionInterface;
use Cake\Database\Type\ExpressionTypeCasterTrait;
use Cake\Database\ValueBinder;
use Closure;
/**
* A Comparison is a type of query expression that represents an operation
* involving a field an operator and a value. In its most common form the
* string representation of a comparison is `field = value`
*/
class ComparisonExpression implements ExpressionInterface, FieldInterface
{
use ExpressionTypeCasterTrait;
use FieldTrait;
/**
* The value to be used in the right hand side of the operation
*
* @var mixed
*/
protected $_value;
/**
* The type to be used for casting the value to a database representation
*
* @var string|null
*/
protected $_type;
/**
* The operator used for comparing field and value
*
* @var string
*/
protected $_operator = '=';
/**
* Whether the value in this expression is a traversable
*
* @var bool
*/
protected $_isMultiple = false;
/**
* A cached list of ExpressionInterface objects that were
* found in the value for this expression.
*
* @var array<\Cake\Database\ExpressionInterface>
*/
protected $_valueExpressions = [];
/**
* Constructor
*
* @param \Cake\Database\ExpressionInterface|string $field the field name to compare to a value
* @param mixed $value The value to be used in comparison
* @param string|null $type the type name used to cast the value
* @param string $operator the operator used for comparing field and value
*/
public function __construct($field, $value, ?string $type = null, string $operator = '=')
{
$this->_type = $type;
$this->setField($field);
$this->setValue($value);
$this->_operator = $operator;
}
/**
* Sets the value
*
* @param mixed $value The value to compare
* @return void
*/
public function setValue($value): void
{
$value = $this->_castToExpression($value, $this->_type);
$isMultiple = $this->_type && strpos($this->_type, '[]') !== false;
if ($isMultiple) {
[$value, $this->_valueExpressions] = $this->_collectExpressions($value);
}
$this->_isMultiple = $isMultiple;
$this->_value = $value;
}
/**
* Returns the value used for comparison
*
* @return mixed
*/
public function getValue()
{
return $this->_value;
}
/**
* Sets the operator to use for the comparison
*
* @param string $operator The operator to be used for the comparison.
* @return void
*/
public function setOperator(string $operator): void
{
$this->_operator = $operator;
}
/**
* Returns the operator used for comparison
*
* @return string
*/
public function getOperator(): string
{
return $this->_operator;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
/** @var \Cake\Database\ExpressionInterface|string $field */
$field = $this->_field;
if ($field instanceof ExpressionInterface) {
$field = $field->sql($binder);
}
if ($this->_value instanceof IdentifierExpression) {
$template = '%s %s %s';
$value = $this->_value->sql($binder);
} elseif ($this->_value instanceof ExpressionInterface) {
$template = '%s %s (%s)';
$value = $this->_value->sql($binder);
} else {
[$template, $value] = $this->_stringExpression($binder);
}
return sprintf($template, $field, $this->_operator, $value);
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
if ($this->_field instanceof ExpressionInterface) {
$callback($this->_field);
$this->_field->traverse($callback);
}
if ($this->_value instanceof ExpressionInterface) {
$callback($this->_value);
$this->_value->traverse($callback);
}
foreach ($this->_valueExpressions as $v) {
$callback($v);
$v->traverse($callback);
}
return $this;
}
/**
* Create a deep clone.
*
* Clones the field and value if they are expression objects.
*
* @return void
*/
public function __clone()
{
foreach (['_value', '_field'] as $prop) {
if ($this->{$prop} instanceof ExpressionInterface) {
$this->{$prop} = clone $this->{$prop};
}
}
}
/**
* Returns a template and a placeholder for the value after registering it
* with the placeholder $binder
*
* @param \Cake\Database\ValueBinder $binder The value binder to use.
* @return array First position containing the template and the second a placeholder
*/
protected function _stringExpression(ValueBinder $binder): array
{
$template = '%s ';
if ($this->_field instanceof ExpressionInterface && !$this->_field instanceof IdentifierExpression) {
$template = '(%s) ';
}
if ($this->_isMultiple) {
$template .= '%s (%s)';
$type = $this->_type;
if ($type !== null) {
$type = str_replace('[]', '', $type);
}
$value = $this->_flattenValue($this->_value, $binder, $type);
// To avoid SQL errors when comparing a field to a list of empty values,
// better just throw an exception here
if ($value === '') {
$field = $this->_field instanceof ExpressionInterface ? $this->_field->sql($binder) : $this->_field;
/** @psalm-suppress PossiblyInvalidCast */
throw new DatabaseException(
"Impossible to generate condition with empty list of values for field ($field)"
);
}
} else {
$template .= '%s %s';
$value = $this->_bindValue($this->_value, $binder, $this->_type);
}
return [$template, $value];
}
/**
* Registers a value in the placeholder generator and returns the generated placeholder
*
* @param mixed $value The value to bind
* @param \Cake\Database\ValueBinder $binder The value binder to use
* @param string|null $type The type of $value
* @return string generated placeholder
*/
protected function _bindValue($value, ValueBinder $binder, ?string $type = null): string
{
$placeholder = $binder->placeholder('c');
$binder->bind($placeholder, $value, $type);
return $placeholder;
}
/**
* Converts a traversable value into a set of placeholders generated by
* $binder and separated by `,`
*
* @param iterable $value the value to flatten
* @param \Cake\Database\ValueBinder $binder The value binder to use
* @param string|null $type the type to cast values to
* @return string
*/
protected function _flattenValue(iterable $value, ValueBinder $binder, ?string $type = null): string
{
$parts = [];
if (is_array($value)) {
foreach ($this->_valueExpressions as $k => $v) {
$parts[$k] = $v->sql($binder);
unset($value[$k]);
}
}
if (!empty($value)) {
$parts += $binder->generateManyNamed($value, $type);
}
return implode(',', $parts);
}
/**
* Returns an array with the original $values in the first position
* and all ExpressionInterface objects that could be found in the second
* position.
*
* @param \Cake\Database\ExpressionInterface|iterable $values The rows to insert
* @return array
*/
protected function _collectExpressions($values): array
{
if ($values instanceof ExpressionInterface) {
return [$values, []];
}
$expressions = $result = [];
$isArray = is_array($values);
if ($isArray) {
/** @var array $result */
$result = $values;
}
foreach ($values as $k => $v) {
if ($v instanceof ExpressionInterface) {
$expressions[$k] = $v;
}
if ($isArray) {
$result[$k] = $v;
}
}
return [$result, $expressions];
}
}
// phpcs:disable
class_alias(
'Cake\Database\Expression\ComparisonExpression',
'Cake\Database\Expression\Comparison'
);
// phpcs:enable
+39
View File
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
/**
* Describes a getter and a setter for the a field property. Useful for expressions
* that contain an identifier to compare against.
*/
interface FieldInterface
{
/**
* Sets the field name
*
* @param \Cake\Database\ExpressionInterface|array|string $field The field to compare with.
* @return void
*/
public function setField($field): void;
/**
* Returns the field name
*
* @return \Cake\Database\ExpressionInterface|array|string
*/
public function getField();
}
+51
View File
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
/**
* Contains the field property with a getter and a setter for it
*/
trait FieldTrait
{
/**
* The field name or expression to be used in the left hand side of the operator
*
* @var \Cake\Database\ExpressionInterface|array|string
*/
protected $_field;
/**
* Sets the field name
*
* @param \Cake\Database\ExpressionInterface|array|string $field The field to compare with.
* @return void
*/
public function setField($field): void
{
$this->_field = $field;
}
/**
* Returns the field name
*
* @return \Cake\Database\ExpressionInterface|array|string
*/
public function getField()
{
return $this->_field;
}
}
@@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\Type\ExpressionTypeCasterTrait;
use Cake\Database\TypedResultInterface;
use Cake\Database\TypedResultTrait;
use Cake\Database\ValueBinder;
/**
* This class represents a function call string in a SQL statement. Calls can be
* constructed by passing the name of the function and a list of params.
* For security reasons, all params passed are quoted by default unless
* explicitly told otherwise.
*/
class FunctionExpression extends QueryExpression implements TypedResultInterface
{
use ExpressionTypeCasterTrait;
use TypedResultTrait;
/**
* The name of the function to be constructed when generating the SQL string
*
* @var string
*/
protected $_name;
/**
* Constructor. Takes a name for the function to be invoked and a list of params
* to be passed into the function. Optionally you can pass a list of types to
* be used for each bound param.
*
* By default, all params that are passed will be quoted. If you wish to use
* literal arguments, you need to explicitly hint this function.
*
* ### Examples:
*
* `$f = new FunctionExpression('CONCAT', ['CakePHP', ' rules']);`
*
* Previous line will generate `CONCAT('CakePHP', ' rules')`
*
* `$f = new FunctionExpression('CONCAT', ['name' => 'literal', ' rules']);`
*
* Will produce `CONCAT(name, ' rules')`
*
* @param string $name the name of the function to be constructed
* @param array $params list of arguments to be passed to the function
* If associative the key would be used as argument when value is 'literal'
* @param array<string, string>|array<string|null> $types Associative array of types to be associated with the
* passed arguments
* @param string $returnType The return type of this expression
*/
public function __construct(string $name, array $params = [], array $types = [], string $returnType = 'string')
{
$this->_name = $name;
$this->_returnType = $returnType;
parent::__construct($params, $types, ',');
}
/**
* Sets the name of the SQL function to be invoke in this expression.
*
* @param string $name The name of the function
* @return $this
*/
public function setName(string $name)
{
$this->_name = $name;
return $this;
}
/**
* Gets the name of the SQL function to be invoke in this expression.
*
* @return string
*/
public function getName(): string
{
return $this->_name;
}
/**
* Adds one or more arguments for the function call.
*
* @param array $conditions list of arguments to be passed to the function
* If associative the key would be used as argument when value is 'literal'
* @param array<string, string> $types Associative array of types to be associated with the
* passed arguments
* @param bool $prepend Whether to prepend or append to the list of arguments
* @see \Cake\Database\Expression\FunctionExpression::__construct() for more details.
* @return $this
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function add($conditions, array $types = [], bool $prepend = false)
{
$put = $prepend ? 'array_unshift' : 'array_push';
$typeMap = $this->getTypeMap()->setTypes($types);
foreach ($conditions as $k => $p) {
if ($p === 'literal') {
$put($this->_conditions, $k);
continue;
}
if ($p === 'identifier') {
$put($this->_conditions, new IdentifierExpression($k));
continue;
}
$type = $typeMap->type($k);
if ($type !== null && !$p instanceof ExpressionInterface) {
$p = $this->_castToExpression($p, $type);
}
if ($p instanceof ExpressionInterface) {
$put($this->_conditions, $p);
continue;
}
$put($this->_conditions, ['value' => $p, 'type' => $type]);
}
return $this;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$parts = [];
foreach ($this->_conditions as $condition) {
if ($condition instanceof Query) {
$condition = sprintf('(%s)', $condition->sql($binder));
} elseif ($condition instanceof ExpressionInterface) {
$condition = $condition->sql($binder);
} elseif (is_array($condition)) {
$p = $binder->placeholder('param');
$binder->bind($p, $condition['value'], $condition['type']);
$condition = $p;
}
$parts[] = $condition;
}
return $this->_name . sprintf('(%s)', implode(
$this->_conjunction . ' ',
$parts
));
}
/**
* The name of the function is in itself an expression to generate, thus
* always adding 1 to the amount of expressions stored in this object.
*
* @return int
*/
public function count(): int
{
return 1 + count($this->_conditions);
}
}
@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use Closure;
/**
* Represents a single identifier name in the database.
*
* Identifier values are unsafe with user supplied data.
* Values will be quoted when identifier quoting is enabled.
*
* @see \Cake\Database\Query::identifier()
*/
class IdentifierExpression implements ExpressionInterface
{
/**
* Holds the identifier string
*
* @var string
*/
protected $_identifier;
/**
* @var string|null
*/
protected $collation;
/**
* Constructor
*
* @param string $identifier The identifier this expression represents
* @param string|null $collation The identifier collation
*/
public function __construct(string $identifier, ?string $collation = null)
{
$this->_identifier = $identifier;
$this->collation = $collation;
}
/**
* Sets the identifier this expression represents
*
* @param string $identifier The identifier
* @return void
*/
public function setIdentifier(string $identifier): void
{
$this->_identifier = $identifier;
}
/**
* Returns the identifier this expression represents
*
* @return string
*/
public function getIdentifier(): string
{
return $this->_identifier;
}
/**
* Sets the collation.
*
* @param string $collation Identifier collation
* @return void
*/
public function setCollation(string $collation): void
{
$this->collation = $collation;
}
/**
* Returns the collation.
*
* @return string|null
*/
public function getCollation(): ?string
{
return $this->collation;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$sql = $this->_identifier;
if ($this->collation) {
$sql .= ' COLLATE ' . $this->collation;
}
return $sql;
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
return $this;
}
}
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use RuntimeException;
/**
* An expression object for ORDER BY clauses
*/
class OrderByExpression extends QueryExpression
{
/**
* Constructor
*
* @param \Cake\Database\ExpressionInterface|array|string $conditions The sort columns
* @param \Cake\Database\TypeMap|array<string, string> $types The types for each column.
* @param string $conjunction The glue used to join conditions together.
*/
public function __construct($conditions = [], $types = [], $conjunction = '')
{
parent::__construct($conditions, $types, $conjunction);
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$order = [];
foreach ($this->_conditions as $k => $direction) {
if ($direction instanceof ExpressionInterface) {
$direction = $direction->sql($binder);
}
$order[] = is_numeric($k) ? $direction : sprintf('%s %s', $k, $direction);
}
return sprintf('ORDER BY %s', implode(', ', $order));
}
/**
* Auxiliary function used for decomposing a nested array of conditions and
* building a tree structure inside this object to represent the full SQL expression.
*
* New order by expressions are merged to existing ones
*
* @param array $conditions list of order by expressions
* @param array $types list of types associated on fields referenced in $conditions
* @return void
*/
protected function _addConditions(array $conditions, array $types): void
{
foreach ($conditions as $key => $val) {
if (
is_string($key) &&
is_string($val) &&
!in_array(strtoupper($val), ['ASC', 'DESC'], true)
) {
throw new RuntimeException(
sprintf(
'Passing extra expressions by associative array (`\'%s\' => \'%s\'`) ' .
'is not allowed to avoid potential SQL injection. ' .
'Use QueryExpression or numeric array instead.',
$key,
$val
)
);
}
}
$this->_conditions = array_merge($this->_conditions, $conditions);
}
}
@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\ValueBinder;
use Closure;
/**
* An expression object for complex ORDER BY clauses
*/
class OrderClauseExpression implements ExpressionInterface, FieldInterface
{
use FieldTrait;
/**
* The direction of sorting.
*
* @var string
*/
protected $_direction;
/**
* Constructor
*
* @param \Cake\Database\ExpressionInterface|string $field The field to order on.
* @param string $direction The direction to sort on.
*/
public function __construct($field, $direction)
{
$this->_field = $field;
$this->_direction = strtolower($direction) === 'asc' ? 'ASC' : 'DESC';
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
/** @var \Cake\Database\ExpressionInterface|string $field */
$field = $this->_field;
if ($field instanceof Query) {
$field = sprintf('(%s)', $field->sql($binder));
} elseif ($field instanceof ExpressionInterface) {
$field = $field->sql($binder);
}
return sprintf('%s %s', $field, $this->_direction);
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
if ($this->_field instanceof ExpressionInterface) {
$callback($this->_field);
$this->_field->traverse($callback);
}
return $this;
}
/**
* Create a deep clone of the order clause.
*
* @return void
*/
public function __clone()
{
if ($this->_field instanceof ExpressionInterface) {
$this->_field = clone $this->_field;
}
}
}
+876
View File
@@ -0,0 +1,876 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\TypeMapTrait;
use Cake\Database\ValueBinder;
use Closure;
use Countable;
use InvalidArgumentException;
use function Cake\Core\deprecationWarning;
/**
* Represents a SQL Query expression. Internally it stores a tree of
* expressions that can be compiled by converting this object to string
* and will contain a correctly parenthesized and nested expression.
*/
class QueryExpression implements ExpressionInterface, Countable
{
use TypeMapTrait;
/**
* String to be used for joining each of the internal expressions
* this object internally stores for example "AND", "OR", etc.
*
* @var string
*/
protected $_conjunction;
/**
* A list of strings or other expression objects that represent the "branches" of
* the expression tree. For example one key of the array might look like "sum > :value"
*
* @var array
*/
protected $_conditions = [];
/**
* Constructor. A new expression object can be created without any params and
* be built dynamically. Otherwise, it is possible to pass an array of conditions
* containing either a tree-like array structure to be parsed and/or other
* expression objects. Optionally, you can set the conjunction keyword to be used
* for joining each part of this level of the expression tree.
*
* @param \Cake\Database\ExpressionInterface|array|string $conditions Tree like array structure
* containing all the conditions to be added or nested inside this expression object.
* @param \Cake\Database\TypeMap|array $types Associative array of types to be associated with the values
* passed in $conditions.
* @param string $conjunction the glue that will join all the string conditions at this
* level of the expression tree. For example "AND", "OR", "XOR"...
* @see \Cake\Database\Expression\QueryExpression::add() for more details on $conditions and $types
*/
public function __construct($conditions = [], $types = [], $conjunction = 'AND')
{
$this->setTypeMap($types);
$this->setConjunction(strtoupper($conjunction));
if (!empty($conditions)) {
$this->add($conditions, $this->getTypeMap()->getTypes());
}
}
/**
* Changes the conjunction for the conditions at this level of the expression tree.
*
* @param string $conjunction Value to be used for joining conditions
* @return $this
*/
public function setConjunction(string $conjunction)
{
$this->_conjunction = strtoupper($conjunction);
return $this;
}
/**
* Gets the currently configured conjunction for the conditions at this level of the expression tree.
*
* @return string
*/
public function getConjunction(): string
{
return $this->_conjunction;
}
/**
* Adds one or more conditions to this expression object. Conditions can be
* expressed in a one dimensional array, that will cause all conditions to
* be added directly at this level of the tree or they can be nested arbitrarily
* making it create more expression objects that will be nested inside and
* configured to use the specified conjunction.
*
* If the type passed for any of the fields is expressed "type[]" (note braces)
* then it will cause the placeholder to be re-written dynamically so if the
* value is an array, it will create as many placeholders as values are in it.
*
* @param \Cake\Database\ExpressionInterface|array|string $conditions single or multiple conditions to
* be added. When using an array and the key is 'OR' or 'AND' a new expression
* object will be created with that conjunction and internal array value passed
* as conditions.
* @param array<int|string, string> $types Associative array of fields pointing to the type of the
* values that are being passed. Used for correctly binding values to statements.
* @see \Cake\Database\Query::where() for examples on conditions
* @return $this
*/
public function add($conditions, array $types = [])
{
if (is_string($conditions) || $conditions instanceof ExpressionInterface) {
$this->_conditions[] = $conditions;
return $this;
}
$this->_addConditions($conditions, $types);
return $this;
}
/**
* Adds a new condition to the expression object in the form "field = value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* If it is suffixed with "[]" and the value is an array then multiple placeholders
* will be created, one per each value in the array.
* @return $this
*/
public function eq($field, $value, ?string $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
return $this->add(new ComparisonExpression($field, $value, $type, '='));
}
/**
* Adds a new condition to the expression object in the form "field != value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* If it is suffixed with "[]" and the value is an array then multiple placeholders
* will be created, one per each value in the array.
* @return $this
*/
public function notEq($field, $value, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
return $this->add(new ComparisonExpression($field, $value, $type, '!='));
}
/**
* Adds a new condition to the expression object in the form "field > value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function gt($field, $value, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
return $this->add(new ComparisonExpression($field, $value, $type, '>'));
}
/**
* Adds a new condition to the expression object in the form "field < value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function lt($field, $value, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
return $this->add(new ComparisonExpression($field, $value, $type, '<'));
}
/**
* Adds a new condition to the expression object in the form "field >= value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function gte($field, $value, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
return $this->add(new ComparisonExpression($field, $value, $type, '>='));
}
/**
* Adds a new condition to the expression object in the form "field <= value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function lte($field, $value, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
return $this->add(new ComparisonExpression($field, $value, $type, '<='));
}
/**
* Adds a new condition to the expression object in the form "field IS NULL".
*
* @param \Cake\Database\ExpressionInterface|string $field database field to be
* tested for null
* @return $this
*/
public function isNull($field)
{
if (!($field instanceof ExpressionInterface)) {
$field = new IdentifierExpression($field);
}
return $this->add(new UnaryExpression('IS NULL', $field, UnaryExpression::POSTFIX));
}
/**
* Adds a new condition to the expression object in the form "field IS NOT NULL".
*
* @param \Cake\Database\ExpressionInterface|string $field database field to be
* tested for not null
* @return $this
*/
public function isNotNull($field)
{
if (!($field instanceof ExpressionInterface)) {
$field = new IdentifierExpression($field);
}
return $this->add(new UnaryExpression('IS NOT NULL', $field, UnaryExpression::POSTFIX));
}
/**
* Adds a new condition to the expression object in the form "field LIKE value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function like($field, $value, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
return $this->add(new ComparisonExpression($field, $value, $type, 'LIKE'));
}
/**
* Adds a new condition to the expression object in the form "field NOT LIKE value".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param mixed $value The value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function notLike($field, $value, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
return $this->add(new ComparisonExpression($field, $value, $type, 'NOT LIKE'));
}
/**
* Adds a new condition to the expression object in the form
* "field IN (value1, value2)".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param \Cake\Database\ExpressionInterface|array|string $values the value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function in($field, $values, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
$type = $type ?: 'string';
$type .= '[]';
$values = $values instanceof ExpressionInterface ? $values : (array)$values;
return $this->add(new ComparisonExpression($field, $values, $type, 'IN'));
}
/**
* Adds a new case expression to the expression object
*
* @param \Cake\Database\ExpressionInterface|array $conditions The conditions to test. Must be a ExpressionInterface
* instance, or an array of ExpressionInterface instances.
* @param \Cake\Database\ExpressionInterface|array $values Associative array of values to be associated with the
* conditions passed in $conditions. If there are more $values than $conditions,
* the last $value is used as the `ELSE` value.
* @param array<string> $types Associative array of types to be associated with the values
* passed in $values
* @return $this
* @deprecated 4.3.0 Use QueryExpression::case() or CaseStatementExpression instead
*/
public function addCase($conditions, $values = [], $types = [])
{
deprecationWarning('QueryExpression::addCase() is deprecated, use case() instead.');
return $this->add(new CaseExpression($conditions, $values, $types));
}
/**
* Returns a new case expression object.
*
* When a value is set, the syntax generated is
* `CASE case_value WHEN when_value ... END` (simple case),
* where the `when_value`'s are compared against the
* `case_value`.
*
* When no value is set, the syntax generated is
* `CASE WHEN when_conditions ... END` (searched case),
* where the conditions hold the comparisons.
*
* Note that `null` is a valid case value, and thus should
* only be passed if you actually want to create the simple
* case expression variant!
*
* @param \Cake\Database\ExpressionInterface|object|scalar|null $value The case value.
* @param string|null $type The case value type. If no type is provided, the type will be tried to be inferred
* from the value.
* @return \Cake\Database\Expression\CaseStatementExpression
*/
public function case($value = null, ?string $type = null): CaseStatementExpression
{
if (func_num_args() > 0) {
$expression = new CaseStatementExpression($value, $type);
} else {
$expression = new CaseStatementExpression();
}
return $expression->setTypeMap($this->getTypeMap());
}
/**
* Adds a new condition to the expression object in the form
* "field NOT IN (value1, value2)".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param \Cake\Database\ExpressionInterface|array|string $values the value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function notIn($field, $values, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
$type = $type ?: 'string';
$type .= '[]';
$values = $values instanceof ExpressionInterface ? $values : (array)$values;
return $this->add(new ComparisonExpression($field, $values, $type, 'NOT IN'));
}
/**
* Adds a new condition to the expression object in the form
* "(field NOT IN (value1, value2) OR field IS NULL".
*
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
* @param \Cake\Database\ExpressionInterface|array|string $values the value to be bound to $field for comparison
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function notInOrNull($field, $values, ?string $type = null)
{
$or = new static([], [], 'OR');
$or
->notIn($field, $values, $type)
->isNull($field);
return $this->add($or);
}
/**
* Adds a new condition to the expression object in the form "EXISTS (...)".
*
* @param \Cake\Database\ExpressionInterface $expression the inner query
* @return $this
*/
public function exists(ExpressionInterface $expression)
{
return $this->add(new UnaryExpression('EXISTS', $expression, UnaryExpression::PREFIX));
}
/**
* Adds a new condition to the expression object in the form "NOT EXISTS (...)".
*
* @param \Cake\Database\ExpressionInterface $expression the inner query
* @return $this
*/
public function notExists(ExpressionInterface $expression)
{
return $this->add(new UnaryExpression('NOT EXISTS', $expression, UnaryExpression::PREFIX));
}
/**
* Adds a new condition to the expression object in the form
* "field BETWEEN from AND to".
*
* @param \Cake\Database\ExpressionInterface|string $field The field name to compare for values inbetween the range.
* @param mixed $from The initial value of the range.
* @param mixed $to The ending value in the comparison range.
* @param string|null $type the type name for $value as configured using the Type map.
* @return $this
*/
public function between($field, $from, $to, $type = null)
{
if ($type === null) {
$type = $this->_calculateType($field);
}
return $this->add(new BetweenExpression($field, $from, $to, $type));
}
/**
* Returns a new QueryExpression object containing all the conditions passed
* and set up the conjunction to be "AND"
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be joined with AND
* @param array<string, string> $types Associative array of fields pointing to the type of the
* values that are being passed. Used for correctly binding values to statements.
* @return \Cake\Database\Expression\QueryExpression
*/
public function and($conditions, $types = [])
{
if ($conditions instanceof Closure) {
return $conditions(new static([], $this->getTypeMap()->setTypes($types)));
}
return new static($conditions, $this->getTypeMap()->setTypes($types));
}
/**
* Returns a new QueryExpression object containing all the conditions passed
* and set up the conjunction to be "OR"
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be joined with OR
* @param array<string, string> $types Associative array of fields pointing to the type of the
* values that are being passed. Used for correctly binding values to statements.
* @return \Cake\Database\Expression\QueryExpression
*/
public function or($conditions, $types = [])
{
if ($conditions instanceof Closure) {
return $conditions(new static([], $this->getTypeMap()->setTypes($types), 'OR'));
}
return new static($conditions, $this->getTypeMap()->setTypes($types), 'OR');
}
// phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
/**
* Returns a new QueryExpression object containing all the conditions passed
* and set up the conjunction to be "AND"
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be joined with AND
* @param array<string, string> $types Associative array of fields pointing to the type of the
* values that are being passed. Used for correctly binding values to statements.
* @return \Cake\Database\Expression\QueryExpression
* @deprecated 4.0.0 Use {@link and()} instead.
*/
public function and_($conditions, $types = [])
{
deprecationWarning('QueryExpression::and_() is deprecated use and() instead.');
return $this->and($conditions, $types);
}
/**
* Returns a new QueryExpression object containing all the conditions passed
* and set up the conjunction to be "OR"
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be joined with OR
* @param array<string, string> $types Associative array of fields pointing to the type of the
* values that are being passed. Used for correctly binding values to statements.
* @return \Cake\Database\Expression\QueryExpression
* @deprecated 4.0.0 Use {@link or()} instead.
*/
public function or_($conditions, $types = [])
{
deprecationWarning('QueryExpression::or_() is deprecated use or() instead.');
return $this->or($conditions, $types);
}
// phpcs:enable
/**
* Adds a new set of conditions to this level of the tree and negates
* the final result by prepending a NOT, it will look like
* "NOT ( (condition1) AND (conditions2) )" conjunction depends on the one
* currently configured for this object.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be added and negated
* @param array<string, string> $types Associative array of fields pointing to the type of the
* values that are being passed. Used for correctly binding values to statements.
* @return $this
*/
public function not($conditions, $types = [])
{
return $this->add(['NOT' => $conditions], $types);
}
/**
* Returns the number of internal conditions that are stored in this expression.
* Useful to determine if this expression object is void or it will generate
* a non-empty string when compiled
*
* @return int
*/
public function count(): int
{
return count($this->_conditions);
}
/**
* Builds equal condition or assignment with identifier wrapping.
*
* @param string $leftField Left join condition field name.
* @param string $rightField Right join condition field name.
* @return $this
*/
public function equalFields(string $leftField, string $rightField)
{
$wrapIdentifier = function ($field) {
if ($field instanceof ExpressionInterface) {
return $field;
}
return new IdentifierExpression($field);
};
return $this->eq($wrapIdentifier($leftField), $wrapIdentifier($rightField));
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$len = $this->count();
if ($len === 0) {
return '';
}
$conjunction = $this->_conjunction;
$template = $len === 1 ? '%s' : '(%s)';
$parts = [];
foreach ($this->_conditions as $part) {
if ($part instanceof Query) {
$part = '(' . $part->sql($binder) . ')';
} elseif ($part instanceof ExpressionInterface) {
$part = $part->sql($binder);
}
if ($part !== '') {
$parts[] = $part;
}
}
return sprintf($template, implode(" $conjunction ", $parts));
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
foreach ($this->_conditions as $c) {
if ($c instanceof ExpressionInterface) {
$callback($c);
$c->traverse($callback);
}
}
return $this;
}
/**
* Executes a callable function for each of the parts that form this expression.
*
* The callable function is required to return a value with which the currently
* visited part will be replaced. If the callable function returns null then
* the part will be discarded completely from this expression.
*
* The callback function will receive each of the conditions as first param and
* the key as second param. It is possible to declare the second parameter as
* passed by reference, this will enable you to change the key under which the
* modified part is stored.
*
* @param callable $callback The callable to apply to each part.
* @return $this
*/
public function iterateParts(callable $callback)
{
$parts = [];
foreach ($this->_conditions as $k => $c) {
$key = &$k;
$part = $callback($c, $key);
if ($part !== null) {
$parts[$key] = $part;
}
}
$this->_conditions = $parts;
return $this;
}
/**
* Check whether a callable is acceptable.
*
* We don't accept ['class', 'method'] style callbacks,
* as they often contain user input and arrays of strings
* are easy to sneak in.
*
* @param \Cake\Database\ExpressionInterface|callable|array|string $callable The callable to check.
* @return bool Valid callable.
* @deprecated 4.2.0 This method is unused.
* @codeCoverageIgnore
*/
public function isCallable($callable): bool
{
if (is_string($callable)) {
return false;
}
if (is_object($callable) && is_callable($callable)) {
return true;
}
return is_array($callable) && isset($callable[0]) && is_object($callable[0]) && is_callable($callable);
}
/**
* Returns true if this expression contains any other nested
* ExpressionInterface objects
*
* @return bool
*/
public function hasNestedExpression(): bool
{
foreach ($this->_conditions as $c) {
if ($c instanceof ExpressionInterface) {
return true;
}
}
return false;
}
/**
* Auxiliary function used for decomposing a nested array of conditions and build
* a tree structure inside this object to represent the full SQL expression.
* String conditions are stored directly in the conditions, while any other
* representation is wrapped around an adequate instance or of this class.
*
* @param array $conditions list of conditions to be stored in this object
* @param array<int|string, string> $types list of types associated on fields referenced in $conditions
* @return void
*/
protected function _addConditions(array $conditions, array $types): void
{
$operators = ['and', 'or', 'xor'];
$typeMap = $this->getTypeMap()->setTypes($types);
foreach ($conditions as $k => $c) {
$numericKey = is_numeric($k);
if ($c instanceof Closure) {
$expr = new static([], $typeMap);
$c = $c($expr, $this);
}
if ($numericKey && empty($c)) {
continue;
}
$isArray = is_array($c);
$isOperator = $isNot = false;
if (!$numericKey) {
$normalizedKey = strtolower($k);
$isOperator = in_array($normalizedKey, $operators);
$isNot = $normalizedKey === 'not';
}
if (($isOperator || $isNot) && ($isArray || $c instanceof Countable) && count($c) === 0) {
continue;
}
if ($numericKey && $c instanceof ExpressionInterface) {
$this->_conditions[] = $c;
continue;
}
if ($numericKey && is_string($c)) {
$this->_conditions[] = $c;
continue;
}
if ($numericKey && $isArray || $isOperator) {
$this->_conditions[] = new static($c, $typeMap, $numericKey ? 'AND' : $k);
continue;
}
if ($isNot) {
$this->_conditions[] = new UnaryExpression('NOT', new static($c, $typeMap));
continue;
}
if (!$numericKey) {
$this->_conditions[] = $this->_parseCondition($k, $c);
}
}
}
/**
* Parses a string conditions by trying to extract the operator inside it if any
* and finally returning either an adequate QueryExpression object or a plain
* string representation of the condition. This function is responsible for
* generating the placeholders and replacing the values by them, while storing
* the value elsewhere for future binding.
*
* @param string $field The value from which the actual field and operator will
* be extracted.
* @param mixed $value The value to be bound to a placeholder for the field
* @return \Cake\Database\ExpressionInterface
* @throws \InvalidArgumentException If operator is invalid or missing on NULL usage.
*/
protected function _parseCondition(string $field, $value)
{
$field = trim($field);
$operator = '=';
$expression = $field;
$spaces = substr_count($field, ' ');
// Handle field values that contain multiple spaces, such as
// operators with a space in them like `field IS NOT` and
// `field NOT LIKE`, or combinations with function expressions
// like `CONCAT(first_name, ' ', last_name) IN`.
if ($spaces > 1) {
$parts = explode(' ', $field);
if (preg_match('/(is not|not \w+)$/i', $field)) {
$last = array_pop($parts);
$second = array_pop($parts);
$parts[] = "{$second} {$last}";
}
$operator = array_pop($parts);
$expression = implode(' ', $parts);
} elseif ($spaces == 1) {
$parts = explode(' ', $field, 2);
[$expression, $operator] = $parts;
}
$operator = strtolower(trim($operator));
$type = $this->getTypeMap()->type($expression);
$typeMultiple = (is_string($type) && strpos($type, '[]') !== false);
if (in_array($operator, ['in', 'not in']) || $typeMultiple) {
$type = $type ?: 'string';
if (!$typeMultiple) {
$type .= '[]';
}
$operator = $operator === '=' ? 'IN' : $operator;
$operator = $operator === '!=' ? 'NOT IN' : $operator;
$typeMultiple = true;
}
if ($typeMultiple) {
$value = $value instanceof ExpressionInterface ? $value : (array)$value;
}
if ($operator === 'is' && $value === null) {
return new UnaryExpression(
'IS NULL',
new IdentifierExpression($expression),
UnaryExpression::POSTFIX
);
}
if ($operator === 'is not' && $value === null) {
return new UnaryExpression(
'IS NOT NULL',
new IdentifierExpression($expression),
UnaryExpression::POSTFIX
);
}
if ($operator === 'is' && $value !== null) {
$operator = '=';
}
if ($operator === 'is not' && $value !== null) {
$operator = '!=';
}
if ($value === null && $this->_conjunction !== ',') {
throw new InvalidArgumentException(
sprintf('Expression `%s` is missing operator (IS, IS NOT) with `null` value.', $expression)
);
}
return new ComparisonExpression($expression, $value, $type, $operator);
}
/**
* Returns the type name for the passed field if it was stored in the typeMap
*
* @param \Cake\Database\ExpressionInterface|string $field The field name to get a type for.
* @return string|null The computed type or null, if the type is unknown.
*/
protected function _calculateType($field): ?string
{
$field = $field instanceof IdentifierExpression ? $field->getIdentifier() : $field;
if (is_string($field)) {
return $this->getTypeMap()->type($field);
}
return null;
}
/**
* Clone this object and its subtree of expressions.
*
* @return void
*/
public function __clone()
{
foreach ($this->_conditions as $i => $condition) {
if ($condition instanceof ExpressionInterface) {
$this->_conditions[$i] = clone $condition;
}
}
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use Closure;
/**
* String expression with collation.
*/
class StringExpression implements ExpressionInterface
{
/**
* @var string
*/
protected $string;
/**
* @var string
*/
protected $collation;
/**
* @param string $string String value
* @param string $collation String collation
*/
public function __construct(string $string, string $collation)
{
$this->string = $string;
$this->collation = $collation;
}
/**
* Sets the string collation.
*
* @param string $collation String collation
* @return void
*/
public function setCollation(string $collation): void
{
$this->collation = $collation;
}
/**
* Returns the string collation.
*
* @return string
*/
public function getCollation(): string
{
return $this->collation;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$placeholder = $binder->placeholder('c');
$binder->bind($placeholder, $this->string, 'string');
return $placeholder . ' COLLATE ' . $this->collation;
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
return $this;
}
}
+231
View File
@@ -0,0 +1,231 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use Closure;
use InvalidArgumentException;
/**
* This expression represents SQL fragments that are used for comparing one tuple
* to another, one tuple to a set of other tuples or one tuple to an expression
*/
class TupleComparison extends ComparisonExpression
{
/**
* The type to be used for casting the value to a database representation
*
* @var array<string|null>
* @psalm-suppress NonInvariantDocblockPropertyType
*/
protected $_type;
/**
* Constructor
*
* @param \Cake\Database\ExpressionInterface|array|string $fields the fields to use to form a tuple
* @param \Cake\Database\ExpressionInterface|array $values the values to use to form a tuple
* @param array<string|null> $types the types names to use for casting each of the values, only
* one type per position in the value array in needed
* @param string $conjunction the operator used for comparing field and value
*/
public function __construct($fields, $values, array $types = [], string $conjunction = '=')
{
$this->_type = $types;
$this->setField($fields);
$this->_operator = $conjunction;
$this->setValue($values);
}
/**
* Returns the type to be used for casting the value to a database representation
*
* @return array<string|null>
*/
public function getType(): array
{
return $this->_type;
}
/**
* Sets the value
*
* @param mixed $value The value to compare
* @return void
*/
public function setValue($value): void
{
if ($this->isMulti()) {
if (is_array($value) && !is_array(current($value))) {
throw new InvalidArgumentException(
'Multi-tuple comparisons require a multi-tuple value, single-tuple given.'
);
}
} else {
if (is_array($value) && is_array(current($value))) {
throw new InvalidArgumentException(
'Single-tuple comparisons require a single-tuple value, multi-tuple given.'
);
}
}
$this->_value = $value;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$template = '(%s) %s (%s)';
$fields = [];
$originalFields = $this->getField();
if (!is_array($originalFields)) {
$originalFields = [$originalFields];
}
foreach ($originalFields as $field) {
$fields[] = $field instanceof ExpressionInterface ? $field->sql($binder) : $field;
}
$values = $this->_stringifyValues($binder);
$field = implode(', ', $fields);
return sprintf($template, $field, $this->_operator, $values);
}
/**
* Returns a string with the values as placeholders in a string to be used
* for the SQL version of this expression
*
* @param \Cake\Database\ValueBinder $binder The value binder to convert expressions with.
* @return string
*/
protected function _stringifyValues(ValueBinder $binder): string
{
$values = [];
$parts = $this->getValue();
if ($parts instanceof ExpressionInterface) {
return $parts->sql($binder);
}
foreach ($parts as $i => $value) {
if ($value instanceof ExpressionInterface) {
$values[] = $value->sql($binder);
continue;
}
$type = $this->_type;
$isMultiOperation = $this->isMulti();
if (empty($type)) {
$type = null;
}
if ($isMultiOperation) {
$bound = [];
foreach ($value as $k => $val) {
/** @var string $valType */
$valType = $type && isset($type[$k]) ? $type[$k] : $type;
$bound[] = $this->_bindValue($val, $binder, $valType);
}
$values[] = sprintf('(%s)', implode(',', $bound));
continue;
}
/** @var string $valType */
$valType = $type && isset($type[$i]) ? $type[$i] : $type;
$values[] = $this->_bindValue($value, $binder, $valType);
}
return implode(', ', $values);
}
/**
* @inheritDoc
*/
protected function _bindValue($value, ValueBinder $binder, ?string $type = null): string
{
$placeholder = $binder->placeholder('tuple');
$binder->bind($placeholder, $value, $type);
return $placeholder;
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
/** @var array<string> $fields */
$fields = $this->getField();
foreach ($fields as $field) {
$this->_traverseValue($field, $callback);
}
$value = $this->getValue();
if ($value instanceof ExpressionInterface) {
$callback($value);
$value->traverse($callback);
return $this;
}
foreach ($value as $val) {
if ($this->isMulti()) {
foreach ($val as $v) {
$this->_traverseValue($v, $callback);
}
} else {
$this->_traverseValue($val, $callback);
}
}
return $this;
}
/**
* Conditionally executes the callback for the passed value if
* it is an ExpressionInterface
*
* @param mixed $value The value to traverse
* @param \Closure $callback The callable to use when traversing
* @return void
*/
protected function _traverseValue($value, Closure $callback): void
{
if ($value instanceof ExpressionInterface) {
$callback($value);
$value->traverse($callback);
}
}
/**
* Determines if each of the values in this expressions is a tuple in
* itself
*
* @return bool
*/
public function isMulti(): bool
{
return in_array(strtolower($this->_operator), ['in', 'not in']);
}
}
+118
View File
@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use Closure;
/**
* An expression object that represents an expression with only a single operand.
*/
class UnaryExpression implements ExpressionInterface
{
/**
* Indicates that the operation is in pre-order
*
* @var int
*/
public const PREFIX = 0;
/**
* Indicates that the operation is in post-order
*
* @var int
*/
public const POSTFIX = 1;
/**
* The operator this unary expression represents
*
* @var string
*/
protected $_operator;
/**
* Holds the value which the unary expression operates
*
* @var mixed
*/
protected $_value;
/**
* Where to place the operator
*
* @var int
*/
protected $position;
/**
* Constructor
*
* @param string $operator The operator to used for the expression
* @param mixed $value the value to use as the operand for the expression
* @param int $position either UnaryExpression::PREFIX or UnaryExpression::POSTFIX
*/
public function __construct(string $operator, $value, $position = self::PREFIX)
{
$this->_operator = $operator;
$this->_value = $value;
$this->position = $position;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
$operand = $this->_value;
if ($operand instanceof ExpressionInterface) {
$operand = $operand->sql($binder);
}
if ($this->position === self::POSTFIX) {
return '(' . $operand . ') ' . $this->_operator;
}
return $this->_operator . ' (' . $operand . ')';
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
if ($this->_value instanceof ExpressionInterface) {
$callback($this->_value);
$this->_value->traverse($callback);
}
return $this;
}
/**
* Perform a deep clone of the inner expression.
*
* @return void
*/
public function __clone()
{
if ($this->_value instanceof ExpressionInterface) {
$this->_value = clone $this->_value;
}
}
}
+325
View File
@@ -0,0 +1,325 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;
use Cake\Database\Exception\DatabaseException;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\Type\ExpressionTypeCasterTrait;
use Cake\Database\TypeMap;
use Cake\Database\TypeMapTrait;
use Cake\Database\ValueBinder;
use Closure;
/**
* An expression object to contain values being inserted.
*
* Helps generate SQL with the correct number of placeholders and bind
* values correctly into the statement.
*/
class ValuesExpression implements ExpressionInterface
{
use ExpressionTypeCasterTrait;
use TypeMapTrait;
/**
* Array of values to insert.
*
* @var array
*/
protected $_values = [];
/**
* List of columns to ensure are part of the insert.
*
* @var array
*/
protected $_columns = [];
/**
* The Query object to use as a values expression
*
* @var \Cake\Database\Query|null
*/
protected $_query;
/**
* Whether values have been casted to expressions
* already.
*
* @var bool
*/
protected $_castedExpressions = false;
/**
* Constructor
*
* @param array $columns The list of columns that are going to be part of the values.
* @param \Cake\Database\TypeMap $typeMap A dictionary of column -> type names
*/
public function __construct(array $columns, TypeMap $typeMap)
{
$this->_columns = $columns;
$this->setTypeMap($typeMap);
}
/**
* Add a row of data to be inserted.
*
* @param \Cake\Database\Query|array $values Array of data to append into the insert, or
* a query for doing INSERT INTO .. SELECT style commands
* @return void
* @throws \Cake\Database\Exception\DatabaseException When mixing array + Query data types.
*/
public function add($values): void
{
if (
(
count($this->_values) &&
$values instanceof Query
) ||
(
$this->_query &&
is_array($values)
)
) {
throw new DatabaseException(
'You cannot mix subqueries and array values in inserts.'
);
}
if ($values instanceof Query) {
$this->setQuery($values);
return;
}
$this->_values[] = $values;
$this->_castedExpressions = false;
}
/**
* Sets the columns to be inserted.
*
* @param array $columns Array with columns to be inserted.
* @return $this
*/
public function setColumns(array $columns)
{
$this->_columns = $columns;
$this->_castedExpressions = false;
return $this;
}
/**
* Gets the columns to be inserted.
*
* @return array
*/
public function getColumns(): array
{
return $this->_columns;
}
/**
* Get the bare column names.
*
* Because column names could be identifier quoted, we
* need to strip the identifiers off of the columns.
*
* @return array
*/
protected function _columnNames(): array
{
$columns = [];
foreach ($this->_columns as $col) {
if (is_string($col)) {
$col = trim($col, '`[]"');
}
$columns[] = $col;
}
return $columns;
}
/**
* Sets the values to be inserted.
*
* @param array $values Array with values to be inserted.
* @return $this
*/
public function setValues(array $values)
{
$this->_values = $values;
$this->_castedExpressions = false;
return $this;
}
/**
* Gets the values to be inserted.
*
* @return array
*/
public function getValues(): array
{
if (!$this->_castedExpressions) {
$this->_processExpressions();
}
return $this->_values;
}
/**
* Sets the query object to be used as the values expression to be evaluated
* to insert records in the table.
*
* @param \Cake\Database\Query $query The query to set
* @return $this
*/
public function setQuery(Query $query)
{
$this->_query = $query;
return $this;
}
/**
* Gets the query object to be used as the values expression to be evaluated
* to insert records in the table.
*
* @return \Cake\Database\Query|null
*/
public function getQuery(): ?Query
{
return $this->_query;
}
/**
* @inheritDoc
*/
public function sql(ValueBinder $binder): string
{
if (empty($this->_values) && empty($this->_query)) {
return '';
}
if (!$this->_castedExpressions) {
$this->_processExpressions();
}
$columns = $this->_columnNames();
$defaults = array_fill_keys($columns, null);
$placeholders = [];
$types = [];
$typeMap = $this->getTypeMap();
foreach ($defaults as $col => $v) {
$types[$col] = $typeMap->type($col);
}
foreach ($this->_values as $row) {
$row += $defaults;
$rowPlaceholders = [];
foreach ($columns as $column) {
$value = $row[$column];
if ($value instanceof ExpressionInterface) {
$rowPlaceholders[] = '(' . $value->sql($binder) . ')';
continue;
}
$placeholder = $binder->placeholder('c');
$rowPlaceholders[] = $placeholder;
$binder->bind($placeholder, $value, $types[$column]);
}
$placeholders[] = implode(', ', $rowPlaceholders);
}
$query = $this->getQuery();
if ($query) {
return ' ' . $query->sql($binder);
}
return sprintf(' VALUES (%s)', implode('), (', $placeholders));
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
if ($this->_query) {
return $this;
}
if (!$this->_castedExpressions) {
$this->_processExpressions();
}
foreach ($this->_values as $v) {
if ($v instanceof ExpressionInterface) {
$v->traverse($callback);
}
if (!is_array($v)) {
continue;
}
foreach ($v as $field) {
if ($field instanceof ExpressionInterface) {
$callback($field);
$field->traverse($callback);
}
}
}
return $this;
}
/**
* Converts values that need to be casted to expressions
*
* @return void
*/
protected function _processExpressions(): void
{
$types = [];
$typeMap = $this->getTypeMap();
$columns = $this->_columnNames();
foreach ($columns as $c) {
if (!is_string($c) && !is_int($c)) {
continue;
}
$types[$c] = $typeMap->type($c);
}
$types = $this->_requiresToExpressionCasting($types);
if (empty($types)) {
return;
}
foreach ($this->_values as $row => $values) {
foreach ($types as $col => $type) {
/** @var \Cake\Database\Type\ExpressionTypeInterface $type */
$this->_values[$row][$col] = $type->toExpression($values[$col]);
}
}
$this->_castedExpressions = true;
}
}
@@ -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.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;
use function Cake\Core\getTypeName;
/**
* 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 $validClauseNames = [
'when',
'then',
];
/**
* The type map to use when using an array of conditions for the
* `WHEN` value.
*
* @var \Cake\Database\TypeMap
*/
protected $_typeMap;
/**
* Then `WHEN` value.
*
* @var \Cake\Database\ExpressionInterface|object|scalar|null
*/
protected $when = null;
/**
* The `WHEN` value type.
*
* @var array|string|null
*/
protected $whenType = null;
/**
* The `THEN` value.
*
* @var \Cake\Database\ExpressionInterface|object|scalar|null
*/
protected $then = null;
/**
* Whether the `THEN` value has been defined, eg whether `then()`
* has been invoked.
*
* @var bool
*/
protected $hasThenBeenDefined = false;
/**
* The `THEN` result type.
*
* @var string|null
*/
protected $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)
{
if ($typeMap === null) {
$typeMap = new TypeMap();
}
$this->_typeMap = $typeMap;
}
/**
* Sets the `WHEN` value.
*
* @param \Cake\Database\ExpressionInterface|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 \InvalidArgumentException In case the `$when` argument is neither a non-empty array, nor a scalar value,
* an object, or an instance of `\Cake\Database\ExpressionInterface`.
* @throws \InvalidArgumentException In case the `$type` argument is neither an array, a string, nor null.
* @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($when, $type = null)
{
if (
!(is_array($when) && !empty($when)) &&
!is_scalar($when) &&
!is_object($when)
) {
throw new InvalidArgumentException(sprintf(
'The `$when` argument must be either a non-empty array, a scalar value, an object, ' .
'or an instance of `\%s`, `%s` given.',
ExpressionInterface::class,
is_array($when) ? '[]' : getTypeName($when)
));
}
if (
$type !== null &&
!is_array($type) &&
!is_string($type)
) {
throw new InvalidArgumentException(sprintf(
'The `$type` argument must be either an array, a string, or `null`, `%s` given.',
getTypeName($type)
));
}
if (is_array($when)) {
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.',
getTypeName($type)
));
}
// avoid dirtying the type map for possible consecutive `when()` calls
$typeMap = clone $this->_typeMap;
if (
is_array($type) &&
count($type) > 0
) {
$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.',
getTypeName($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($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,
getTypeName($result)
));
}
$this->then = $result;
if ($type === null) {
$type = $this->inferType($result);
}
$this->thenType = $type;
$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)
{
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;
}
}
}
+335
View File
@@ -0,0 +1,335 @@
<?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;
/**
* This represents a SQL window expression used by aggregate and window functions.
*/
class WindowExpression implements ExpressionInterface, WindowInterface
{
/**
* @var \Cake\Database\Expression\IdentifierExpression
*/
protected $name;
/**
* @var array<\Cake\Database\ExpressionInterface>
*/
protected $partitions = [];
/**
* @var \Cake\Database\Expression\OrderByExpression|null
*/
protected $order;
/**
* @var array|null
*/
protected $frame;
/**
* @var string|null
*/
protected $exclusion;
/**
* @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($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($fields)
{
if (!$fields) {
return $this;
}
if ($this->order === null) {
$this->order = new OrderByExpression();
}
if ($fields instanceof Closure) {
$fields = $fields(new QueryExpression([], [], ''));
}
$this->order->add($fields);
return $this;
}
/**
* @inheritDoc
*/
public function range($start, $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,
$startOffset,
string $startDirection,
$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, $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;
}
}
}
+163
View File
@@ -0,0 +1,163 @@
<?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;
/**
* 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($partitions);
/**
* Adds one or more order clauses to the window.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array<\Cake\Database\ExpressionInterface|string>|string $fields Order expressions
* @return $this
*/
public function order($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($start, $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.
* @psalm-param self::RANGE|self::ROWS|self::GROUPS $type
* @psalm-param self::PRECEDING|self::FOLLOWING $startDirection
* @psalm-param self::PRECEDING|self::FOLLOWING $endDirection
*/
public function frame(
string $type,
$startOffset,
string $startDirection,
$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 callable
* passing as first parameter the instance of the expression currently
* being iterated.
*
* @param \Closure $callback The callable to apply to all nodes.
* @return $this
*/
public function traverse(Closure $callback);
}
+140
View File
@@ -0,0 +1,140 @@
<?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;
/**
* A callable 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.
*/
class FieldTypeConverter
{
/**
* An array containing the name of the fields and the Type objects
* each should use when converting them.
*
* @var array<\Cake\Database\TypeInterface>
*/
protected $_typeMap;
/**
* An array containing the name of the fields and the Type objects
* each should use when converting them using batching.
*
* @var array<string, array>
*/
protected $batchingTypeMap;
/**
* An array containing all the types registered in the Type system
* at the moment this object is created. Used so that the types list
* is not fetched on each single row of the results.
*
* @var array<\Cake\Database\TypeInterface|\Cake\Database\Type\BatchCastingInterface>
*/
protected $types;
/**
* The driver object to be used in the type conversion
*
* @var \Cake\Database\DriverInterface
*/
protected $_driver;
/**
* Builds the type map
*
* @param \Cake\Database\TypeMap $typeMap Contains the types to use for converting results
* @param \Cake\Database\DriverInterface $driver The driver to use for the type conversion
*/
public function __construct(TypeMap $typeMap, DriverInterface $driver)
{
$this->_driver = $driver;
$map = $typeMap->toArray();
$types = TypeFactory::buildAll();
$simpleMap = $batchingMap = [];
$simpleResult = $batchingResult = [];
foreach ($types as $k => $type) {
if ($type instanceof OptionalConvertInterface && !$type->requiresToPhpCast()) {
continue;
}
if ($type instanceof BatchCastingInterface) {
$batchingMap[$k] = $type;
continue;
}
$simpleMap[$k] = $type;
}
foreach ($map as $field => $type) {
if (isset($simpleMap[$type])) {
$simpleResult[$field] = $simpleMap[$type];
continue;
}
if (isset($batchingMap[$type])) {
$batchingResult[$type][] = $field;
}
}
// Using batching when there is only a couple for the type is actually slower,
// so, let's check for that case here.
foreach ($batchingResult as $type => $fields) {
if (count($fields) > 2) {
continue;
}
foreach ($fields as $f) {
$simpleResult[$f] = $batchingMap[$type];
}
unset($batchingResult[$type]);
}
$this->types = $types;
$this->_typeMap = $simpleResult;
$this->batchingTypeMap = $batchingResult;
}
/**
* Converts each of the fields in the array that are present in the type map
* using the corresponding Type class.
*
* @param array $row The array with the fields to be casted
* @return array<string, mixed>
*/
public function __invoke(array $row): array
{
if (!empty($this->_typeMap)) {
foreach ($this->_typeMap as $field => $type) {
$row[$field] = $type->toPHP($row[$field], $this->_driver);
}
}
if (!empty($this->batchingTypeMap)) {
foreach ($this->batchingTypeMap as $t => $fields) {
/** @psalm-suppress PossiblyUndefinedMethod */
$row = $this->types[$t]->manyToPHP($row, $fields, $this->_driver);
}
}
return $row;
}
}
+376
View File
@@ -0,0 +1,376 @@
<?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;
use function Cake\Core\deprecationWarning;
/**
* 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($expression, $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($expression, $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($expression, $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($expression, $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($expression, $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 $type The SQL data type
* @return \Cake\Database\Expression\FunctionExpression
*/
public function cast($field, string $type = ''): FunctionExpression
{
if (is_array($field)) {
deprecationWarning(
'Build cast function by FunctionsBuilder::cast(array $args) is deprecated. ' .
'Use FunctionsBuilder::cast($field, string $type) instead.'
);
return new FunctionExpression('CAST', $field);
}
if (empty($type)) {
throw new InvalidArgumentException('The `$type` in a cast cannot be empty.');
}
$expression = new FunctionExpression('CAST', $this->toLiteralParam($field));
$expression->setConjunction(' AS')->add([$type => 'literal']);
return $expression;
}
/**
* 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, $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, $expression, array $types = []): FunctionExpression
{
$expression = new FunctionExpression('EXTRACT', $this->toLiteralParam($expression), $types, 'integer');
$expression->setConjunction(' FROM')->add([$part => 'literal'], [], true);
return $expression;
}
/**
* 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($expression, $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');
$expression->setConjunction(', INTERVAL')->add([$interval => 'literal']);
return $expression;
}
/**
* 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($expression, $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($expression, $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
{
if ($type === 'datetime') {
return new FunctionExpression('NOW', [], [], 'datetime');
}
if ($type === 'date') {
return new FunctionExpression('CURRENT_DATE', [], [], 'date');
}
if ($type === 'time') {
return new FunctionExpression('CURRENT_TIME', [], [], 'time');
}
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 $type The output type of the lag expression. Defaults to float.
* @return \Cake\Database\Expression\AggregateExpression
*/
public function lag($expression, int $offset, $default = null, $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 $type The output type of the lead expression. Defaults to float.
* @return \Cake\Database\Expression\AggregateExpression
*/
public function lead($expression, int $offset, $default = null, $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();
}
/**
* 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')
{
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($expression)
{
if (is_string($expression)) {
return [$expression => 'literal'];
}
return [$expression];
}
}
+269
View File
@@ -0,0 +1,269 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database;
use Cake\Database\Expression\FieldInterface;
use Cake\Database\Expression\IdentifierExpression;
use Cake\Database\Expression\OrderByExpression;
/**
* Contains all the logic related to quoting identifiers in a Query object
*
* @internal
*/
class IdentifierQuoter
{
/**
* The driver instance used to do the identifier quoting
*
* @var \Cake\Database\Driver
*/
protected $_driver;
/**
* Constructor
*
* @param \Cake\Database\Driver $driver The driver instance used to do the identifier quoting
*/
public function __construct(Driver $driver)
{
$this->_driver = $driver;
}
/**
* 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);
if ($query->type() === 'insert') {
$this->_quoteInsert($query);
} elseif ($query->type() === 'update') {
$this->_quoteUpdate($query);
} else {
$this->_quoteParts($query);
}
$query->traverseExpressions([$this, 'quoteExpression']);
$query->setValueBinder($binder);
return $query;
}
/**
* 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
{
if ($expression instanceof FieldInterface) {
$this->_quoteComparison($expression);
return;
}
if ($expression instanceof OrderByExpression) {
$this->_quoteOrderBy($expression);
return;
}
if ($expression instanceof IdentifierExpression) {
$this->_quoteIdentifierExpression($expression);
return;
}
}
/**
* Quotes all identifiers in each of the clauses of a query
*
* @param \Cake\Database\Query $query The query to quote.
* @return void
*/
protected function _quoteParts(Query $query): void
{
foreach (['distinct', 'select', 'from', 'group'] as $part) {
$contents = $query->clause($part);
if (!is_array($contents)) {
continue;
}
$result = $this->_basicQuoter($contents);
if (!empty($result)) {
$query->{$part}($result, true);
}
}
$joins = $query->clause('join');
if ($joins) {
$joins = $this->_quoteJoins($joins);
$query->join($joins, [], 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) ? $value : $this->_driver->quoteIdentifier($value);
$alias = is_numeric($alias) ? $alias : $this->_driver->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 $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->_driver->quoteIdentifier($value['alias']);
$value['alias'] = $alias;
}
if (is_string($value['table'])) {
$value['table'] = $this->_driver->quoteIdentifier($value['table']);
}
$result[$alias] = $value;
}
return $result;
}
/**
* Quotes the table name and columns for an insert query
*
* @param \Cake\Database\Query $query The insert query to quote.
* @return void
*/
protected function _quoteInsert(Query $query): void
{
$insert = $query->clause('insert');
if (!isset($insert[0]) || !isset($insert[1])) {
return;
}
[$table, $columns] = $insert;
$table = $this->_driver->quoteIdentifier($table);
foreach ($columns as &$column) {
if (is_scalar($column)) {
$column = $this->_driver->quoteIdentifier((string)$column);
}
}
$query->insert($columns)->into($table);
}
/**
* Quotes the table name for an update query
*
* @param \Cake\Database\Query $query The update query to quote.
* @return void
*/
protected function _quoteUpdate(Query $query): void
{
$table = $query->clause('update')[0];
if (is_string($table)) {
$query->update($this->_driver->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->_driver->quoteIdentifier($field));
} elseif (is_array($field)) {
$quoted = [];
foreach ($field as $f) {
$quoted[] = $this->_driver->quoteIdentifier($f);
}
$expression->setField($quoted);
} elseif ($field instanceof ExpressionInterface) {
$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->_driver->quoteIdentifier($field);
return $part;
}
if (is_string($part) && strpos($part, ' ') === false) {
return $this->_driver->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->_driver->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.
+176
View File
@@ -0,0 +1,176 @@
<?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\Sqlserver;
use JsonSerializable;
/**
* 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
{
/**
* Driver executing the query
*
* @var \Cake\Database\DriverInterface|null
*/
public $driver = null;
/**
* Query string that was executed
*
* @var string
*/
public $query = '';
/**
* Number of milliseconds this query took to complete
*
* @var float
*/
public $took = 0;
/**
* Associative array with the params bound to the query string
*
* @var array
*/
public $params = [];
/**
* Number of rows affected or returned by the query execution
*
* @var int
*/
public $numRows = 0;
/**
* The exception that was thrown by the execution of this query
*
* @var \Exception|null
*/
public $error;
/**
* 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 preg_replace($keys, $params, $this->query, $limit);
}
/**
* Get the logging context data for a query.
*
* @return array<string, mixed>
*/
public function getContext(): array
{
return [
'numRows' => $this->numRows,
'took' => $this->took,
'role' => $this->driver ? $this->driver->getRole() : '',
];
}
/**
* Returns data that will be serialized as JSON
*
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
$error = $this->error;
if ($error !== null) {
$error = [
'class' => get_class($error),
'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
{
$sql = $this->query;
if (!empty($this->params)) {
$sql = $this->interpolate();
}
return $sql;
}
}
+219
View File
@@ -0,0 +1,219 @@
<?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\Core\Configure;
use Cake\Database\Exception\DatabaseException;
use Cake\Database\Statement\StatementDecorator;
use Exception;
use Psr\Log\LoggerInterface;
use function Cake\Core\deprecationWarning;
/**
* Statement decorator used to
*
* @internal
*/
class LoggingStatement extends StatementDecorator
{
/**
* Logger instance responsible for actually doing the logging task
*
* @var \Psr\Log\LoggerInterface
*/
protected $_logger;
/**
* Holds bound params
*
* @var array<array>
*/
protected $_compiledParams = [];
/**
* Query execution start time.
*
* @var float
*/
protected $startTime = 0.0;
/**
* Logged query
*
* @var \Cake\Database\Log\LoggedQuery|null
*/
protected $loggedQuery;
/**
* Wrapper for the execute function to calculate time spent
* and log the query afterwards.
*
* @param array|null $params List of values to be bound to query
* @return bool True on success, false otherwise
* @throws \Exception Re-throws any exception raised during query execution.
*/
public function execute(?array $params = null): bool
{
$this->startTime = microtime(true);
$this->loggedQuery = new LoggedQuery();
$this->loggedQuery->driver = $this->_driver;
$this->loggedQuery->params = $params ?: $this->_compiledParams;
try {
$result = parent::execute($params);
$this->loggedQuery->took = (int)round((microtime(true) - $this->startTime) * 1000, 0);
} catch (Exception $e) {
$this->loggedQuery->error = $e;
$this->_log();
if (Configure::read('Error.convertStatementToDatabaseException', false) === true) {
$code = $e->getCode();
if (!is_int($code)) {
$code = null;
}
throw new DatabaseException([
'message' => $e->getMessage(),
'queryString' => $this->queryString,
], $code, $e);
}
if (version_compare(PHP_VERSION, '8.2.0', '<')) {
deprecationWarning(
'4.4.12 - Having queryString set on exceptions is deprecated.' .
'If you are not using this attribute there is no action to take.' .
'Otherwise, enable Error.convertStatementToDatabaseException.'
);
/** @psalm-suppress UndefinedPropertyAssignment */
$e->queryString = $this->queryString;
}
throw $e;
}
if (preg_match('/^(?!SELECT)/i', $this->queryString)) {
$this->rowCount();
}
return $result;
}
/**
* @inheritDoc
*/
public function fetch($type = self::FETCH_TYPE_NUM)
{
$record = parent::fetch($type);
if ($this->loggedQuery) {
$this->rowCount();
}
return $record;
}
/**
* @inheritDoc
*/
public function fetchAll($type = self::FETCH_TYPE_NUM)
{
$results = parent::fetchAll($type);
if ($this->loggedQuery) {
$this->rowCount();
}
return $results;
}
/**
* @inheritDoc
*/
public function rowCount(): int
{
$result = parent::rowCount();
if ($this->loggedQuery) {
$this->loggedQuery->numRows = $result;
$this->_log();
}
return $result;
}
/**
* Copies the logging data to the passed LoggedQuery and sends it
* to the logging system.
*
* @return void
*/
protected function _log(): void
{
if ($this->loggedQuery === null) {
return;
}
$this->loggedQuery->query = $this->queryString;
$this->getLogger()->debug((string)$this->loggedQuery, ['query' => $this->loggedQuery]);
$this->loggedQuery = null;
}
/**
* Wrapper for bindValue function to gather each parameter to be later used
* in the logger function.
*
* @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 PDO type or name of configured Type class
* @return void
*/
public function bindValue($column, $value, $type = 'string'): void
{
parent::bindValue($column, $value, $type);
if ($type === null) {
$type = 'string';
}
if (!ctype_digit($type)) {
$value = $this->cast($value, $type)[0];
}
$this->_compiledParams[$column] = $value;
}
/**
* Sets a logger
*
* @param \Psr\Log\LoggerInterface $logger Logger object
* @return void
*/
public function setLogger(LoggerInterface $logger): void
{
$this->_logger = $logger;
}
/**
* Gets the logger object
*
* @return \Psr\Log\LoggerInterface logger instance
*/
public function getLogger(): LoggerInterface
{
return $this->_logger;
}
}
+57
View File
@@ -0,0 +1,57 @@
<?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;
/**
* 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'];
$this->_defaultConfig['connection'] = '';
parent::__construct($config);
}
/**
* @inheritDoc
*/
public function log($level, $message, array $context = [])
{
$context['scope'] = $this->scopes() ?: ['queriesLog'];
$context['connection'] = $this->getConfig('connection');
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);
}
}
+93
View File
@@ -0,0 +1,93 @@
<?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 $_quotedSelectAliases = true;
/**
* @inheritDoc
*/
protected $_templates = [
'delete' => 'DELETE',
'where' => ' WHERE %s',
'group' => ' GROUP BY %s',
'order' => ' %s',
'limit' => ' LIMIT %s',
'offset' => ' OFFSET %s',
'epilog' => ' %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($parts, $query, $binder)
{
$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));
}
}
+2516
View File
File diff suppressed because it is too large Load Diff
+222
View File
@@ -0,0 +1,222 @@
<?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;
/**
* Delete Query forward compatibility shim.
*/
class DeleteQuery extends Query
{
/**
* Type of this query (select, insert, update, delete).
*
* @var string
*/
protected $_type = 'delete';
/**
* @inheritDoc
*/
public function select($fields = [], bool $overwrite = false)
{
$this->_deprecatedMethod('select()', 'Create your query with selectQuery() instead.');
return parent::select($fields, $overwrite);
}
/**
* @inheritDoc
*/
public function distinct($on = [], $overwrite = false)
{
$this->_deprecatedMethod('distint()');
return parent::distinct($on, $overwrite);
}
/**
* @inheritDoc
*/
public function modifier($modifiers, $overwrite = false)
{
$this->_deprecatedMethod('modifier()');
return parent::modifier($modifiers, $overwrite);
}
/**
* @inheritDoc
*/
public function order($fields, $overwrite = false)
{
$this->_deprecatedMethod('order()');
return parent::order($fields, $overwrite);
}
/**
* @inheritDoc
*/
public function orderAsc($field, $overwrite = false)
{
$this->_deprecatedMethod('orderAsc()');
return parent::orderAsc($field, $overwrite);
}
/**
* @inheritDoc
*/
public function orderDesc($field, $overwrite = false)
{
$this->_deprecatedMethod('orderDesc()');
return parent::orderDesc($field, $overwrite);
}
/**
* @inheritDoc
*/
public function group($fields, $overwrite = false)
{
$this->_deprecatedMethod('group()');
return parent::group($fields, $overwrite);
}
/**
* @inheritDoc
*/
public function having($conditions = null, $types = [], $overwrite = false)
{
$this->_deprecatedMethod('having()');
return parent::having($conditions, $types, $overwrite);
}
/**
* @inheritDoc
*/
public function andHaving($conditions, $types = [])
{
$this->_deprecatedMethod('andHaving()');
return parent::andHaving($conditions, $types);
}
/**
* @inheritDoc
*/
public function page(int $num, ?int $limit = null)
{
$this->_deprecatedMethod('page()');
return parent::page($num, $limit);
}
/**
* @inheritDoc
*/
public function limit($limit)
{
$this->_deprecatedMethod('limit()');
return parent::limit($limit);
}
/**
* @inheritDoc
*/
public function offset($offset)
{
$this->_deprecatedMethod('offset()');
return parent::offset($offset);
}
/**
* @inheritDoc
*/
public function union($query, $overwrite = false)
{
$this->_deprecatedMethod('union()');
return parent::union($query, $overwrite);
}
/**
* @inheritDoc
*/
public function unionAll($query, $overwrite = false)
{
$this->_deprecatedMethod('unionAll()');
return parent::unionAll($query, $overwrite);
}
/**
* @inheritDoc
*/
public function insert(array $columns, array $types = [])
{
$this->_deprecatedMethod('insert()', 'Create your query with insertQuery() instead.');
return parent::insert($columns, $types);
}
/**
* @inheritDoc
*/
public function into(string $table)
{
$this->_deprecatedMethod('into()');
return parent::into($table);
}
/**
* @inheritDoc
*/
public function values($data)
{
$this->_deprecatedMethod('values()');
return parent::values($data);
}
/**
* @inheritDoc
*/
public function update($table)
{
$this->_deprecatedMethod('update()', 'Create your query with updateQuery() instead.');
return parent::update($table);
}
/**
* @inheritDoc
*/
public function set($key, $value = null, $types = [])
{
$this->_deprecatedMethod('set()');
return parent::set($key, $value, $types);
}
}
+322
View File
@@ -0,0 +1,322 @@
<?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;
/**
* Insert Query forward compatibility shim.
*/
class InsertQuery extends Query
{
/**
* Type of this query (select, insert, update, delete).
*
* @var string
*/
protected $_type = 'insert';
/**
* @inheritDoc
*/
public function delete(?string $table = null)
{
$this->_deprecatedMethod('delete()', 'Create your query with deleteQuery() instead.');
return parent::delete($table);
}
/**
* @inheritDoc
*/
public function select($fields = [], bool $overwrite = false)
{
$this->_deprecatedMethod('select()', 'Create your query with selectQuery() instead.');
return parent::select($fields, $overwrite);
}
/**
* @inheritDoc
*/
public function distinct($on = [], $overwrite = false)
{
$this->_deprecatedMethod('distinct()');
return parent::distinct($on, $overwrite);
}
/**
* @inheritDoc
*/
public function modifier($modifiers, $overwrite = false)
{
$this->_deprecatedMethod('modifier()');
return parent::modifier($modifiers, $overwrite);
}
/**
* @inheritDoc
*/
public function join($tables, $types = [], $overwrite = false)
{
$this->_deprecatedMethod('join()');
return parent::join($tables, $types, $overwrite);
}
/**
* @inheritDoc
*/
public function removeJoin(string $name)
{
$this->_deprecatedMethod('removeJoin()');
return parent::removeJoin($name);
}
/**
* @inheritDoc
*/
public function leftJoin($table, $conditions = [], $types = [])
{
$this->_deprecatedMethod('leftJoin()');
return parent::leftJoin($table, $conditions, $types);
}
/**
* @inheritDoc
*/
public function rightJoin($table, $conditions = [], $types = [])
{
$this->_deprecatedMethod('rightJoin()');
return parent::rightJoin($table, $conditions, $types);
}
/**
* @inheritDoc
*/
public function innerJoin($table, $conditions = [], $types = [])
{
$this->_deprecatedMethod('innerJoin()');
return parent::innerJoin($table, $conditions, $types);
}
/**
* @inheritDoc
*/
public function group($fields, $overwrite = false)
{
$this->_deprecatedMethod('group()');
return parent::group($fields, $overwrite);
}
/**
* @inheritDoc
*/
public function having($conditions = null, $types = [], $overwrite = false)
{
$this->_deprecatedMethod('having()');
return parent::having($conditions, $types, $overwrite);
}
/**
* @inheritDoc
*/
public function andHaving($conditions, $types = [])
{
$this->_deprecatedMethod('andHaving()');
return parent::andHaving($conditions, $types);
}
/**
* @inheritDoc
*/
public function page(int $num, ?int $limit = null)
{
$this->_deprecatedMethod('page()');
return parent::page($num, $limit);
}
/**
* @inheritDoc
*/
public function offset($offset)
{
$this->_deprecatedMethod('offset()');
return parent::offset($offset);
}
/**
* @inheritDoc
*/
public function union($query, $overwrite = false)
{
$this->_deprecatedMethod('union()');
return parent::union($query, $overwrite);
}
/**
* @inheritDoc
*/
public function unionAll($query, $overwrite = false)
{
$this->_deprecatedMethod('union()');
return parent::unionAll($query, $overwrite);
}
/**
* @inheritDoc
*/
public function from($tables = [], $overwrite = false)
{
$this->_deprecatedMethod('from()', 'Use into() instead.');
return parent::from($tables, $overwrite);
}
/**
* @inheritDoc
*/
public function update($table)
{
$this->_deprecatedMethod('update()', 'Create your query with updateQuery() instead.');
return parent::update($table);
}
/**
* @inheritDoc
*/
public function set($key, $value = null, $types = [])
{
$this->_deprecatedMethod('set()');
return parent::set($key, $value, $types);
}
/**
* @inheritDoc
*/
public function where($conditions = null, array $types = [], bool $overwrite = false)
{
$this->_deprecatedMethod('where()');
return parent::where($conditions, $types, $overwrite);
}
/**
* @inheritDoc
*/
public function whereNotNull($fields)
{
$this->_deprecatedMethod('whereNotNull()');
return parent::whereNotNull($fields);
}
/**
* @inheritDoc
*/
public function whereNull($fields)
{
$this->_deprecatedMethod('whereNull()');
return parent::whereNull($fields);
}
/**
* @inheritDoc
*/
public function whereInList(string $field, array $values, array $options = [])
{
$this->_deprecatedMethod('whereInList()');
return parent::whereInList($field, $values, $options);
}
/**
* @inheritDoc
*/
public function whereNotInList(string $field, array $values, array $options = [])
{
$this->_deprecatedMethod('whereNotInList()');
return parent::whereNotInList($field, $values, $options);
}
/**
* @inheritDoc
*/
public function whereNotInListOrNull(string $field, array $values, array $options = [])
{
$this->_deprecatedMethod('whereNotInListOrNull()');
return parent::whereNotInListOrNull($field, $values, $options);
}
/**
* @inheritDoc
*/
public function andWhere($conditions, array $types = [])
{
$this->_deprecatedMethod('andWhere()');
return parent::andWhere($conditions, $types);
}
/**
* @inheritDoc
*/
public function order($fields, $overwrite = false)
{
$this->_deprecatedMethod('order()');
return parent::order($fields, $overwrite);
}
/**
* @inheritDoc
*/
public function orderAsc($field, $overwrite = false)
{
$this->_deprecatedMethod('orderAsc()');
return parent::orderAsc($field, $overwrite);
}
/**
* @inheritDoc
*/
public function orderDesc($field, $overwrite = false)
{
$this->_deprecatedMethod('orderDesc()');
return parent::orderDesc($field, $overwrite);
}
}
+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\Connection;
use Cake\Database\Query;
/**
* Select Query forward compatibility shim.
*/
class SelectQuery extends Query
{
/**
* Type of this query (select, insert, update, delete).
*
* @var string
*/
protected $_type = 'select';
/**
* @inheritDoc
*/
public function delete(?string $table = null)
{
$this->_deprecatedMethod('delete()', 'Create your query with deleteQuery() instead.');
return parent::delete($table);
}
/**
* @inheritDoc
*/
public function insert(array $columns, array $types = [])
{
$this->_deprecatedMethod('insert()', 'Create your query with insertQuery() instead.');
return parent::insert($columns, $types);
}
/**
* @inheritDoc
*/
public function into(string $table)
{
$this->_deprecatedMethod('into()', 'Use from() instead.');
return parent::into($table);
}
/**
* @inheritDoc
*/
public function values($data)
{
$this->_deprecatedMethod('values()');
return parent::values($data);
}
/**
* @inheritDoc
*/
public function update($table)
{
$this->_deprecatedMethod('update()', 'Create your query with updateQuery() instead.');
return parent::update($table);
}
/**
* @inheritDoc
*/
public function set($key, $value = null, $types = [])
{
$this->_deprecatedMethod('set()');
return parent::set($key, $value, $types);
}
/**
* 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);
}
}
+182
View File
@@ -0,0 +1,182 @@
<?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;
/**
* Update Query forward compatibility shim.
*/
class UpdateQuery extends Query
{
/**
* Type of this query (select, insert, update, delete).
*
* @var string
*/
protected $_type = 'update';
/**
* @inheritDoc
*/
public function delete(?string $table = null)
{
$this->_deprecatedMethod('delete()', 'Create your query with deleteQuery() instead.');
return parent::delete($table);
}
/**
* @inheritDoc
*/
public function select($fields = [], bool $overwrite = false)
{
$this->_deprecatedMethod('select()', 'Create your query with selectQuery() instead.');
return parent::select($fields, $overwrite);
}
/**
* @inheritDoc
*/
public function distinct($on = [], $overwrite = false)
{
$this->_deprecatedMethod('distinct()');
return parent::distinct($on, $overwrite);
}
/**
* @inheritDoc
*/
public function modifier($modifiers, $overwrite = false)
{
$this->_deprecatedMethod('modifier()');
return parent::modifier($modifiers, $overwrite);
}
/**
* @inheritDoc
*/
public function group($fields, $overwrite = false)
{
$this->_deprecatedMethod('group()');
return parent::group($fields, $overwrite);
}
/**
* @inheritDoc
*/
public function having($conditions = null, $types = [], $overwrite = false)
{
$this->_deprecatedMethod('having()');
return parent::having($conditions, $types, $overwrite);
}
/**
* @inheritDoc
*/
public function andHaving($conditions, $types = [])
{
$this->_deprecatedMethod('andHaving()');
return parent::andHaving($conditions, $types);
}
/**
* @inheritDoc
*/
public function page(int $num, ?int $limit = null)
{
$this->_deprecatedMethod('page()');
return parent::page($num, $limit);
}
/**
* @inheritDoc
*/
public function offset($offset)
{
$this->_deprecatedMethod('offset()');
return parent::offset($offset);
}
/**
* @inheritDoc
*/
public function union($query, $overwrite = false)
{
$this->_deprecatedMethod('union()');
return parent::union($query, $overwrite);
}
/**
* @inheritDoc
*/
public function unionAll($query, $overwrite = false)
{
$this->_deprecatedMethod('union()');
return parent::unionAll($query, $overwrite);
}
/**
* @inheritDoc
*/
public function insert(array $columns, array $types = [])
{
$this->_deprecatedMethod('insert()', 'Create your query with insertQuery() instead.');
return parent::insert($columns, $types);
}
/**
* @inheritDoc
*/
public function into(string $table)
{
$this->_deprecatedMethod('into()', 'Use update() instead.');
return parent::into($table);
}
/**
* @inheritDoc
*/
public function values($data)
{
$this->_deprecatedMethod('values()');
return parent::values($data);
}
/**
* @inheritDoc
*/
public function from($tables = [], $overwrite = false)
{
$this->_deprecatedMethod('from()', 'Use update() instead.');
return parent::from($tables, $overwrite);
}
}
+462
View File
@@ -0,0 +1,462 @@
<?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 $_templates = [
'delete' => 'DELETE',
'where' => ' WHERE %s',
'group' => ' GROUP BY %s ',
'having' => ' HAVING %s ',
'order' => ' %s',
'limit' => ' LIMIT %s',
'offset' => ' OFFSET %s',
'epilog' => ' %s',
];
/**
* The list of query clauses to traverse for generating a SELECT statement
*
* @var array<string>
*/
protected $_selectParts = [
'with', 'select', 'from', 'join', 'where', 'group', 'having', 'window', 'order',
'limit', 'offset', 'union', 'epilog',
];
/**
* The list of query clauses to traverse for generating an UPDATE statement
*
* @var array<string>
*/
protected $_updateParts = ['with', 'update', 'set', 'where', 'epilog'];
/**
* The list of query clauses to traverse for generating a DELETE statement
*
* @var array<string>
*/
protected $_deleteParts = ['with', 'delete', 'modifier', 'from', 'where', 'epilog'];
/**
* The list of query clauses to traverse for generating an INSERT statement
*
* @var array<string>
*/
protected $_insertParts = ['with', 'insert', 'values', 'epilog'];
/**
* Indicate whether this query dialect supports ordered unions.
*
* Overridden in subclasses.
*
* @var bool
*/
protected $_orderedUnion = true;
/**
* Indicate whether aliases in SELECT clause need to be always quoted.
*
* @var bool
*/
protected $_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.
if ($query->getValueBinder() !== $binder) {
foreach ($query->getValueBinder()->bindings() as $binding) {
$placeholder = ':' . $binding['placeholder'];
if (preg_match('/' . $placeholder . '(?:\W|$)/', $sql) > 0) {
$binder->bind($placeholder, $binding['value'], $binding['type']);
}
}
}
return $sql;
}
/**
* Returns a callable object 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) {
if (
$part === null ||
(is_array($part) && empty($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 $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
{
$select = 'SELECT%s %s%s';
if ($this->_orderedUnion && $query->clause('union')) {
$select = '(SELECT%s %s%s';
}
$distinct = $query->clause('distinct');
$modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder);
$driver = $query->getConnection()->getDriver($query->getConnectionRole());
$quoteIdentifiers = $driver->isAutoQuotingEnabled() || $this->_quotedSelectAliases;
$normalized = [];
$parts = $this->_stringifyExpressions($parts, $binder);
foreach ($parts as $k => $p) {
if (!is_numeric($k)) {
$p = $p . ' AS ';
if ($quoteIdentifiers) {
$p .= $driver->quoteIdentifier($k);
} else {
$p .= $k;
}
}
$normalized[] = $p;
}
if ($distinct === true) {
$distinct = 'DISTINCT ';
}
if (is_array($distinct)) {
$distinct = $this->_stringifyExpressions($distinct, $binder);
$distinct = sprintf('DISTINCT ON (%s) ', implode(', ', $distinct));
}
return sprintf($select, $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) {
$windows[] = $window['name']->sql($binder) . ' AS (' . $window['window']->sql($binder) . ')';
}
return ' WINDOW ' . implode(', ', $windows);
}
/**
* Helper function to generate SQL for SET expressions.
*
* @param array $parts List of keys & 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 ($part[0] === '(') {
$part = substr($part, 1, -1);
}
$set[] = $part;
}
return ' SET ' . implode('', $set);
}
/**
* 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
{
$parts = array_map(function ($p) use ($binder) {
$p['query'] = $p['query']->sql($binder);
$p['query'] = $p['query'][0] === '(' ? trim($p['query'], '()') : $p['query'];
$prefix = $p['all'] ? 'ALL ' : '';
if ($this->_orderedUnion) {
return "{$prefix}({$p['query']})";
}
return $prefix . $p['query'];
}, $parts);
if ($this->_orderedUnion) {
return sprintf(")\nUNION %s", implode("\nUNION ", $parts));
}
return sprintf("\nUNION %s", implode("\nUNION ", $parts));
}
/**
* 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);
$modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder);
return sprintf('INSERT%s INTO %s (%s)', $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);
$modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder);
return sprintf('UPDATE%s %s', $modifiers, implode(',', $table));
}
/**
* 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;
}
}
+358
View File
@@ -0,0 +1,358 @@
[![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 the
`newQuery()` for using a query builder.
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('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('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('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('assoc');
$statement->bind(['id' => 1], ['id' => 'integer']);
$results = $statement->fetchAll('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->newQuery();
```
### 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 > 2
$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 ($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 ($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('assoc');
```
## Official API
You can read the official [official API docs](https://api.cakephp.org/4.x/namespace-Cake.Database.html) to learn more of what this library
has to offer.
+69
View File
@@ -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 $errorCodes;
/**
* @var int
*/
protected $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;
}
}
+124
View File
@@ -0,0 +1,124 @@
<?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 $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;
/**
* 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 (strstr($message, $cause) !== false) {
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 $e) {
}
try {
$this->connection->connect();
if ($this->connection->isQueryLoggingEnabled()) {
$this->connection->log('[RECONNECT]');
}
return true;
} catch (Exception $e) {
// If there was an error connecting again, don't report it back,
// let the retry handler do it.
return false;
}
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
use function Cake\Core\deprecationWarning;
deprecationWarning(
'Since 4.1.0: Cake\Database\Schema\BaseSchema is deprecated. ' .
'Use Cake\Database\Schema\SchemaDialect instead.'
);
class_exists('Cake\Database\Schema\SchemaDialect');
+131
View File
@@ -0,0 +1,131 @@
<?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 $cacher;
/**
* The decorated schema collection
*
* @var \Cake\Database\Schema\CollectionInterface
*/
protected $collection;
/**
* The cache key prefix
*
* @var string
*/
protected $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();
}
/**
* @inheritDoc
*/
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;
}
}
+168
View File
@@ -0,0 +1,168 @@
<?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;
use Cake\Database\Exception\DatabaseException;
use PDOException;
/**
* Represents a database schema collection
*
* Used to access information about the tables,
* and other data in a database.
*/
class Collection implements CollectionInterface
{
/**
* Connection object
*
* @var \Cake\Database\Connection
*/
protected $_connection;
/**
* Schema dialect instance.
*
* @var \Cake\Database\Schema\SchemaDialect
*/
protected $_dialect;
/**
* Constructor.
*
* @param \Cake\Database\Connection $connection The connection instance.
*/
public function __construct(Connection $connection)
{
$this->_connection = $connection;
$this->_dialect = $connection->getDriver()->schemaDialect();
}
/**
* 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->_dialect->listTablesWithoutViewsSql($this->_connection->getDriver()->config());
$result = [];
$statement = $this->_connection->execute($sql, $params);
while ($row = $statement->fetch()) {
$result[] = $row[0];
}
$statement->closeCursor();
return $result;
}
/**
* 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
{
[$sql, $params] = $this->_dialect->listTablesSql($this->_connection->getDriver()->config());
$result = [];
$statement = $this->_connection->execute($sql, $params);
while ($row = $statement->fetch()) {
$result[] = $row[0];
}
$statement->closeCursor();
return $result;
}
/**
* 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\TableSchema Object with column metadata.
* @throws \Cake\Database\Exception\DatabaseException when table cannot be described.
*/
public function describe(string $name, array $options = []): TableSchemaInterface
{
$config = $this->_connection->getDriver()->config();
if (strpos($name, '.')) {
[$config['schema'], $name] = explode('.', $name);
}
$table = $this->_connection->getDriver()->newTableSchema($name);
$this->_reflect('Column', $name, $config, $table);
if (count($table->columns()) === 0) {
throw new DatabaseException(sprintf('Cannot describe %s. It has 0 columns.', $name));
}
$this->_reflect('Index', $name, $config, $table);
$this->_reflect('ForeignKey', $name, $config, $table);
$this->_reflect('Options', $name, $config, $table);
return $table;
}
/**
* Helper method for running each step of the reflection process.
*
* @param string $stage The stage name.
* @param string $name The table name.
* @param array<string, mixed> $config The config data.
* @param \Cake\Database\Schema\TableSchema $schema The table schema instance.
* @return void
* @throws \Cake\Database\Exception\DatabaseException on query failure.
* @uses \Cake\Database\Schema\SchemaDialect::describeColumnSql
* @uses \Cake\Database\Schema\SchemaDialect::describeIndexSql
* @uses \Cake\Database\Schema\SchemaDialect::describeForeignKeySql
* @uses \Cake\Database\Schema\SchemaDialect::describeOptionsSql
* @uses \Cake\Database\Schema\SchemaDialect::convertColumnDescription
* @uses \Cake\Database\Schema\SchemaDialect::convertIndexDescription
* @uses \Cake\Database\Schema\SchemaDialect::convertForeignKeyDescription
* @uses \Cake\Database\Schema\SchemaDialect::convertOptionsDescription
*/
protected function _reflect(string $stage, string $name, array $config, TableSchema $schema): void
{
$describeMethod = "describe{$stage}Sql";
$convertMethod = "convert{$stage}Description";
[$sql, $params] = $this->_dialect->{$describeMethod}($name, $config);
if (empty($sql)) {
return;
}
try {
$statement = $this->_connection->execute($sql, $params);
} catch (PDOException $e) {
throw new DatabaseException($e->getMessage(), 500, $e);
}
/** @psalm-suppress PossiblyFalseIterator */
foreach ($statement->fetchAll('assoc') as $row) {
$this->_dialect->{$convertMethod}($schema, $row);
}
$statement->closeCursor();
}
}
+54
View File
@@ -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;
}
+10
View File
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
use function Cake\Core\deprecationWarning;
deprecationWarning(
'Since 4.1.0: Cake\Database\Schema\MysqlSchema is deprecated. ' .
'Use Cake\Database\Schema\MysqlSchemaDialect instead.'
);
class_exists('Cake\Database\Schema\MysqlSchemaDialect');
+669
View File
@@ -0,0 +1,669 @@
<?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\DriverInterface;
use Cake\Database\Exception\DatabaseException;
/**
* Schema generation/reflection features for MySQL
*
* @internal
*/
class MysqlSchemaDialect extends SchemaDialect
{
/**
* The driver instance being used.
*
* @var \Cake\Database\Driver\Mysql
*/
protected $_driver;
/**
* 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<mixed> An array of (sql, params) to execute.
*/
public function listTablesSql(array $config): array
{
return ['SHOW FULL TABLES FROM ' . $this->_driver->quoteIdentifier($config['database']), []];
}
/**
* 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
{
return [
'SHOW FULL TABLES FROM ' . $this->_driver->quoteIdentifier($config['database'])
. ' WHERE TABLE_TYPE = "BASE TABLE"'
, []];
}
/**
* @inheritDoc
*/
public function describeColumnSql(string $tableName, array $config): array
{
return ['SHOW FULL COLUMNS FROM ' . $this->_driver->quoteIdentifier($tableName), []];
}
/**
* @inheritDoc
*/
public function describeIndexSql(string $tableName, array $config): array
{
return ['SHOW INDEXES FROM ' . $this->_driver->quoteIdentifier($tableName), []];
}
/**
* @inheritDoc
*/
public function describeOptionsSql(string $tableName, array $config): array
{
return ['SHOW TABLE STATUS WHERE Name = ?', [$tableName]];
}
/**
* @inheritDoc
*/
public function convertOptionsDescription(TableSchema $schema, array $row): void
{
$schema->setOptions([
'engine' => $row['Engine'],
'collation' => $row['Collation'],
]);
}
/**
* Convert a MySQL column type into an abstract type.
*
* The returned type will be a type that Cake\Database\TypeFactory can handle.
*
* @param string $column The column type + length
* @return array<string, mixed> Array of column information.
* @throws \Cake\Database\Exception\DatabaseException When column type cannot be parsed.
*/
protected function _convertColumn(string $column): array
{
preg_match('/([a-z]+)(?:\(([0-9,]+)\))?\s*([a-z]+)?/i', $column, $matches);
if (empty($matches)) {
throw new DatabaseException(sprintf('Unable to parse column type from "%s"', $column));
}
$col = strtolower($matches[1]);
$length = $precision = $scale = null;
if (isset($matches[2]) && strlen($matches[2])) {
$length = $matches[2];
if (strpos($matches[2], ',') !== false) {
[$length, $precision] = explode(',', $length);
}
$length = (int)$length;
$precision = (int)$precision;
}
$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 (in_array($col, ['datetime', 'timestamp'])) {
$typeName = $col;
if ($length > 0) {
$typeName = $col . 'fractional';
}
return ['type' => $typeName, 'length' => null, 'precision' => $length];
}
if (($col === 'tinyint' && $length === 1) || $col === 'boolean') {
return ['type' => TableSchema::TYPE_BOOLEAN, 'length' => null];
}
$unsigned = (isset($matches[3]) && strtolower($matches[3]) === 'unsigned');
if (strpos($col, 'bigint') !== false || $col === 'bigint') {
return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => null, 'unsigned' => $unsigned];
}
if ($col === 'tinyint') {
return ['type' => TableSchema::TYPE_TINYINTEGER, 'length' => null, 'unsigned' => $unsigned];
}
if ($col === 'smallint') {
return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => null, 'unsigned' => $unsigned];
}
if (in_array($col, ['int', 'integer', 'mediumint'])) {
return ['type' => TableSchema::TYPE_INTEGER, 'length' => null, 'unsigned' => $unsigned];
}
if ($col === 'char' && $length === 36) {
return ['type' => TableSchema::TYPE_UUID, 'length' => null];
}
if ($col === 'char') {
return ['type' => TableSchema::TYPE_CHAR, 'length' => $length];
}
if (strpos($col, 'char') !== false) {
return ['type' => TableSchema::TYPE_STRING, 'length' => $length];
}
if (strpos($col, 'text') !== false) {
$lengthName = substr($col, 0, -4);
$length = TableSchema::$columnLengths[$lengthName] ?? null;
return ['type' => TableSchema::TYPE_TEXT, 'length' => $length];
}
if ($col === 'binary' && $length === 16) {
return ['type' => TableSchema::TYPE_BINARY_UUID, 'length' => null];
}
if (strpos($col, 'blob') !== false || in_array($col, ['binary', 'varbinary'])) {
$lengthName = substr($col, 0, -4);
$length = TableSchema::$columnLengths[$lengthName] ?? $length;
return ['type' => TableSchema::TYPE_BINARY, 'length' => $length];
}
if (strpos($col, 'float') !== false || strpos($col, 'double') !== false) {
return [
'type' => TableSchema::TYPE_FLOAT,
'length' => $length,
'precision' => $precision,
'unsigned' => $unsigned,
];
}
if (strpos($col, 'decimal') !== false) {
return [
'type' => TableSchema::TYPE_DECIMAL,
'length' => $length,
'precision' => $precision,
'unsigned' => $unsigned,
];
}
if (strpos($col, 'json') !== false) {
return ['type' => TableSchema::TYPE_JSON, 'length' => null];
}
return ['type' => TableSchema::TYPE_STRING, 'length' => null];
}
/**
* @inheritDoc
*/
public function convertColumnDescription(TableSchema $schema, array $row): void
{
$field = $this->_convertColumn($row['Type']);
$field += [
'null' => $row['Null'] === 'YES',
'default' => $row['Default'],
'collate' => $row['Collation'],
'comment' => $row['Comment'],
];
if (isset($row['Extra']) && $row['Extra'] === 'auto_increment') {
$field['autoIncrement'] = true;
}
$schema->addColumn($row['Field'], $field);
}
/**
* @inheritDoc
*/
public function convertIndexDescription(TableSchema $schema, array $row): void
{
$type = null;
$columns = $length = [];
$name = $row['Key_name'];
if ($name === 'PRIMARY') {
$name = $type = TableSchema::CONSTRAINT_PRIMARY;
}
if (!empty($row['Column_name'])) {
$columns[] = $row['Column_name'];
}
if ($row['Index_type'] === 'FULLTEXT') {
$type = TableSchema::INDEX_FULLTEXT;
} elseif ((int)$row['Non_unique'] === 0 && $type !== 'primary') {
$type = TableSchema::CONSTRAINT_UNIQUE;
} elseif ($type !== 'primary') {
$type = TableSchema::INDEX_INDEX;
}
if (!empty($row['Sub_part'])) {
$length[$row['Column_name']] = $row['Sub_part'];
}
$isIndex = (
$type === TableSchema::INDEX_INDEX ||
$type === TableSchema::INDEX_FULLTEXT
);
if ($isIndex) {
$existing = $schema->getIndex($name);
} else {
$existing = $schema->getConstraint($name);
}
// MySQL multi column indexes come back as multiple rows.
if (!empty($existing)) {
$columns = array_merge($existing['columns'], $columns);
$length = array_merge($existing['length'], $length);
}
if ($isIndex) {
$schema->addIndex($name, [
'type' => $type,
'columns' => $columns,
'length' => $length,
]);
} else {
$schema->addConstraint($name, [
'type' => $type,
'columns' => $columns,
'length' => $length,
]);
}
}
/**
* @inheritDoc
*/
public function describeForeignKeySql(string $tableName, array $config): array
{
$sql = 'SELECT * FROM information_schema.key_column_usage AS kcu
INNER JOIN information_schema.referential_constraints AS rc
ON (
kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
)
WHERE kcu.TABLE_SCHEMA = ? AND kcu.TABLE_NAME = ? AND rc.TABLE_NAME = ?
ORDER BY kcu.ORDINAL_POSITION ASC';
return [$sql, [$config['database'], $tableName, $tableName]];
}
/**
* @inheritDoc
*/
public function convertForeignKeyDescription(TableSchema $schema, array $row): void
{
$data = [
'type' => TableSchema::CONSTRAINT_FOREIGN,
'columns' => [$row['COLUMN_NAME']],
'references' => [$row['REFERENCED_TABLE_NAME'], $row['REFERENCED_COLUMN_NAME']],
'update' => $this->_convertOnClause($row['UPDATE_RULE']),
'delete' => $this->_convertOnClause($row['DELETE_RULE']),
];
$name = $row['CONSTRAINT_NAME'];
$schema->addConstraint($name, $data);
}
/**
* @inheritDoc
*/
public function truncateTableSql(TableSchema $schema): array
{
return [sprintf('TRUNCATE TABLE `%s`', $schema->name())];
}
/**
* @inheritDoc
*/
public function createTableSql(TableSchema $schema, array $columns, array $constraints, array $indexes): array
{
$content = implode(",\n", array_merge($columns, $constraints, $indexes));
$temporary = $schema->isTemporary() ? ' TEMPORARY ' : ' ';
$content = sprintf("CREATE%sTABLE `%s` (\n%s\n)", $temporary, $schema->name(), $content);
$options = $schema->getOptions();
if (isset($options['engine'])) {
$content .= sprintf(' ENGINE=%s', $options['engine']);
}
if (isset($options['charset'])) {
$content .= sprintf(' DEFAULT CHARSET=%s', $options['charset']);
}
if (isset($options['collate'])) {
$content .= sprintf(' COLLATE=%s', $options['collate']);
}
return [$content];
}
/**
* @inheritDoc
*/
public function columnSql(TableSchema $schema, string $name): string
{
/** @var array $data */
$data = $schema->getColumn($name);
$sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name);
if ($sql !== null) {
return $sql;
}
$out = $this->_driver->quoteIdentifier($name);
$nativeJson = $this->_driver->supports(DriverInterface::FEATURE_JSON);
$typeMap = [
TableSchema::TYPE_TINYINTEGER => ' TINYINT',
TableSchema::TYPE_SMALLINTEGER => ' SMALLINT',
TableSchema::TYPE_INTEGER => ' INTEGER',
TableSchema::TYPE_BIGINTEGER => ' BIGINT',
TableSchema::TYPE_BINARY_UUID => ' BINARY(16)',
TableSchema::TYPE_BOOLEAN => ' BOOLEAN',
TableSchema::TYPE_FLOAT => ' FLOAT',
TableSchema::TYPE_DECIMAL => ' DECIMAL',
TableSchema::TYPE_DATE => ' DATE',
TableSchema::TYPE_TIME => ' TIME',
TableSchema::TYPE_DATETIME => ' DATETIME',
TableSchema::TYPE_DATETIME_FRACTIONAL => ' DATETIME',
TableSchema::TYPE_TIMESTAMP => ' TIMESTAMP',
TableSchema::TYPE_TIMESTAMP_FRACTIONAL => ' TIMESTAMP',
TableSchema::TYPE_TIMESTAMP_TIMEZONE => ' TIMESTAMP',
TableSchema::TYPE_CHAR => ' CHAR',
TableSchema::TYPE_UUID => ' CHAR(36)',
TableSchema::TYPE_JSON => $nativeJson ? ' JSON' : ' LONGTEXT',
];
$specialMap = [
'string' => true,
'text' => true,
'char' => true,
'binary' => true,
];
if (isset($typeMap[$data['type']])) {
$out .= $typeMap[$data['type']];
}
if (isset($specialMap[$data['type']])) {
switch ($data['type']) {
case TableSchema::TYPE_STRING:
$out .= ' VARCHAR';
if (!isset($data['length'])) {
$data['length'] = 255;
}
break;
case TableSchema::TYPE_TEXT:
$isKnownLength = in_array($data['length'], TableSchema::$columnLengths);
if (empty($data['length']) || !$isKnownLength) {
$out .= ' TEXT';
break;
}
/** @var string $length */
$length = array_search($data['length'], TableSchema::$columnLengths);
$out .= ' ' . strtoupper($length) . 'TEXT';
break;
case TableSchema::TYPE_BINARY:
$isKnownLength = in_array($data['length'], TableSchema::$columnLengths);
if ($isKnownLength) {
/** @var string $length */
$length = array_search($data['length'], TableSchema::$columnLengths);
$out .= ' ' . strtoupper($length) . 'BLOB';
break;
}
if (empty($data['length'])) {
$out .= ' BLOB';
break;
}
if ($data['length'] > 2) {
$out .= ' VARBINARY(' . $data['length'] . ')';
} else {
$out .= ' BINARY(' . $data['length'] . ')';
}
break;
}
}
$hasLength = [
TableSchema::TYPE_INTEGER,
TableSchema::TYPE_CHAR,
TableSchema::TYPE_SMALLINTEGER,
TableSchema::TYPE_TINYINTEGER,
TableSchema::TYPE_STRING,
];
if (in_array($data['type'], $hasLength, true) && isset($data['length'])) {
$out .= '(' . $data['length'] . ')';
}
$lengthAndPrecisionTypes = [TableSchema::TYPE_FLOAT, TableSchema::TYPE_DECIMAL];
if (in_array($data['type'], $lengthAndPrecisionTypes, true) && isset($data['length'])) {
if (isset($data['precision'])) {
$out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')';
} else {
$out .= '(' . (int)$data['length'] . ')';
}
}
$precisionTypes = [TableSchema::TYPE_DATETIME_FRACTIONAL, TableSchema::TYPE_TIMESTAMP_FRACTIONAL];
if (in_array($data['type'], $precisionTypes, true) && isset($data['precision'])) {
$out .= '(' . (int)$data['precision'] . ')';
}
$hasUnsigned = [
TableSchema::TYPE_TINYINTEGER,
TableSchema::TYPE_SMALLINTEGER,
TableSchema::TYPE_INTEGER,
TableSchema::TYPE_BIGINTEGER,
TableSchema::TYPE_FLOAT,
TableSchema::TYPE_DECIMAL,
];
if (
in_array($data['type'], $hasUnsigned, true) &&
isset($data['unsigned']) &&
$data['unsigned'] === true
) {
$out .= ' UNSIGNED';
}
$hasCollate = [
TableSchema::TYPE_TEXT,
TableSchema::TYPE_CHAR,
TableSchema::TYPE_STRING,
];
if (in_array($data['type'], $hasCollate, true) && isset($data['collate']) && $data['collate'] !== '') {
$out .= ' COLLATE ' . $data['collate'];
}
if (isset($data['null']) && $data['null'] === false) {
$out .= ' NOT NULL';
}
$addAutoIncrement = (
$schema->getPrimaryKey() === [$name] &&
!$schema->hasAutoincrement() &&
!isset($data['autoIncrement'])
);
if (
in_array($data['type'], [TableSchema::TYPE_INTEGER, TableSchema::TYPE_BIGINTEGER]) &&
(
$data['autoIncrement'] === true ||
$addAutoIncrement
)
) {
$out .= ' AUTO_INCREMENT';
}
$timestampTypes = [
TableSchema::TYPE_TIMESTAMP,
TableSchema::TYPE_TIMESTAMP_FRACTIONAL,
TableSchema::TYPE_TIMESTAMP_TIMEZONE,
];
if (isset($data['null']) && $data['null'] === true && in_array($data['type'], $timestampTypes, true)) {
$out .= ' NULL';
unset($data['default']);
}
$dateTimeTypes = [
TableSchema::TYPE_DATETIME,
TableSchema::TYPE_DATETIME_FRACTIONAL,
TableSchema::TYPE_TIMESTAMP,
TableSchema::TYPE_TIMESTAMP_FRACTIONAL,
TableSchema::TYPE_TIMESTAMP_TIMEZONE,
];
if (
isset($data['default']) &&
in_array($data['type'], $dateTimeTypes) &&
strpos(strtolower($data['default']), 'current_timestamp') !== false
) {
$out .= ' DEFAULT CURRENT_TIMESTAMP';
if (isset($data['precision'])) {
$out .= '(' . $data['precision'] . ')';
}
unset($data['default']);
}
if (isset($data['default'])) {
$out .= ' DEFAULT ' . $this->_driver->schemaValue($data['default']);
unset($data['default']);
}
if (isset($data['comment']) && $data['comment'] !== '') {
$out .= ' COMMENT ' . $this->_driver->schemaValue($data['comment']);
}
return $out;
}
/**
* @inheritDoc
*/
public function constraintSql(TableSchema $schema, string $name): string
{
/** @var array $data */
$data = $schema->getConstraint($name);
if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) {
$columns = array_map(
[$this->_driver, 'quoteIdentifier'],
$data['columns']
);
return sprintf('PRIMARY KEY (%s)', implode(', ', $columns));
}
$out = '';
if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) {
$out = 'UNIQUE KEY ';
}
if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) {
$out = 'CONSTRAINT ';
}
$out .= $this->_driver->quoteIdentifier($name);
return $this->_keySql($out, $data);
}
/**
* @inheritDoc
*/
public function addConstraintSql(TableSchema $schema): array
{
$sqlPattern = 'ALTER TABLE %s ADD %s;';
$sql = [];
foreach ($schema->constraints() as $name) {
/** @var array $constraint */
$constraint = $schema->getConstraint($name);
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 FOREIGN KEY %s;';
$sql = [];
foreach ($schema->constraints() as $name) {
/** @var array $constraint */
$constraint = $schema->getConstraint($name);
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
{
/** @var array $data */
$data = $schema->getIndex($name);
$out = '';
if ($data['type'] === TableSchema::INDEX_INDEX) {
$out = 'KEY ';
}
if ($data['type'] === TableSchema::INDEX_FULLTEXT) {
$out = 'FULLTEXT KEY ';
}
$out .= $this->_driver->quoteIdentifier($name);
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']
);
foreach ($data['columns'] as $i => $column) {
if (isset($data['length'][$column])) {
$columns[$i] .= sprintf('(%d)', $data['length'][$column]);
}
}
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) . ')';
}
}
// phpcs:disable
class_alias(
'Cake\Database\Schema\MysqlSchemaDialect',
'Cake\Database\Schema\MysqlSchema'
);
// phpcs:enable
+10
View File
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
use function Cake\Core\deprecationWarning;
deprecationWarning(
'Since 4.1.0: Cake\Database\Schema\PostgresSchema is deprecated. ' .
'Use Cake\Database\Schema\PostgresSchemaDialect instead.'
);
class_exists('Cake\Database\Schema\PostgresSchemaDialect');
+704
View File
@@ -0,0 +1,704 @@
<?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\Exception\DatabaseException;
/**
* Schema management/reflection features for Postgres.
*
* @internal
*/
class PostgresSchemaDialect extends SchemaDialect
{
/**
* 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 as name FROM information_schema.tables
WHERE table_schema = ? ORDER BY name';
$schema = empty($config['schema']) ? 'public' : $config['schema'];
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 as name FROM information_schema.tables
WHERE table_schema = ? AND table_type = \'BASE TABLE\' ORDER BY name';
$schema = empty($config['schema']) ? 'public' : $config['schema'];
return [$sql, [$schema]];
}
/**
* @inheritDoc
*/
public function describeColumnSql(string $tableName, array $config): array
{
$sql = 'SELECT DISTINCT table_schema AS schema,
column_name AS name,
data_type AS type,
is_nullable AS null, column_default AS default,
character_maximum_length AS char_length,
c.collation_name,
d.description as comment,
ordinal_position,
c.datetime_precision,
c.numeric_precision as column_precision,
c.numeric_scale as column_scale,
pg_get_serial_sequence(attr.attrelid::regclass::text, attr.attname) IS NOT NULL AS has_serial
FROM information_schema.columns c
INNER JOIN pg_catalog.pg_namespace ns ON (ns.nspname = table_schema)
INNER JOIN pg_catalog.pg_class cl ON (cl.relnamespace = ns.oid AND cl.relname = table_name)
LEFT JOIN pg_catalog.pg_index i ON (i.indrelid = cl.oid AND i.indkey[0] = c.ordinal_position)
LEFT JOIN pg_catalog.pg_description d on (cl.oid = d.objoid AND d.objsubid = c.ordinal_position)
LEFT JOIN pg_catalog.pg_attribute attr ON (cl.oid = attr.attrelid AND column_name = attr.attname)
WHERE table_name = ? AND table_schema = ? AND table_catalog = ?
ORDER BY ordinal_position';
$schema = empty($config['schema']) ? 'public' : $config['schema'];
return [$sql, [$tableName, $schema, $config['database']]];
}
/**
* Convert a column definition to the abstract types.
*
* The returned type will be a type that
* Cake\Database\TypeFactory can handle.
*
* @param string $column The column type + length
* @throws \Cake\Database\Exception\DatabaseException when column cannot be parsed.
* @return array<string, mixed> Array of column information.
*/
protected function _convertColumn(string $column): array
{
preg_match('/([a-z\s]+)(?:\(([0-9,]+)\))?/i', $column, $matches);
if (empty($matches)) {
throw new DatabaseException(sprintf('Unable to parse column type from "%s"', $column));
}
$col = strtolower($matches[1]);
$length = $precision = $scale = null;
if (isset($matches[2])) {
$length = (int)$matches[2];
}
$type = $this->_applyTypeSpecificColumnConversion(
$col,
compact('length', 'precision', 'scale')
);
if ($type !== null) {
return $type;
}
if (in_array($col, ['date', 'time', 'boolean'], true)) {
return ['type' => $col, 'length' => null];
}
if (in_array($col, ['timestamptz', 'timestamp with time zone'], true)) {
return ['type' => TableSchema::TYPE_TIMESTAMP_TIMEZONE, 'length' => null];
}
if (strpos($col, 'timestamp') !== false) {
return ['type' => TableSchema::TYPE_TIMESTAMP_FRACTIONAL, 'length' => null];
}
if (strpos($col, 'time') !== false) {
return ['type' => TableSchema::TYPE_TIME, 'length' => null];
}
if ($col === 'serial' || $col === 'integer') {
return ['type' => TableSchema::TYPE_INTEGER, 'length' => 10];
}
if ($col === 'bigserial' || $col === 'bigint') {
return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => 20];
}
if ($col === 'smallint') {
return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => 5];
}
if ($col === 'inet') {
return ['type' => TableSchema::TYPE_STRING, 'length' => 39];
}
if ($col === 'uuid') {
return ['type' => TableSchema::TYPE_UUID, 'length' => null];
}
if ($col === 'char') {
return ['type' => TableSchema::TYPE_CHAR, 'length' => $length];
}
if (strpos($col, 'character') !== false) {
return ['type' => TableSchema::TYPE_STRING, 'length' => $length];
}
// money is 'string' as it includes arbitrary text content
// before the number value.
if (strpos($col, 'money') !== false || $col === 'string') {
return ['type' => TableSchema::TYPE_STRING, 'length' => $length];
}
if (strpos($col, 'text') !== false) {
return ['type' => TableSchema::TYPE_TEXT, 'length' => null];
}
if ($col === 'bytea') {
return ['type' => TableSchema::TYPE_BINARY, 'length' => null];
}
if ($col === 'real' || strpos($col, 'double') !== false) {
return ['type' => TableSchema::TYPE_FLOAT, 'length' => null];
}
if (
strpos($col, 'numeric') !== false ||
strpos($col, 'decimal') !== false
) {
return ['type' => TableSchema::TYPE_DECIMAL, 'length' => null];
}
if (strpos($col, 'json') !== false) {
return ['type' => TableSchema::TYPE_JSON, 'length' => null];
}
$length = is_numeric($length) ? $length : null;
return ['type' => TableSchema::TYPE_STRING, 'length' => $length];
}
/**
* @inheritDoc
*/
public function convertColumnDescription(TableSchema $schema, array $row): void
{
$field = $this->_convertColumn($row['type']);
if ($field['type'] === TableSchema::TYPE_BOOLEAN) {
if ($row['default'] === 'true') {
$row['default'] = 1;
}
if ($row['default'] === 'false') {
$row['default'] = 0;
}
}
if (!empty($row['has_serial'])) {
$field['autoIncrement'] = true;
}
$field += [
'default' => $this->_defaultValue($row['default']),
'null' => $row['null'] === 'YES',
'collate' => $row['collation_name'],
'comment' => $row['comment'],
];
$field['length'] = $row['char_length'] ?: $field['length'];
if ($field['type'] === 'numeric' || $field['type'] === 'decimal') {
$field['length'] = $row['column_precision'];
$field['precision'] = $row['column_scale'] ?: null;
}
if ($field['type'] === TableSchema::TYPE_TIMESTAMP_FRACTIONAL) {
$field['precision'] = $row['datetime_precision'];
if ($field['precision'] === 0) {
$field['type'] = TableSchema::TYPE_TIMESTAMP;
}
}
if ($field['type'] === TableSchema::TYPE_TIMESTAMP_TIMEZONE) {
$field['precision'] = $row['datetime_precision'];
}
$schema->addColumn($row['name'], $field);
}
/**
* Manipulate the default value.
*
* Postgres includes sequence data and casting information in default values.
* We need to remove those.
*
* @param string|int|null $default The default value.
* @return string|int|null
*/
protected function _defaultValue($default)
{
if (is_numeric($default) || $default === null) {
return $default;
}
// Sequences
if (strpos($default, 'nextval') === 0) {
return null;
}
if (strpos($default, 'NULL::') === 0) {
return null;
}
// Remove quotes and postgres casts
return preg_replace(
"/^'(.*)'(?:::.*)$/",
'$1',
$default
);
}
/**
* @inheritDoc
*/
public function describeIndexSql(string $tableName, array $config): array
{
$sql = 'SELECT
c2.relname,
a.attname,
i.indisprimary,
i.indisunique
FROM pg_catalog.pg_namespace n
INNER JOIN pg_catalog.pg_class c ON (n.oid = c.relnamespace)
INNER JOIN pg_catalog.pg_index i ON (c.oid = i.indrelid)
INNER JOIN pg_catalog.pg_class c2 ON (c2.oid = i.indexrelid)
INNER JOIN pg_catalog.pg_attribute a ON (a.attrelid = c.oid AND i.indrelid::regclass = a.attrelid::regclass)
WHERE n.nspname = ?
AND a.attnum = ANY(i.indkey)
AND c.relname = ?
ORDER BY i.indisprimary DESC, i.indisunique DESC, c.relname, a.attnum';
$schema = 'public';
if (!empty($config['schema'])) {
$schema = $config['schema'];
}
return [$sql, [$schema, $tableName]];
}
/**
* @inheritDoc
*/
public function convertIndexDescription(TableSchema $schema, array $row): void
{
$type = TableSchema::INDEX_INDEX;
$name = $row['relname'];
if ($row['indisprimary']) {
$name = $type = TableSchema::CONSTRAINT_PRIMARY;
}
if ($row['indisunique'] && $type === TableSchema::INDEX_INDEX) {
$type = TableSchema::CONSTRAINT_UNIQUE;
}
if ($type === TableSchema::CONSTRAINT_PRIMARY || $type === TableSchema::CONSTRAINT_UNIQUE) {
$this->_convertConstraint($schema, $name, $type, $row);
return;
}
$index = $schema->getIndex($name);
if (!$index) {
$index = [
'type' => $type,
'columns' => [],
];
}
$index['columns'][] = $row['attname'];
$schema->addIndex($name, $index);
}
/**
* Add/update a constraint into the schema object.
*
* @param \Cake\Database\Schema\TableSchema $schema The table to update.
* @param string $name The index name.
* @param string $type The index type.
* @param array $row The metadata record to update with.
* @return void
*/
protected function _convertConstraint(TableSchema $schema, string $name, string $type, array $row): void
{
$constraint = $schema->getConstraint($name);
if (!$constraint) {
$constraint = [
'type' => $type,
'columns' => [],
];
}
$constraint['columns'][] = $row['attname'];
$schema->addConstraint($name, $constraint);
}
/**
* @inheritDoc
*/
public function describeForeignKeySql(string $tableName, array $config): array
{
// phpcs:disable Generic.Files.LineLength
$sql = 'SELECT
c.conname AS name,
c.contype AS type,
a.attname AS column_name,
c.confmatchtype AS match_type,
c.confupdtype AS on_update,
c.confdeltype AS on_delete,
c.confrelid::regclass AS references_table,
ab.attname AS references_field
FROM pg_catalog.pg_namespace n
INNER JOIN pg_catalog.pg_class cl ON (n.oid = cl.relnamespace)
INNER JOIN pg_catalog.pg_constraint c ON (n.oid = c.connamespace)
INNER JOIN pg_catalog.pg_attribute a ON (a.attrelid = cl.oid AND c.conrelid = a.attrelid AND a.attnum = ANY(c.conkey))
INNER JOIN pg_catalog.pg_attribute ab ON (a.attrelid = cl.oid AND c.confrelid = ab.attrelid AND ab.attnum = ANY(c.confkey))
WHERE n.nspname = ?
AND cl.relname = ?
ORDER BY name, a.attnum, ab.attnum DESC';
// phpcs:enable Generic.Files.LineLength
$schema = empty($config['schema']) ? 'public' : $config['schema'];
return [$sql, [$schema, $tableName]];
}
/**
* @inheritDoc
*/
public function convertForeignKeyDescription(TableSchema $schema, array $row): void
{
$data = [
'type' => TableSchema::CONSTRAINT_FOREIGN,
'columns' => $row['column_name'],
'references' => [$row['references_table'], $row['references_field']],
'update' => $this->_convertOnClause($row['on_update']),
'delete' => $this->_convertOnClause($row['on_delete']),
];
$schema->addConstraint($row['name'], $data);
}
/**
* @inheritDoc
*/
protected function _convertOnClause(string $clause): string
{
if ($clause === 'r') {
return TableSchema::ACTION_RESTRICT;
}
if ($clause === 'a') {
return TableSchema::ACTION_NO_ACTION;
}
if ($clause === 'c') {
return TableSchema::ACTION_CASCADE;
}
return TableSchema::ACTION_SET_NULL;
}
/**
* @inheritDoc
*/
public function columnSql(TableSchema $schema, string $name): string
{
/** @var array $data */
$data = $schema->getColumn($name);
$sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name);
if ($sql !== null) {
return $sql;
}
$out = $this->_driver->quoteIdentifier($name);
$typeMap = [
TableSchema::TYPE_TINYINTEGER => ' SMALLINT',
TableSchema::TYPE_SMALLINTEGER => ' SMALLINT',
TableSchema::TYPE_BINARY_UUID => ' UUID',
TableSchema::TYPE_BOOLEAN => ' BOOLEAN',
TableSchema::TYPE_FLOAT => ' FLOAT',
TableSchema::TYPE_DECIMAL => ' DECIMAL',
TableSchema::TYPE_DATE => ' DATE',
TableSchema::TYPE_TIME => ' TIME',
TableSchema::TYPE_DATETIME => ' TIMESTAMP',
TableSchema::TYPE_DATETIME_FRACTIONAL => ' TIMESTAMP',
TableSchema::TYPE_TIMESTAMP => ' TIMESTAMP',
TableSchema::TYPE_TIMESTAMP_FRACTIONAL => ' TIMESTAMP',
TableSchema::TYPE_TIMESTAMP_TIMEZONE => ' TIMESTAMPTZ',
TableSchema::TYPE_UUID => ' UUID',
TableSchema::TYPE_CHAR => ' CHAR',
TableSchema::TYPE_JSON => ' JSONB',
];
if (isset($typeMap[$data['type']])) {
$out .= $typeMap[$data['type']];
}
if ($data['type'] === TableSchema::TYPE_INTEGER || $data['type'] === TableSchema::TYPE_BIGINTEGER) {
$type = $data['type'] === TableSchema::TYPE_INTEGER ? ' INTEGER' : ' BIGINT';
if ($schema->getPrimaryKey() === [$name] || $data['autoIncrement'] === true) {
$type = $data['type'] === TableSchema::TYPE_INTEGER ? ' SERIAL' : ' BIGSERIAL';
unset($data['null'], $data['default']);
}
$out .= $type;
}
if ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] !== TableSchema::LENGTH_TINY) {
$out .= ' TEXT';
}
if ($data['type'] === TableSchema::TYPE_BINARY) {
$out .= ' BYTEA';
}
if ($data['type'] === TableSchema::TYPE_CHAR) {
$out .= '(' . $data['length'] . ')';
}
if (
$data['type'] === TableSchema::TYPE_STRING ||
(
$data['type'] === TableSchema::TYPE_TEXT &&
$data['length'] === TableSchema::LENGTH_TINY
)
) {
$out .= ' VARCHAR';
if (isset($data['length']) && $data['length'] !== '') {
$out .= '(' . $data['length'] . ')';
}
}
$hasCollate = [TableSchema::TYPE_TEXT, TableSchema::TYPE_STRING, TableSchema::TYPE_CHAR];
if (in_array($data['type'], $hasCollate, true) && isset($data['collate']) && $data['collate'] !== '') {
$out .= ' COLLATE "' . $data['collate'] . '"';
}
$hasPrecision = [
TableSchema::TYPE_FLOAT,
TableSchema::TYPE_DATETIME,
TableSchema::TYPE_DATETIME_FRACTIONAL,
TableSchema::TYPE_TIMESTAMP,
TableSchema::TYPE_TIMESTAMP_FRACTIONAL,
TableSchema::TYPE_TIMESTAMP_TIMEZONE,
];
if (in_array($data['type'], $hasPrecision) && isset($data['precision'])) {
$out .= '(' . $data['precision'] . ')';
}
if (
$data['type'] === TableSchema::TYPE_DECIMAL &&
(
isset($data['length']) ||
isset($data['precision'])
)
) {
$out .= '(' . $data['length'] . ',' . (int)$data['precision'] . ')';
}
if (isset($data['null']) && $data['null'] === false) {
$out .= ' NOT NULL';
}
$datetimeTypes = [
TableSchema::TYPE_DATETIME,
TableSchema::TYPE_DATETIME_FRACTIONAL,
TableSchema::TYPE_TIMESTAMP,
TableSchema::TYPE_TIMESTAMP_FRACTIONAL,
TableSchema::TYPE_TIMESTAMP_TIMEZONE,
];
if (
isset($data['default']) &&
in_array($data['type'], $datetimeTypes) &&
strtolower($data['default']) === 'current_timestamp'
) {
$out .= ' DEFAULT CURRENT_TIMESTAMP';
} elseif (isset($data['default'])) {
$defaultValue = $data['default'];
if ($data['type'] === 'boolean') {
$defaultValue = (bool)$defaultValue;
}
$out .= ' DEFAULT ' . $this->_driver->schemaValue($defaultValue);
} elseif (isset($data['null']) && $data['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) {
/** @var array $constraint */
$constraint = $schema->getConstraint($name);
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) {
/** @var array $constraint */
$constraint = $schema->getConstraint($name);
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
{
/** @var array $data */
$data = $schema->getIndex($name);
$columns = array_map(
[$this->_driver, 'quoteIdentifier'],
$data['columns']
);
return sprintf(
'CREATE INDEX %s ON %s (%s)',
$this->_driver->quoteIdentifier($name),
$this->_driver->quoteIdentifier($schema->name()),
implode(', ', $columns)
);
}
/**
* @inheritDoc
*/
public function constraintSql(TableSchema $schema, string $name): string
{
/** @var array<string, mixed> $data */
$data = $schema->getConstraint($name);
$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<string, mixed> $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 DEFERRABLE INITIALLY IMMEDIATE',
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());
$dbSchema = $this->_driver->schema();
if ($dbSchema != 'public') {
$tableName = $this->_driver->quoteIdentifier($dbSchema) . '.' . $tableName;
}
$temporary = $schema->isTemporary() ? ' TEMPORARY ' : ' ';
$out = [];
$out[] = sprintf("CREATE%sTABLE %s (\n%s\n)", $temporary, $tableName, $content);
foreach ($indexes as $index) {
$out[] = $index;
}
foreach ($schema->columns() as $column) {
$columnData = $schema->getColumn($column);
if (isset($columnData['comment'])) {
$out[] = sprintf(
'COMMENT ON COLUMN %s.%s IS %s',
$tableName,
$this->_driver->quoteIdentifier($column),
$this->_driver->schemaValue($columnData['comment'])
);
}
}
return $out;
}
/**
* @inheritDoc
*/
public function truncateTableSql(TableSchema $schema): array
{
$name = $this->_driver->quoteIdentifier($schema->name());
return [
sprintf('TRUNCATE %s RESTART IDENTITY CASCADE', $name),
];
}
/**
* Generate the SQL to drop a table.
*
* @param \Cake\Database\Schema\TableSchema $schema Table instance
* @return array SQL statements to drop a table.
*/
public function dropTableSql(TableSchema $schema): array
{
$sql = sprintf(
'DROP TABLE %s CASCADE',
$this->_driver->quoteIdentifier($schema->name())
);
return [$sql];
}
}
// phpcs:disable
class_alias(
'Cake\Database\Schema\PostgresSchemaDialect',
'Cake\Database\Schema\PostgresSchema'
);
// phpcs:enable
+346
View File
@@ -0,0 +1,346 @@
<?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\DriverInterface;
use Cake\Database\Type\ColumnSchemaAwareInterface;
use Cake\Database\TypeFactory;
use InvalidArgumentException;
/**
* Base class for schema implementations.
*
* This class contains methods that are common across
* the various SQL dialects.
*
* @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\DriverInterface
*/
protected $_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\DriverInterface $driver The driver to use.
*/
public function __construct(DriverInterface $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($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::getMap($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|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::getMap($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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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
*/
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
*/
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
*/
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
*/
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;
}
// phpcs:disable
class_alias(
'Cake\Database\Schema\SchemaDialect',
'Cake\Database\Schema\BaseSchema'
);
// phpcs:enable

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