This commit is contained in:
2024-11-05 12:10:06 +08:00
commit 6b687feee2
3995 changed files with 418583 additions and 0 deletions
+154
View File
@@ -0,0 +1,154 @@
<?php
declare(strict_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\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
/**
* This interface defines the methods you can depend on in
* a connection.
*
* @method object getDriver() Gets the driver instance. {@see \Cake\Database\Connnection::getDriver()}
* @method $this setLogger($logger) Set the current logger. {@see \Cake\Database\Connnection::setLogger()}
* @method bool supportsDynamicConstraints() Returns whether the driver supports adding or dropping constraints to
* already created tables. {@see \Cake\Database\Connnection::supportsDynamicConstraints()}
* @method \Cake\Database\Schema\Collection getSchemaCollection() Gets a Schema\Collection object for this connection.
* {@see \Cake\Database\Connnection::getSchemaCollection()}
* @method \Cake\Database\StatementInterface prepare($sql) Prepares a SQL statement to be executed.
* {@see \Cake\Database\Connnection::prepare()}
* @method \Cake\Database\StatementInterface execute($query, $params = [], array $types = []) Executes a query using
* `$params` for interpolating values and $types as a hint for each those params.
* {@see \Cake\Database\Connnection::execute()}
* @method \Cake\Database\StatementInterface query(string $sql) Executes a SQL statement and returns the Statement
* object as result. {@see \Cake\Database\Connnection::query()}
*/
interface ConnectionInterface extends LoggerAwareInterface
{
/**
* @var string
*/
public const ROLE_WRITE = 'write';
/**
* @var string
*/
public const ROLE_READ = 'read';
/**
* Gets the current logger object.
*
* @return \Psr\Log\LoggerInterface logger instance
*/
public function getLogger(): LoggerInterface;
/**
* 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;
/**
* Executes a callable function inside a transaction, if any exception occurs
* while executing the passed callable, the transaction will be rolled back
* If the result of the callable function is `false`, the transaction will
* also be rolled back. Otherwise, the transaction is committed after executing
* the callback.
*
* The callback will receive the connection instance as its first argument.
*
* ### Example:
*
* ```
* $connection->transactional(function ($connection) {
* $connection->newQuery()->delete('users')->execute();
* });
* ```
*
* @param callable $callback The callback to execute within a transaction.
* @return mixed The return value of the callback.
* @throws \Exception Will re-throw any exception raised in $callback after
* rolling back the transaction.
*/
public function transactional(callable $callback);
/**
* Run an operation with constraints disabled.
*
* Constraints should be re-enabled after the callback succeeds/fails.
*
* ### Example:
*
* ```
* $connection->disableConstraints(function ($connection) {
* $connection->newQuery()->delete('users')->execute();
* });
* ```
*
* @param callable $callback The callback to execute within a transaction.
* @return mixed The return value of the callback.
* @throws \Exception Will re-throw any exception raised in $callback after
* rolling back the transaction.
*/
public function disableConstraints(callable $callback);
/**
* Enable/disable query logging
*
* @param bool $enable Enable/disable query logging
* @return $this
*/
public function enableQueryLogging(bool $enable = true);
/**
* Disable query logging
*
* @return $this
*/
public function disableQueryLogging();
/**
* Check if query logging is enabled.
*
* @return bool
*/
public function isQueryLoggingEnabled(): bool;
}
+216
View File
@@ -0,0 +1,216 @@
<?php
declare(strict_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;
/**
* 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 $_aliasMap = [];
/**
* An array mapping url schemes to fully qualified driver class names
*
* @var array<string, string>
* @psalm-var array<string, class-string>
*/
protected static $_dsnClassMap = [
'mysql' => Mysql::class,
'postgres' => Postgres::class,
'sqlite' => Sqlite::class,
'sqlserver' => Sqlserver::class,
];
/**
* The ConnectionRegistry used by the manager.
*
* @var \Cake\Datasource\ConnectionRegistry|null
*/
protected static $_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 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($key, $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 $config The DSN string to convert to a configuration array
* @return array<string, mixed> The configuration array to be stored after parsing the DSN
*/
public static function parseDsn(string $config): array
{
$config = static::_parseDsn($config);
if (isset($config['path']) && empty($config['database'])) {
$config['database'] = substr($config['path'], 1);
}
if (empty($config['driver'])) {
$config['driver'] = $config['className'];
$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)
{
if ($useAliases && isset(static::$_aliasMap[$name])) {
$name = static::$_aliasMap[$name];
}
if (!isset(static::$_config[$name])) {
throw new MissingDatasourceConfigException(['name' => $name]);
}
if (!isset(static::$_registry)) {
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;
/**
* 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 string|null Either the correct class name or null.
* @psalm-return class-string|null
*/
protected function _resolveClassName(string $class): ?string
{
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 callable is passed as first argument, The returned value of this
* function will be the result of the callable.
*
* @param \Cake\Datasource\ConnectionInterface|callable|string $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($class, string $alias, array $config)
{
if (is_callable($class)) {
return $class($alias);
}
if (is_object($class)) {
return $class;
}
unset($config['className']);
/** @var \Cake\Datasource\ConnectionInterface */
return new $class($config);
}
/**
* 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;
}
}
+288
View File
@@ -0,0 +1,288 @@
<?php
declare(strict_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;
/**
* Describes the methods that any class representing a data storage should
* comply with.
*
* @property mixed $id Alias for commonly used primary key.
* @method bool[] getAccessible() Accessible configuration for this entity.
* @template-extends \ArrayAccess<string, mixed>
*/
interface EntityInterface extends ArrayAccess, JsonSerializable
{
/**
* 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;
/**
* 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, $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($field, bool $set);
/**
* 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
*/
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
*/
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($field, $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);
/**
* Returns the original value of a field.
*
* @param string $field The name of the field.
* @return mixed
*/
public function getOriginal(string $field);
/**
* Gets all original values of the entity.
*
* @return array
*/
public function getOriginalValues(): array;
/**
* Returns whether this entity contains a field named $field
* and is not set to null.
*
* @param array<string>|string $field The field to check.
* @return bool
*/
public function has($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($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
*/
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
*/
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;
}
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 $_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 $_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 $_messageTemplate = 'Model class "%s" of type "%s" could not be found.';
}
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
use function Cake\Core\deprecationWarning;
deprecationWarning(
'Since 4.2.0: Cake\Datasource\Exception\PageOutOfBoundsException is deprecated. ' .
'Use Cake\Datasource\Paging\Exception\PageOutOfBoundsException instead.'
);
class_exists('Cake\Datasource\Paging\Exception\PageOutOfBoundsException');
@@ -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 a particular record was not found
*/
class RecordNotFoundException extends CakeException
{
}
+103
View File
@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource;
use Cake\Datasource\Locator\LocatorInterface;
use Cake\ORM\Locator\TableLocator;
use InvalidArgumentException;
use function Cake\Core\deprecationWarning;
use function Cake\Core\getTypeName;
/**
* Class FactoryLocator
*/
class FactoryLocator
{
/**
* A list of model factory functions.
*
* @var array<callable|\Cake\Datasource\Locator\LocatorInterface>
*/
protected static $_modelFactories = [];
/**
* Register a 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 static function add(string $type, $factory): void
{
if ($factory instanceof LocatorInterface) {
static::$_modelFactories[$type] = $factory;
return;
}
if (is_callable($factory)) {
deprecationWarning(
'Using a callable as a locator has been deprecated.'
. ' Use an instance of Cake\Datasource\Locator\LocatorInterface instead.'
);
static::$_modelFactories[$type] = $factory;
return;
}
throw new InvalidArgumentException(sprintf(
'`$factory` must be an instance of Cake\Datasource\Locator\LocatorInterface or a callable.'
. ' Got type `%s` instead.',
getTypeName($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|callable The factory for the repository type.
*/
public static function get(string $type)
{
if (!isset(static::$_modelFactories['Table'])) {
static::$_modelFactories['Table'] = new TableLocator();
}
if (!isset(static::$_modelFactories[$type])) {
throw new InvalidArgumentException(sprintf(
'Unknown repository type "%s". Make sure you register a type before trying to use it.',
$type
));
}
return static::$_modelFactories[$type];
}
}
+73
View File
@@ -0,0 +1,73 @@
<?php
declare(strict_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
{
/**
* Create the fixture schema/mapping/definition
*
* @param \Cake\Datasource\ConnectionInterface $connection An instance of the connection the fixture should be created on.
* @return bool True on success, false on failure.
*/
public function create(ConnectionInterface $connection): bool;
/**
* Run after all tests executed, should remove the table/collection from the connection.
*
* @param \Cake\Datasource\ConnectionInterface $connection An instance of the connection the fixture should be removed from.
* @return bool True on success, false on failure.
*/
public function drop(ConnectionInterface $connection): bool;
/**
* 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 \Cake\Database\StatementInterface|bool on success or if there are no records to insert,
* or false on failure.
*/
public function insert(ConnectionInterface $connection);
/**
* 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;
}
+61
View File
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.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);
/**
* 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, $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.
+115
View File
@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource\Locator;
use Cake\Datasource\RepositoryInterface;
use RuntimeException;
/**
* Provides an abstract registry/factory for repository objects.
*/
abstract class AbstractLocator implements LocatorInterface
{
/**
* Instances that belong to the registry.
*
* @var array<string, \Cake\Datasource\RepositoryInterface>
*/
protected $instances = [];
/**
* Contains a list of options that were passed to get() method.
*
* @var array<string, array>
*/
protected $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 \Cake\Datasource\RepositoryInterface
* @throws \RuntimeException When trying to get alias for which instance
* has already been created with different options.
*/
public function get(string $alias, array $options = [])
{
$storeOptions = $options;
unset($storeOptions['allowFallbackClass']);
if (isset($this->instances[$alias])) {
if (!empty($storeOptions) && isset($this->options[$alias]) && $this->options[$alias] !== $storeOptions) {
throw new RuntimeException(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 \Cake\Datasource\RepositoryInterface
*/
abstract protected function createInstance(string $alias, array $options);
/**
* @inheritDoc
*/
public function set(string $alias, RepositoryInterface $repository)
{
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 = [];
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
declare(strict_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.
*/
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 \Cake\Datasource\RepositoryInterface
* @throws \RuntimeException When trying to get alias for which instance
* has already been created with different options.
*/
public function get(string $alias, array $options = []);
/**
* Set a repository instance.
*
* @param string $alias The alias to set.
* @param \Cake\Datasource\RepositoryInterface $repository The repository to set.
* @return \Cake\Datasource\RepositoryInterface
*/
public function set(string $alias, RepositoryInterface $repository);
/**
* 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;
}
+241
View File
@@ -0,0 +1,241 @@
<?php
declare(strict_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 InvalidArgumentException;
use UnexpectedValueException;
use function Cake\Core\deprecationWarning;
use function Cake\Core\getTypeName;
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 Cake\Controller\Controller and
* Cake\Console\Shell.
*/
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 $modelClass;
/**
* A list of overridden model factory functions.
*
* @var array<callable|\Cake\Datasource\Locator\LocatorInterface>
*/
protected $_modelFactories = [];
/**
* The model type to use.
*
* @var string
*/
protected $_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
{
if ($this->modelClass === null) {
$this->modelClass = $name;
}
}
/**
* Fetch or construct a model and set it to a property on this object.
*
* Uses a modelFactory based on `$modelType` to fetch and construct a `RepositoryInterface`
* and set it as a property on the current object. The default `modelType`
* can be defined with `setModelType()`.
*
* 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.
* @deprecated 4.3.0 Prefer `LocatorAwareTrait::fetchTable()` or `ModelAwareTrait::fetchModel()` instead.
*/
public function loadModel(?string $modelClass = null, ?string $modelType = null): RepositoryInterface
{
$modelClass = $modelClass ?? $this->modelClass;
if (empty($modelClass)) {
throw new UnexpectedValueException('Default modelClass is empty');
}
$modelType = $modelType ?? $this->getModelType();
$options = [];
if (strpos($modelClass, '\\') === false) {
[, $alias] = pluginSplit($modelClass, true);
} else {
$options['className'] = $modelClass;
/** @psalm-suppress PossiblyFalseOperand */
$alias = substr(
$modelClass,
strrpos($modelClass, '\\') + 1,
-strlen($modelType)
);
$modelClass = $alias;
}
if (!property_exists($this, $alias)) {
deprecationWarning(
'4.5.0 - Dynamic properties will be removed in PHP 8.2. ' .
"Add `public \${$alias} = null;` to your class definition or use `#[AllowDynamicProperties]` attribute."
);
}
if (isset($this->{$alias})) {
return $this->{$alias};
}
$factory = $this->_modelFactories[$modelType] ?? FactoryLocator::get($modelType);
if ($factory instanceof LocatorInterface) {
$this->{$alias} = $factory->get($modelClass, $options);
} else {
$this->{$alias} = $factory($modelClass, $options);
}
if (!$this->{$alias}) {
throw new MissingModelException([$modelClass, $modelType]);
}
return $this->{$alias};
}
/**
* 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 = $modelClass ?? $this->modelClass;
if (empty($modelClass)) {
throw new UnexpectedValueException('Default modelClass is empty');
}
$modelType = $modelType ?? $this->getModelType();
$options = [];
if (strpos($modelClass, '\\') === false) {
[, $alias] = pluginSplit($modelClass, true);
} else {
$options['className'] = $modelClass;
/** @psalm-suppress PossiblyFalseOperand */
$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, $factory): void
{
if (!$factory instanceof LocatorInterface && !is_callable($factory)) {
throw new InvalidArgumentException(sprintf(
'`$factory` must be an instance of Cake\Datasource\Locator\LocatorInterface or a callable.'
. ' Got type `%s` instead.',
getTypeName($factory)
));
}
$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;
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
use function Cake\Core\deprecationWarning;
deprecationWarning(
'Since 4.2.0: Cake\Datasource\Paginator is deprecated.' .
'Use Cake\Datasource\Paging\NumericPaginator instead.'
);
class_exists('Cake\Datasource\Paging\NumericPaginator');
+10
View File
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
use function Cake\Core\deprecationWarning;
deprecationWarning(
'Since 4.2.0: Cake\Datasource\PaginatorInterface is deprecated. ' .
'Use Cake\Datasource\Paging\PaginatorInterface instead.'
);
class_exists('Cake\Datasource\Paging\PaginatorInterface');
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* 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;
/**
* Exception raised when requested page number does not exist.
*/
class PageOutOfBoundsException extends CakeException
{
/**
* @inheritDoc
*/
protected $_messageTemplate = 'Page number %s could not be found.';
}
// phpcs:disable
class_alias(
'Cake\Datasource\Paging\Exception\PageOutOfBoundsException',
'Cake\Datasource\Exception\PageOutOfBoundsException'
);
// phpcs:enable
+734
View File
@@ -0,0 +1,734 @@
<?php
declare(strict_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\deprecationWarning;
/**
* 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` - A list of fields which can be used for sorting. By
* default all table columns can be used for sorting. You can use this option
* to restrict sorting only by particular fields. If you want to allow
* sorting on either associated columns or calculated fields then you will
* have to explicity specify them (along with other fields). Using an empty
* array will disable sorting alltogether.
* - `finder` - The table finder to use. Defaults to `all`.
*
* @var array<string, mixed>
*/
protected $_defaultConfig = [
'page' => 1,
'limit' => 20,
'maxLimit' => 100,
'allowedParameters' => ['limit', 'sort', 'page', 'direction'],
'sortableFields' => null,
'finder' => 'all',
];
/**
* Paging params after pagination operation is done.
*
* @var array<string, array>
*/
protected $_pagingParams = [];
/**
* 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 \Cake\Datasource\RepositoryInterface|\Cake\Datasource\QueryInterface $object The repository or query
* to paginate.
* @param array $params Request params
* @param array $settings The settings/configuration used for pagination.
* @return \Cake\Datasource\ResultSetInterface Query results
* @throws \Cake\Datasource\Paging\Exception\PageOutOfBoundsException
*/
public function paginate(object $object, array $params = [], array $settings = []): ResultSetInterface
{
$query = null;
if ($object instanceof QueryInterface) {
$query = $object;
$object = $query->getRepository();
if ($object === null) {
throw new CakeException('No repository set for query.');
}
}
$data = $this->extractData($object, $params, $settings);
$query = $this->getQuery($object, $query, $data);
$cleanQuery = clone $query;
$results = $query->all();
$data['numResults'] = count($results);
$data['count'] = $this->getCount($cleanQuery, $data);
$pagingParams = $this->buildParams($data);
$alias = $object->getAlias();
$this->_pagingParams = [$alias => $pagingParams];
if ($pagingParams['requestedPage'] > $pagingParams['page']) {
throw new PageOutOfBoundsException([
'requestedPage' => $pagingParams['requestedPage'],
'pagingParams' => $this->_pagingParams,
]);
}
return $results;
}
/**
* 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'];
unset(
$options['scope'],
$options['sort'],
$options['direction'],
);
if ($query === null) {
$query = $object->find($data['finder'], $options);
} else {
$query->applyOptions($options);
}
return $query;
}
/**
* 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 Array with keys 'defaults', 'options' and 'finder'
*/
protected function extractData(RepositoryInterface $object, array $params, array $settings): array
{
$alias = $object->getAlias();
$defaults = $this->getDefaults($alias, $settings);
$validSettings = array_merge(
array_keys($this->_defaultConfig),
['whitelist', 'sortWhitelist', 'order', 'scope']
);
$extraSettings = array_diff_key($defaults, array_flip($validSettings));
if ($extraSettings) {
deprecationWarning(
'Passing query options as paginator settings is deprecated.'
. ' Use a custom finder through `finder` config instead.'
. ' Extra keys found are: ' . implode(',', array_keys($extraSettings))
);
}
$options = $this->mergeOptions($params, $defaults);
$options = $this->validateSort($object, $options);
$options = $this->checkLimit($options);
$options += ['page' => 1, 'scope' => null];
$options['page'] = (int)$options['page'] < 1 ? 1 : (int)$options['page'];
[$finder, $options] = $this->_extractFinder($options);
return compact('defaults', 'options', 'finder');
}
/**
* Build pagination params.
*
* @param array<string, mixed> $data Paginator data containing keys 'options',
* 'count', 'defaults', 'finder', 'numResults'.
* @return array<string, mixed> Paging params.
*/
protected function buildParams(array $data): array
{
$limit = $data['options']['limit'];
$paging = [
'count' => $data['count'],
'current' => $data['numResults'],
'perPage' => $limit,
'page' => $data['options']['page'],
'requestedPage' => $data['options']['page'],
];
$paging = $this->addPageCountParams($paging, $data);
$paging = $this->addStartEndParams($paging, $data);
$paging = $this->addPrevNextParams($paging, $data);
$paging = $this->addSortingParams($paging, $data);
$paging += [
'limit' => $data['defaults']['limit'] != $limit ? $limit : null,
'scope' => $data['options']['scope'],
'finder' => $data['finder'],
];
return $paging;
}
/**
* Add "page" and "pageCount" params.
*
* @param array<string, mixed> $params Paging params.
* @param array $data Paginator data.
* @return array<string, mixed> Updated params.
*/
protected function addPageCountParams(array $params, array $data): array
{
$page = $params['page'];
$pageCount = 0;
if ($params['count'] !== null) {
$pageCount = max((int)ceil($params['count'] / $params['perPage']), 1);
$page = min($page, $pageCount);
} elseif ($params['current'] === 0 && $params['requestedPage'] > 1) {
$page = 1;
}
$params['page'] = $page;
$params['pageCount'] = $pageCount;
return $params;
}
/**
* Add "start" and "end" params.
*
* @param array<string, mixed> $params Paging params.
* @param array $data Paginator data.
* @return array<string, mixed> Updated params.
*/
protected function addStartEndParams(array $params, array $data): array
{
$start = $end = 0;
if ($params['current'] > 0) {
$start = (($params['page'] - 1) * $params['perPage']) + 1;
$end = $start + $params['current'] - 1;
}
$params['start'] = $start;
$params['end'] = $end;
return $params;
}
/**
* Add "prevPage" and "nextPage" params.
*
* @param array<string, mixed> $params Paginator params.
* @param array $data Paging data.
* @return array<string, mixed> Updated params.
*/
protected function addPrevNextParams(array $params, array $data): array
{
$params['prevPage'] = $params['page'] > 1;
if ($params['count'] === null) {
$params['nextPage'] = true;
} else {
$params['nextPage'] = $params['count'] > $params['page'] * $params['perPage'];
}
return $params;
}
/**
* Add sorting / ordering params.
*
* @param array<string, mixed> $params Paginator params.
* @param array $data Paging data.
* @return array<string, mixed> Updated params.
*/
protected function addSortingParams(array $params, array $data): array
{
$defaults = $data['defaults'];
$order = (array)$data['options']['order'];
$sortDefault = $directionDefault = false;
if (!empty($defaults['order']) && count($defaults['order']) >= 1) {
$sortDefault = key($defaults['order']);
$directionDefault = current($defaults['order']);
}
$params += [
'sort' => $data['options']['sort'],
'direction' => isset($data['options']['sort']) && count($order) ? current($order) : null,
'sortDefault' => $sortDefault,
'directionDefault' => $directionDefault,
'completeSort' => $order,
];
return $params;
}
/**
* Extracts the finder name and options out of the provided pagination options.
*
* @param array<string, mixed> $options the pagination options.
* @return array An array containing in the first position the finder name
* and in the second the options to be passed to it.
*/
protected function _extractFinder(array $options): array
{
$type = !empty($options['finder']) ? $options['finder'] : 'all';
unset(
$options['finder'],
$options['maxLimit'],
$options['allowedParameters'],
$options['whitelist'],
$options['sortableFields'],
$options['sortWhitelist'],
);
if (is_array($type)) {
$options = (array)current($type) + $options;
$type = key($type);
}
return [$type, $options];
}
/**
* Get paging params after pagination operation.
*
* @return array<string, array>
*/
public function getPagingParams(): array
{
return $this->_pagingParams;
}
/**
* Shim method for reading the deprecated whitelist or allowedParameters options
*
* @return array<string>
*/
protected function getAllowedParameters(): array
{
$allowed = $this->getConfig('allowedParameters');
if (!$allowed) {
$allowed = [];
}
$whitelist = $this->getConfig('whitelist');
if ($whitelist) {
deprecationWarning('The `whitelist` option is deprecated. Use the `allowedParameters` option instead.');
return array_merge($allowed, $whitelist);
}
return $allowed;
}
/**
* Shim method for reading the deprecated sortWhitelist or sortableFields options.
*
* @param array<string, mixed> $config The configuration data to coalesce and emit warnings on.
* @return array<string>|null
*/
protected function getSortableFields(array $config): ?array
{
$allowed = $config['sortableFields'] ?? null;
if ($allowed !== null) {
return $allowed;
}
$deprecated = $config['sortWhitelist'] ?? null;
if ($deprecated !== null) {
deprecationWarning('The `sortWhitelist` option is deprecated. Use `sortableFields` instead.');
}
return $deprecated;
}
/**
* 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.
*/
public function mergeOptions(array $params, array $settings): array
{
if (!empty($settings['scope'])) {
$scope = $settings['scope'];
$params = !empty($params[$scope]) ? (array)$params[$scope] : [];
}
$allowed = $this->getAllowedParameters();
$params = array_intersect_key($params, array_flip($allowed));
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.
*/
public function getDefaults(string $alias, array $settings): array
{
if (isset($settings[$alias])) {
$settings = $settings[$alias];
}
$defaults = $this->getConfig();
$defaults['whitelist'] = $defaults['allowedParameters'] = $this->getAllowedParameters();
$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.
*/
public function validateSort(RepositoryInterface $object, array $options): array
{
if (isset($options['sort'])) {
$direction = null;
if (isset($options['direction'])) {
$direction = strtolower($options['direction']);
}
if (!in_array($direction, ['asc', 'desc'], true)) {
$direction = 'asc';
}
$order = isset($options['order']) && is_array($options['order']) ? $options['order'] : [];
if ($order && $options['sort'] && strpos($options['sort'], '.') === false) {
$order = $this->_removeAliases($order, $object->getAlias());
}
$options['order'] = [$options['sort'] => $direction] + $order;
} else {
$options['sort'] = null;
}
unset($options['direction']);
if (empty($options['order'])) {
$options['order'] = [];
}
if (!is_array($options['order'])) {
return $options;
}
$sortAllowed = false;
$allowed = $this->getSortableFields($options);
if ($allowed !== null) {
$options['sortableFields'] = $options['sortWhitelist'] = $allowed;
$field = key($options['order']);
$sortAllowed = in_array($field, $allowed, true);
if (!$sortAllowed) {
$options['order'] = [];
$options['sort'] = null;
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 (strpos($field, '.') === false) {
$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 (strpos($key, '.') !== false) {
[$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;
}
/**
* 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.
*/
public 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;
}
}
// phpcs:disable
class_alias(
'Cake\Datasource\Paging\NumericPaginator',
'Cake\Datasource\Paginator'
);
// phpcs:enable
+50
View File
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.5.0
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource\Paging;
use Cake\Datasource\ResultSetInterface;
/**
* This interface describes the methods for paginator instance.
*/
interface PaginatorInterface
{
/**
* Handles pagination of datasource records.
*
* @param \Cake\Datasource\RepositoryInterface|\Cake\Datasource\QueryInterface $object The repository or query
* to paginate.
* @param array $params Request params
* @param array $settings The settings/configuration used for pagination.
* @return \Cake\Datasource\ResultSetInterface Query results
*/
public function paginate(object $object, array $params = [], array $settings = []): ResultSetInterface;
/**
* Get paging params after pagination operation.
*
* @return array
*/
public function getPagingParams(): array;
}
// phpcs:disable
class_alias(
'Cake\Datasource\Paging\PaginatorInterface',
'Cake\Datasource\PaginatorInterface'
);
// phpcs:enable
+49
View File
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.9.0
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource\Paging;
use Cake\Datasource\QueryInterface;
/**
* Simplified paginator which avoids potentially expensives 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, and handle 404 errors
* when pagination goes past the available result set.
*/
class SimplePaginator extends NumericPaginator
{
/**
* 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;
}
}
// phpcs:disable
class_alias(
'Cake\Datasource\Paging\SimplePaginator',
'Cake\Datasource\SimplePaginator'
);
// phpcs:enable
+137
View File
@@ -0,0 +1,137 @@
<?php
declare(strict_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 Closure;
use Psr\SimpleCache\CacheInterface;
use RuntimeException;
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 $_key;
/**
* Config for cache engine.
*
* @var \Psr\SimpleCache\CacheInterface|string
*/
protected $_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.
* @throws \RuntimeException
*/
public function __construct($key, $config)
{
if (!is_string($key) && !($key instanceof Closure)) {
throw new RuntimeException('Cache keys must be strings or callables.');
}
$this->_key = $key;
if (!is_string($config) && !($config instanceof CacheInterface)) {
throw new RuntimeException('Cache configs must be strings or \Psr\SimpleCache\CacheInterface instances.');
}
$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)
{
$key = $this->_resolveKey($query);
$storage = $this->_resolveCacher();
$result = $storage->get($key);
if (empty($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 \RuntimeException
*/
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 RuntimeException($msg);
}
return $key;
}
/**
* Get the cache engine.
*
* @return \Psr\SimpleCache\CacheInterface
*/
protected function _resolveCacher()
{
if (is_string($this->_config)) {
return Cache::pool($this->_config);
}
return $this->_config;
}
}
+406
View File
@@ -0,0 +1,406 @@
<?php
declare(strict_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;
/**
* 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()}
* @method $this setRepository(\Cake\Datasource\RepositoryInterface $repository) Set the default repository object that will be used by this query.
*/
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 \Cake\Database\ExpressionInterface|\Cake\ORM\Association|\Cake\ORM\Table|callable|array|string $fields Fields.
* @param bool $overwrite whether to reset fields with passed list or not
* @return $this
*/
public function select($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 $fields The fields to alias
* @param string|null $defaultAlias The default alias
* @return array<string, string>
*/
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.
*
* @return \Cake\Datasource\ResultSetInterface
*/
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 array<string, mixed> $options The options for the finder.
* @return static Returns a modified query.
*/
public function find(string $finder, array $options = []);
/**
* 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 \Cake\Datasource\EntityInterface|array|null the first result from the ResultSet
*/
public function first();
/**
* 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->newExpr()->add(['1 + 1'])); // LIMIT (1 + 1)
* ```
*
* @param \Cake\Database\ExpressionInterface|int|null $limit number of records to be returned
* @return $this
*/
public function limit($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->newExpr()->add(['1 + 1'])); // OFFSET (1 + 1)
* ```
*
* @param \Cake\Database\ExpressionInterface|int|null $offset number of records to be skipped
* @return $this
*/
public function offset($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->order(['title' => 'DESC', 'author_id' => 'ASC']);
* ```
*
* Produces:
*
* `ORDER BY title DESC, author_id ASC`
*
* ```
* $query
* ->order(['title' => $query->newExpr('DESC NULLS FIRST')])
* ->order('author_id');
* ```
*
* Will generate:
*
* `ORDER BY title DESC NULLS FIRST, author_id`
*
* ```
* $expression = $query->newExpr()->add(['id % 2 = 0']);
* $query->order($expression)->order(['title' => 'ASC']);
* ```
*
* Will become:
*
* `ORDER BY (id %2 = 0), title ASC`
*
* If you need to set complex expressions as order conditions, you
* should use `orderAsc()` or `orderDesc()`.
*
* @param \Cake\Database\ExpressionInterface|\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 order($fields, $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 repository(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 methods 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->newExpr()->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 callable functions 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 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($conditions = null, array $types = [], bool $overwrite = false);
}
File diff suppressed because it is too large Load Diff
+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 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)
+252
View File
@@ -0,0 +1,252 @@
<?php
declare(strict_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;
/**
* 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;
/**
* 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 array<string, mixed> $options An array that will be passed to Query::applyOptions()
* @return \Cake\Datasource\QueryInterface
*/
public function find(string $type = 'all', array $options = []);
/**
* 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, mixed> $options options accepted by `Table::find()`
* @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($primaryKey, array $options = []): EntityInterface;
/**
* Creates a new Query instance for this repository
*
* @return \Cake\Datasource\QueryInterface
*/
public function query();
/**
* 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 \Cake\Database\Expression\QueryExpression|\Closure|array|string $fields A hash of field => new value.
* @param mixed $conditions Conditions to be used, accepts anything Query::where()
* can take.
* @return int Count Returns the affected rows.
*/
public function updateAll($fields, $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 mixed $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($conditions): int;
/**
* Returns true if there is any record in this repository matching the specified
* conditions.
*
* @param array $conditions list of conditions to pass to the query
* @return bool
*/
public function exists($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 \ArrayAccess|array $options The options to use when saving.
* @return \Cake\Datasource\EntityInterface|false
*/
public function save(EntityInterface $entity, $options = []);
/**
* 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 \ArrayAccess|array $options The options for the delete.
* @return bool success
*/
public function delete(EntityInterface $entity, $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;
}
+61
View File
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource;
use Cake\Collection\Collection;
use Cake\Core\Configure;
use Countable;
/**
* Generic ResultSet decorator. This will make any traversable object appear to
* be a database result
*
* @template T of \Cake\Datasource\EntityInterface|array
* @implements \Cake\Datasource\ResultSetInterface<T>
*/
class ResultSetDecorator extends Collection implements ResultSetInterface
{
/**
* Make this object countable.
*
* Part of the Countable interface. Calling this method
* will convert the underlying traversable object into an array and
* get the count of the underlying data.
*
* @return int
*/
public function count(): int
{
$iterator = $this->getInnerIterator();
if ($iterator instanceof Countable) {
return $iterator->count();
}
return count($this->toArray());
}
/**
* @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;
use Countable;
use Serializable;
/**
* Describes how a collection of datasource results should look like
*
* @template T
*/
interface ResultSetInterface extends CollectionInterface, Countable, Serializable
{
}
+142
View File
@@ -0,0 +1,142 @@
<?php
declare(strict_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;
/**
* 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 $name;
/**
* Rule options
*
* @var array<string, mixed>
*/
protected $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 $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 || empty($this->options['errorField'])) {
return $pass === true;
}
$message = $this->options['message'] ?? 'invalid';
if (is_string($pass)) {
$message = $pass;
}
if ($this->name) {
$message = [$this->name => $message];
} else {
$message = [$message];
}
$errorField = $this->options['errorField'];
$entity->setError($errorField, $message);
if ($entity instanceof InvalidPropertyInterface && isset($entity->{$errorField})) {
$invalidValue = $entity->{$errorField};
$entity->setInvalidField($errorField, $invalidValue);
}
/** @phpstan-ignore-next-line */
return $pass === true;
}
}
+120
View File
@@ -0,0 +1,120 @@
<?php
declare(strict_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
*/
protected $_rulesChecker;
/**
* 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|array|null $options The options To be passed to the rules.
* @return bool
*/
public function checkRules(EntityInterface $entity, string $operation = RulesChecker::CREATE, $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;
}
/** @psalm-var class-string<\Cake\Datasource\RulesChecker> $class */
$class = defined('static::RULES_CLASS') ? static::RULES_CLASS : RulesChecker::class;
/** @psalm-suppress ArgumentTypeCoercion */
$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;
}
}
+330
View File
@@ -0,0 +1,330 @@
<?php
declare(strict_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 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 $_rules = [];
/**
* The list of rules to check during create operations
*
* @var array<\Cake\Datasource\RuleInvoker>
*/
protected $_createRules = [];
/**
* The list of rules to check during update operations
*
* @var array<\Cake\Datasource\RuleInvoker>
*/
protected $_updateRules = [];
/**
* The list of rules to check during delete operations
*
* @var array<\Cake\Datasource\RuleInvoker>
*/
protected $_deleteRules = [];
/**
* List of options to pass to every callable rule
*
* @var array
*/
protected $_options = [];
/**
* Whether to use I18n functions for translating default error messages
*
* @var bool
*/
protected $_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 both on create and 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
*/
public function add(callable $rule, $name = null, array $options = [])
{
$this->_rules[] = $this->_addError($rule, $name, $options);
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
*/
public function addCreate(callable $rule, $name = null, array $options = [])
{
$this->_createRules[] = $this->_addError($rule, $name, $options);
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
*/
public function addUpdate(callable $rule, $name = null, array $options = [])
{
$this->_updateRules[] = $this->_addError($rule, $name, $options);
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
*/
public function addDelete(callable $rule, $name = null, array $options = [])
{
$this->_deleteRules[] = $this->_addError($rule, $name, $options);
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
{
if ($mode === self::CREATE) {
return $this->checkCreate($entity, $options);
}
if ($mode === self::UPDATE) {
return $this->checkUpdate($entity, $options);
}
if ($mode === self::DELETE) {
return $this->checkDelete($entity, $options);
}
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($this->_rules, $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($this->_rules, $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|\Cake\Datasource\RuleInvoker $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, $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;
}
}
+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, $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 a 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 represent 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;
}
+10
View File
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
use function Cake\Core\deprecationWarning;
deprecationWarning(
'Since 4.2.0: Cake\Datasource\SimplePaginator is deprecated. ' .
'Use Cake\Datasource\Paging\SimplePaginator instead.'
);
class_exists('Cake\Datasource\Paging\SimplePaginator');
+42
View File
@@ -0,0 +1,42 @@
{
"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": ">=7.4.0",
"cakephp/core": "^4.0",
"psr/log": "^1.0 || ^2.0",
"psr/simple-cache": "^1.0 || ^2.0"
},
"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."
},
"autoload": {
"psr-4": {
"Cake\\Datasource\\": "."
}
}
}