This commit is contained in:
Sebastian Molenda
2026-05-12 21:10:38 +02:00
commit ab96d82fcf
2544 changed files with 721700 additions and 0 deletions
@@ -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.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource;
use Psr\SimpleCache\CacheInterface;
/**
* This interface defines the methods you can depend on in a connection.
*/
interface ConnectionInterface
{
/**
* @var string
*/
public const ROLE_WRITE = 'write';
/**
* @var string
*/
public const ROLE_READ = 'read';
/**
* Gets the driver instance.
*
* @param string $role
* @return object
*/
public function getDriver(string $role = self::ROLE_WRITE): object;
/**
* Set a cacher.
*
* @param \Psr\SimpleCache\CacheInterface $cacher Cacher object
* @return $this
*/
public function setCacher(CacheInterface $cacher);
/**
* Get a cacher.
*
* @return \Psr\SimpleCache\CacheInterface $cacher Cacher object
*/
public function getCacher(): CacheInterface;
/**
* Get the configuration name for this connection.
*
* @return string
*/
public function configName(): string;
/**
* Get the configuration data used to create the connection.
*
* @return array<string, mixed>
*/
public function config(): array;
}
+214
View File
@@ -0,0 +1,214 @@
<?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 0.10.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource;
use Cake\Core\StaticConfigTrait;
use Cake\Database\Connection;
use Cake\Database\Driver\Mysql;
use Cake\Database\Driver\Postgres;
use Cake\Database\Driver\Sqlite;
use Cake\Database\Driver\Sqlserver;
use Cake\Datasource\Exception\MissingDatasourceConfigException;
use Closure;
/**
* Manages and loads instances of Connection
*
* Provides an interface to loading and creating connection objects. Acts as
* a registry for the connections defined in an application.
*
* Provides an interface for loading and enumerating connections defined in
* config/app.php
*/
class ConnectionManager
{
use StaticConfigTrait {
setConfig as protected _setConfig;
parseDsn as protected _parseDsn;
}
/**
* A map of connection aliases.
*
* @var array<string, string>
*/
protected static array $_aliasMap = [];
/**
* An array mapping url schemes to fully qualified driver class names
*
* @var array<string, string>
* @phpstan-var array<string, class-string>
*/
protected static array $_dsnClassMap = [
'mysql' => Mysql::class,
'postgres' => Postgres::class,
'sqlite' => Sqlite::class,
'sqlserver' => Sqlserver::class,
];
/**
* The ConnectionRegistry used by the manager.
*
* @var \Cake\Datasource\ConnectionRegistry
*/
protected static ConnectionRegistry $_registry;
/**
* Configure a new connection object.
*
* The connection will not be constructed until it is first used.
*
* @param array<string, mixed>|string $key The name of the connection config, or an array of multiple configs.
* @param \Cake\Datasource\ConnectionInterface|\Closure|array<string, mixed>|null $config An array of name => config data for adapter.
* @return void
* @throws \Cake\Core\Exception\CakeException When trying to modify an existing config.
* @see \Cake\Core\StaticConfigTrait::config()
*/
public static function setConfig(array|string $key, ConnectionInterface|Closure|array|null $config = null): void
{
if (is_array($config)) {
$config['name'] = $key;
}
static::_setConfig($key, $config);
}
/**
* 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\Database\Driver\Mysql://localhost:3306/database?className=Cake\Database\Connection';
* $config = ConnectionManager::parseDsn($dsn);
*
* $dsn = 'Cake\Database\Connection://localhost:3306/database?driver=Cake\Database\Driver\Mysql';
* $config = ConnectionManager::parseDsn($dsn);
* ```
*
* For all classes, the value of `scheme` is set as the value of both the `className` and `driver`
* unless they have been otherwise specified.
*
* Note that query-string 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<int|string, array|bool|string|null> The configuration array to be stored after parsing the DSN
*/
public static function parseDsn(string $dsn): array
{
$config = static::_parseDsn($dsn);
if (isset($config['path']) && empty($config['database']) && is_string($config['path'])) {
$config['database'] = substr($config['path'], 1);
}
if (empty($config['driver'])) {
$config['driver'] = $config['className'] ?? null;
$config['className'] = Connection::class;
}
unset($config['path']);
return $config;
}
/**
* Set one or more connection aliases.
*
* Connection aliases allow you to rename active connections without overwriting
* the aliased connection. This is most useful in the test-suite for replacing
* connections with their test variant.
*
* Defined aliases will take precedence over normal connection names. For example,
* if you alias 'default' to 'test', fetching 'default' will always return the 'test'
* connection as long as the alias is defined.
*
* You can remove aliases with ConnectionManager::dropAlias().
*
* ### Usage
*
* ```
* // Make 'things' resolve to 'test_things' connection
* ConnectionManager::alias('test_things', 'things');
* ```
*
* @param string $source The existing connection to alias.
* @param string $alias The alias name that resolves to `$source`.
* @return void
*/
public static function alias(string $source, string $alias): void
{
static::$_aliasMap[$alias] = $source;
}
/**
* Drop an alias.
*
* Removes an alias from ConnectionManager. Fetching the aliased
* connection may fail if there is no other connection with that name.
*
* @param string $alias The connection alias to drop
* @return void
*/
public static function dropAlias(string $alias): void
{
unset(static::$_aliasMap[$alias]);
}
/**
* Returns the current connection aliases and what they alias.
*
* @return array<string, string>
*/
public static function aliases(): array
{
return static::$_aliasMap;
}
/**
* Get a connection.
*
* If the connection has not been constructed an instance will be added
* to the registry. This method will use any aliases that have been
* defined. If you want the original unaliased connections pass `false`
* as second parameter.
*
* @param string $name The connection name.
* @param bool $useAliases Whether connection aliases are used
* @return \Cake\Datasource\ConnectionInterface
* @throws \Cake\Datasource\Exception\MissingDatasourceConfigException When config
* data is missing.
*/
public static function get(string $name, bool $useAliases = true): ConnectionInterface
{
if ($useAliases && isset(static::$_aliasMap[$name])) {
$name = static::$_aliasMap[$name];
}
if (!isset(static::$_config[$name])) {
throw new MissingDatasourceConfigException(['name' => $name]);
}
static::$_registry ??= new ConnectionRegistry();
return static::$_registry->{$name} ?? static::$_registry->load($name, static::$_config[$name]);
}
}
+104
View File
@@ -0,0 +1,104 @@
<?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\Datasource;
use Cake\Core\App;
use Cake\Core\ObjectRegistry;
use Cake\Datasource\Exception\MissingDatasourceException;
use Closure;
/**
* A registry object for connection instances.
*
* @see \Cake\Datasource\ConnectionManager
* @extends \Cake\Core\ObjectRegistry<\Cake\Datasource\ConnectionInterface>
*/
class ConnectionRegistry extends ObjectRegistry
{
/**
* Resolve a datasource classname.
*
* Part of the template method for Cake\Core\ObjectRegistry::load()
*
* @param string $class Partial classname to resolve.
* @return class-string<\Cake\Datasource\ConnectionInterface>|null Either the correct class name or null.
*/
protected function _resolveClassName(string $class): ?string
{
/** @var class-string<\Cake\Datasource\ConnectionInterface>|null */
return App::className($class, 'Datasource');
}
/**
* Throws an exception when a datasource is missing
*
* Part of the template method for Cake\Core\ObjectRegistry::load()
*
* @param string $class The classname that is missing.
* @param string|null $plugin The plugin the datasource is missing in.
* @return void
* @throws \Cake\Datasource\Exception\MissingDatasourceException
*/
protected function _throwMissingClassError(string $class, ?string $plugin): void
{
throw new MissingDatasourceException([
'class' => $class,
'plugin' => $plugin,
]);
}
/**
* Create the connection object with the correct settings.
*
* Part of the template method for Cake\Core\ObjectRegistry::load()
*
* If a closure is passed as first argument, The returned value of this
* function will be the result from calling the closure.
*
* @param \Cake\Datasource\ConnectionInterface|\Closure|class-string<\Cake\Datasource\ConnectionInterface> $class The classname or object to make.
* @param string $alias The alias of the object.
* @param array<string, mixed> $config An array of settings to use for the datasource.
* @return \Cake\Datasource\ConnectionInterface A connection with the correct settings.
*/
protected function _create(object|string $class, string $alias, array $config): ConnectionInterface
{
if (is_string($class)) {
unset($config['className']);
return new $class($config);
}
if ($class instanceof Closure) {
return $class($alias);
}
return $class;
}
/**
* Remove a single adapter from the registry.
*
* @param string $name The adapter name.
* @return $this
*/
public function unload(string $name)
{
unset($this->_loaded[$name]);
return $this;
}
}
+341
View File
@@ -0,0 +1,341 @@
<?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\Datasource;
use ArrayAccess;
use JsonSerializable;
use Stringable;
/**
* Describes the methods that any class representing a data storage should
* comply with.
*
* @property mixed $id Alias for commonly used primary key.
* @template-extends \ArrayAccess<string, mixed>
* @method bool hasValue(string $field)
* @method static patch(array $values, array $options = [])
*/
interface EntityInterface extends ArrayAccess, JsonSerializable, Stringable
{
/**
* Sets hidden fields.
*
* @param array<string> $fields An array of fields to hide from array exports.
* @param bool $merge Merge the new fields with the existing. By default false.
* @return $this
*/
public function setHidden(array $fields, bool $merge = false);
/**
* Gets the hidden fields.
*
* @return array<string>
*/
public function getHidden(): array;
/**
* Sets the virtual fields on this entity.
*
* @param array<string> $fields An array of fields to treat as virtual.
* @param bool $merge Merge the new fields with the existing. By default false.
* @return $this
*/
public function setVirtual(array $fields, bool $merge = false);
/**
* Gets the virtual fields on this entity.
*
* @return array<string>
*/
public function getVirtual(): array;
/**
* Returns whether a field is an original one.
* Original fields are those that an entity was instantiated with.
*
* @param string $name Name
* @return bool
*/
public function isOriginalField(string $name): bool;
/**
* Returns an array of original fields.
* Original fields are those that an entity was initialized with.
*
* @return array<string>
*/
public function getOriginalFields(): array;
/**
* Sets the dirty status of a single field.
*
* @param string $field the field to set or check status for
* @param bool $isDirty true means the field was changed, false means
* it was not changed. Default true.
* @return $this
*/
public function setDirty(string $field, bool $isDirty = true);
/**
* Checks if the entity is dirty or if a single field of it is dirty.
*
* @param string|null $field The field to check the status for. Null for the whole entity.
* @return bool Whether the field was changed or not
*/
public function isDirty(?string $field = null): bool;
/**
* Gets the dirty fields.
*
* @return array<string>
*/
public function getDirty(): array;
/**
* Returns whether this entity has errors.
*
* @param bool $includeNested true will check nested entities for hasErrors()
* @return bool
*/
public function hasErrors(bool $includeNested = true): bool;
/**
* Returns all validation errors.
*
* @return array
*/
public function getErrors(): array;
/**
* Returns validation errors of a field
*
* @param string $field Field name to get the errors from
* @return array
*/
public function getError(string $field): array;
/**
* Sets error messages to the entity
*
* @param array $errors The array of errors to set.
* @param bool $overwrite Whether to overwrite pre-existing errors for $fields
* @return $this
*/
public function setErrors(array $errors, bool $overwrite = false);
/**
* Sets errors for a single field
*
* @param string $field The field to get errors for, or the array of errors to set.
* @param array|string $errors The errors to be set for $field
* @param bool $overwrite Whether to overwrite pre-existing errors for $field
* @return $this
*/
public function setError(string $field, array|string $errors, bool $overwrite = false);
/**
* Stores whether a field value can be changed or set in this entity.
*
* @param array<string>|string $field single or list of fields to change its accessibility
* @param bool $set true marks the field as accessible, false will
* mark it as protected.
* @return $this
*/
public function setAccess(array|string $field, bool $set);
/**
* Accessible configuration for this entity.
*
* @return array<bool>
*/
public function getAccessible(): array;
/**
* Checks if a field is accessible
*
* @param string $field Field name to check
* @return bool
*/
public function isAccessible(string $field): bool;
/**
* Sets the source alias
*
* @param string $alias the alias of the repository
* @return $this
*/
public function setSource(string $alias);
/**
* Returns the alias of the repository from which this entity came from.
*
* @return string
*/
public function getSource(): string;
/**
* Returns an array with the requested original fields
* stored in this entity, indexed by field name.
*
* @param array<string> $fields List of fields to be returned
* @return array<string, mixed>
*/
public function extractOriginal(array $fields): array;
/**
* Returns an array with only the original fields
* stored in this entity, indexed by field name.
*
* @param array<string> $fields List of fields to be returned
* @return array<string, mixed>
*/
public function extractOriginalChanged(array $fields): array;
/**
* Sets one or multiple fields to the specified value
*
* @param array<string, mixed>|string $field the name of field to set or a list of
* fields with their respective values
* @param mixed $value The value to set to the field or an array if the
* first argument is also an array, in which case will be treated as $options
* @param array<string, mixed> $options Options to be used for setting the field. Allowed option
* keys are `setter` and `guard`
* @return $this
*/
public function set(array|string $field, mixed $value = null, array $options = []);
/**
* Returns the value of a field by name
*
* @param string $field the name of the field to retrieve
* @return mixed
*/
public function &get(string $field): mixed;
/**
* Enable/disable field presence check when accessing a property.
*
* If enabled an exception will be thrown when trying to access a non-existent property.
*
* @param bool $value `true` to enable, `false` to disable.
*/
public function requireFieldPresence(bool $value = true): void;
/**
* Returns whether a field has an original value
*
* @param string $field
* @return bool
*/
public function hasOriginal(string $field): bool;
/**
* Returns the original value of a field.
*
* @param string $field The name of the field.
* @param bool $allowFallback whether to allow falling back to the current field value if no original exists
* @return mixed
*/
public function getOriginal(string $field, bool $allowFallback = true): mixed;
/**
* Gets all original values of the entity.
*
* @return array
*/
public function getOriginalValues(): array;
/**
* Returns whether this entity contains a field named $field.
*
* The method will return `true` even when the field is set to `null`.
*
* @param array<string>|string $field The field to check.
* @return bool
*/
public function has(array|string $field): bool;
/**
* Removes a field or list of fields from this entity
*
* @param array<string>|string $field The field to unset.
* @return $this
*/
public function unset(array|string $field);
/**
* Get the list of visible fields.
*
* @return array<string> A list of fields that are 'visible' in all representations.
*/
public function getVisible(): array;
/**
* Returns an array with all the visible fields set in this entity.
*
* *Note* hidden fields are not visible, and will not be output
* by toArray().
*
* @return array<string, mixed>
*/
public function toArray(): array;
/**
* Returns an array with the requested fields
* stored in this entity, indexed by field name
*
* @param array<string> $fields list of fields to be returned
* @param bool $onlyDirty Return the requested field only if it is dirty
* @return array<string, mixed>
*/
public function extract(array $fields, bool $onlyDirty = false): array;
/**
* Sets the entire entity as clean, which means that it will appear as
* no fields being modified or added at all. This is an useful call
* for an initial object hydration
*
* @return void
*/
public function clean(): void;
/**
* Set the status of this entity.
*
* Using `true` means that the entity has not been persisted in the database,
* `false` indicates that the entity has been persisted.
*
* @param bool $new Indicate whether this entity has been persisted.
* @return $this
*/
public function setNew(bool $new);
/**
* Returns whether this entity has already been persisted.
*
* @return bool Whether the entity has been persisted.
*/
public function isNew(): bool;
/**
* Returns a string representation of this object.
*
* @return string
* @deprecated 5.2.0 Casting an entity to string is deprecated. Use `json_encode()` instead to get a string representation of the entity.
*/
public function __toString(): string;
}
File diff suppressed because it is too large Load Diff
@@ -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
* 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\Datasource\Exception;
use Cake\Core\Exception\CakeException;
/**
* Exception raised when the provided primary key does not match the table primary key
*/
class InvalidPrimaryKeyException extends CakeException
{
}
@@ -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
* 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\Datasource\Exception;
use Cake\Core\Exception\CakeException;
/**
* Exception class to be thrown when a datasource configuration is not found
*/
class MissingDatasourceConfigException extends CakeException
{
/**
* @var string
*/
protected string $_messageTemplate = 'The datasource configuration `%s` was not found.';
}
@@ -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
* 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\Datasource\Exception;
use Cake\Core\Exception\CakeException;
/**
* Used when a datasource cannot be found.
*/
class MissingDatasourceException extends CakeException
{
/**
* @var string
*/
protected string $_messageTemplate = 'Datasource class `%s` could not be found. %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\Datasource\Exception;
use Cake\Core\Exception\CakeException;
/**
* Used when a model cannot be found.
*/
class MissingModelException extends CakeException
{
/**
* @var string
*/
protected string $_messageTemplate = 'Model class `%s` of type `%s` could not be found.';
}
@@ -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 5.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource\Exception;
use Cake\Core\Exception\CakeException;
/**
* A required property does not exist for an entity.
*/
class MissingPropertyException extends CakeException
{
/**
* @var string
*/
protected string $_messageTemplate = 'Property `%s` does not exist for the entity `%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\Datasource\Exception;
use Cake\Core\Exception\CakeException;
use Cake\Core\Exception\HttpErrorCodeInterface;
/**
* Exception raised when a particular record was not found
*/
class RecordNotFoundException extends CakeException implements HttpErrorCodeInterface
{
/**
* @inheritDoc
*/
protected int $_defaultCode = 404;
}
+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.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource;
use Cake\Datasource\Locator\LocatorInterface;
use InvalidArgumentException;
/**
* Class FactoryLocator
*/
class FactoryLocator
{
/**
* A list of model factory functions.
*
* @var array<string, \Cake\Datasource\Locator\LocatorInterface>
*/
protected static array $_modelFactories = [];
/**
* Register a locator to return repositories of a given type.
*
* @param string $type The name of the repository type the factory function is for.
* @param \Cake\Datasource\Locator\LocatorInterface $factory The factory function used to create instances.
* @return void
*/
public static function add(string $type, LocatorInterface $factory): void
{
static::$_modelFactories[$type] = $factory;
}
/**
* Drop a model factory.
*
* @param string $type The name of the repository type to drop the factory for.
* @return void
*/
public static function drop(string $type): void
{
unset(static::$_modelFactories[$type]);
}
/**
* Get the factory for the specified repository type.
*
* @param string $type The repository type to get the factory for.
* @throws \InvalidArgumentException If the specified repository type has no factory.
* @return \Cake\Datasource\Locator\LocatorInterface The factory for the repository type.
*/
public static function get(string $type): LocatorInterface
{
if (isset(static::$_modelFactories[$type])) {
return static::$_modelFactories[$type];
}
throw new InvalidArgumentException(sprintf(
'Unknown repository type `%s`. Make sure you register a type before trying to use it.',
$type,
));
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource;
/**
* Defines the interface that testing fixtures use.
*/
interface FixtureInterface
{
/**
* Run before each test is executed.
*
* Should insert all the records into the test database.
*
* @param \Cake\Datasource\ConnectionInterface $connection An instance of the connection
* into which the records will be inserted.
* @return bool
*/
public function insert(ConnectionInterface $connection): bool;
/**
* Truncates the current fixture.
*
* @param \Cake\Datasource\ConnectionInterface $connection A reference to a db instance
* @return bool
*/
public function truncate(ConnectionInterface $connection): bool;
/**
* Get the connection name this fixture should be inserted into.
*
* @return string
*/
public function connection(): string;
/**
* Get the table/collection name for this fixture.
*
* @return string
*/
public function sourceName(): string;
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource;
/**
* Describes the methods that any class representing a data storage should
* comply with.
*/
interface InvalidPropertyInterface
{
/**
* Get a list of invalid fields and their data for errors upon validation/patching
*
* @return array
*/
public function getInvalid(): array;
/**
* Set fields as invalid and not patchable into the entity.
*
* This is useful for batch operations when one needs to get the original value for an error message after patching.
* This value could not be patched into the entity and is simply copied into the _invalid property for debugging
* purposes or to be able to log it away.
*
* @param array<string, mixed> $fields The values to set.
* @param bool $overwrite Whether to overwrite pre-existing values for $field.
* @return $this
*/
public function setInvalid(array $fields, bool $overwrite = false);
/**
* Get a single value of an invalid field. Returns null if not set.
*
* @param string $field The name of the field.
* @return mixed|null
*/
public function getInvalidField(string $field): mixed;
/**
* Sets a field as invalid and not patchable into the entity.
*
* @param string $field The value to set.
* @param mixed $value The invalid value to be set for $field.
* @return $this
*/
public function setInvalidField(string $field, mixed $value);
}
+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.
@@ -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 4.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource\Locator;
use Cake\Core\Exception\CakeException;
use Cake\Datasource\RepositoryInterface;
/**
* Provides an abstract registry/factory for repository objects.
*
* @template TRepo of \Cake\Datasource\RepositoryInterface
* @implements \Cake\Datasource\Locator\LocatorInterface<TRepo>
*/
abstract class AbstractLocator implements LocatorInterface
{
/**
* Instances that belong to the registry.
*
* @var array<string, TRepo>
*/
protected array $instances = [];
/**
* Contains a list of options that were passed to get() method.
*
* @var array<string, array>
*/
protected array $options = [];
/**
* {@inheritDoc}
*
* @param string $alias The alias name you want to get.
* @param array<string, mixed> $options The options you want to build the table with.
* @return TRepo
* @throws \Cake\Core\Exception\CakeException When trying to get alias for which instance
* has already been created with different options.
*/
public function get(string $alias, array $options = []): RepositoryInterface
{
$storeOptions = $options;
unset($storeOptions['allowFallbackClass']);
if (isset($this->instances[$alias])) {
if ($storeOptions && isset($this->options[$alias]) && $this->options[$alias] !== $storeOptions) {
throw new CakeException(sprintf(
'You cannot configure `%s`, it already exists in the registry.',
$alias,
));
}
return $this->instances[$alias];
}
$this->options[$alias] = $storeOptions;
return $this->instances[$alias] = $this->createInstance($alias, $options);
}
/**
* Create an instance of a given classname.
*
* @param string $alias Repository alias.
* @param array<string, mixed> $options The options you want to build the instance with.
* @return TRepo
*/
abstract protected function createInstance(string $alias, array $options): RepositoryInterface;
/**
* @inheritDoc
*/
public function set(string $alias, RepositoryInterface $repository): RepositoryInterface
{
return $this->instances[$alias] = $repository;
}
/**
* @inheritDoc
*/
public function exists(string $alias): bool
{
return isset($this->instances[$alias]);
}
/**
* @inheritDoc
*/
public function remove(string $alias): void
{
unset(
$this->instances[$alias],
$this->options[$alias],
);
}
/**
* @inheritDoc
*/
public function clear(): void
{
$this->instances = [];
$this->options = [];
}
}
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource\Locator;
use Cake\Datasource\RepositoryInterface;
/**
* Registries for repository objects should implement this interface.
*
* @template TRepo of \Cake\Datasource\RepositoryInterface
*/
interface LocatorInterface
{
/**
* Get a repository instance from the registry.
*
* @param string $alias The alias name you want to get.
* @param array<string, mixed> $options The options you want to build the table with.
* @return TRepo
* @throws \RuntimeException When trying to get alias for which instance
* has already been created with different options.
*/
public function get(string $alias, array $options = []): RepositoryInterface;
/**
* Set a repository instance.
*
* @param string $alias The alias to set.
* @param TRepo $repository The repository to set.
* @return TRepo
*/
public function set(string $alias, RepositoryInterface $repository): RepositoryInterface;
/**
* Check to see if an instance exists in the registry.
*
* @param string $alias The alias to check for.
* @return bool
*/
public function exists(string $alias): bool;
/**
* Removes an repository instance from the registry.
*
* @param string $alias The alias to remove.
* @return void
*/
public function remove(string $alias): void;
/**
* Clears the registry of configuration and instances.
*
* @return void
*/
public function clear(): void;
}
+162
View File
@@ -0,0 +1,162 @@
<?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\Datasource;
use Cake\Datasource\Exception\MissingModelException;
use Cake\Datasource\Locator\LocatorInterface;
use UnexpectedValueException;
use function Cake\Core\pluginSplit;
/**
* Provides functionality for loading table classes
* and other repositories onto properties of the host object.
*
* Example users of this trait are {@link \Cake\Controller\Controller} and
* {@link \Cake\Command\Command}.
*/
trait ModelAwareTrait
{
/**
* This object's primary model class name. Should be a plural form.
* CakePHP will not inflect the name.
*
* Example: For an object named 'Comments', the modelClass would be 'Comments'.
* Plugin classes should use `Plugin.Comments` style names to correctly load
* models from the correct plugin.
*
* Use empty string to not use auto-loading on this object. Null auto-detects based on
* controller name.
*
* @var string|null
*/
protected ?string $modelClass = null;
/**
* A list of overridden model factory functions.
*
* @var array<callable|\Cake\Datasource\Locator\LocatorInterface>
*/
protected array $_modelFactories = [];
/**
* The model type to use.
*
* @var string
*/
protected string $_modelType = 'Table';
/**
* Set the modelClass property based on conventions.
*
* If the property is already set it will not be overwritten
*
* @param string $name Class name.
* @return void
*/
protected function _setModelClass(string $name): void
{
$this->modelClass ??= $name;
}
/**
* Fetch or construct a model instance from a locator.
*
* Uses a modelFactory based on `$modelType` to fetch and construct a `RepositoryInterface`
* and return it. The default `modelType` can be defined with `setModelType()`.
*
* Unlike `loadModel()` this method will *not* set an object property.
*
* If a repository provider does not return an object a MissingModelException will
* be thrown.
*
* @param string|null $modelClass Name of model class to load. Defaults to $this->modelClass.
* The name can be an alias like `'Post'` or FQCN like `App\Model\Table\PostsTable::class`.
* @param string|null $modelType The type of repository to load. Defaults to the getModelType() value.
* @return \Cake\Datasource\RepositoryInterface The model instance created.
* @throws \Cake\Datasource\Exception\MissingModelException If the model class cannot be found.
* @throws \UnexpectedValueException If $modelClass argument is not provided
* and ModelAwareTrait::$modelClass property value is empty.
*/
public function fetchModel(?string $modelClass = null, ?string $modelType = null): RepositoryInterface
{
$modelClass ??= $this->modelClass;
if (!$modelClass) {
throw new UnexpectedValueException('Default modelClass is empty');
}
$modelType ??= $this->getModelType();
$options = [];
if (!str_contains($modelClass, '\\')) {
[, $alias] = pluginSplit($modelClass, true);
} else {
$options['className'] = $modelClass;
$alias = substr(
$modelClass,
strrpos($modelClass, '\\') + 1,
-strlen($modelType),
);
$modelClass = $alias;
}
$factory = $this->_modelFactories[$modelType] ?? FactoryLocator::get($modelType);
if ($factory instanceof LocatorInterface) {
$instance = $factory->get($modelClass, $options);
} else {
$instance = $factory($modelClass, $options);
}
if ($instance) {
return $instance;
}
throw new MissingModelException([$modelClass, $modelType]);
}
/**
* Override a existing callable to generate repositories of a given type.
*
* @param string $type The name of the repository type the factory function is for.
* @param \Cake\Datasource\Locator\LocatorInterface|callable $factory The factory function used to create instances.
* @return void
*/
public function modelFactory(string $type, LocatorInterface|callable $factory): void
{
$this->_modelFactories[$type] = $factory;
}
/**
* Get the model type to be used by this class
*
* @return string
*/
public function getModelType(): string
{
return $this->_modelType;
}
/**
* Set the model type to be used by this class
*
* @param string $modelType The model type
* @return $this
*/
public function setModelType(string $modelType)
{
$this->_modelType = $modelType;
return $this;
}
}
@@ -0,0 +1,34 @@
<?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.5.0
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource\Paging\Exception;
use Cake\Core\Exception\CakeException;
use Cake\Core\Exception\HttpErrorCodeInterface;
/**
* Exception raised when requested page number does not exist.
*/
class PageOutOfBoundsException extends CakeException implements HttpErrorCodeInterface
{
/**
* @inheritDoc
*/
protected int $_defaultCode = 404;
/**
* @inheritDoc
*/
protected string $_messageTemplate = 'Page number `%s` could not be found.';
}
@@ -0,0 +1,792 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.5.0
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource\Paging;
use Cake\Core\Exception\CakeException;
use Cake\Core\InstanceConfigTrait;
use Cake\Datasource\Paging\Exception\PageOutOfBoundsException;
use Cake\Datasource\QueryInterface;
use Cake\Datasource\RepositoryInterface;
use Cake\Datasource\ResultSetInterface;
use function Cake\Core\triggerWarning;
/**
* This class is used to handle automatic model data pagination.
*/
class NumericPaginator implements PaginatorInterface
{
use InstanceConfigTrait;
/**
* Default pagination settings.
*
* When calling paginate() these settings will be merged with the configuration
* you provide.
*
* - `maxLimit` - The maximum limit users can choose to view. Defaults to 100
* - `limit` - The initial number of items per page. Defaults to 20.
* - `page` - The starting page, defaults to 1.
* - `allowedParameters` - A list of parameters users are allowed to set using request
* parameters. Modifying this list will allow users to have more influence
* over pagination, be careful with what you permit.
* - `sortableFields` - Controls which fields can be used for sorting. Accepts multiple formats:
* - Simple array: A list of field names that can be sorted. By default all table
* columns can be used. Use this to restrict sorting to specific fields. An empty
* array will disable sorting altogether.
* - Map with SortField objects: A map of sort keys to their corresponding database fields.
* Allows creating friendly sort keys that map to one or more actual fields. Supports
* simple mapping, multi-column sorting, locked directions, and default directions.
* Can accept a callable that receives a SortableFieldsBuilder instance.
*
* Examples:
* ```
* // Simple array (traditional)
* 'sortableFields' => ['title', 'created', 'author_id']
*
* // Map with SortField objects
* 'sortableFields' => [
* 'name' => 'Users.name',
* 'newest' => [
* SortField::desc('created'),
* SortField::asc('title'),
* ],
* ]
*
* // Callable with builder
* 'sortableFields' => function(SortableFieldsBuilder $builder) {
* return $builder
* ->add('name', SortField::asc('Users.name'))
* ->add('popularity', SortField::desc('score', locked: true), 'created');
* }
* ```
* - `finder` - The table finder to use. Defaults to `all`.
* - `scope` - If specified this scope will be used to get the paging options
* from the query params passed to paginate(). Scopes allow namespacing the
* paging options and allows paginating multiple models in the same action.
* Default `null`.
*
* @var array<string, mixed>
*/
protected array $_defaultConfig = [
'page' => 1,
'limit' => 20,
'maxLimit' => 100,
'allowedParameters' => ['limit', 'sort', 'page', 'direction'],
'sortableFields' => null,
'finder' => 'all',
'scope' => null,
];
/**
* Calculated paging params.
*
* @var array
*/
protected array $pagingParams = [
'limit' => null,
'maxLimit' => null,
'count' => null,
'totalCount' => null,
'perPage' => null,
'pageCount' => null,
'currentPage' => null,
'requestedPage' => null,
'start' => null,
'end' => null,
'hasPrevPage' => null,
'hasNextPage' => null,
'sort' => null,
'sortDefault' => null,
'direction' => null,
'directionDefault' => null,
'completeSort' => null,
'alias' => null,
'scope' => null,
];
/**
* Handles automatic pagination of model records.
*
* ### Configuring pagination
*
* When calling `paginate()` you can use the $settings parameter to pass in
* pagination settings. These settings are used to build the queries made
* and control other pagination settings.
*
* If your settings contain a key with the current table's alias. The data
* inside that key will be used. Otherwise, the top level configuration will
* be used.
*
* ```
* $settings = [
* 'limit' => 20,
* 'maxLimit' => 100
* ];
* $results = $paginator->paginate($table, $settings);
* ```
*
* The above settings will be used to paginate any repository. You can configure
* repository specific settings by keying the settings with the repository alias.
*
* ```
* $settings = [
* 'Articles' => [
* 'limit' => 20,
* 'maxLimit' => 100
* ],
* 'Comments' => [ ... ]
* ];
* $results = $paginator->paginate($table, $settings);
* ```
*
* This would allow you to have different pagination settings for
* `Articles` and `Comments` repositories.
*
* ### Controlling sort fields
*
* By default CakePHP will automatically allow sorting on any column on the
* repository object being paginated. Often times you will want to allow
* sorting on either associated columns or calculated fields. In these cases
* you will need to define an allowed list of all the columns you wish to allow
* sorting on. You can define the allowed sort fields in the `$settings` parameter:
*
* ```
* $settings = [
* 'Articles' => [
* 'finder' => 'custom',
* 'sortableFields' => ['title', 'author_id', 'comment_count'],
* ]
* ];
* ```
*
* Passing an empty array as sortableFields disallows sorting altogether.
*
* ### Paginating with custom finders
*
* You can paginate with any find type defined on your table using the
* `finder` option.
*
* ```
* $settings = [
* 'Articles' => [
* 'finder' => 'popular'
* ]
* ];
* $results = $paginator->paginate($table, $settings);
* ```
*
* Would paginate using the `find('popular')` method.
*
* You can also pass an already created instance of a query to this method:
*
* ```
* $query = $this->Articles->find('popular')->matching('Tags', function ($q) {
* return $q->where(['name' => 'CakePHP'])
* });
* $results = $paginator->paginate($query);
* ```
*
* ### Scoping Request parameters
*
* By using request parameter scopes you can paginate multiple queries in
* the same controller action:
*
* ```
* $articles = $paginator->paginate($articlesQuery, ['scope' => 'articles']);
* $tags = $paginator->paginate($tagsQuery, ['scope' => 'tags']);
* ```
*
* Each of the above queries will use different query string parameter sets
* for pagination data. An example URL paginating both results would be:
*
* ```
* /dashboard?articles[page]=1&tags[page]=2
* ```
*
* @param mixed $target The repository or query
* to paginate.
* @param array $params Request params
* @param array $settings The settings/configuration used for pagination.
* @return \Cake\Datasource\Paging\PaginatedInterface
* @throws \Cake\Datasource\Paging\Exception\PageOutOfBoundsException
*/
public function paginate(
mixed $target,
array $params = [],
array $settings = [],
): PaginatedInterface {
$query = null;
if ($target instanceof QueryInterface) {
$query = $target;
$target = $query->getRepository();
if ($target === null) {
throw new CakeException('No repository set for query.');
}
}
assert(
$target instanceof RepositoryInterface,
'Pagination target must be an instance of `' . QueryInterface::class
. '` or `' . RepositoryInterface::class . '`.',
);
$data = $this->extractData($target, $params, $settings);
$query = $this->getQuery($target, $query, $data);
$countQuery = clone $query;
$items = $this->getItems($query, $data);
$this->pagingParams['count'] = count($items);
$this->pagingParams['totalCount'] = $this->getCount($countQuery, $data);
$pagingParams = $this->buildParams($data);
if ($pagingParams['requestedPage'] > $pagingParams['currentPage']) {
throw new PageOutOfBoundsException([
'requestedPage' => $pagingParams['requestedPage'],
'pagingParams' => $pagingParams,
]);
}
return $this->buildPaginated($items, $pagingParams);
}
/**
* Build paginated result set.
*
* @param \Cake\Datasource\ResultSetInterface $items
* @param array $pagingParams
* @return \Cake\Datasource\Paging\PaginatedInterface
*/
protected function buildPaginated(ResultSetInterface $items, array $pagingParams): PaginatedInterface
{
return new PaginatedResultSet($items, $pagingParams);
}
/**
* Get query for fetching paginated results.
*
* @param \Cake\Datasource\RepositoryInterface $object Repository instance.
* @param \Cake\Datasource\QueryInterface|null $query Query Instance.
* @param array<string, mixed> $data Pagination data.
* @return \Cake\Datasource\QueryInterface
*/
protected function getQuery(RepositoryInterface $object, ?QueryInterface $query, array $data): QueryInterface
{
$options = $data['options'];
$queryOptions = array_intersect_key(
$options,
['order' => null, 'page' => null, 'limit' => null],
);
$args = [];
$type = $options['finder'] ?? null;
if (is_array($type)) {
$args = (array)current($type);
$type = key($type);
}
if ($query === null) {
$query = $object->find($type ?? 'all', ...$args);
} elseif ($type !== null) {
$query->find($type, ...$args);
}
$query->applyOptions($queryOptions);
return $query;
}
/**
* Get paginated items.
*
* @param \Cake\Datasource\QueryInterface $query Query to fetch items.
* @param array $data Paging data.
* @return \Cake\Datasource\ResultSetInterface
*/
protected function getItems(QueryInterface $query, array $data): ResultSetInterface
{
return $query->all();
}
/**
* Get total count of records.
*
* @param \Cake\Datasource\QueryInterface $query Query instance.
* @param array $data Pagination data.
* @return int|null
*/
protected function getCount(QueryInterface $query, array $data): ?int
{
return $query->count();
}
/**
* Extract pagination data needed
*
* @param \Cake\Datasource\RepositoryInterface $object The repository object.
* @param array<string, mixed> $params Request params
* @param array<string, mixed> $settings The settings/configuration used for pagination.
* @return array
*/
protected function extractData(RepositoryInterface $object, array $params, array $settings): array
{
$alias = $object->getAlias();
$defaults = $this->getDefaults($alias, $settings);
$validSettings = array_keys($this->_defaultConfig);
$validSettings[] = 'order';
$extraSettings = array_diff_key($defaults, array_flip($validSettings));
if ($extraSettings) {
triggerWarning(
'Passing query options as paginator settings is no longer supported.'
. ' Use a custom finder through the `finder` config or pass a SelectQuery instance to paginate().'
. ' Extra keys found are: `' . implode('`, `', array_keys($extraSettings)) . '`.',
);
}
$options = $this->mergeOptions($params, $defaults);
$options = $this->validateSort($object, $options);
$options = $this->checkLimit($options);
$options['page'] = max((int)$options['page'], 1);
return compact('defaults', 'options', 'alias');
}
/**
* Build pagination params.
*
* @param array<string, mixed> $data Paginator data containing keys 'options',
* 'defaults', 'alias'.
* @return array<string, mixed> Paging params.
*/
protected function buildParams(array $data): array
{
$this->pagingParams = [
'perPage' => $data['options']['limit'],
'requestedPage' => $data['options']['page'],
'alias' => $data['alias'],
'scope' => $data['options']['scope'],
'maxLimit' => $data['options']['maxLimit'],
] + $this->pagingParams;
$this->addPageCountParams($data);
$this->addStartEndParams($data);
$this->addPrevNextParams($data);
$this->addSortingParams($data);
$this->pagingParams['limit'] = $data['defaults']['limit'] != $data['options']['limit']
? $data['options']['limit']
: null;
// Add sortableFields configuration for view helpers
if (isset($data['options']['sortableFields'])) {
$sortableFields = $data['options']['sortableFields'];
if ($sortableFields instanceof SortableFieldsBuilder) {
$this->pagingParams['sortableFields'] = $sortableFields->toArray();
}
}
return $this->pagingParams;
}
/**
* Add "currentPage" and "pageCount" params.
*
* @param array $data Paginator data.
* @return void
*/
protected function addPageCountParams(array $data): void
{
$page = $data['options']['page'];
$pageCount = null;
if ($this->pagingParams['totalCount'] !== null) {
$pageCount = max((int)ceil($this->pagingParams['totalCount'] / $this->pagingParams['perPage']), 1);
$page = min($page, $pageCount);
} elseif ($this->pagingParams['count'] === 0 && $this->pagingParams['requestedPage'] > 1) {
$page = 1;
}
$this->pagingParams['currentPage'] = $page;
$this->pagingParams['pageCount'] = $pageCount;
}
/**
* Add "start" and "end" params.
*
* @param array $data Paginator data.
* @return void
*/
protected function addStartEndParams(array $data): void
{
$start = 0;
$end = 0;
if ($this->pagingParams['count'] > 0) {
$start = (($this->pagingParams['currentPage'] - 1) * $this->pagingParams['perPage']) + 1;
$end = $start + $this->pagingParams['count'] - 1;
}
$this->pagingParams['start'] = $start;
$this->pagingParams['end'] = $end;
}
/**
* Add "prevPage" and "nextPage" params.
*
* @param array $data Paging data.
* @return void
*/
protected function addPrevNextParams(array $data): void
{
$this->pagingParams['hasPrevPage'] = $this->pagingParams['currentPage'] > 1;
if ($this->pagingParams['totalCount'] === null) {
$this->pagingParams['hasNextPage'] = true;
} else {
$this->pagingParams['hasNextPage'] = $this->pagingParams['totalCount']
> $this->pagingParams['currentPage'] * $this->pagingParams['perPage'];
}
}
/**
* Add sorting / ordering params.
*
* @param array $data Paging data.
* @return void
*/
protected function addSortingParams(array $data): void
{
$defaults = $data['defaults'];
$order = (array)$data['options']['order'];
$sortDefault = false;
$directionDefault = false;
if (!empty($defaults['order']) && count($defaults['order']) >= 1) {
$sortDefault = key($defaults['order']);
$directionDefault = current($defaults['order']);
}
if (isset($data['options']['sortDirection'])) {
$direction = $data['options']['sortDirection'];
} else {
$direction = isset($data['options']['sort']) && count($order) ? current($order) : null;
}
$this->pagingParams = [
'sort' => $data['options']['sort'],
'direction' => $direction,
'sortDefault' => $sortDefault,
'directionDefault' => $directionDefault,
'completeSort' => $order,
] + $this->pagingParams;
}
/**
* Merges the various options that Paginator uses.
* Pulls settings together from the following places:
*
* - General pagination settings
* - Model specific settings.
* - Request parameters
*
* The result of this method is the aggregate of all the option sets
* combined together. You can change config value `allowedParameters` to modify
* which options/values can be set using request parameters.
*
* @param array<string, mixed> $params Request params.
* @param array $settings The settings to merge with the request data.
* @return array<string, mixed> Array of merged options.
*/
protected function mergeOptions(array $params, array $settings): array
{
if (!empty($settings['scope'])) {
$scope = $settings['scope'];
$params = !empty($params[$scope]) ? (array)$params[$scope] : [];
}
$params = array_intersect_key($params, array_flip($this->getConfig('allowedParameters')));
return array_merge($settings, $params);
}
/**
* Get the settings for a $model. If there are no settings for a specific
* repository, the general settings will be used.
*
* @param string $alias Model name to get settings for.
* @param array<string, mixed> $settings The settings which is used for combining.
* @return array<string, mixed> An array of pagination settings for a model,
* or the general settings.
*/
protected function getDefaults(string $alias, array $settings): array
{
if (isset($settings[$alias])) {
$settings = $settings[$alias];
}
$defaults = $this->getConfig();
$maxLimit = $settings['maxLimit'] ?? $defaults['maxLimit'];
$limit = $settings['limit'] ?? $defaults['limit'];
if ($limit > $maxLimit) {
$limit = $maxLimit;
}
$settings['maxLimit'] = $maxLimit;
$settings['limit'] = $limit;
return $settings + $defaults;
}
/**
* Validate that the desired sorting can be performed on the $object.
*
* Only fields or virtualFields can be sorted on. The direction param will
* also be sanitized. Lastly sort + direction keys will be converted into
* the model friendly order key.
*
/**
* You can use the allowedParameters option to control which columns/fields are
* available for sorting via URL parameters. This helps prevent users from ordering large
* result sets on un-indexed values.
*
* If you need to sort on associated columns or synthetic properties you
* will need to use the `sortableFields` option.
*
* Any columns listed in the allowed sort fields will be implicitly trusted.
* You can use this to sort on synthetic columns, or columns added in custom
* find operations that may not exist in the schema.
*
* The default order options provided to paginate() will be merged with the user's
* requested sorting field/direction.
*
* @param \Cake\Datasource\RepositoryInterface $object Repository object.
* @param array<string, mixed> $options The pagination options being used for this request.
* @return array<string, mixed> An array of options with sort + direction removed and
* replaced with order if possible.
*/
protected function validateSort(RepositoryInterface $object, array $options): array
{
// Check if we have sortableFields configured
$sortableFields = $options['sortableFields'] ?? null;
$builder = $sortableFields instanceof SortableFieldsBuilder
? $sortableFields
: SortableFieldsBuilder::create($sortableFields);
// Store the converted builder for later use in paging params
if ($builder !== null) {
$options['sortableFields'] = $builder;
}
$sortAllowed = $builder !== null;
if (isset($options['sort'])) {
// Parse sort and direction parameters
$sortParams = $this->parseSortParams($options);
// Update options with parsed sort key (handles combined format)
$options['sort'] = $sortParams['sortKey'];
if ($builder !== null) {
// Use builder to resolve sort key
$order = $builder->resolve(
$sortParams['sortKey'],
$sortParams['direction'],
$sortParams['directionSpecified'],
);
if ($order === null) {
// Invalid sort key, clear sort
$options['order'] = [];
$options['sort'] = null;
unset($options['direction']);
return $options;
}
// Merge with existing order - existing order comes AFTER our resolved order
$existingOrder = isset($options['order']) && is_array($options['order']) ? $options['order'] : [];
// Only keep fields from existing order that aren't already in our resolved order
foreach ($existingOrder as $field => $dir) {
if (!isset($order[$field])) {
$order[$field] = $dir;
}
}
$options['order'] = $order;
$options['sortDirection'] = $sortParams['direction'];
} else {
// No sortableFields configured - allow any field (default behavior)
$order = isset($options['order']) && is_array($options['order']) ? $options['order'] : [];
if ($order && $sortParams['sortKey'] && !str_contains($sortParams['sortKey'], '.')) {
$order = $this->_removeAliases($order, $object->getAlias());
}
$options['order'] = [$sortParams['sortKey'] => $sortParams['direction']] + $order;
}
} else {
$options['sort'] = null;
}
unset($options['direction']);
if (empty($options['order'])) {
$options['order'] = [];
}
if (!is_array($options['order'])) {
return $options;
}
if (
$options['sort'] === null
&& count($options['order']) >= 1
&& !is_numeric(key($options['order']))
) {
$options['sort'] = key($options['order']);
}
$options['order'] = $this->_prefix($object, $options['order'], $sortAllowed);
return $options;
}
/**
* Remove alias if needed.
*
* @param array<string, mixed> $fields Current fields
* @param string $model Current model alias
* @return array<string, mixed> $fields Unaliased fields where applicable
*/
protected function _removeAliases(array $fields, string $model): array
{
$result = [];
foreach ($fields as $field => $sort) {
if (is_int($field)) {
throw new CakeException(sprintf(
'The `order` config must be an associative array. Found invalid value with numeric key: `%s`',
$sort,
));
}
if (!str_contains($field, '.')) {
$result[$field] = $sort;
continue;
}
[$alias, $currentField] = explode('.', $field);
if ($alias === $model) {
$result[$currentField] = $sort;
continue;
}
$result[$field] = $sort;
}
return $result;
}
/**
* Prefixes the field with the table alias if possible.
*
* @param \Cake\Datasource\RepositoryInterface $object Repository object.
* @param array $order Order array.
* @param bool $allowed Whether the field was allowed.
* @return array Final order array.
*/
protected function _prefix(RepositoryInterface $object, array $order, bool $allowed = false): array
{
$tableAlias = $object->getAlias();
$tableOrder = [];
foreach ($order as $key => $value) {
if (is_numeric($key)) {
$tableOrder[] = $value;
continue;
}
$field = $key;
$alias = $tableAlias;
if (str_contains($key, '.')) {
[$alias, $field] = explode('.', $key);
}
$correctAlias = ($tableAlias === $alias);
if ($correctAlias && $allowed) {
// Disambiguate fields in schema. As id is quite common.
if ($object->hasField($field)) {
$field = $alias . '.' . $field;
}
$tableOrder[$field] = $value;
} elseif ($correctAlias && $object->hasField($field)) {
$tableOrder[$tableAlias . '.' . $field] = $value;
} elseif (!$correctAlias && $allowed) {
$tableOrder[$alias . '.' . $field] = $value;
}
}
return $tableOrder;
}
/**
* Parse sort parameters from options.
*
* Extracts and normalizes sort key and direction from pagination options.
* Supports both traditional format (?sort=field&direction=asc) and
* combined format (?sort=field-asc).
*
* @param array<string, mixed> $options The options array
* @return array{sortKey: string, direction: string, directionSpecified: bool}
*/
protected function parseSortParams(array $options): array
{
$sortKey = $options['sort'];
$direction = isset($options['direction']) ? strtolower($options['direction']) : SortField::ASC;
$directionSpecified = isset($options['direction']);
// Check for combined sort-direction format (e.g., 'title-asc' or 'title-desc')
if (preg_match('/^(.+)-(asc|desc)$/i', $sortKey, $matches)) {
$sortKey = $matches[1];
$direction = strtolower($matches[2]);
$directionSpecified = true;
}
// Validate direction
if (!in_array($direction, [SortField::ASC, SortField::DESC], true)) {
$direction = SortField::ASC;
}
return [
'sortKey' => $sortKey,
'direction' => $direction,
'directionSpecified' => $directionSpecified,
];
}
/**
* Check the limit parameter and ensure it's within the maxLimit bounds.
*
* @param array<string, mixed> $options An array of options with a limit key to be checked.
* @return array<string, mixed> An array of options for pagination.
*/
protected function checkLimit(array $options): array
{
$options['limit'] = (int)$options['limit'];
if ($options['limit'] < 1) {
$options['limit'] = 1;
}
$options['limit'] = max(min($options['limit'], $options['maxLimit']), 1);
return $options;
}
}
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://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. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since 5.0.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource\Paging;
use Countable;
use Traversable;
/**
* This interface describes the methods for pagination instance.
*
* @template TKey
* @template-covariant TValue
* @template-extends \Traversable<TKey, TValue>
* @method array<mixed> toArray() Get the paginated items as an array
*/
interface PaginatedInterface extends Countable, Traversable
{
/**
* Get current page number.
*
* @return int
*/
public function currentPage(): int;
/**
* Get items per page.
*
* @return int
*/
public function perPage(): int;
/**
* Get Total items counts.
*
* @return int|null
*/
public function totalCount(): ?int;
/**
* Get total page count.
*
* @return int|null
*/
public function pageCount(): ?int;
/**
* Get whether there's a previous page.
*
* @return bool
*/
public function hasPrevPage(): bool;
/**
* Get whether there's a next page.
*
* @return bool
*/
public function hasNextPage(): bool;
/**
* Get paginated items.
*
* @return iterable<TKey, TValue>
*/
public function items(): iterable;
/**
* Get paging param.
*
* @param string $name
* @return mixed
*/
public function pagingParam(string $name): mixed;
/**
* Get all paging params.
*
* @return array
*/
public function pagingParams(): array;
}
@@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 5.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource\Paging;
use IteratorAggregate;
use JsonSerializable;
use Traversable;
use function Cake\Core\deprecationWarning;
/**
* Paginated result set.
*
* @template TKey
* @template TValue
* @implements \IteratorAggregate<TKey, TValue>
* @implements \Cake\Datasource\Paging\PaginatedInterface<TKey, TValue>
*/
class PaginatedResultSet implements IteratorAggregate, JsonSerializable, PaginatedInterface
{
/**
* Resultset instance.
*
* @var \Traversable<TKey, TValue>
*/
protected Traversable $results;
/**
* Paging params.
*
* @var array
*/
protected array $params = [];
/**
* Constructor
*
* @param \Traversable<TKey, TValue> $results Resultset instance.
* @param array $params Paging params.
*/
public function __construct(Traversable $results, array $params)
{
$this->results = $results;
$this->params = $params;
}
/**
* @inheritDoc
*/
public function count(): int
{
return $this->params['count'];
}
/**
* Get the paginated items as an array.
*
* This will exhaust the iterator `items`.
*
* @return array<array-key, TValue>
*/
public function toArray(): array
{
return $this->jsonSerialize();
}
/**
* Get paginated items.
*
* @return \Traversable<TKey, TValue> The paginated items result set.
*/
public function items(): Traversable
{
return $this->results;
}
/**
* Provide data which should be serialized to JSON.
*
* @return array
*/
public function jsonSerialize(): array
{
return iterator_to_array($this->items());
}
/**
* @inheritDoc
*/
public function totalCount(): ?int
{
return $this->params['totalCount'];
}
/**
* @inheritDoc
*/
public function perPage(): int
{
return $this->params['perPage'];
}
/**
* @inheritDoc
*/
public function pageCount(): ?int
{
return $this->params['pageCount'];
}
/**
* @inheritDoc
*/
public function currentPage(): int
{
return $this->params['currentPage'];
}
/**
* @inheritDoc
*/
public function hasPrevPage(): bool
{
return $this->params['hasPrevPage'];
}
/**
* @inheritDoc
*/
public function hasNextPage(): bool
{
return $this->params['hasNextPage'];
}
/**
* @inheritDoc
*/
public function pagingParam(string $name): mixed
{
return $this->params[$name] ?? null;
}
/**
* @inheritDoc
*/
public function pagingParams(): array
{
return $this->params;
}
/**
* @inheritDoc
*/
public function getIterator(): Traversable
{
return $this->results;
}
/**
* Proxies method calls to internal result set instance.
*
* @param string $name Method name
* @param array $arguments Arguments
* @return mixed
*/
public function __call(string $name, array $arguments): mixed
{
deprecationWarning(
'5.1.0',
sprintf(
'Calling `%s` methods, such as `%s()`, on PaginatedResultSet is deprecated. ' .
'You must call `items()` first (for example, `items()->%s()`).',
$this->results::class,
$name,
$name,
),
);
return $this->results->$name(...$arguments);
}
}
@@ -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. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since 5.0.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource\Paging;
/**
* This interface describes the methods for paginator instance.
*/
interface PaginatorInterface
{
/**
* Handles pagination of data.
*
* @param mixed $target Anything that needs to be paginated.
* @param array $params Request params.
* @param array $settings The settings/configuration used for pagination.
* @return \Cake\Datasource\Paging\PaginatedInterface
*/
public function paginate(
mixed $target,
array $params = [],
array $settings = [],
): PaginatedInterface;
}
@@ -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 3.9.0
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource\Paging;
use Cake\Datasource\QueryInterface;
use Cake\Datasource\ResultSetInterface;
/**
* Simplified paginator which avoids potentially expensive queries
* to get the total count of records.
*
* When using a simple paginator you will not be able to generate page numbers.
* Instead use only the prev/next pagination controls.
*/
class SimplePaginator extends NumericPaginator
{
/**
* Get paginated items.
*
* Get one additional record than the limit. This helps deduce if next page exits.
*
* @param \Cake\Datasource\QueryInterface $query Query to fetch items.
* @param array $data Paging data.
* @return \Cake\Datasource\ResultSetInterface
*/
protected function getItems(QueryInterface $query, array $data): ResultSetInterface
{
return $query->limit($data['options']['limit'] + 1)->all();
}
/**
* @inheritDoc
*/
protected function buildParams(array $data): array
{
$hasNextPage = false;
if ($this->pagingParams['count'] > $data['options']['limit']) {
$hasNextPage = true;
$this->pagingParams['count'] -= 1;
}
parent::buildParams($data);
$this->pagingParams['hasNextPage'] = $hasNextPage;
return $this->pagingParams;
}
/**
* Build paginated result set.
*
* Since the query fetches an extra record, drop the last record if records
* fetched exceeds the limit/per page.
*
* @param \Cake\Datasource\ResultSetInterface $items
* @param array $pagingParams
* @return \Cake\Datasource\Paging\PaginatedInterface
*/
protected function buildPaginated(ResultSetInterface $items, array $pagingParams): PaginatedInterface
{
if (count($items) > $this->pagingParams['perPage']) {
$items = $items->take($this->pagingParams['perPage']);
}
return new PaginatedResultSet($items, $pagingParams);
}
/**
* Simple pagination does not perform any count query, so this method returns `null`.
*
* @param \Cake\Datasource\QueryInterface $query Query instance.
* @param array $data Pagination data.
* @return int|null
*/
protected function getCount(QueryInterface $query, array $data): ?int
{
return null;
}
}
+119
View File
@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 5.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource\Paging;
/**
* Represents a sort field configuration for pagination.
*/
class SortField
{
/**
* Ascending sort direction
*
* @var string
*/
public const ASC = 'asc';
/**
* Descending sort direction
*
* @var string
*/
public const DESC = 'desc';
/**
* Constructor.
*
* @param string $field The field name to sort by
* @param string|null $defaultDirection The default sort direction
* @param bool $locked Whether the sort direction is locked
*/
public function __construct(
protected string $field,
protected ?string $defaultDirection = null,
protected bool $locked = false,
) {
}
/**
* Create a sort field with ascending default direction.
*
* @param string $field The field name to sort by
* @param bool $locked Whether the sort direction is locked
* @return self
*/
public static function asc(string $field, bool $locked = false): self
{
return new self($field, self::ASC, $locked);
}
/**
* Create a sort field with descending default direction.
*
* @param string $field The field name to sort by
* @param bool $locked Whether the sort direction is locked
* @return self
*/
public static function desc(string $field, bool $locked = false): self
{
return new self($field, self::DESC, $locked);
}
/**
* Get the field name.
*
* @return string
*/
public function getField(): string
{
return $this->field;
}
/**
* Get the sort direction to use.
*
* @param string $requestedDirection The direction requested by the user
* @param bool $directionSpecified Whether a direction was explicitly specified
* @return string
*/
public function getDirection(string $requestedDirection, bool $directionSpecified): string
{
if ($this->locked) {
return $this->defaultDirection ?? self::ASC;
}
if (!$directionSpecified && $this->defaultDirection) {
return $this->defaultDirection;
}
if ($this->defaultDirection === static::DESC) {
return $requestedDirection === static::DESC ? static::ASC : static::DESC;
}
return $requestedDirection;
}
/**
* Check if the sort direction is locked.
*
* @return bool
*/
public function isLocked(): bool
{
return $this->locked;
}
}
@@ -0,0 +1,270 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 5.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource\Paging;
use Closure;
use InvalidArgumentException;
/**
* Builder for creating complete sortable fields configurations.
*
* Provides interface for building sortable fields with multiple sort keys and fields.
* Also handles resolution of sort keys to database ORDER BY clauses.
*/
class SortableFieldsBuilder
{
/**
* @var array<string, array<\Cake\Datasource\Paging\SortField|string>|string> The sortable fields map being built
*/
protected array $map = [];
/**
* @var bool Whether this builder represents a simple array format
*/
protected bool $isSimpleArray = false;
/**
* Create builder from various sortableFields configurations.
*
* @param \Closure|array<mixed>|null $config The sortableFields configuration
* @return static|null Builder instance or null if no config
*/
public static function create(array|Closure|null $config): ?static
{
if ($config === null) {
return null;
}
if ($config instanceof Closure) {
return static::fromCallable($config);
}
return static::fromArray($config);
}
/**
* Create builder from array configuration.
*
* Handles both simple array format (['field1', 'field2']) and
* associative map format (['key' => 'field', ...]).
*
* @param array<mixed> $config Array configuration
* @return static
*/
public static function fromArray(array $config): static
{
$builder = new static();
$hasNumericKeys = false;
// Check if it's a simple array format
foreach ($config as $key => $value) {
if (is_int($key)) {
$hasNumericKeys = true;
break;
}
}
if ($hasNumericKeys) {
// Simple or mixed format - convert numeric keys
$builder->isSimpleArray = true;
foreach ($config as $key => $value) {
if (is_int($key) && is_string($value)) {
// Numeric key with string value: 'field' becomes 'field' => ['field']
$builder->add($value, $value);
} else {
// String key: use as-is
$builder->set($key, $value);
}
}
} else {
// Associative map format
foreach ($config as $key => $value) {
$builder->set($key, $value);
}
}
return $builder;
}
/**
* Create builder from callable factory.
*
* @param \Closure $factory Closure that receives builder and returns it
* @return static
*/
public static function fromCallable(Closure $factory): static
{
$builder = new static();
$builder = $factory($builder);
return $builder;
}
/**
* Add a sort key with its associated SortField objects.
*
* @param string $sortKey The sort key name
* @param \Cake\Datasource\Paging\SortField|string ...$fields The sort fields to add
* @return $this
*/
public function add(string $sortKey, SortField|string ...$fields)
{
if ($fields === []) {
// If no fields provided, use the key as the field name
$this->map[$sortKey] = [$sortKey];
} else {
$this->map[$sortKey] = $fields;
}
return $this;
}
/**
* Set a sort key with type-safe validation.
*
* Internal method used by fromArray() to ensure type safety while preserving
* backward compatibility with string and array representations.
*
* @param string $sortKey The sort key name
* @param mixed $value The sort field(s) - can be string, SortField, or array
* @return $this
*/
protected function set(string $sortKey, mixed $value)
{
if (is_string($value)) {
$this->map[$sortKey] = $value;
} elseif ($value instanceof SortField) {
$this->map[$sortKey] = [$value];
} elseif (is_array($value)) {
$this->add($sortKey, ...$value);
} else {
throw new InvalidArgumentException(sprintf(
'Invalid sortable field value type for key `%s`. Expected string, array, or SortField, got `%s`.',
$sortKey,
get_debug_type($value),
));
}
return $this;
}
/**
* Return the complete sortable fields map.
*
* @return array<string, array<\Cake\Datasource\Paging\SortField|string>|string>
*/
public function toArray(): array
{
return $this->map;
}
/**
* Resolve a sort key to its corresponding ORDER BY clause.
*
* @param string $sortKey The sort key from URL
* @param string $direction The requested direction (asc/desc)
* @param bool $directionSpecified Whether direction was explicitly specified
* @return array<string, string>|null Array of field => direction pairs, or null if invalid
*/
public function resolve(
string $sortKey,
string $direction,
bool $directionSpecified = true,
): ?array {
// Check if sort key exists in map
if (!isset($this->map[$sortKey])) {
return null;
}
$mapping = $this->map[$sortKey];
// Empty array means use key as field
if ($mapping === []) {
return [$sortKey => $direction];
}
return $this->resolveMapping($mapping, $direction, $directionSpecified);
}
/**
* Resolve a mapping configuration to ORDER BY clause.
*
* @param mixed $mapping The mapping to resolve
* @param string $direction The requested direction
* @param bool $directionSpecified Whether direction was explicitly specified
* @return array<string, string> Array of field => direction pairs
*/
protected function resolveMapping(mixed $mapping, string $direction, bool $directionSpecified): array
{
// Single string: 'name' => 'Users.name'
if (is_string($mapping)) {
return [$mapping => $direction];
}
// Array of fields/SortField objects
if (is_array($mapping)) {
return $this->resolveArrayMapping($mapping, $direction, $directionSpecified);
}
return [];
}
/**
* Resolve an array mapping to ORDER BY clause.
*
* @param array<mixed> $fields Array of fields or SortField objects
* @param string $direction The requested direction
* @param bool $directionSpecified Whether direction was explicitly specified
* @return array<string, string> Array of field => direction pairs
*/
protected function resolveArrayMapping(array $fields, string $direction, bool $directionSpecified): array
{
$order = [];
$shouldInvert = $directionSpecified && $direction === SortField::DESC;
foreach ($fields as $key => $value) {
if ($value instanceof SortField) {
// SortField object with locked/default directions
$field = $value->getField();
$fieldDirection = $value->getDirection($direction, $directionSpecified);
$order[$field] = $fieldDirection;
} elseif (is_int($key)) {
// Numeric array: ['field1', 'field2'] - use requested direction
$order[$value] = $direction;
} elseif (is_string($value)) {
// Associative array with default directions per field
// Format: ['field1' => 'ASC', 'field2' => 'DESC']
$defaultDirection = strtolower($value);
if ($shouldInvert) {
// Invert the direction when toggling to desc
$fieldDirection = $defaultDirection === SortField::ASC ? SortField::DESC : SortField::ASC;
} else {
// Use default direction (for asc or no direction specified)
$fieldDirection = $defaultDirection;
}
$order[$key] = $fieldDirection;
} else {
// Fallback for other cases
$order[$key] = $direction;
}
}
return $order;
}
}
+129
View File
@@ -0,0 +1,129 @@
<?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\Datasource;
use Cake\Cache\Cache;
use Cake\Core\Exception\CakeException;
use Closure;
use Psr\SimpleCache\CacheInterface;
use Traversable;
/**
* Handles caching queries and loading results from the cache.
*
* Used by {@link \Cake\Datasource\QueryTrait} internally.
*
* @internal
* @see \Cake\Datasource\QueryTrait::cache() for the public interface.
*/
class QueryCacher
{
/**
* The key or function to generate a key.
*
* @var \Closure|string
*/
protected Closure|string $_key;
/**
* Config for cache engine.
*
* @var \Psr\SimpleCache\CacheInterface|string
*/
protected CacheInterface|string $_config;
/**
* Constructor.
*
* @param \Closure|string $key The key or function to generate a key.
* @param \Psr\SimpleCache\CacheInterface|string $config The cache config name or cache engine instance.
*/
public function __construct(Closure|string $key, CacheInterface|string $config)
{
$this->_key = $key;
$this->_config = $config;
}
/**
* Load the cached results from the cache or run the query.
*
* @param object $query The query the cache read is for.
* @return mixed|null Either the cached results or null.
*/
public function fetch(object $query): mixed
{
$key = $this->_resolveKey($query);
$storage = $this->_resolveCacher();
$result = $storage->get($key);
if (!$result) {
return null;
}
return $result;
}
/**
* Store the result set into the cache.
*
* @param object $query The query the cache read is for.
* @param \Traversable $results The result set to store.
* @return bool True if the data was successfully cached, false on failure
*/
public function store(object $query, Traversable $results): bool
{
$key = $this->_resolveKey($query);
$storage = $this->_resolveCacher();
return $storage->set($key, $results);
}
/**
* Get/generate the cache key.
*
* @param object $query The query to generate a key for.
* @return string
* @throws \Cake\Core\Exception\CakeException
*/
protected function _resolveKey(object $query): string
{
if (is_string($this->_key)) {
return $this->_key;
}
$func = $this->_key;
$key = $func($query);
if (!is_string($key)) {
$msg = sprintf('Cache key functions must return a string. Got %s.', var_export($key, true));
throw new CakeException($msg);
}
return $key;
}
/**
* Get the cache engine.
*
* @return \Psr\SimpleCache\CacheInterface
*/
protected function _resolveCacher(): CacheInterface
{
if (is_string($this->_config)) {
return Cache::pool($this->_config);
}
return $this->_config;
}
}
+461
View File
@@ -0,0 +1,461 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.1
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource;
use Closure;
/**
* The basis for every query object
*
* @method $this andWhere($conditions, array $types = []) Connects any previously defined set of conditions to the
* provided list using the AND operator. {@see \Cake\Database\Query::andWhere()}
* @method \Cake\Datasource\EntityInterface|array firstOrFail() Get the first result from the executing query or raise an exception.
* {@see \Cake\Database\Query::firstOrFail()}
*/
interface QueryInterface
{
/**
* Adds fields to be selected from datasource.
*
* Calling this function multiple times will append more fields to the list
* of fields to be selected.
*
* If `true` is passed in the second argument, any previous selections will
* be overwritten with the list passed in the first argument.
*
* @param \Closure|array|string|float|int $fields Fields.
* @param bool $overwrite whether to reset fields with passed list or not
* @return $this
*/
public function select(Closure|array|string|float|int $fields, bool $overwrite = false);
/**
* Returns a key => value array representing a single aliased field
* that can be passed directly to the select() method.
* The key will contain the alias and the value the actual field name.
*
* If the field is already aliased, then it will not be changed.
* If no $alias is passed, the default table for this query will be used.
*
* @param string $field The field to alias
* @param string|null $alias the alias used to prefix the field
* @return array<string, string>
*/
public function aliasField(string $field, ?string $alias = null): array;
/**
* Runs `aliasField()` for each field in the provided list and returns
* the result under a single array.
*
* @param array<int|string, string|\Cake\Database\Expression\IdentifierExpression> $fields The fields to alias
* @param string|null $defaultAlias The default alias
* @return array<int|string, string|\Cake\Database\Expression\IdentifierExpression>
*/
public function aliasFields(array $fields, ?string $defaultAlias = null): array;
/**
* Fetch the results for this query.
*
* Will return either the results set through setResult(), or execute this query
* and return the ResultSetDecorator object ready for streaming of results.
*
* ResultSetDecorator is a traversable object that implements the methods found
* on Cake\Collection\Collection.
*
* @template TKey of array-key
* @template TValue of mixed
* @return \Cake\Datasource\ResultSetInterface<TKey, TValue>
*/
public function all(): ResultSetInterface;
/**
* Populates or adds parts to current query clauses using an array.
* This is handy for passing all query clauses at once. The option array accepts:
*
* - fields: Maps to the select method
* - conditions: Maps to the where method
* - limit: Maps to the limit method
* - order: Maps to the order method
* - offset: Maps to the offset method
* - group: Maps to the group method
* - having: Maps to the having method
* - contain: Maps to the contain options for eager loading
* - join: Maps to the join method
* - page: Maps to the page method
*
* ### Example:
*
* ```
* $query->applyOptions([
* 'fields' => ['id', 'name'],
* 'conditions' => [
* 'created >=' => '2013-01-01'
* ],
* 'limit' => 10
* ]);
* ```
*
* Is equivalent to:
*
* ```
* $query
* ->select(['id', 'name'])
* ->where(['created >=' => '2013-01-01'])
* ->limit(10)
* ```
*
* @param array<string, mixed> $options list of query clauses to apply new parts to.
* @return $this
*/
public function applyOptions(array $options);
/**
* Apply custom finds to against an existing query object.
*
* Allows custom find methods to be combined and applied to each other.
*
* ```
* $repository->find('all')->find('recent');
* ```
*
* The above is an example of stacking multiple finder methods onto
* a single query.
*
* @param string $finder The finder method to use.
* @param mixed ...$args Arguments that match up to finder-specific parameters
* @return static Returns a modified query.
*/
public function find(string $finder, mixed ...$args): static;
/**
* Returns the first result out of executing this query, if the query has not been
* executed before, it will set the limit clause to 1 for performance reasons.
*
* ### Example:
*
* ```
* $singleUser = $query->select(['id', 'username'])->first();
* ```
*
* @return mixed the first result from the ResultSet
*/
public function first(): mixed;
/**
* Returns the total amount of results for the query.
*
* @return int
*/
public function count(): int;
/**
* Sets the number of records that should be retrieved from database,
* accepts an integer or an expression object that evaluates to an integer.
* In some databases, this operation might not be supported or will require
* the query to be transformed in order to limit the result set size.
*
* ### Examples
*
* ```
* $query->limit(10) // generates LIMIT 10
* $query->limit($query->expr()->add(['1 + 1'])); // LIMIT (1 + 1)
* ```
*
* @param int|null $limit number of records to be returned
* @return $this
*/
public function limit(?int $limit);
/**
* Sets the number of records that should be skipped from the original result set
* This is commonly used for paginating large results. Accepts an integer or an
* expression object that evaluates to an integer.
*
* In some databases, this operation might not be supported or will require
* the query to be transformed in order to limit the result set size.
*
* ### Examples
*
* ```
* $query->offset(10) // generates OFFSET 10
* $query->offset($query->expr()->add(['1 + 1'])); // OFFSET (1 + 1)
* ```
*
* @param int|null $offset number of records to be skipped
* @return $this
*/
public function offset(?int $offset);
/**
* Adds a single or multiple fields to be used in the ORDER clause for this query.
* Fields can be passed as an array of strings, array of expression
* objects, a single expression or a single string.
*
* If an array is passed, keys will be used as the field itself and the value will
* represent the order in which such field should be ordered. When called multiple
* times with the same fields as key, the last order definition will prevail over
* the others.
*
* By default this function will append any passed argument to the list of fields
* to be selected, unless the second argument is set to true.
*
* ### Examples:
*
* ```
* $query->orderBy(['title' => 'DESC', 'author_id' => 'ASC']);
* ```
*
* Produces:
*
* `ORDER BY title DESC, author_id ASC`
*
* ```
* $query
* ->orderBy(['title' => $query->expr('DESC NULLS FIRST')])
* ->orderBy('author_id');
* ```
*
* Will generate:
*
* `ORDER BY title DESC NULLS FIRST, author_id`
*
* ```
* $expression = $query->expr()->add(['id % 2 = 0']);
* $query->orderBy($expression)->orderBy(['title' => 'ASC']);
* ```
*
* Will become:
*
* `ORDER BY (id %2 = 0), title ASC`
*
* If you need to set complex expressions as order conditions, you
* should use `orderByAsc()` or `orderByDesc()`.
*
* @param \Closure|array|string $fields fields to be added to the list
* @param bool $overwrite whether to reset order with field list or not
* @return $this
* @deprecated 5.0.0 Use orderBy() instead now that CollectionInterface methods are no longer proxied.
*/
public function order(Closure|array|string $fields, bool $overwrite = false);
/**
* Adds a single or multiple fields to be used in the ORDER clause for this query.
* Fields can be passed as an array of strings, array of expression
* objects, a single expression or a single string.
*
* If an array is passed, keys will be used as the field itself and the value will
* represent the order in which such field should be ordered. When called multiple
* times with the same fields as key, the last order definition will prevail over
* the others.
*
* By default this function will append any passed argument to the list of fields
* to be selected, unless the second argument is set to true.
*
* ### Examples:
*
* ```
* $query->orderBy(['title' => 'DESC', 'author_id' => 'ASC']);
* ```
*
* Produces:
*
* `ORDER BY title DESC, author_id ASC`
*
* ```
* $query
* ->orderBy(['title' => $query->expr('DESC NULLS FIRST')])
* ->orderBy('author_id');
* ```
*
* Will generate:
*
* `ORDER BY title DESC NULLS FIRST, author_id`
*
* ```
* $expression = $query->expr()->add(['id % 2 = 0']);
* $query->orderBy($expression)->orderBy(['title' => 'ASC']);
* ```
*
* Will become:
*
* `ORDER BY (id %2 = 0), title ASC`
*
* If you need to set complex expressions as order conditions, you
* should use `orderByAsc()` or `orderByDesc()`.
*
* @param \Closure|array|string $fields fields to be added to the list
* @param bool $overwrite whether to reset order with field list or not
* @return $this
*/
public function orderBy(Closure|array|string $fields, bool $overwrite = false);
/**
* Set the page of results you want.
*
* This method provides an easier to use interface to set the limit + offset
* in the record set you want as results. If empty the limit will default to
* the existing limit clause, and if that too is empty, then `25` will be used.
*
* Pages must start at 1.
*
* @param int $num The page number you want.
* @param int|null $limit The number of rows you want in the page. If null
* the current limit clause will be used.
* @return $this
* @throws \InvalidArgumentException If page number < 1.
*/
public function page(int $num, ?int $limit = null);
/**
* Returns an array representation of the results after executing the query.
*
* @return array
*/
public function toArray(): array;
/**
* Set the default Table object that will be used by this query
* and form the `FROM` clause.
*
* @param \Cake\Datasource\RepositoryInterface $repository The default repository object to use
* @return $this
*/
public function setRepository(RepositoryInterface $repository);
/**
* Returns the default repository object that will be used by this query,
* that is, the repository that will appear in the "from" clause.
*
* @return \Cake\Datasource\RepositoryInterface|null $repository The default repository object to use
*/
public function getRepository(): ?RepositoryInterface;
/**
* Adds a condition or set of conditions to be used in the WHERE clause for this
* query. Conditions can be expressed as an array of fields as keys with
* comparison operators in it, the values for the array will be used for comparing
* the field to such literal. Finally, conditions can be expressed as a single
* string or an array of strings.
*
* When using arrays, each entry will be joined to the rest of the conditions using
* an AND operator. Consecutive calls to this function will also join the new
* conditions specified using the AND operator. Additionally, values can be
* expressed using expression objects which can include other query objects.
*
* Any conditions created with this method can be used with any SELECT, UPDATE
* and DELETE type of queries.
*
* ### Conditions using operators:
*
* ```
* $query->where([
* 'posted >=' => new DateTime('3 days ago'),
* 'title LIKE' => 'Hello W%',
* 'author_id' => 1,
* ], ['posted' => 'datetime']);
* ```
*
* The previous example produces:
*
* `WHERE posted >= 2012-01-27 AND title LIKE 'Hello W%' AND author_id = 1`
*
* Second parameter is used to specify what type is expected for each passed
* key. Valid types can be used from the mapped with Database\Type class.
*
* ### Nesting conditions with conjunctions:
*
* ```
* $query->where([
* 'author_id !=' => 1,
* 'OR' => ['published' => true, 'posted <' => new DateTime('now')],
* 'NOT' => ['title' => 'Hello']
* ], ['published' => boolean, 'posted' => 'datetime']
* ```
*
* The previous example produces:
*
* `WHERE author_id = 1 AND (published = 1 OR posted < '2012-02-01') AND NOT (title = 'Hello')`
*
* You can nest conditions using conjunctions as much as you like. Sometimes, you
* may want to define 2 different options for the same key, in that case, you can
* wrap each condition inside a new array:
*
* `$query->where(['OR' => [['published' => false], ['published' => true]])`
*
* Keep in mind that every time you call where() with the third param set to false
* (default), it will join the passed conditions to the previous stored list using
* the AND operator. Also, using the same array key twice in consecutive calls to
* this method will not override the previous value.
*
* ### Using expressions objects:
*
* ```
* $exp = $query->expr()->add(['id !=' => 100, 'author_id' != 1])->tieWith('OR');
* $query->where(['published' => true], ['published' => 'boolean'])->where($exp);
* ```
*
* The previous example produces:
*
* `WHERE (id != 100 OR author_id != 1) AND published = 1`
*
* Other Query objects that be used as conditions for any field.
*
* ### Adding conditions in multiple steps:
*
* You can use callback to construct complex expressions, functions
* receive as first argument a new QueryExpression object and this query instance
* as second argument. Functions must return an expression object that will be
* added to the list of conditions for the query using the AND operator.
*
* ```
* $query
* ->where(['title !=' => 'Hello World'])
* ->where(function ($exp, $query) {
* $or = $exp->or(['id' => 1]);
* $and = $exp->and(['id >' => 2, 'id <' => 10]);
* return $or->add($and);
* });
* ```
*
* * The previous example produces:
*
* `WHERE title != 'Hello World' AND (id = 1 OR (id > 2 AND id < 10))`
*
* ### Conditions as strings:
*
* ```
* $query->where(['articles.author_id = authors.id', 'modified IS NULL']);
* ```
*
* The previous example produces:
*
* `WHERE articles.author_id = authors.id AND modified IS NULL`
*
* Please note that when using the array notation or the expression objects, all
* values will be correctly quoted and transformed to the correspondent database
* data type automatically for you, thus securing your application from SQL injections.
* If you use string conditions, make sure that your values are correctly quoted.
* The safest thing you can do is to never use string conditions.
*
* @param \Closure|array|string|null $conditions The conditions to filter on.
* @param array<string, string> $types Associative array of type names used to bind values to query
* @param bool $overwrite whether to reset conditions with passed list or not
* @return $this
*/
public function where(Closure|array|string|null $conditions = null, array $types = [], bool $overwrite = false);
}
+82
View File
@@ -0,0 +1,82 @@
[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/datasource.svg?style=flat-square)](https://packagist.org/packages/cakephp/datasource)
[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt)
# CakePHP Datasource Library
This library contains interfaces for implementing Repositories and Entities using any data source,
a class for managing connections to datasources and traits to help you quickly implement the
interfaces provided by this package.
## Repositories
A repository is a class capable of interfacing with a data source using operations such as
`find`, `save` and `delete` by using intermediate query objects for expressing commands to
the data store and returning Entities as the single result unit of such a system.
In the case of a Relational database, a Repository would be a `Table`, which can be return single
or multiple `Entity` objects by using a `Query`.
This library exposes the following interfaces for creating a system that implements the
repository pattern and is compatible with the CakePHP framework:
* `RepositoryInterface` - Describes the methods for a base repository class.
* `EntityInterface` - Describes the methods for a single result object.
* `ResultSetInterface` - Represents the idea of a collection of Entities as a result of a query.
Additionally, this package provides a few traits and classes you can use in your own implementations:
* `EntityTrait` - Contains the default implementation for the `EntityInterface`.
* `QueryTrait` - Exposes the methods for creating a query object capable of returning decoratable collections.
* `ResultSetDecorator` - Decorates any traversable object, so it complies with `ResultSetInterface`.
## Connections
This library contains a couple of utility classes meant to create and manage
connection objects. Connections are typically used in repositories for
interfacing with the actual data source system.
The `ConnectionManager` class acts as a registry to access database connections
your application has. It provides a place that other objects can get references
to existing connections. Creating connections with the `ConnectionManager` is
easy:
```php
use Cake\Datasource\ConnectionManager;
ConnectionManager::config('connection-one', [
'className' => 'MyApp\Connections\CustomConnection',
'param1' => 'value',
'param2' => 'another value'
]);
ConnectionManager::config('connection-two', [
'className' => 'MyApp\Connections\CustomConnection',
'param1' => 'different value',
'param2' => 'another value'
]);
```
When requested, the `ConnectionManager` will instantiate
`MyApp\Connections\CustomConnection` by passing `param1` and `param2` inside an
array as the first argument of the constructor.
Once configured connections can be fetched using `ConnectionManager::get()`.
This method will construct and load a connection if it has not been built
before, or return the existing known connection:
```php
use Cake\Datasource\ConnectionManager;
$conn = ConnectionManager::get('master');
```
It is also possible to store connection objects by passing the instance directly to the manager:
```php
use Cake\Datasource\ConnectionManager;
$conn = ConnectionManager::config('other', $connectionInstance);
```
## Documentation
Please make sure you check the [official API documentation](https://api.cakephp.org/4.x/namespace-Cake.Datasource.html)
+275
View File
@@ -0,0 +1,275 @@
<?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\Datasource;
use Closure;
use Psr\SimpleCache\CacheInterface;
/**
* Describes the methods that any class representing a data storage should
* comply with.
*/
interface RepositoryInterface
{
/**
* Sets the repository alias.
*
* @param string $alias Table alias
* @return $this
*/
public function setAlias(string $alias);
/**
* Returns the repository alias.
*
* @return string
*/
public function getAlias(): string;
/**
* Alias a field with the repository's current alias.
*
* If field is already aliased, it will result in no-op.
*
* @param string $field The field to alias.
* @return string The field prefixed with the repository alias.
*/
public function aliasField(string $field): string;
/**
* Sets the table registry key used to create this table instance.
*
* @param string $registryAlias The key used to access this object.
* @return $this
*/
public function setRegistryAlias(string $registryAlias);
/**
* Returns the table registry key used to create this table instance.
*
* @return string
*/
public function getRegistryAlias(): string;
/**
* Test to see if a Repository has a specific field/column.
*
* @param string $field The field to check for.
* @return bool True if the field exists, false if it does not.
*/
public function hasField(string $field): bool;
/**
* Creates a new Query for this repository and applies some defaults based on the
* type of search that was selected.
*
* @param string $type the type of query to perform
* @param mixed ...$args Arguments that match up to finder-specific parameters
* @return \Cake\Datasource\QueryInterface
*/
public function find(string $type = 'all', mixed ...$args): QueryInterface;
/**
* Returns a single record after finding it by its primary key, if no record is
* found this method throws an exception.
*
* ### Example:
*
* ```
* $id = 10;
* $article = $articles->get($id);
*
* $article = $articles->get($id, ['contain' => ['Comments']]);
* ```
*
* @param mixed $primaryKey primary key value to find
* @param array|string $finder The finder to use. Passing an options array is deprecated.
* @param \Psr\SimpleCache\CacheInterface|string|null $cache The cache config to use.
* Defaults to `null`, i.e. no caching.
* @param \Closure|string|null $cacheKey The cache key to use. If not provided
* one will be autogenerated if `$cache` is not null.
* @throws \Cake\Datasource\Exception\RecordNotFoundException if the record with such id
* could not be found
* @return \Cake\Datasource\EntityInterface
* @see \Cake\Datasource\RepositoryInterface::find()
*/
public function get(
mixed $primaryKey,
array|string $finder = 'all',
CacheInterface|string|null $cache = null,
Closure|string|null $cacheKey = null,
mixed ...$args,
): EntityInterface;
/**
* Creates a new Query instance for this repository
*
* @return \Cake\Datasource\QueryInterface
*/
public function query(): QueryInterface;
/**
* Update all matching records.
*
* Sets the $fields to the provided values based on $conditions.
* This method will *not* trigger beforeSave/afterSave events. If you need those,
* first load a collection of records and update them.
*
* @param \Closure|array|string $fields A hash of field => new value.
* @param \Closure|array|string|null $conditions Conditions to be used, accepts anything Query::where()
* can take.
* @return int Count Returns the affected rows.
*/
public function updateAll(Closure|array|string $fields, Closure|array|string|null $conditions): int;
/**
* Deletes all records matching the provided conditions.
*
* This method will *not* trigger beforeDelete/afterDelete events. If you
* need those, first load a collection of records and delete them.
*
* This method will *not* execute on associations' `cascade` attribute. You should
* use database foreign keys + ON CASCADE rules if you need cascading deletes combined
* with this method.
*
* @param \Closure|array|string|null $conditions Conditions to be used, accepts anything Query::where()
* can take.
* @return int Returns the number of affected rows.
* @see \Cake\Datasource\RepositoryInterface::delete()
*/
public function deleteAll(Closure|array|string|null $conditions): int;
/**
* Returns true if there is any record in this repository matching the specified
* conditions.
*
* @param \Closure|array|string|null $conditions list of conditions to pass to the query
* @return bool
*/
public function exists(Closure|array|string|null $conditions): bool;
/**
* Persists an entity based on the fields that are marked as dirty and
* returns the same entity after a successful save or false in case
* of any error.
*
* @param \Cake\Datasource\EntityInterface $entity the entity to be saved
* @param array<string, mixed> $options The options to use when saving.
* @return \Cake\Datasource\EntityInterface|false
*/
public function save(EntityInterface $entity, array $options = []): EntityInterface|false;
/**
* Delete a single entity.
*
* Deletes an entity and possibly related associations from the database
* based on the 'dependent' option used when defining the association.
*
* @param \Cake\Datasource\EntityInterface $entity The entity to remove.
* @param array<string, mixed> $options The options for the delete.
* @return bool success
*/
public function delete(EntityInterface $entity, array $options = []): bool;
/**
* This creates a new entity object.
*
* Careful: This does not trigger any field validation.
* This entity can be persisted without validation error as empty record.
* Always patch in required fields before saving.
*
* @return \Cake\Datasource\EntityInterface
*/
public function newEmptyEntity(): EntityInterface;
/**
* Create a new entity + associated entities from an array.
*
* This is most useful when hydrating request data back into entities.
* For example, in your controller code:
*
* ```
* $article = $this->Articles->newEntity($this->request->getData());
* ```
*
* The hydrated entity will correctly do an insert/update based
* on the primary key data existing in the database when the entity
* is saved. Until the entity is saved, it will be a detached record.
*
* @param array $data The data to build an entity with.
* @param array<string, mixed> $options A list of options for the object hydration.
* @return \Cake\Datasource\EntityInterface
*/
public function newEntity(array $data, array $options = []): EntityInterface;
/**
* Create a list of entities + associated entities from an array.
*
* This is most useful when hydrating request data back into entities.
* For example, in your controller code:
*
* ```
* $articles = $this->Articles->newEntities($this->request->getData());
* ```
*
* The hydrated entities can then be iterated and saved.
*
* @param array $data The data to build an entity with.
* @param array<string, mixed> $options A list of options for the objects hydration.
* @return array<\Cake\Datasource\EntityInterface> An array of hydrated records.
*/
public function newEntities(array $data, array $options = []): array;
/**
* Merges the passed `$data` into `$entity` respecting the accessible
* fields configured on the entity. Returns the same entity after being
* altered.
*
* This is most useful when editing an existing entity using request data:
*
* ```
* $article = $this->Articles->patchEntity($article, $this->request->getData());
* ```
*
* @param \Cake\Datasource\EntityInterface $entity the entity that will get the
* data merged in
* @param array $data key value list of fields to be merged into the entity
* @param array<string, mixed> $options A list of options for the object hydration.
* @return \Cake\Datasource\EntityInterface
*/
public function patchEntity(EntityInterface $entity, array $data, array $options = []): EntityInterface;
/**
* Merges each of the elements passed in `$data` into the entities
* found in `$entities` respecting the accessible fields configured on the entities.
* Merging is done by matching the primary key in each of the elements in `$data`
* and `$entities`.
*
* This is most useful when editing a list of existing entities using request data:
*
* ```
* $article = $this->Articles->patchEntities($articles, $this->request->getData());
* ```
*
* @param iterable<\Cake\Datasource\EntityInterface> $entities the entities that will get the
* data merged in
* @param array $data list of arrays to be merged into the entities
* @param array<string, mixed> $options A list of options for the objects hydration.
* @return array<\Cake\Datasource\EntityInterface>
*/
public function patchEntities(iterable $entities, array $data, array $options = []): array;
}
+43
View File
@@ -0,0 +1,43 @@
<?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\Datasource;
use Cake\Collection\Collection;
use Cake\Core\Configure;
/**
* Generic ResultSet decorator. This will make any traversable object appear to
* be a database result
*
* @template TKey
* @template TValue
* @extends \Cake\Collection\Collection<TKey, TValue>
* @implements \Cake\Datasource\ResultSetInterface<TKey, TValue>
*/
class ResultSetDecorator extends Collection implements ResultSetInterface
{
/**
* @inheritDoc
*/
public function __debugInfo(): array
{
$parentInfo = parent::__debugInfo();
$limit = Configure::read('App.ResultSetDebugLimit', 10);
return array_merge($parentInfo, ['items' => $this->take($limit)->toArray()]);
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource;
use Cake\Collection\CollectionInterface;
/**
* Describes how a collection of datasource results should look like
*
* @template TKey
* @template-covariant TValue
* @extends \Cake\Collection\CollectionInterface<TKey, TValue>
*/
interface ResultSetInterface extends CollectionInterface
{
}
+147
View File
@@ -0,0 +1,147 @@
<?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.12
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource;
use Closure;
/**
* Contains logic for invoking an application rule.
*
* Combined with {@link \Cake\Datasource\RulesChecker} as an implementation
* detail to de-duplicate rule decoration and provide cleaner separation
* of duties.
*
* @internal
*/
class RuleInvoker
{
/**
* The rule name
*
* @var string|null
*/
protected ?string $name = null;
/**
* Rule options
*
* @var array<string, mixed>
*/
protected array $options = [];
/**
* Rule callable
*
* @var callable
*/
protected $rule;
/**
* Constructor
*
* ### Options
*
* - `errorField` The field errors should be set onto.
* - `message` The error message.
*
* Individual rules may have additional options that can be
* set here. Any options will be passed into the rule as part of the
* rule $scope.
*
* @param callable $rule The rule to be invoked.
* @param string|null $name The name of the rule. Used in error messages.
* @param array<string, mixed> $options The options for the rule. See above.
*/
public function __construct(callable $rule, ?string $name, array $options = [])
{
$this->rule = $rule;
$this->name = $name;
$this->options = $options;
}
/**
* Set options for the rule invocation.
*
* Old options will be merged with the new ones.
*
* @param array<string, mixed> $options The options to set.
* @return $this
*/
public function setOptions(array $options)
{
$this->options = $options + $this->options;
return $this;
}
/**
* Set the rule name.
*
* Only truthy names will be set.
*
* @param string|null $name The name to set.
* @return $this
*/
public function setName(?string $name)
{
if ($name) {
$this->name = $name;
}
return $this;
}
/**
* Invoke the rule.
*
* @param \Cake\Datasource\EntityInterface $entity The entity the rule
* should apply to.
* @param array $scope The rule's scope/options.
* @return bool Whether the rule passed.
*/
public function __invoke(EntityInterface $entity, array $scope): bool
{
$rule = $this->rule;
$pass = $rule($entity, $this->options + $scope);
if ($pass === true) {
return true;
}
$message = $this->options['message'] ?? 'invalid';
if (is_string($pass)) {
$message = $pass;
}
if ($message instanceof Closure) {
$message = $message($entity, $this->options + $scope);
}
if ($this->name) {
$message = [$this->name => $message];
} else {
$message = [$message];
}
$errorField = $this->options['errorField'] ?? ($this->name ?? '_rule');
$entity->setError($errorField, $message);
if ($entity instanceof InvalidPropertyInterface && isset($entity->{$errorField})) {
$invalidValue = $entity->{$errorField};
$entity->setInvalidField($errorField, $invalidValue);
}
return false;
}
}
+125
View File
@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.7
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource;
use ArrayObject;
use Cake\Event\EventDispatcherInterface;
/**
* A trait that allows a class to build and apply application.
* rules.
*
* If the implementing class also implements EventAwareTrait, then
* events will be emitted when rules are checked.
*
* The implementing class is expected to define the `RULES_CLASS` constant
* if they need to customize which class is used for rules objects.
*/
trait RulesAwareTrait
{
/**
* The domain rules to be applied to entities saved by this table
*
* @var \Cake\Datasource\RulesChecker|null
*/
protected ?RulesChecker $_rulesChecker = null;
/**
* Returns whether the passed entity complies with all the rules stored in
* the rules checker.
*
* @param \Cake\Datasource\EntityInterface $entity The entity to check for validity.
* @param string $operation The operation being run. Either 'create', 'update' or 'delete'.
* @param \ArrayObject<string, mixed>|array|null $options The options To be passed to the rules.
* @return bool
*/
public function checkRules(
EntityInterface $entity,
string $operation = RulesChecker::CREATE,
ArrayObject|array|null $options = null,
): bool {
$rules = $this->rulesChecker();
$options = $options ?: new ArrayObject();
$options = is_array($options) ? new ArrayObject($options) : $options;
$hasEvents = ($this instanceof EventDispatcherInterface);
if ($hasEvents) {
$event = $this->dispatchEvent(
'Model.beforeRules',
compact('entity', 'options', 'operation'),
);
if ($event->isStopped()) {
return $event->getResult();
}
}
$result = $rules->check($entity, $operation, $options->getArrayCopy());
if ($hasEvents) {
$event = $this->dispatchEvent(
'Model.afterRules',
compact('entity', 'options', 'result', 'operation'),
);
if ($event->isStopped()) {
return $event->getResult();
}
}
return $result;
}
/**
* Returns the RulesChecker for this instance.
*
* A RulesChecker object is used to test an entity for validity
* on rules that may involve complex logic or data that
* needs to be fetched from relevant datasources.
*
* @see \Cake\Datasource\RulesChecker
* @return \Cake\Datasource\RulesChecker
*/
public function rulesChecker(): RulesChecker
{
if ($this->_rulesChecker !== null) {
return $this->_rulesChecker;
}
/** @var class-string<\Cake\Datasource\RulesChecker> $class */
$class = defined('static::RULES_CLASS') ? static::RULES_CLASS : RulesChecker::class;
/**
* @phpstan-ignore-next-line
*/
$this->_rulesChecker = $this->buildRules(new $class(['repository' => $this]));
$this->dispatchEvent('Model.buildRules', ['rules' => $this->_rulesChecker]);
return $this->_rulesChecker;
}
/**
* Returns a RulesChecker object after modifying the one that was supplied.
*
* Subclasses should override this method in order to initialize the rules to be applied to
* entities saved by this instance.
*
* @param \Cake\Datasource\RulesChecker $rules The rules object to be modified.
* @return \Cake\Datasource\RulesChecker
*/
public function buildRules(RulesChecker $rules): RulesChecker
{
return $rules;
}
}
+427
View File
@@ -0,0 +1,427 @@
<?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.7
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource;
use Cake\Core\Exception\CakeException;
use InvalidArgumentException;
/**
* Contains logic for storing and checking rules on entities
*
* RulesCheckers are used by Table classes to ensure that the
* current entity state satisfies the application logic and business rules.
*
* RulesCheckers afford different rules to be applied in the create and update
* scenario.
*
* ### Adding rules
*
* Rules must be callable objects that return true/false depending on whether
* the rule has been satisfied. You can use RulesChecker::add(), RulesChecker::addCreate(),
* RulesChecker::addUpdate() and RulesChecker::addDelete to add rules to a checker.
*
* ### Running checks
*
* Generally a Table object will invoke the rules objects, but you can manually
* invoke the checks by calling RulesChecker::checkCreate(), RulesChecker::checkUpdate() or
* RulesChecker::checkDelete().
*/
class RulesChecker
{
/**
* Indicates that the checking rules to apply are those used for creating entities
*
* @var string
*/
public const CREATE = 'create';
/**
* Indicates that the checking rules to apply are those used for updating entities
*
* @var string
*/
public const UPDATE = 'update';
/**
* Indicates that the checking rules to apply are those used for deleting entities
*
* @var string
*/
public const DELETE = 'delete';
/**
* The list of rules to be checked on both create and update operations
*
* @var array<\Cake\Datasource\RuleInvoker>
*/
protected array $_rules = [];
/**
* The list of rules to check during create operations
*
* @var array<\Cake\Datasource\RuleInvoker>
*/
protected array $_createRules = [];
/**
* The list of rules to check during update operations
*
* @var array<\Cake\Datasource\RuleInvoker>
*/
protected array $_updateRules = [];
/**
* The list of rules to check during delete operations
*
* @var array<\Cake\Datasource\RuleInvoker>
*/
protected array $_deleteRules = [];
/**
* List of options to pass to every callable rule
*
* @var array
*/
protected array $_options = [];
/**
* Whether to use I18n functions for translating default error messages
*
* @var bool
*/
protected bool $_useI18n = false;
/**
* Constructor. Takes the options to be passed to all rules.
*
* @param array<string, mixed> $options The options to pass to every rule
*/
public function __construct(array $options = [])
{
$this->_options = $options;
$this->_useI18n = function_exists('\Cake\I18n\__d');
}
/**
* Adds a rule that will be applied to the entity on create, update and delete
* operations.
*
* ### Options
*
* The options array accept the following special keys:
*
* - `errorField`: The name of the entity field that will be marked as invalid
* if the rule does not pass.
* - `message`: The error message to set to `errorField` if the rule does not pass.
*
* @param callable $rule A callable function or object that will return whether
* the entity is valid or not.
* @param array|string|null $name The alias for a rule, or an array of options.
* @param array<string, mixed> $options List of extra options to pass to the rule callable as
* second argument.
* @return $this
* @throws \Cake\Core\Exception\CakeException If a rule with the same name already exists
*/
public function add(callable $rule, array|string|null $name = null, array $options = [])
{
if (is_string($name)) {
$this->checkName($name, $this->_rules);
$this->_rules[$name] = $this->_addError($rule, $name, $options);
} else {
$this->_rules[] = $this->_addError($rule, $name, $options);
}
return $this;
}
/**
* Removes a rule from the set.
*
* @param string $name The name of the rule to remove.
* @return $this
* @since 5.1.0
*/
public function remove(string $name)
{
unset($this->_rules[$name]);
return $this;
}
/**
* Adds a rule that will be applied to the entity on create operations.
*
* ### Options
*
* The options array accept the following special keys:
*
* - `errorField`: The name of the entity field that will be marked as invalid
* if the rule does not pass.
* - `message`: The error message to set to `errorField` if the rule does not pass.
*
* @param callable $rule A callable function or object that will return whether
* the entity is valid or not.
* @param array|string|null $name The alias for a rule or an array of options.
* @param array<string, mixed> $options List of extra options to pass to the rule callable as
* second argument.
* @return $this
* @throws \Cake\Core\Exception\CakeException If a rule with the same name already exists
*/
public function addCreate(callable $rule, array|string|null $name = null, array $options = [])
{
if (is_string($name)) {
$this->checkName($name, $this->_createRules);
$this->_createRules[$name] = $this->_addError($rule, $name, $options);
} else {
$this->_createRules[] = $this->_addError($rule, $name, $options);
}
return $this;
}
/**
* Removes a rule from the create set.
*
* @param string $name The name of the rule to remove.
* @return $this
* @since 5.1.0
*/
public function removeCreate(string $name)
{
unset($this->_createRules[$name]);
return $this;
}
/**
* Adds a rule that will be applied to the entity on update operations.
*
* ### Options
*
* The options array accept the following special keys:
*
* - `errorField`: The name of the entity field that will be marked as invalid
* if the rule does not pass.
* - `message`: The error message to set to `errorField` if the rule does not pass.
*
* @param callable $rule A callable function or object that will return whether
* the entity is valid or not.
* @param array|string|null $name The alias for a rule, or an array of options.
* @param array<string, mixed> $options List of extra options to pass to the rule callable as
* second argument.
* @return $this
* @throws \Cake\Core\Exception\CakeException If a rule with the same name already exists
*/
public function addUpdate(callable $rule, array|string|null $name = null, array $options = [])
{
if (is_string($name)) {
$this->checkName($name, $this->_updateRules);
$this->_updateRules[$name] = $this->_addError($rule, $name, $options);
} else {
$this->_updateRules[] = $this->_addError($rule, $name, $options);
}
return $this;
}
/**
* Removes a rule from the update set.
*
* @param string $name The name of the rule to remove.
* @return $this
* @since 5.1.0
*/
public function removeUpdate(string $name)
{
unset($this->_updateRules[$name]);
return $this;
}
/**
* Adds a rule that will be applied to the entity on delete operations.
*
* ### Options
*
* The options array accept the following special keys:
*
* - `errorField`: The name of the entity field that will be marked as invalid
* if the rule does not pass.
* - `message`: The error message to set to `errorField` if the rule does not pass.
*
* @param callable $rule A callable function or object that will return whether
* the entity is valid or not.
* @param array|string|null $name The alias for a rule, or an array of options.
* @param array<string, mixed> $options List of extra options to pass to the rule callable as
* second argument.
* @return $this
* @throws \Cake\Core\Exception\CakeException If a rule with the same name already exists
*/
public function addDelete(callable $rule, array|string|null $name = null, array $options = [])
{
if (is_string($name)) {
$this->checkName($name, $this->_deleteRules);
$this->_deleteRules[$name] = $this->_addError($rule, $name, $options);
} else {
$this->_deleteRules[] = $this->_addError($rule, $name, $options);
}
return $this;
}
/**
* Removes a rule from the delete set.
*
* @param string $name The name of the rule to remove.
* @return $this
* @since 5.1.0
*/
public function removeDelete(string $name)
{
unset($this->_deleteRules[$name]);
return $this;
}
/**
* Runs each of the rules by passing the provided entity and returns true if all
* of them pass. The rules to be applied are depended on the $mode parameter which
* can only be RulesChecker::CREATE, RulesChecker::UPDATE or RulesChecker::DELETE
*
* @param \Cake\Datasource\EntityInterface $entity The entity to check for validity.
* @param string $mode Either 'create, 'update' or 'delete'.
* @param array<string, mixed> $options Extra options to pass to checker functions.
* @return bool
* @throws \InvalidArgumentException if an invalid mode is passed.
*/
public function check(EntityInterface $entity, string $mode, array $options = []): bool
{
return match ($mode) {
self::CREATE => $this->checkCreate($entity, $options),
self::UPDATE => $this->checkUpdate($entity, $options),
self::DELETE => $this->checkDelete($entity, $options),
default => throw new InvalidArgumentException('Wrong checking mode: ' . $mode),
};
}
/**
* Runs each of the rules by passing the provided entity and returns true if all
* of them pass. The rules selected will be only those specified to be run on 'create'
*
* @param \Cake\Datasource\EntityInterface $entity The entity to check for validity.
* @param array<string, mixed> $options Extra options to pass to checker functions.
* @return bool
*/
public function checkCreate(EntityInterface $entity, array $options = []): bool
{
return $this->_checkRules(
$entity,
$options,
array_merge(array_values($this->_rules), array_values($this->_createRules)),
);
}
/**
* Runs each of the rules by passing the provided entity and returns true if all
* of them pass. The rules selected will be only those specified to be run on 'update'
*
* @param \Cake\Datasource\EntityInterface $entity The entity to check for validity.
* @param array<string, mixed> $options Extra options to pass to checker functions.
* @return bool
*/
public function checkUpdate(EntityInterface $entity, array $options = []): bool
{
return $this->_checkRules(
$entity,
$options,
array_merge(array_values($this->_rules), array_values($this->_updateRules)),
);
}
/**
* Runs each of the rules by passing the provided entity and returns true if all
* of them pass. The rules selected will be only those specified to be run on 'delete'
*
* @param \Cake\Datasource\EntityInterface $entity The entity to check for validity.
* @param array<string, mixed> $options Extra options to pass to checker functions.
* @return bool
*/
public function checkDelete(EntityInterface $entity, array $options = []): bool
{
return $this->_checkRules($entity, $options, $this->_deleteRules);
}
/**
* Used by top level functions checkDelete, checkCreate and checkUpdate, this function
* iterates an array containing the rules to be checked and checks them all.
*
* @param \Cake\Datasource\EntityInterface $entity The entity to check for validity.
* @param array<string, mixed> $options Extra options to pass to checker functions.
* @param array<\Cake\Datasource\RuleInvoker> $rules The list of rules that must be checked.
* @return bool
*/
protected function _checkRules(EntityInterface $entity, array $options = [], array $rules = []): bool
{
$success = true;
$options += $this->_options;
foreach ($rules as $rule) {
$success = $rule($entity, $options) && $success;
}
return $success;
}
/**
* Utility method for decorating any callable so that if it returns false, the correct
* property in the entity is marked as invalid.
*
* @param callable $rule The rule to decorate
* @param array|string|null $name The alias for a rule or an array of options
* @param array<string, mixed> $options The options containing the error message and field.
* @return \Cake\Datasource\RuleInvoker
*/
protected function _addError(callable $rule, array|string|null $name = null, array $options = []): RuleInvoker
{
if (is_array($name)) {
$options = $name;
$name = null;
}
if (!($rule instanceof RuleInvoker)) {
$rule = new RuleInvoker($rule, $name, $options);
} else {
$rule->setOptions($options)->setName($name);
}
return $rule;
}
/**
* Checks that a rule with the same name doesn't already exist
*
* @param string $name The name to check
* @param array<\Cake\Datasource\RuleInvoker> $rules The rules array to check
* @return void
* @throws \Cake\Core\Exception\CakeException
*/
protected function checkName(string $name, array $rules): void
{
if (array_key_exists($name, $rules)) {
throw new CakeException('A rule with the same name already exists');
}
}
}
+166
View File
@@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.5.0
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource;
/**
* An interface used by TableSchema objects.
*/
interface SchemaInterface
{
/**
* Get the name of the table.
*
* @return string
*/
public function name(): string;
/**
* Add a column to the table.
*
* ### Attributes
*
* Columns can have several attributes:
*
* - `type` The type of the column. This should be
* one of CakePHP's abstract types.
* - `length` The length of the column.
* - `precision` The number of decimal places to store
* for float and decimal types.
* - `default` The default value of the column.
* - `null` Whether the column can hold nulls.
* - `fixed` Whether the column is a fixed length column.
* This is only present/valid with string columns.
* - `unsigned` Whether the column is an unsigned column.
* This is only present/valid for integer, decimal, float columns.
*
* In addition to the above keys, the following keys are
* implemented in some database dialects, but not all:
*
* - `comment` The comment for the column.
*
* @param string $name The name of the column
* @param array<string, mixed>|string $attrs The attributes for the column or the type name.
* @return $this
*/
public function addColumn(string $name, array|string $attrs);
/**
* Get column data in the table.
*
* @param string $name The column name.
* @return array<string, mixed>|null Column data or null.
*/
public function getColumn(string $name): ?array;
/**
* Returns true if a column exists in the schema.
*
* @param string $name Column name.
* @return bool
*/
public function hasColumn(string $name): bool;
/**
* Remove a column from the table schema.
*
* If the column is not defined in the table, no error will be raised.
*
* @param string $name The name of the column
* @return $this
*/
public function removeColumn(string $name);
/**
* Get the column names in the table.
*
* @return array<string>
*/
public function columns(): array;
/**
* Returns column type or null if a column does not exist.
*
* @param string $name The column to get the type of.
* @return string|null
*/
public function getColumnType(string $name): ?string;
/**
* Sets the type of column.
*
* @param string $name The column to set the type of.
* @param string $type The type to set the column to.
* @return $this
*/
public function setColumnType(string $name, string $type);
/**
* Returns the base type name for the provided column.
* This represents the database type a more complex class is
* based upon.
*
* @param string $column The column name to get the base type from
* @return string|null The base type name
*/
public function baseColumnType(string $column): ?string;
/**
* Check whether a field is nullable
*
* Missing columns are nullable.
*
* @param string $name The column to get the type of.
* @return bool Whether the field is nullable.
*/
public function isNullable(string $name): bool;
/**
* Returns an array where the keys are the column names in the schema
* and the values the database type they have.
*
* @return array<string, string>
*/
public function typeMap(): array;
/**
* Get a hash of columns and their default values.
*
* @return array<string, mixed>
*/
public function defaultValues(): array;
/**
* Sets the options for a table.
*
* Table options allow you to set platform specific table level options.
* For example the engine type in MySQL.
*
* @param array<string, mixed> $options The options to set, or null to read options.
* @return $this
*/
public function setOptions(array $options);
/**
* Gets the options for a table.
*
* Table options allow you to set platform specific table level options.
* For example the engine type in MySQL.
*
* @return array<string, mixed> An array of options.
*/
public function getOptions(): array;
}
+53
View File
@@ -0,0 +1,53 @@
{
"name": "cakephp/datasource",
"description": "Provides connection managing and traits for Entities and Queries that can be reused for different datastores",
"type": "library",
"keywords": [
"cakephp",
"datasource",
"connection management",
"entity",
"query"
],
"homepage": "https://cakephp.org",
"license": "MIT",
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/datasource/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/datasource"
},
"require": {
"php": ">=8.2",
"cakephp/core": "^5.3.0",
"psr/simple-cache": "^2.0 || ^3.0"
},
"require-dev": {
"cakephp/cache": "^5.3.0",
"cakephp/collection": "^5.3.0",
"cakephp/utility": "^5.3.0"
},
"autoload": {
"psr-4": {
"Cake\\Datasource\\": "."
}
},
"suggest": {
"cakephp/utility": "If you decide to use EntityTrait.",
"cakephp/collection": "If you decide to use ResultSetInterface.",
"cakephp/cache": "If you decide to use Query caching."
},
"minimum-stability": "dev",
"prefer-stable": true,
"extra": {
"branch-alias": {
"dev-5.next": "5.3.x-dev"
}
}
}