├── ConnectionInterface.php ├── ConnectionManager.php ├── ConnectionRegistry.php ├── EntityInterface.php ├── EntityTrait.php ├── Exception ├── InvalidPrimaryKeyException.php ├── MissingDatasourceConfigException.php ├── MissingDatasourceException.php ├── MissingModelException.php ├── PageOutOfBoundsException.php └── RecordNotFoundException.php ├── FactoryLocator.php ├── FixtureInterface.php ├── InvalidPropertyInterface.php ├── LICENSE.txt ├── Locator ├── AbstractLocator.php └── LocatorInterface.php ├── ModelAwareTrait.php ├── Paginator.php ├── PaginatorInterface.php ├── QueryCacher.php ├── QueryInterface.php ├── QueryTrait.php ├── README.md ├── RepositoryInterface.php ├── ResultSetDecorator.php ├── ResultSetInterface.php ├── RuleInvoker.php ├── RulesAwareTrait.php ├── RulesChecker.php ├── SchemaInterface.php ├── SimplePaginator.php └── composer.json /ConnectionInterface.php: -------------------------------------------------------------------------------- 1 | transactional(function ($connection) { 94 | * $connection->newQuery()->delete('users')->execute(); 95 | * }); 96 | * ``` 97 | * 98 | * @param callable $callback The callback to execute within a transaction. 99 | * @return mixed The return value of the callback. 100 | * @throws \Exception Will re-throw any exception raised in $callback after 101 | * rolling back the transaction. 102 | */ 103 | public function transactional(callable $callback); 104 | 105 | /** 106 | * Run an operation with constraints disabled. 107 | * 108 | * Constraints should be re-enabled after the callback succeeds/fails. 109 | * 110 | * ### Example: 111 | * 112 | * ``` 113 | * $connection->disableConstraints(function ($connection) { 114 | * $connection->newQuery()->delete('users')->execute(); 115 | * }); 116 | * ``` 117 | * 118 | * @param callable $callback The callback to execute within a transaction. 119 | * @return mixed The return value of the callback. 120 | * @throws \Exception Will re-throw any exception raised in $callback after 121 | * rolling back the transaction. 122 | */ 123 | public function disableConstraints(callable $callback); 124 | 125 | /** 126 | * Enable/disable query logging 127 | * 128 | * @param bool $enable Enable/disable query logging 129 | * @return $this 130 | */ 131 | public function enableQueryLogging(bool $enable = true); 132 | 133 | /** 134 | * Disable query logging 135 | * 136 | * @return $this 137 | */ 138 | public function disableQueryLogging(); 139 | 140 | /** 141 | * Check if query logging is enabled. 142 | * 143 | * @return bool 144 | */ 145 | public function isQueryLoggingEnabled(): bool; 146 | } 147 | -------------------------------------------------------------------------------- /ConnectionManager.php: -------------------------------------------------------------------------------- 1 | 55 | */ 56 | protected static $_dsnClassMap = [ 57 | 'mysql' => Mysql::class, 58 | 'postgres' => Postgres::class, 59 | 'sqlite' => Sqlite::class, 60 | 'sqlserver' => Sqlserver::class, 61 | ]; 62 | 63 | /** 64 | * The ConnectionRegistry used by the manager. 65 | * 66 | * @var \Cake\Datasource\ConnectionRegistry 67 | */ 68 | protected static $_registry; 69 | 70 | /** 71 | * Configure a new connection object. 72 | * 73 | * The connection will not be constructed until it is first used. 74 | * 75 | * @param string|array $key The name of the connection config, or an array of multiple configs. 76 | * @param array|null $config An array of name => config data for adapter. 77 | * @return void 78 | * @throws \Cake\Core\Exception\CakeException When trying to modify an existing config. 79 | * @see \Cake\Core\StaticConfigTrait::config() 80 | */ 81 | public static function setConfig($key, $config = null): void 82 | { 83 | if (is_array($config)) { 84 | $config['name'] = $key; 85 | } 86 | 87 | static::_setConfig($key, $config); 88 | } 89 | 90 | /** 91 | * Parses a DSN into a valid connection configuration 92 | * 93 | * This method allows setting a DSN using formatting similar to that used by PEAR::DB. 94 | * The following is an example of its usage: 95 | * 96 | * ``` 97 | * $dsn = 'mysql://user:pass@localhost/database'; 98 | * $config = ConnectionManager::parseDsn($dsn); 99 | * 100 | * $dsn = 'Cake\Database\Driver\Mysql://localhost:3306/database?className=Cake\Database\Connection'; 101 | * $config = ConnectionManager::parseDsn($dsn); 102 | * 103 | * $dsn = 'Cake\Database\Connection://localhost:3306/database?driver=Cake\Database\Driver\Mysql'; 104 | * $config = ConnectionManager::parseDsn($dsn); 105 | * ``` 106 | * 107 | * For all classes, the value of `scheme` is set as the value of both the `className` and `driver` 108 | * unless they have been otherwise specified. 109 | * 110 | * Note that query-string arguments are also parsed and set as values in the returned configuration. 111 | * 112 | * @param string $config The DSN string to convert to a configuration array 113 | * @return array The configuration array to be stored after parsing the DSN 114 | */ 115 | public static function parseDsn(string $config): array 116 | { 117 | $config = static::_parseDsn($config); 118 | 119 | if (isset($config['path']) && empty($config['database'])) { 120 | $config['database'] = substr($config['path'], 1); 121 | } 122 | 123 | if (empty($config['driver'])) { 124 | $config['driver'] = $config['className']; 125 | $config['className'] = Connection::class; 126 | } 127 | 128 | unset($config['path']); 129 | 130 | return $config; 131 | } 132 | 133 | /** 134 | * Set one or more connection aliases. 135 | * 136 | * Connection aliases allow you to rename active connections without overwriting 137 | * the aliased connection. This is most useful in the test-suite for replacing 138 | * connections with their test variant. 139 | * 140 | * Defined aliases will take precedence over normal connection names. For example, 141 | * if you alias 'default' to 'test', fetching 'default' will always return the 'test' 142 | * connection as long as the alias is defined. 143 | * 144 | * You can remove aliases with ConnectionManager::dropAlias(). 145 | * 146 | * ### Usage 147 | * 148 | * ``` 149 | * // Make 'things' resolve to 'test_things' connection 150 | * ConnectionManager::alias('test_things', 'things'); 151 | * ``` 152 | * 153 | * @param string $alias The alias to add. Fetching $source will return $alias when loaded with get. 154 | * @param string $source The connection to add an alias to. 155 | * @return void 156 | * @throws \Cake\Datasource\Exception\MissingDatasourceConfigException When aliasing a 157 | * connection that does not exist. 158 | */ 159 | public static function alias(string $alias, string $source): void 160 | { 161 | if (empty(static::$_config[$source]) && empty(static::$_config[$alias])) { 162 | throw new MissingDatasourceConfigException( 163 | sprintf('Cannot create alias of "%s" as it does not exist.', $alias) 164 | ); 165 | } 166 | static::$_aliasMap[$source] = $alias; 167 | } 168 | 169 | /** 170 | * Drop an alias. 171 | * 172 | * Removes an alias from ConnectionManager. Fetching the aliased 173 | * connection may fail if there is no other connection with that name. 174 | * 175 | * @param string $name The connection name to remove aliases for. 176 | * @return void 177 | */ 178 | public static function dropAlias(string $name): void 179 | { 180 | unset(static::$_aliasMap[$name]); 181 | } 182 | 183 | /** 184 | * Get a connection. 185 | * 186 | * If the connection has not been constructed an instance will be added 187 | * to the registry. This method will use any aliases that have been 188 | * defined. If you want the original unaliased connections pass `false` 189 | * as second parameter. 190 | * 191 | * @param string $name The connection name. 192 | * @param bool $useAliases Set to false to not use aliased connections. 193 | * @return \Cake\Datasource\ConnectionInterface A connection object. 194 | * @throws \Cake\Datasource\Exception\MissingDatasourceConfigException When config 195 | * data is missing. 196 | */ 197 | public static function get(string $name, bool $useAliases = true) 198 | { 199 | if ($useAliases && isset(static::$_aliasMap[$name])) { 200 | $name = static::$_aliasMap[$name]; 201 | } 202 | if (empty(static::$_config[$name])) { 203 | throw new MissingDatasourceConfigException(['name' => $name]); 204 | } 205 | if (empty(static::$_registry)) { 206 | static::$_registry = new ConnectionRegistry(); 207 | } 208 | if (isset(static::$_registry->{$name})) { 209 | return static::$_registry->{$name}; 210 | } 211 | 212 | return static::$_registry->load($name, static::$_config[$name]); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /ConnectionRegistry.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | class ConnectionRegistry extends ObjectRegistry 30 | { 31 | /** 32 | * Resolve a datasource classname. 33 | * 34 | * Part of the template method for Cake\Core\ObjectRegistry::load() 35 | * 36 | * @param string $class Partial classname to resolve. 37 | * @return string|null Either the correct class name or null. 38 | * @psalm-return class-string|null 39 | */ 40 | protected function _resolveClassName(string $class): ?string 41 | { 42 | return App::className($class, 'Datasource'); 43 | } 44 | 45 | /** 46 | * Throws an exception when a datasource is missing 47 | * 48 | * Part of the template method for Cake\Core\ObjectRegistry::load() 49 | * 50 | * @param string $class The classname that is missing. 51 | * @param string|null $plugin The plugin the datasource is missing in. 52 | * @return void 53 | * @throws \Cake\Datasource\Exception\MissingDatasourceException 54 | */ 55 | protected function _throwMissingClassError(string $class, ?string $plugin): void 56 | { 57 | throw new MissingDatasourceException([ 58 | 'class' => $class, 59 | 'plugin' => $plugin, 60 | ]); 61 | } 62 | 63 | /** 64 | * Create the connection object with the correct settings. 65 | * 66 | * Part of the template method for Cake\Core\ObjectRegistry::load() 67 | * 68 | * If a callable is passed as first argument, The returned value of this 69 | * function will be the result of the callable. 70 | * 71 | * @param string|\Cake\Datasource\ConnectionInterface|callable $class The classname or object to make. 72 | * @param string $alias The alias of the object. 73 | * @param array $config An array of settings to use for the datasource. 74 | * @return \Cake\Datasource\ConnectionInterface A connection with the correct settings. 75 | */ 76 | protected function _create($class, string $alias, array $config) 77 | { 78 | if (is_callable($class)) { 79 | return $class($alias); 80 | } 81 | 82 | if (is_object($class)) { 83 | return $class; 84 | } 85 | 86 | unset($config['className']); 87 | 88 | /** @var \Cake\Datasource\ConnectionInterface */ 89 | return new $class($config); 90 | } 91 | 92 | /** 93 | * Remove a single adapter from the registry. 94 | * 95 | * @param string $name The adapter name. 96 | * @return $this 97 | */ 98 | public function unload(string $name) 99 | { 100 | unset($this->_loaded[$name]); 101 | 102 | return $this; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /EntityInterface.php: -------------------------------------------------------------------------------- 1 | true` 108 | * means that any field not defined in the map will be accessible by default 109 | * 110 | * @var bool[] 111 | */ 112 | protected $_accessible = ['*' => true]; 113 | 114 | /** 115 | * The alias of the repository this entity came from 116 | * 117 | * @var string 118 | */ 119 | protected $_registryAlias = ''; 120 | 121 | /** 122 | * Magic getter to access fields that have been set in this entity 123 | * 124 | * @param string $field Name of the field to access 125 | * @return mixed 126 | */ 127 | public function &__get(string $field) 128 | { 129 | return $this->get($field); 130 | } 131 | 132 | /** 133 | * Magic setter to add or edit a field in this entity 134 | * 135 | * @param string $field The name of the field to set 136 | * @param mixed $value The value to set to the field 137 | * @return void 138 | */ 139 | public function __set(string $field, $value): void 140 | { 141 | $this->set($field, $value); 142 | } 143 | 144 | /** 145 | * Returns whether this entity contains a field named $field 146 | * regardless of if it is empty. 147 | * 148 | * @param string $field The field to check. 149 | * @return bool 150 | * @see \Cake\ORM\Entity::has() 151 | */ 152 | public function __isset(string $field): bool 153 | { 154 | return $this->has($field); 155 | } 156 | 157 | /** 158 | * Removes a field from this entity 159 | * 160 | * @param string $field The field to unset 161 | * @return void 162 | */ 163 | public function __unset(string $field): void 164 | { 165 | $this->unset($field); 166 | } 167 | 168 | /** 169 | * Sets a single field inside this entity. 170 | * 171 | * ### Example: 172 | * 173 | * ``` 174 | * $entity->set('name', 'Andrew'); 175 | * ``` 176 | * 177 | * It is also possible to mass-assign multiple fields to this entity 178 | * with one call by passing a hashed array as fields in the form of 179 | * field => value pairs 180 | * 181 | * ### Example: 182 | * 183 | * ``` 184 | * $entity->set(['name' => 'andrew', 'id' => 1]); 185 | * echo $entity->name // prints andrew 186 | * echo $entity->id // prints 1 187 | * ``` 188 | * 189 | * Some times it is handy to bypass setter functions in this entity when assigning 190 | * fields. You can achieve this by disabling the `setter` option using the 191 | * `$options` parameter: 192 | * 193 | * ``` 194 | * $entity->set('name', 'Andrew', ['setter' => false]); 195 | * $entity->set(['name' => 'Andrew', 'id' => 1], ['setter' => false]); 196 | * ``` 197 | * 198 | * Mass assignment should be treated carefully when accepting user input, by default 199 | * entities will guard all fields when fields are assigned in bulk. You can disable 200 | * the guarding for a single set call with the `guard` option: 201 | * 202 | * ``` 203 | * $entity->set(['name' => 'Andrew', 'id' => 1], ['guard' => false]); 204 | * ``` 205 | * 206 | * You do not need to use the guard option when assigning fields individually: 207 | * 208 | * ``` 209 | * // No need to use the guard option. 210 | * $entity->set('name', 'Andrew'); 211 | * ``` 212 | * 213 | * @param string|array $field the name of field to set or a list of 214 | * fields with their respective values 215 | * @param mixed $value The value to set to the field or an array if the 216 | * first argument is also an array, in which case will be treated as $options 217 | * @param array $options options to be used for setting the field. Allowed option 218 | * keys are `setter` and `guard` 219 | * @return $this 220 | * @throws \InvalidArgumentException 221 | */ 222 | public function set($field, $value = null, array $options = []) 223 | { 224 | if (is_string($field) && $field !== '') { 225 | $guard = false; 226 | $field = [$field => $value]; 227 | } else { 228 | $guard = true; 229 | $options = (array)$value; 230 | } 231 | 232 | if (!is_array($field)) { 233 | throw new InvalidArgumentException('Cannot set an empty field'); 234 | } 235 | $options += ['setter' => true, 'guard' => $guard]; 236 | 237 | foreach ($field as $name => $value) { 238 | $name = (string)$name; 239 | if ($options['guard'] === true && !$this->isAccessible($name)) { 240 | continue; 241 | } 242 | 243 | $this->setDirty($name, true); 244 | 245 | if ( 246 | !array_key_exists($name, $this->_original) && 247 | array_key_exists($name, $this->_fields) && 248 | $this->_fields[$name] !== $value 249 | ) { 250 | $this->_original[$name] = $this->_fields[$name]; 251 | } 252 | 253 | if (!$options['setter']) { 254 | $this->_fields[$name] = $value; 255 | continue; 256 | } 257 | 258 | $setter = static::_accessor($name, 'set'); 259 | if ($setter) { 260 | $value = $this->{$setter}($value); 261 | } 262 | $this->_fields[$name] = $value; 263 | } 264 | 265 | return $this; 266 | } 267 | 268 | /** 269 | * Returns the value of a field by name 270 | * 271 | * @param string $field the name of the field to retrieve 272 | * @return mixed 273 | * @throws \InvalidArgumentException if an empty field name is passed 274 | */ 275 | public function &get(string $field) 276 | { 277 | if ($field === '') { 278 | throw new InvalidArgumentException('Cannot get an empty field'); 279 | } 280 | 281 | $value = null; 282 | $method = static::_accessor($field, 'get'); 283 | 284 | if (isset($this->_fields[$field])) { 285 | $value = &$this->_fields[$field]; 286 | } 287 | 288 | if ($method) { 289 | $result = $this->{$method}($value); 290 | 291 | return $result; 292 | } 293 | 294 | return $value; 295 | } 296 | 297 | /** 298 | * Returns the value of an original field by name 299 | * 300 | * @param string $field the name of the field for which original value is retrieved. 301 | * @return mixed 302 | * @throws \InvalidArgumentException if an empty field name is passed. 303 | */ 304 | public function getOriginal(string $field) 305 | { 306 | if (!strlen($field)) { 307 | throw new InvalidArgumentException('Cannot get an empty field'); 308 | } 309 | if (array_key_exists($field, $this->_original)) { 310 | return $this->_original[$field]; 311 | } 312 | 313 | return $this->get($field); 314 | } 315 | 316 | /** 317 | * Gets all original values of the entity. 318 | * 319 | * @return array 320 | */ 321 | public function getOriginalValues(): array 322 | { 323 | $originals = $this->_original; 324 | $originalKeys = array_keys($originals); 325 | foreach ($this->_fields as $key => $value) { 326 | if (!in_array($key, $originalKeys, true)) { 327 | $originals[$key] = $value; 328 | } 329 | } 330 | 331 | return $originals; 332 | } 333 | 334 | /** 335 | * Returns whether this entity contains a field named $field 336 | * that contains a non-null value. 337 | * 338 | * ### Example: 339 | * 340 | * ``` 341 | * $entity = new Entity(['id' => 1, 'name' => null]); 342 | * $entity->has('id'); // true 343 | * $entity->has('name'); // false 344 | * $entity->has('last_name'); // false 345 | * ``` 346 | * 347 | * You can check multiple fields by passing an array: 348 | * 349 | * ``` 350 | * $entity->has(['name', 'last_name']); 351 | * ``` 352 | * 353 | * All fields must not be null to get a truthy result. 354 | * 355 | * When checking multiple fields. All fields must not be null 356 | * in order for true to be returned. 357 | * 358 | * @param string|string[] $field The field or fields to check. 359 | * @return bool 360 | */ 361 | public function has($field): bool 362 | { 363 | foreach ((array)$field as $prop) { 364 | if ($this->get($prop) === null) { 365 | return false; 366 | } 367 | } 368 | 369 | return true; 370 | } 371 | 372 | /** 373 | * Checks that a field is empty 374 | * 375 | * This is not working like the PHP `empty()` function. The method will 376 | * return true for: 377 | * 378 | * - `''` (empty string) 379 | * - `null` 380 | * - `[]` 381 | * 382 | * and false in all other cases. 383 | * 384 | * @param string $field The field to check. 385 | * @return bool 386 | */ 387 | public function isEmpty(string $field): bool 388 | { 389 | $value = $this->get($field); 390 | if ( 391 | $value === null || 392 | ( 393 | is_array($value) && 394 | empty($value) || 395 | ( 396 | is_string($value) && 397 | $value === '' 398 | ) 399 | ) 400 | ) { 401 | return true; 402 | } 403 | 404 | return false; 405 | } 406 | 407 | /** 408 | * Checks tha a field has a value. 409 | * 410 | * This method will return true for 411 | * 412 | * - Non-empty strings 413 | * - Non-empty arrays 414 | * - Any object 415 | * - Integer, even `0` 416 | * - Float, even 0.0 417 | * 418 | * and false in all other cases. 419 | * 420 | * @param string $field The field to check. 421 | * @return bool 422 | */ 423 | public function hasValue(string $field): bool 424 | { 425 | return !$this->isEmpty($field); 426 | } 427 | 428 | /** 429 | * Removes a field or list of fields from this entity 430 | * 431 | * ### Examples: 432 | * 433 | * ``` 434 | * $entity->unset('name'); 435 | * $entity->unset(['name', 'last_name']); 436 | * ``` 437 | * 438 | * @param string|string[] $field The field to unset. 439 | * @return $this 440 | */ 441 | public function unset($field) 442 | { 443 | $field = (array)$field; 444 | foreach ($field as $p) { 445 | unset($this->_fields[$p], $this->_original[$p], $this->_dirty[$p]); 446 | } 447 | 448 | return $this; 449 | } 450 | 451 | /** 452 | * Removes a field or list of fields from this entity 453 | * 454 | * @deprecated 4.0.0 Use {@link unset()} instead. Will be removed in 5.0. 455 | * @param string|string[] $field The field to unset. 456 | * @return $this 457 | */ 458 | public function unsetProperty($field) 459 | { 460 | deprecationWarning('EntityTrait::unsetProperty() is deprecated. Use unset() instead.'); 461 | 462 | return $this->unset($field); 463 | } 464 | 465 | /** 466 | * Sets hidden fields. 467 | * 468 | * @param string[] $fields An array of fields to hide from array exports. 469 | * @param bool $merge Merge the new fields with the existing. By default false. 470 | * @return $this 471 | */ 472 | public function setHidden(array $fields, bool $merge = false) 473 | { 474 | if ($merge === false) { 475 | $this->_hidden = $fields; 476 | 477 | return $this; 478 | } 479 | 480 | $fields = array_merge($this->_hidden, $fields); 481 | $this->_hidden = array_unique($fields); 482 | 483 | return $this; 484 | } 485 | 486 | /** 487 | * Gets the hidden fields. 488 | * 489 | * @return string[] 490 | */ 491 | public function getHidden(): array 492 | { 493 | return $this->_hidden; 494 | } 495 | 496 | /** 497 | * Sets the virtual fields on this entity. 498 | * 499 | * @param string[] $fields An array of fields to treat as virtual. 500 | * @param bool $merge Merge the new fields with the existing. By default false. 501 | * @return $this 502 | */ 503 | public function setVirtual(array $fields, bool $merge = false) 504 | { 505 | if ($merge === false) { 506 | $this->_virtual = $fields; 507 | 508 | return $this; 509 | } 510 | 511 | $fields = array_merge($this->_virtual, $fields); 512 | $this->_virtual = array_unique($fields); 513 | 514 | return $this; 515 | } 516 | 517 | /** 518 | * Gets the virtual fields on this entity. 519 | * 520 | * @return string[] 521 | */ 522 | public function getVirtual(): array 523 | { 524 | return $this->_virtual; 525 | } 526 | 527 | /** 528 | * Gets the list of visible fields. 529 | * 530 | * The list of visible fields is all standard fields 531 | * plus virtual fields minus hidden fields. 532 | * 533 | * @return string[] A list of fields that are 'visible' in all 534 | * representations. 535 | */ 536 | public function getVisible(): array 537 | { 538 | $fields = array_keys($this->_fields); 539 | $fields = array_merge($fields, $this->_virtual); 540 | 541 | return array_diff($fields, $this->_hidden); 542 | } 543 | 544 | /** 545 | * Returns an array with all the fields that have been set 546 | * to this entity 547 | * 548 | * This method will recursively transform entities assigned to fields 549 | * into arrays as well. 550 | * 551 | * @return array 552 | */ 553 | public function toArray(): array 554 | { 555 | $result = []; 556 | foreach ($this->getVisible() as $field) { 557 | $value = $this->get($field); 558 | if (is_array($value)) { 559 | $result[$field] = []; 560 | foreach ($value as $k => $entity) { 561 | if ($entity instanceof EntityInterface) { 562 | $result[$field][$k] = $entity->toArray(); 563 | } else { 564 | $result[$field][$k] = $entity; 565 | } 566 | } 567 | } elseif ($value instanceof EntityInterface) { 568 | $result[$field] = $value->toArray(); 569 | } else { 570 | $result[$field] = $value; 571 | } 572 | } 573 | 574 | return $result; 575 | } 576 | 577 | /** 578 | * Returns the fields that will be serialized as JSON 579 | * 580 | * @return array 581 | */ 582 | public function jsonSerialize(): array 583 | { 584 | return $this->extract($this->getVisible()); 585 | } 586 | 587 | /** 588 | * Implements isset($entity); 589 | * 590 | * @param string $offset The offset to check. 591 | * @return bool Success 592 | */ 593 | public function offsetExists($offset): bool 594 | { 595 | return $this->has($offset); 596 | } 597 | 598 | /** 599 | * Implements $entity[$offset]; 600 | * 601 | * @param string $offset The offset to get. 602 | * @return mixed 603 | */ 604 | public function &offsetGet($offset) 605 | { 606 | return $this->get($offset); 607 | } 608 | 609 | /** 610 | * Implements $entity[$offset] = $value; 611 | * 612 | * @param string $offset The offset to set. 613 | * @param mixed $value The value to set. 614 | * @return void 615 | */ 616 | public function offsetSet($offset, $value): void 617 | { 618 | $this->set($offset, $value); 619 | } 620 | 621 | /** 622 | * Implements unset($result[$offset]); 623 | * 624 | * @param string $offset The offset to remove. 625 | * @return void 626 | */ 627 | public function offsetUnset($offset): void 628 | { 629 | $this->unset($offset); 630 | } 631 | 632 | /** 633 | * Fetch accessor method name 634 | * Accessor methods (available or not) are cached in $_accessors 635 | * 636 | * @param string $property the field name to derive getter name from 637 | * @param string $type the accessor type ('get' or 'set') 638 | * @return string method name or empty string (no method available) 639 | */ 640 | protected static function _accessor(string $property, string $type): string 641 | { 642 | $class = static::class; 643 | 644 | if (isset(static::$_accessors[$class][$type][$property])) { 645 | return static::$_accessors[$class][$type][$property]; 646 | } 647 | 648 | if (!empty(static::$_accessors[$class])) { 649 | return static::$_accessors[$class][$type][$property] = ''; 650 | } 651 | 652 | if (static::class === Entity::class) { 653 | return ''; 654 | } 655 | 656 | foreach (get_class_methods($class) as $method) { 657 | $prefix = substr($method, 1, 3); 658 | if ($method[0] !== '_' || ($prefix !== 'get' && $prefix !== 'set')) { 659 | continue; 660 | } 661 | $field = lcfirst(substr($method, 4)); 662 | $snakeField = Inflector::underscore($field); 663 | $titleField = ucfirst($field); 664 | static::$_accessors[$class][$prefix][$snakeField] = $method; 665 | static::$_accessors[$class][$prefix][$field] = $method; 666 | static::$_accessors[$class][$prefix][$titleField] = $method; 667 | } 668 | 669 | if (!isset(static::$_accessors[$class][$type][$property])) { 670 | static::$_accessors[$class][$type][$property] = ''; 671 | } 672 | 673 | return static::$_accessors[$class][$type][$property]; 674 | } 675 | 676 | /** 677 | * Returns an array with the requested fields 678 | * stored in this entity, indexed by field name 679 | * 680 | * @param string[] $fields list of fields to be returned 681 | * @param bool $onlyDirty Return the requested field only if it is dirty 682 | * @return array 683 | */ 684 | public function extract(array $fields, bool $onlyDirty = false): array 685 | { 686 | $result = []; 687 | foreach ($fields as $field) { 688 | if (!$onlyDirty || $this->isDirty($field)) { 689 | $result[$field] = $this->get($field); 690 | } 691 | } 692 | 693 | return $result; 694 | } 695 | 696 | /** 697 | * Returns an array with the requested original fields 698 | * stored in this entity, indexed by field name. 699 | * 700 | * Fields that are unchanged from their original value will be included in the 701 | * return of this method. 702 | * 703 | * @param string[] $fields List of fields to be returned 704 | * @return array 705 | */ 706 | public function extractOriginal(array $fields): array 707 | { 708 | $result = []; 709 | foreach ($fields as $field) { 710 | $result[$field] = $this->getOriginal($field); 711 | } 712 | 713 | return $result; 714 | } 715 | 716 | /** 717 | * Returns an array with only the original fields 718 | * stored in this entity, indexed by field name. 719 | * 720 | * This method will only return fields that have been modified since 721 | * the entity was built. Unchanged fields will be omitted. 722 | * 723 | * @param string[] $fields List of fields to be returned 724 | * @return array 725 | */ 726 | public function extractOriginalChanged(array $fields): array 727 | { 728 | $result = []; 729 | foreach ($fields as $field) { 730 | $original = $this->getOriginal($field); 731 | if ($original !== $this->get($field)) { 732 | $result[$field] = $original; 733 | } 734 | } 735 | 736 | return $result; 737 | } 738 | 739 | /** 740 | * Sets the dirty status of a single field. 741 | * 742 | * @param string $field the field to set or check status for 743 | * @param bool $isDirty true means the field was changed, false means 744 | * it was not changed. Defaults to true. 745 | * @return $this 746 | */ 747 | public function setDirty(string $field, bool $isDirty = true) 748 | { 749 | if ($isDirty === false) { 750 | unset($this->_dirty[$field]); 751 | 752 | return $this; 753 | } 754 | 755 | $this->_dirty[$field] = true; 756 | unset($this->_errors[$field], $this->_invalid[$field]); 757 | 758 | return $this; 759 | } 760 | 761 | /** 762 | * Checks if the entity is dirty or if a single field of it is dirty. 763 | * 764 | * @param string|null $field The field to check the status for. Null for the whole entity. 765 | * @return bool Whether the field was changed or not 766 | */ 767 | public function isDirty(?string $field = null): bool 768 | { 769 | if ($field === null) { 770 | return !empty($this->_dirty); 771 | } 772 | 773 | return isset($this->_dirty[$field]); 774 | } 775 | 776 | /** 777 | * Gets the dirty fields. 778 | * 779 | * @return string[] 780 | */ 781 | public function getDirty(): array 782 | { 783 | return array_keys($this->_dirty); 784 | } 785 | 786 | /** 787 | * Sets the entire entity as clean, which means that it will appear as 788 | * no fields being modified or added at all. This is an useful call 789 | * for an initial object hydration 790 | * 791 | * @return void 792 | */ 793 | public function clean(): void 794 | { 795 | $this->_dirty = []; 796 | $this->_errors = []; 797 | $this->_invalid = []; 798 | $this->_original = []; 799 | } 800 | 801 | /** 802 | * Set the status of this entity. 803 | * 804 | * Using `true` means that the entity has not been persisted in the database, 805 | * `false` that it already is. 806 | * 807 | * @param bool $new Indicate whether or not this entity has been persisted. 808 | * @return $this 809 | */ 810 | public function setNew(bool $new) 811 | { 812 | if ($new) { 813 | foreach ($this->_fields as $k => $p) { 814 | $this->_dirty[$k] = true; 815 | } 816 | } 817 | 818 | $this->_new = $new; 819 | 820 | return $this; 821 | } 822 | 823 | /** 824 | * Returns whether or not this entity has already been persisted. 825 | * 826 | * @return bool Whether or not the entity has been persisted. 827 | */ 828 | public function isNew(): bool 829 | { 830 | if (func_num_args()) { 831 | deprecationWarning('Using isNew() as setter is deprecated. Use setNew() instead.'); 832 | 833 | $this->setNew(func_get_arg(0)); 834 | } 835 | 836 | return $this->_new; 837 | } 838 | 839 | /** 840 | * Returns whether this entity has errors. 841 | * 842 | * @param bool $includeNested true will check nested entities for hasErrors() 843 | * @return bool 844 | */ 845 | public function hasErrors(bool $includeNested = true): bool 846 | { 847 | if (Hash::filter($this->_errors)) { 848 | return true; 849 | } 850 | 851 | if ($includeNested === false) { 852 | return false; 853 | } 854 | 855 | foreach ($this->_fields as $field) { 856 | if ($this->_readHasErrors($field)) { 857 | return true; 858 | } 859 | } 860 | 861 | return false; 862 | } 863 | 864 | /** 865 | * Returns all validation errors. 866 | * 867 | * @return array 868 | */ 869 | public function getErrors(): array 870 | { 871 | $diff = array_diff_key($this->_fields, $this->_errors); 872 | 873 | return $this->_errors + (new Collection($diff)) 874 | ->filter(function ($value) { 875 | return is_array($value) || $value instanceof EntityInterface; 876 | }) 877 | ->map(function ($value) { 878 | return $this->_readError($value); 879 | }) 880 | ->filter() 881 | ->toArray(); 882 | } 883 | 884 | /** 885 | * Returns validation errors of a field 886 | * 887 | * @param string $field Field name to get the errors from 888 | * @return array 889 | */ 890 | public function getError(string $field): array 891 | { 892 | $errors = $this->_errors[$field] ?? []; 893 | if ($errors) { 894 | return $errors; 895 | } 896 | 897 | return $this->_nestedErrors($field); 898 | } 899 | 900 | /** 901 | * Sets error messages to the entity 902 | * 903 | * ## Example 904 | * 905 | * ``` 906 | * // Sets the error messages for multiple fields at once 907 | * $entity->setErrors(['salary' => ['message'], 'name' => ['another message']]); 908 | * ``` 909 | * 910 | * @param array $errors The array of errors to set. 911 | * @param bool $overwrite Whether or not to overwrite pre-existing errors for $fields 912 | * @return $this 913 | */ 914 | public function setErrors(array $errors, bool $overwrite = false) 915 | { 916 | if ($overwrite) { 917 | foreach ($errors as $f => $error) { 918 | $this->_errors[$f] = (array)$error; 919 | } 920 | 921 | return $this; 922 | } 923 | 924 | foreach ($errors as $f => $error) { 925 | $this->_errors += [$f => []]; 926 | 927 | // String messages are appended to the list, 928 | // while more complex error structures need their 929 | // keys preserved for nested validator. 930 | if (is_string($error)) { 931 | $this->_errors[$f][] = $error; 932 | } else { 933 | foreach ($error as $k => $v) { 934 | $this->_errors[$f][$k] = $v; 935 | } 936 | } 937 | } 938 | 939 | return $this; 940 | } 941 | 942 | /** 943 | * Sets errors for a single field 944 | * 945 | * ### Example 946 | * 947 | * ``` 948 | * // Sets the error messages for a single field 949 | * $entity->setError('salary', ['must be numeric', 'must be a positive number']); 950 | * ``` 951 | * 952 | * @param string $field The field to get errors for, or the array of errors to set. 953 | * @param string|array $errors The errors to be set for $field 954 | * @param bool $overwrite Whether or not to overwrite pre-existing errors for $field 955 | * @return $this 956 | */ 957 | public function setError(string $field, $errors, bool $overwrite = false) 958 | { 959 | if (is_string($errors)) { 960 | $errors = [$errors]; 961 | } 962 | 963 | return $this->setErrors([$field => $errors], $overwrite); 964 | } 965 | 966 | /** 967 | * Auxiliary method for getting errors in nested entities 968 | * 969 | * @param string $field the field in this entity to check for errors 970 | * @return array errors in nested entity if any 971 | */ 972 | protected function _nestedErrors(string $field): array 973 | { 974 | // Only one path element, check for nested entity with error. 975 | if (strpos($field, '.') === false) { 976 | return $this->_readError($this->get($field)); 977 | } 978 | // Try reading the errors data with field as a simple path 979 | $error = Hash::get($this->_errors, $field); 980 | if ($error !== null) { 981 | return $error; 982 | } 983 | $path = explode('.', $field); 984 | 985 | // Traverse down the related entities/arrays for 986 | // the relevant entity. 987 | $entity = $this; 988 | $len = count($path); 989 | while ($len) { 990 | $part = array_shift($path); 991 | $len = count($path); 992 | $val = null; 993 | if ($entity instanceof EntityInterface) { 994 | $val = $entity->get($part); 995 | } elseif (is_array($entity)) { 996 | $val = $entity[$part] ?? false; 997 | } 998 | 999 | if ( 1000 | is_array($val) || 1001 | $val instanceof Traversable || 1002 | $val instanceof EntityInterface 1003 | ) { 1004 | $entity = $val; 1005 | } else { 1006 | $path[] = $part; 1007 | break; 1008 | } 1009 | } 1010 | if (count($path) <= 1) { 1011 | return $this->_readError($entity, array_pop($path)); 1012 | } 1013 | 1014 | return []; 1015 | } 1016 | 1017 | /** 1018 | * Reads if there are errors for one or many objects. 1019 | * 1020 | * @param array|\Cake\Datasource\EntityInterface $object The object to read errors from. 1021 | * @return bool 1022 | */ 1023 | protected function _readHasErrors($object): bool 1024 | { 1025 | if ($object instanceof EntityInterface && $object->hasErrors()) { 1026 | return true; 1027 | } 1028 | 1029 | if (is_array($object)) { 1030 | foreach ($object as $value) { 1031 | if ($this->_readHasErrors($value)) { 1032 | return true; 1033 | } 1034 | } 1035 | } 1036 | 1037 | return false; 1038 | } 1039 | 1040 | /** 1041 | * Read the error(s) from one or many objects. 1042 | * 1043 | * @param iterable|\Cake\Datasource\EntityInterface $object The object to read errors from. 1044 | * @param string|null $path The field name for errors. 1045 | * @return array 1046 | */ 1047 | protected function _readError($object, $path = null): array 1048 | { 1049 | if ($path !== null && $object instanceof EntityInterface) { 1050 | return $object->getError($path); 1051 | } 1052 | if ($object instanceof EntityInterface) { 1053 | return $object->getErrors(); 1054 | } 1055 | if (is_iterable($object)) { 1056 | $array = array_map(function ($val) { 1057 | if ($val instanceof EntityInterface) { 1058 | return $val->getErrors(); 1059 | } 1060 | }, (array)$object); 1061 | 1062 | return array_filter($array); 1063 | } 1064 | 1065 | return []; 1066 | } 1067 | 1068 | /** 1069 | * Get a list of invalid fields and their data for errors upon validation/patching 1070 | * 1071 | * @return array 1072 | */ 1073 | public function getInvalid(): array 1074 | { 1075 | return $this->_invalid; 1076 | } 1077 | 1078 | /** 1079 | * Get a single value of an invalid field. Returns null if not set. 1080 | * 1081 | * @param string $field The name of the field. 1082 | * @return mixed|null 1083 | */ 1084 | public function getInvalidField(string $field) 1085 | { 1086 | return $this->_invalid[$field] ?? null; 1087 | } 1088 | 1089 | /** 1090 | * Set fields as invalid and not patchable into the entity. 1091 | * 1092 | * This is useful for batch operations when one needs to get the original value for an error message after patching. 1093 | * This value could not be patched into the entity and is simply copied into the _invalid property for debugging 1094 | * purposes or to be able to log it away. 1095 | * 1096 | * @param array $fields The values to set. 1097 | * @param bool $overwrite Whether or not to overwrite pre-existing values for $field. 1098 | * @return $this 1099 | */ 1100 | public function setInvalid(array $fields, bool $overwrite = false) 1101 | { 1102 | foreach ($fields as $field => $value) { 1103 | if ($overwrite === true) { 1104 | $this->_invalid[$field] = $value; 1105 | continue; 1106 | } 1107 | $this->_invalid += [$field => $value]; 1108 | } 1109 | 1110 | return $this; 1111 | } 1112 | 1113 | /** 1114 | * Sets a field as invalid and not patchable into the entity. 1115 | * 1116 | * @param string $field The value to set. 1117 | * @param mixed $value The invalid value to be set for $field. 1118 | * @return $this 1119 | */ 1120 | public function setInvalidField(string $field, $value) 1121 | { 1122 | $this->_invalid[$field] = $value; 1123 | 1124 | return $this; 1125 | } 1126 | 1127 | /** 1128 | * Stores whether or not a field value can be changed or set in this entity. 1129 | * The special field `*` can also be marked as accessible or protected, meaning 1130 | * that any other field specified before will take its value. For example 1131 | * `$entity->setAccess('*', true)` means that any field not specified already 1132 | * will be accessible by default. 1133 | * 1134 | * You can also call this method with an array of fields, in which case they 1135 | * will each take the accessibility value specified in the second argument. 1136 | * 1137 | * ### Example: 1138 | * 1139 | * ``` 1140 | * $entity->setAccess('id', true); // Mark id as not protected 1141 | * $entity->setAccess('author_id', false); // Mark author_id as protected 1142 | * $entity->setAccess(['id', 'user_id'], true); // Mark both fields as accessible 1143 | * $entity->setAccess('*', false); // Mark all fields as protected 1144 | * ``` 1145 | * 1146 | * @param string|array $field Single or list of fields to change its accessibility 1147 | * @param bool $set True marks the field as accessible, false will 1148 | * mark it as protected. 1149 | * @return $this 1150 | */ 1151 | public function setAccess($field, bool $set) 1152 | { 1153 | if ($field === '*') { 1154 | $this->_accessible = array_map(function ($p) use ($set) { 1155 | return $set; 1156 | }, $this->_accessible); 1157 | $this->_accessible['*'] = $set; 1158 | 1159 | return $this; 1160 | } 1161 | 1162 | foreach ((array)$field as $prop) { 1163 | $this->_accessible[$prop] = $set; 1164 | } 1165 | 1166 | return $this; 1167 | } 1168 | 1169 | /** 1170 | * Returns the raw accessible configuration for this entity. 1171 | * The `*` wildcard refers to all fields. 1172 | * 1173 | * @return bool[] 1174 | */ 1175 | public function getAccessible(): array 1176 | { 1177 | return $this->_accessible; 1178 | } 1179 | 1180 | /** 1181 | * Checks if a field is accessible 1182 | * 1183 | * ### Example: 1184 | * 1185 | * ``` 1186 | * $entity->isAccessible('id'); // Returns whether it can be set or not 1187 | * ``` 1188 | * 1189 | * @param string $field Field name to check 1190 | * @return bool 1191 | */ 1192 | public function isAccessible(string $field): bool 1193 | { 1194 | $value = $this->_accessible[$field] ?? null; 1195 | 1196 | return ($value === null && !empty($this->_accessible['*'])) || $value; 1197 | } 1198 | 1199 | /** 1200 | * Returns the alias of the repository from which this entity came from. 1201 | * 1202 | * @return string 1203 | */ 1204 | public function getSource(): string 1205 | { 1206 | return $this->_registryAlias; 1207 | } 1208 | 1209 | /** 1210 | * Sets the source alias 1211 | * 1212 | * @param string $alias the alias of the repository 1213 | * @return $this 1214 | */ 1215 | public function setSource(string $alias) 1216 | { 1217 | $this->_registryAlias = $alias; 1218 | 1219 | return $this; 1220 | } 1221 | 1222 | /** 1223 | * Returns a string representation of this object in a human readable format. 1224 | * 1225 | * @return string 1226 | */ 1227 | public function __toString(): string 1228 | { 1229 | return (string)json_encode($this, JSON_PRETTY_PRINT); 1230 | } 1231 | 1232 | /** 1233 | * Returns an array that can be used to describe the internal state of this 1234 | * object. 1235 | * 1236 | * @return array 1237 | */ 1238 | public function __debugInfo(): array 1239 | { 1240 | $fields = $this->_fields; 1241 | foreach ($this->_virtual as $field) { 1242 | $fields[$field] = $this->$field; 1243 | } 1244 | 1245 | return $fields + [ 1246 | '[new]' => $this->isNew(), 1247 | '[accessible]' => $this->_accessible, 1248 | '[dirty]' => $this->_dirty, 1249 | '[original]' => $this->_original, 1250 | '[virtual]' => $this->_virtual, 1251 | '[hasErrors]' => $this->hasErrors(), 1252 | '[errors]' => $this->_errors, 1253 | '[invalid]' => $this->_invalid, 1254 | '[repository]' => $this->_registryAlias, 1255 | ]; 1256 | } 1257 | } 1258 | -------------------------------------------------------------------------------- /Exception/InvalidPrimaryKeyException.php: -------------------------------------------------------------------------------- 1 | instances[$alias])) { 50 | if (!empty($storeOptions) && $this->options[$alias] !== $storeOptions) { 51 | throw new RuntimeException(sprintf( 52 | 'You cannot configure "%s", it already exists in the registry.', 53 | $alias 54 | )); 55 | } 56 | 57 | return $this->instances[$alias]; 58 | } 59 | 60 | $this->options[$alias] = $storeOptions; 61 | 62 | return $this->instances[$alias] = $this->createInstance($alias, $options); 63 | } 64 | 65 | /** 66 | * Create an instance of a given classname. 67 | * 68 | * @param string $alias Repository alias. 69 | * @param array $options The options you want to build the instance with. 70 | * @return \Cake\Datasource\RepositoryInterface 71 | */ 72 | abstract protected function createInstance(string $alias, array $options); 73 | 74 | /** 75 | * @inheritDoc 76 | */ 77 | public function set(string $alias, RepositoryInterface $repository) 78 | { 79 | return $this->instances[$alias] = $repository; 80 | } 81 | 82 | /** 83 | * @inheritDoc 84 | */ 85 | public function exists(string $alias): bool 86 | { 87 | return isset($this->instances[$alias]); 88 | } 89 | 90 | /** 91 | * @inheritDoc 92 | */ 93 | public function remove(string $alias): void 94 | { 95 | unset( 96 | $this->instances[$alias], 97 | $this->options[$alias] 98 | ); 99 | } 100 | 101 | /** 102 | * @inheritDoc 103 | */ 104 | public function clear(): void 105 | { 106 | $this->instances = []; 107 | $this->options = []; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Locator/LocatorInterface.php: -------------------------------------------------------------------------------- 1 | modelClass === null) { 73 | $this->modelClass = $name; 74 | } 75 | } 76 | 77 | /** 78 | * Loads and constructs repository objects required by this object 79 | * 80 | * Typically used to load ORM Table objects as required. Can 81 | * also be used to load other types of repository objects your application uses. 82 | * 83 | * If a repository provider does not return an object a MissingModelException will 84 | * be thrown. 85 | * 86 | * @param string|null $modelClass Name of model class to load. Defaults to $this->modelClass. 87 | * The name can be an alias like `'Post'` or FQCN like `App\Model\Table\PostsTable::class`. 88 | * @param string|null $modelType The type of repository to load. Defaults to the getModelType() value. 89 | * @return \Cake\Datasource\RepositoryInterface The model instance created. 90 | * @throws \Cake\Datasource\Exception\MissingModelException If the model class cannot be found. 91 | * @throws \UnexpectedValueException If $modelClass argument is not provided 92 | * and ModelAwareTrait::$modelClass property value is empty. 93 | */ 94 | public function loadModel(?string $modelClass = null, ?string $modelType = null): RepositoryInterface 95 | { 96 | $modelClass = $modelClass ?? $this->modelClass; 97 | if (empty($modelClass)) { 98 | throw new UnexpectedValueException('Default modelClass is empty'); 99 | } 100 | $modelType = $modelType ?? $this->getModelType(); 101 | 102 | $options = []; 103 | if (strpos($modelClass, '\\') === false) { 104 | [, $alias] = pluginSplit($modelClass, true); 105 | } else { 106 | $options['className'] = $modelClass; 107 | /** @psalm-suppress PossiblyFalseOperand */ 108 | $alias = substr( 109 | $modelClass, 110 | strrpos($modelClass, '\\') + 1, 111 | -strlen($modelType) 112 | ); 113 | $modelClass = $alias; 114 | } 115 | 116 | if (isset($this->{$alias})) { 117 | return $this->{$alias}; 118 | } 119 | 120 | $factory = $this->_modelFactories[$modelType] ?? FactoryLocator::get($modelType); 121 | if ($factory instanceof LocatorInterface) { 122 | $this->{$alias} = $factory->get($modelClass, $options); 123 | } else { 124 | $this->{$alias} = $factory($modelClass, $options); 125 | } 126 | 127 | if (!$this->{$alias}) { 128 | throw new MissingModelException([$modelClass, $modelType]); 129 | } 130 | 131 | return $this->{$alias}; 132 | } 133 | 134 | /** 135 | * Override a existing callable to generate repositories of a given type. 136 | * 137 | * @param string $type The name of the repository type the factory function is for. 138 | * @param callable|\Cake\Datasource\Locator\LocatorInterface $factory The factory function used to create instances. 139 | * @return void 140 | */ 141 | public function modelFactory(string $type, $factory): void 142 | { 143 | if (!$factory instanceof LocatorInterface && !is_callable($factory)) { 144 | throw new InvalidArgumentException(sprintf( 145 | '`$factory` must be an instance of Cake\Datasource\Locator\LocatorInterface or a callable.' 146 | . ' Got type `%s` instead.', 147 | getTypeName($factory) 148 | )); 149 | } 150 | 151 | $this->_modelFactories[$type] = $factory; 152 | } 153 | 154 | /** 155 | * Get the model type to be used by this class 156 | * 157 | * @return string 158 | */ 159 | public function getModelType(): string 160 | { 161 | return $this->_modelType; 162 | } 163 | 164 | /** 165 | * Set the model type to be used by this class 166 | * 167 | * @param string $modelType The model type 168 | * @return $this 169 | */ 170 | public function setModelType(string $modelType) 171 | { 172 | $this->_modelType = $modelType; 173 | 174 | return $this; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Paginator.php: -------------------------------------------------------------------------------- 1 | 1, 47 | 'limit' => 20, 48 | 'maxLimit' => 100, 49 | 'allowedParameters' => ['limit', 'sort', 'page', 'direction'], 50 | ]; 51 | 52 | /** 53 | * Paging params after pagination operation is done. 54 | * 55 | * @var array 56 | */ 57 | protected $_pagingParams = []; 58 | 59 | /** 60 | * Handles automatic pagination of model records. 61 | * 62 | * ### Configuring pagination 63 | * 64 | * When calling `paginate()` you can use the $settings parameter to pass in 65 | * pagination settings. These settings are used to build the queries made 66 | * and control other pagination settings. 67 | * 68 | * If your settings contain a key with the current table's alias. The data 69 | * inside that key will be used. Otherwise the top level configuration will 70 | * be used. 71 | * 72 | * ``` 73 | * $settings = [ 74 | * 'limit' => 20, 75 | * 'maxLimit' => 100 76 | * ]; 77 | * $results = $paginator->paginate($table, $settings); 78 | * ``` 79 | * 80 | * The above settings will be used to paginate any repository. You can configure 81 | * repository specific settings by keying the settings with the repository alias. 82 | * 83 | * ``` 84 | * $settings = [ 85 | * 'Articles' => [ 86 | * 'limit' => 20, 87 | * 'maxLimit' => 100 88 | * ], 89 | * 'Comments' => [ ... ] 90 | * ]; 91 | * $results = $paginator->paginate($table, $settings); 92 | * ``` 93 | * 94 | * This would allow you to have different pagination settings for 95 | * `Articles` and `Comments` repositories. 96 | * 97 | * ### Controlling sort fields 98 | * 99 | * By default CakePHP will automatically allow sorting on any column on the 100 | * repository object being paginated. Often times you will want to allow 101 | * sorting on either associated columns or calculated fields. In these cases 102 | * you will need to define an allowed list of all the columns you wish to allow 103 | * sorting on. You can define the allowed sort fields in the `$settings` parameter: 104 | * 105 | * ``` 106 | * $settings = [ 107 | * 'Articles' => [ 108 | * 'finder' => 'custom', 109 | * 'sortableFields' => ['title', 'author_id', 'comment_count'], 110 | * ] 111 | * ]; 112 | * ``` 113 | * 114 | * Passing an empty array as sortableFields disallows sorting altogether. 115 | * 116 | * ### Paginating with custom finders 117 | * 118 | * You can paginate with any find type defined on your table using the 119 | * `finder` option. 120 | * 121 | * ``` 122 | * $settings = [ 123 | * 'Articles' => [ 124 | * 'finder' => 'popular' 125 | * ] 126 | * ]; 127 | * $results = $paginator->paginate($table, $settings); 128 | * ``` 129 | * 130 | * Would paginate using the `find('popular')` method. 131 | * 132 | * You can also pass an already created instance of a query to this method: 133 | * 134 | * ``` 135 | * $query = $this->Articles->find('popular')->matching('Tags', function ($q) { 136 | * return $q->where(['name' => 'CakePHP']) 137 | * }); 138 | * $results = $paginator->paginate($query); 139 | * ``` 140 | * 141 | * ### Scoping Request parameters 142 | * 143 | * By using request parameter scopes you can paginate multiple queries in 144 | * the same controller action: 145 | * 146 | * ``` 147 | * $articles = $paginator->paginate($articlesQuery, ['scope' => 'articles']); 148 | * $tags = $paginator->paginate($tagsQuery, ['scope' => 'tags']); 149 | * ``` 150 | * 151 | * Each of the above queries will use different query string parameter sets 152 | * for pagination data. An example URL paginating both results would be: 153 | * 154 | * ``` 155 | * /dashboard?articles[page]=1&tags[page]=2 156 | * ``` 157 | * 158 | * @param \Cake\Datasource\RepositoryInterface|\Cake\Datasource\QueryInterface $object The repository or query 159 | * to paginate. 160 | * @param array $params Request params 161 | * @param array $settings The settings/configuration used for pagination. 162 | * @return \Cake\Datasource\ResultSetInterface Query results 163 | * @throws \Cake\Datasource\Exception\PageOutOfBoundsException 164 | */ 165 | public function paginate(object $object, array $params = [], array $settings = []): ResultSetInterface 166 | { 167 | $query = null; 168 | if ($object instanceof QueryInterface) { 169 | $query = $object; 170 | $object = $query->getRepository(); 171 | if ($object === null) { 172 | throw new CakeException('No repository set for query.'); 173 | } 174 | } 175 | 176 | $data = $this->extractData($object, $params, $settings); 177 | $query = $this->getQuery($object, $query, $data); 178 | 179 | $cleanQuery = clone $query; 180 | $results = $query->all(); 181 | $data['numResults'] = count($results); 182 | $data['count'] = $this->getCount($cleanQuery, $data); 183 | 184 | $pagingParams = $this->buildParams($data); 185 | $alias = $object->getAlias(); 186 | $this->_pagingParams = [$alias => $pagingParams]; 187 | if ($pagingParams['requestedPage'] > $pagingParams['page']) { 188 | throw new PageOutOfBoundsException([ 189 | 'requestedPage' => $pagingParams['requestedPage'], 190 | 'pagingParams' => $this->_pagingParams, 191 | ]); 192 | } 193 | 194 | return $results; 195 | } 196 | 197 | /** 198 | * Get query for fetching paginated results. 199 | * 200 | * @param \Cake\Datasource\RepositoryInterface $object Repository instance. 201 | * @param \Cake\Datasource\QueryInterface|null $query Query Instance. 202 | * @param array $data Pagination data. 203 | * @return \Cake\Datasource\QueryInterface 204 | */ 205 | protected function getQuery(RepositoryInterface $object, ?QueryInterface $query = null, array $data): QueryInterface 206 | { 207 | if ($query === null) { 208 | $query = $object->find($data['finder'], $data['options']); 209 | } else { 210 | $query->applyOptions($data['options']); 211 | } 212 | 213 | return $query; 214 | } 215 | 216 | /** 217 | * Get total count of records. 218 | * 219 | * @param \Cake\Datasource\QueryInterface $query Query instance. 220 | * @param array $data Pagination data. 221 | * @return int|null 222 | */ 223 | protected function getCount(QueryInterface $query, array $data): ?int 224 | { 225 | return $query->count(); 226 | } 227 | 228 | /** 229 | * Extract pagination data needed 230 | * 231 | * @param \Cake\Datasource\RepositoryInterface $object The repository object. 232 | * @param array $params Request params 233 | * @param array $settings The settings/configuration used for pagination. 234 | * @return array Array with keys 'defaults', 'options' and 'finder' 235 | */ 236 | protected function extractData(RepositoryInterface $object, array $params, array $settings): array 237 | { 238 | $alias = $object->getAlias(); 239 | $defaults = $this->getDefaults($alias, $settings); 240 | $options = $this->mergeOptions($params, $defaults); 241 | $options = $this->validateSort($object, $options); 242 | $options = $this->checkLimit($options); 243 | 244 | $options += ['page' => 1, 'scope' => null]; 245 | $options['page'] = (int)$options['page'] < 1 ? 1 : (int)$options['page']; 246 | [$finder, $options] = $this->_extractFinder($options); 247 | 248 | return compact('defaults', 'options', 'finder'); 249 | } 250 | 251 | /** 252 | * Build pagination params. 253 | * 254 | * @param array $data Paginator data containing keys 'options', 255 | * 'count', 'defaults', 'finder', 'numResults'. 256 | * @return array Paging params. 257 | */ 258 | protected function buildParams(array $data): array 259 | { 260 | $limit = $data['options']['limit']; 261 | 262 | $paging = [ 263 | 'count' => $data['count'], 264 | 'current' => $data['numResults'], 265 | 'perPage' => $limit, 266 | 'page' => $data['options']['page'], 267 | 'requestedPage' => $data['options']['page'], 268 | ]; 269 | 270 | $paging = $this->addPageCountParams($paging, $data); 271 | $paging = $this->addStartEndParams($paging, $data); 272 | $paging = $this->addPrevNextParams($paging, $data); 273 | $paging = $this->addSortingParams($paging, $data); 274 | 275 | $paging += [ 276 | 'limit' => $data['defaults']['limit'] != $limit ? $limit : null, 277 | 'scope' => $data['options']['scope'], 278 | 'finder' => $data['finder'], 279 | ]; 280 | 281 | return $paging; 282 | } 283 | 284 | /** 285 | * Add "page" and "pageCount" params. 286 | * 287 | * @param array $params Paging params. 288 | * @param array $data Paginator data. 289 | * @return array Updated params. 290 | */ 291 | protected function addPageCountParams(array $params, array $data): array 292 | { 293 | $page = $params['page']; 294 | $pageCount = 0; 295 | 296 | if ($params['count'] !== null) { 297 | $pageCount = max((int)ceil($params['count'] / $params['perPage']), 1); 298 | $page = min($page, $pageCount); 299 | } elseif ($params['current'] === 0 && $params['requestedPage'] > 1) { 300 | $page = 1; 301 | } 302 | 303 | $params['page'] = $page; 304 | $params['pageCount'] = $pageCount; 305 | 306 | return $params; 307 | } 308 | 309 | /** 310 | * Add "start" and "end" params. 311 | * 312 | * @param array $params Paging params. 313 | * @param array $data Paginator data. 314 | * @return array Updated params. 315 | */ 316 | protected function addStartEndParams(array $params, array $data): array 317 | { 318 | $start = $end = 0; 319 | 320 | if ($params['current'] > 0) { 321 | $start = (($params['page'] - 1) * $params['perPage']) + 1; 322 | $end = $start + $params['current'] - 1; 323 | } 324 | 325 | $params['start'] = $start; 326 | $params['end'] = $end; 327 | 328 | return $params; 329 | } 330 | 331 | /** 332 | * Add "prevPage" and "nextPage" params. 333 | * 334 | * @param array $params Paginator params. 335 | * @param array $data Paging data. 336 | * @return array Updated params. 337 | */ 338 | protected function addPrevNextParams(array $params, array $data): array 339 | { 340 | $params['prevPage'] = $params['page'] > 1; 341 | if ($params['count'] === null) { 342 | $params['nextPage'] = true; 343 | } else { 344 | $params['nextPage'] = $params['count'] > $params['page'] * $params['perPage']; 345 | } 346 | 347 | return $params; 348 | } 349 | 350 | /** 351 | * Add sorting / ordering params. 352 | * 353 | * @param array $params Paginator params. 354 | * @param array $data Paging data. 355 | * @return array Updated params. 356 | */ 357 | protected function addSortingParams(array $params, array $data): array 358 | { 359 | $defaults = $data['defaults']; 360 | $order = (array)$data['options']['order']; 361 | $sortDefault = $directionDefault = false; 362 | 363 | if (!empty($defaults['order']) && count($defaults['order']) === 1) { 364 | $sortDefault = key($defaults['order']); 365 | $directionDefault = current($defaults['order']); 366 | } 367 | 368 | $params += [ 369 | 'sort' => $data['options']['sort'], 370 | 'direction' => isset($data['options']['sort']) && count($order) ? current($order) : null, 371 | 'sortDefault' => $sortDefault, 372 | 'directionDefault' => $directionDefault, 373 | 'completeSort' => $order, 374 | ]; 375 | 376 | return $params; 377 | } 378 | 379 | /** 380 | * Extracts the finder name and options out of the provided pagination options. 381 | * 382 | * @param array $options the pagination options. 383 | * @return array An array containing in the first position the finder name 384 | * and in the second the options to be passed to it. 385 | */ 386 | protected function _extractFinder(array $options): array 387 | { 388 | $type = !empty($options['finder']) ? $options['finder'] : 'all'; 389 | unset($options['finder'], $options['maxLimit']); 390 | 391 | if (is_array($type)) { 392 | $options = (array)current($type) + $options; 393 | $type = key($type); 394 | } 395 | 396 | return [$type, $options]; 397 | } 398 | 399 | /** 400 | * Get paging params after pagination operation. 401 | * 402 | * @return array 403 | */ 404 | public function getPagingParams(): array 405 | { 406 | return $this->_pagingParams; 407 | } 408 | 409 | /** 410 | * Shim method for reading the deprecated whitelist or allowedParameters options 411 | * 412 | * @return string[] 413 | */ 414 | protected function getAllowedParameters(): array 415 | { 416 | $allowed = $this->getConfig('allowedParameters'); 417 | if (!$allowed) { 418 | $allowed = []; 419 | } 420 | $whitelist = $this->getConfig('whitelist'); 421 | if ($whitelist) { 422 | deprecationWarning('The `whitelist` option is deprecated. Use the `allowedParameters` option instead.'); 423 | 424 | return array_merge($allowed, $whitelist); 425 | } 426 | 427 | return $allowed; 428 | } 429 | 430 | /** 431 | * Shim method for reading the deprecated sortWhitelist or sortableFields options. 432 | * 433 | * @param array $config The configuration data to coalesce and emit warnings on. 434 | * @return string[]|null 435 | */ 436 | protected function getSortableFields(array $config): ?array 437 | { 438 | $allowed = $config['sortableFields'] ?? null; 439 | if ($allowed !== null) { 440 | return $allowed; 441 | } 442 | $deprecated = $config['sortWhitelist'] ?? null; 443 | if ($deprecated !== null) { 444 | deprecationWarning('The `sortWhitelist` option is deprecated. Use `sortableFields` instead.'); 445 | } 446 | 447 | return $deprecated; 448 | } 449 | 450 | /** 451 | * Merges the various options that Paginator uses. 452 | * Pulls settings together from the following places: 453 | * 454 | * - General pagination settings 455 | * - Model specific settings. 456 | * - Request parameters 457 | * 458 | * The result of this method is the aggregate of all the option sets 459 | * combined together. You can change config value `allowedParameters` to modify 460 | * which options/values can be set using request parameters. 461 | * 462 | * @param array $params Request params. 463 | * @param array $settings The settings to merge with the request data. 464 | * @return array Array of merged options. 465 | */ 466 | public function mergeOptions(array $params, array $settings): array 467 | { 468 | if (!empty($settings['scope'])) { 469 | $scope = $settings['scope']; 470 | $params = !empty($params[$scope]) ? (array)$params[$scope] : []; 471 | } 472 | 473 | $allowed = $this->getAllowedParameters(); 474 | $params = array_intersect_key($params, array_flip($allowed)); 475 | 476 | return array_merge($settings, $params); 477 | } 478 | 479 | /** 480 | * Get the settings for a $model. If there are no settings for a specific 481 | * repository, the general settings will be used. 482 | * 483 | * @param string $alias Model name to get settings for. 484 | * @param array $settings The settings which is used for combining. 485 | * @return array An array of pagination settings for a model, 486 | * or the general settings. 487 | */ 488 | public function getDefaults(string $alias, array $settings): array 489 | { 490 | if (isset($settings[$alias])) { 491 | $settings = $settings[$alias]; 492 | } 493 | 494 | $defaults = $this->getConfig(); 495 | $defaults['whitelist'] = $defaults['allowedParameters'] = $this->getAllowedParameters(); 496 | 497 | $maxLimit = $settings['maxLimit'] ?? $defaults['maxLimit']; 498 | $limit = $settings['limit'] ?? $defaults['limit']; 499 | 500 | if ($limit > $maxLimit) { 501 | $limit = $maxLimit; 502 | } 503 | 504 | $settings['maxLimit'] = $maxLimit; 505 | $settings['limit'] = $limit; 506 | 507 | return $settings + $defaults; 508 | } 509 | 510 | /** 511 | * Validate that the desired sorting can be performed on the $object. 512 | * 513 | * Only fields or virtualFields can be sorted on. The direction param will 514 | * also be sanitized. Lastly sort + direction keys will be converted into 515 | * the model friendly order key. 516 | * 517 | * You can use the allowedParameters option to control which columns/fields are 518 | * available for sorting via URL parameters. This helps prevent users from ordering large 519 | * result sets on un-indexed values. 520 | * 521 | * If you need to sort on associated columns or synthetic properties you 522 | * will need to use the `sortableFields` option. 523 | * 524 | * Any columns listed in the allowed sort fields will be implicitly trusted. 525 | * You can use this to sort on synthetic columns, or columns added in custom 526 | * find operations that may not exist in the schema. 527 | * 528 | * The default order options provided to paginate() will be merged with the user's 529 | * requested sorting field/direction. 530 | * 531 | * @param \Cake\Datasource\RepositoryInterface $object Repository object. 532 | * @param array $options The pagination options being used for this request. 533 | * @return array An array of options with sort + direction removed and 534 | * replaced with order if possible. 535 | */ 536 | public function validateSort(RepositoryInterface $object, array $options): array 537 | { 538 | if (isset($options['sort'])) { 539 | $direction = null; 540 | if (isset($options['direction'])) { 541 | $direction = strtolower($options['direction']); 542 | } 543 | if (!in_array($direction, ['asc', 'desc'], true)) { 544 | $direction = 'asc'; 545 | } 546 | 547 | $order = isset($options['order']) && is_array($options['order']) ? $options['order'] : []; 548 | if ($order && $options['sort'] && strpos($options['sort'], '.') === false) { 549 | $order = $this->_removeAliases($order, $object->getAlias()); 550 | } 551 | 552 | $options['order'] = [$options['sort'] => $direction] + $order; 553 | } else { 554 | $options['sort'] = null; 555 | } 556 | unset($options['direction']); 557 | 558 | if (empty($options['order'])) { 559 | $options['order'] = []; 560 | } 561 | if (!is_array($options['order'])) { 562 | return $options; 563 | } 564 | 565 | $sortAllowed = false; 566 | $allowed = $this->getSortableFields($options); 567 | if ($allowed !== null) { 568 | $options['sortableFields'] = $options['sortWhitelist'] = $allowed; 569 | 570 | $field = key($options['order']); 571 | $sortAllowed = in_array($field, $allowed, true); 572 | if (!$sortAllowed) { 573 | $options['order'] = []; 574 | $options['sort'] = null; 575 | 576 | return $options; 577 | } 578 | } 579 | 580 | if ( 581 | $options['sort'] === null 582 | && count($options['order']) === 1 583 | && !is_numeric(key($options['order'])) 584 | ) { 585 | $options['sort'] = key($options['order']); 586 | } 587 | 588 | $options['order'] = $this->_prefix($object, $options['order'], $sortAllowed); 589 | 590 | return $options; 591 | } 592 | 593 | /** 594 | * Remove alias if needed. 595 | * 596 | * @param array $fields Current fields 597 | * @param string $model Current model alias 598 | * @return array $fields Unaliased fields where applicable 599 | */ 600 | protected function _removeAliases(array $fields, string $model): array 601 | { 602 | $result = []; 603 | foreach ($fields as $field => $sort) { 604 | if (strpos($field, '.') === false) { 605 | $result[$field] = $sort; 606 | continue; 607 | } 608 | 609 | [$alias, $currentField] = explode('.', $field); 610 | 611 | if ($alias === $model) { 612 | $result[$currentField] = $sort; 613 | continue; 614 | } 615 | 616 | $result[$field] = $sort; 617 | } 618 | 619 | return $result; 620 | } 621 | 622 | /** 623 | * Prefixes the field with the table alias if possible. 624 | * 625 | * @param \Cake\Datasource\RepositoryInterface $object Repository object. 626 | * @param array $order Order array. 627 | * @param bool $allowed Whether or not the field was allowed. 628 | * @return array Final order array. 629 | */ 630 | protected function _prefix(RepositoryInterface $object, array $order, bool $allowed = false): array 631 | { 632 | $tableAlias = $object->getAlias(); 633 | $tableOrder = []; 634 | foreach ($order as $key => $value) { 635 | if (is_numeric($key)) { 636 | $tableOrder[] = $value; 637 | continue; 638 | } 639 | $field = $key; 640 | $alias = $tableAlias; 641 | 642 | if (strpos($key, '.') !== false) { 643 | [$alias, $field] = explode('.', $key); 644 | } 645 | $correctAlias = ($tableAlias === $alias); 646 | 647 | if ($correctAlias && $allowed) { 648 | // Disambiguate fields in schema. As id is quite common. 649 | if ($object->hasField($field)) { 650 | $field = $alias . '.' . $field; 651 | } 652 | $tableOrder[$field] = $value; 653 | } elseif ($correctAlias && $object->hasField($field)) { 654 | $tableOrder[$tableAlias . '.' . $field] = $value; 655 | } elseif (!$correctAlias && $allowed) { 656 | $tableOrder[$alias . '.' . $field] = $value; 657 | } 658 | } 659 | 660 | return $tableOrder; 661 | } 662 | 663 | /** 664 | * Check the limit parameter and ensure it's within the maxLimit bounds. 665 | * 666 | * @param array $options An array of options with a limit key to be checked. 667 | * @return array An array of options for pagination. 668 | */ 669 | public function checkLimit(array $options): array 670 | { 671 | $options['limit'] = (int)$options['limit']; 672 | if (empty($options['limit']) || $options['limit'] < 1) { 673 | $options['limit'] = 1; 674 | } 675 | $options['limit'] = max(min($options['limit'], $options['maxLimit']), 1); 676 | 677 | return $options; 678 | } 679 | } 680 | -------------------------------------------------------------------------------- /PaginatorInterface.php: -------------------------------------------------------------------------------- 1 | _key = $key; 62 | 63 | if (!is_string($config) && !($config instanceof CacheInterface)) { 64 | throw new RuntimeException('Cache configs must be strings or \Psr\SimpleCache\CacheInterface instances.'); 65 | } 66 | $this->_config = $config; 67 | } 68 | 69 | /** 70 | * Load the cached results from the cache or run the query. 71 | * 72 | * @param object $query The query the cache read is for. 73 | * @return mixed|null Either the cached results or null. 74 | */ 75 | public function fetch(object $query) 76 | { 77 | $key = $this->_resolveKey($query); 78 | $storage = $this->_resolveCacher(); 79 | $result = $storage->get($key); 80 | if (empty($result)) { 81 | return null; 82 | } 83 | 84 | return $result; 85 | } 86 | 87 | /** 88 | * Store the result set into the cache. 89 | * 90 | * @param object $query The query the cache read is for. 91 | * @param \Traversable $results The result set to store. 92 | * @return bool True if the data was successfully cached, false on failure 93 | */ 94 | public function store(object $query, Traversable $results): bool 95 | { 96 | $key = $this->_resolveKey($query); 97 | $storage = $this->_resolveCacher(); 98 | 99 | return $storage->set($key, $results); 100 | } 101 | 102 | /** 103 | * Get/generate the cache key. 104 | * 105 | * @param object $query The query to generate a key for. 106 | * @return string 107 | * @throws \RuntimeException 108 | */ 109 | protected function _resolveKey(object $query): string 110 | { 111 | if (is_string($this->_key)) { 112 | return $this->_key; 113 | } 114 | $func = $this->_key; 115 | $key = $func($query); 116 | if (!is_string($key)) { 117 | $msg = sprintf('Cache key functions must return a string. Got %s.', var_export($key, true)); 118 | throw new RuntimeException($msg); 119 | } 120 | 121 | return $key; 122 | } 123 | 124 | /** 125 | * Get the cache engine. 126 | * 127 | * @return \Psr\SimpleCache\CacheInterface 128 | */ 129 | protected function _resolveCacher() 130 | { 131 | if (is_string($this->_config)) { 132 | return Cache::pool($this->_config); 133 | } 134 | 135 | return $this->_config; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /QueryInterface.php: -------------------------------------------------------------------------------- 1 | value array representing a single aliased field 47 | * that can be passed directly to the select() method. 48 | * The key will contain the alias and the value the actual field name. 49 | * 50 | * If the field is already aliased, then it will not be changed. 51 | * If no $alias is passed, the default table for this query will be used. 52 | * 53 | * @param string $field The field to alias 54 | * @param string|null $alias the alias used to prefix the field 55 | * @return array 56 | */ 57 | public function aliasField(string $field, ?string $alias = null): array; 58 | 59 | /** 60 | * Runs `aliasField()` for each field in the provided list and returns 61 | * the result under a single array. 62 | * 63 | * @param array $fields The fields to alias 64 | * @param string|null $defaultAlias The default alias 65 | * @return string[] 66 | */ 67 | public function aliasFields(array $fields, ?string $defaultAlias = null): array; 68 | 69 | /** 70 | * Fetch the results for this query. 71 | * 72 | * Will return either the results set through setResult(), or execute this query 73 | * and return the ResultSetDecorator object ready for streaming of results. 74 | * 75 | * ResultSetDecorator is a traversable object that implements the methods found 76 | * on Cake\Collection\Collection. 77 | * 78 | * @return \Cake\Datasource\ResultSetInterface 79 | */ 80 | public function all(): ResultSetInterface; 81 | 82 | /** 83 | * Populates or adds parts to current query clauses using an array. 84 | * This is handy for passing all query clauses at once. The option array accepts: 85 | * 86 | * - fields: Maps to the select method 87 | * - conditions: Maps to the where method 88 | * - limit: Maps to the limit method 89 | * - order: Maps to the order method 90 | * - offset: Maps to the offset method 91 | * - group: Maps to the group method 92 | * - having: Maps to the having method 93 | * - contain: Maps to the contain options for eager loading 94 | * - join: Maps to the join method 95 | * - page: Maps to the page method 96 | * 97 | * ### Example: 98 | * 99 | * ``` 100 | * $query->applyOptions([ 101 | * 'fields' => ['id', 'name'], 102 | * 'conditions' => [ 103 | * 'created >=' => '2013-01-01' 104 | * ], 105 | * 'limit' => 10 106 | * ]); 107 | * ``` 108 | * 109 | * Is equivalent to: 110 | * 111 | * ``` 112 | * $query 113 | * ->select(['id', 'name']) 114 | * ->where(['created >=' => '2013-01-01']) 115 | * ->limit(10) 116 | * ``` 117 | * 118 | * @param array $options list of query clauses to apply new parts to. 119 | * @return $this 120 | */ 121 | public function applyOptions(array $options); 122 | 123 | /** 124 | * Apply custom finds to against an existing query object. 125 | * 126 | * Allows custom find methods to be combined and applied to each other. 127 | * 128 | * ``` 129 | * $repository->find('all')->find('recent'); 130 | * ``` 131 | * 132 | * The above is an example of stacking multiple finder methods onto 133 | * a single query. 134 | * 135 | * @param string $finder The finder method to use. 136 | * @param array $options The options for the finder. 137 | * @return static Returns a modified query. 138 | */ 139 | public function find(string $finder, array $options = []); 140 | 141 | /** 142 | * Returns the first result out of executing this query, if the query has not been 143 | * executed before, it will set the limit clause to 1 for performance reasons. 144 | * 145 | * ### Example: 146 | * 147 | * ``` 148 | * $singleUser = $query->select(['id', 'username'])->first(); 149 | * ``` 150 | * 151 | * @return \Cake\Datasource\EntityInterface|array|null the first result from the ResultSet 152 | */ 153 | public function first(); 154 | 155 | /** 156 | * Returns the total amount of results for the query. 157 | * 158 | * @return int 159 | */ 160 | public function count(): int; 161 | 162 | /** 163 | * Sets the number of records that should be retrieved from database, 164 | * accepts an integer or an expression object that evaluates to an integer. 165 | * In some databases, this operation might not be supported or will require 166 | * the query to be transformed in order to limit the result set size. 167 | * 168 | * ### Examples 169 | * 170 | * ``` 171 | * $query->limit(10) // generates LIMIT 10 172 | * $query->limit($query->newExpr()->add(['1 + 1'])); // LIMIT (1 + 1) 173 | * ``` 174 | * 175 | * @param int|\Cake\Database\ExpressionInterface|null $num number of records to be returned 176 | * @return $this 177 | */ 178 | public function limit($num); 179 | 180 | /** 181 | * Sets the number of records that should be skipped from the original result set 182 | * This is commonly used for paginating large results. Accepts an integer or an 183 | * expression object that evaluates to an integer. 184 | * 185 | * In some databases, this operation might not be supported or will require 186 | * the query to be transformed in order to limit the result set size. 187 | * 188 | * ### Examples 189 | * 190 | * ``` 191 | * $query->offset(10) // generates OFFSET 10 192 | * $query->offset($query->newExpr()->add(['1 + 1'])); // OFFSET (1 + 1) 193 | * ``` 194 | * 195 | * @param int|\Cake\Database\ExpressionInterface|null $num number of records to be skipped 196 | * @return $this 197 | */ 198 | public function offset($num); 199 | 200 | /** 201 | * Adds a single or multiple fields to be used in the ORDER clause for this query. 202 | * Fields can be passed as an array of strings, array of expression 203 | * objects, a single expression or a single string. 204 | * 205 | * If an array is passed, keys will be used as the field itself and the value will 206 | * represent the order in which such field should be ordered. When called multiple 207 | * times with the same fields as key, the last order definition will prevail over 208 | * the others. 209 | * 210 | * By default this function will append any passed argument to the list of fields 211 | * to be selected, unless the second argument is set to true. 212 | * 213 | * ### Examples: 214 | * 215 | * ``` 216 | * $query->order(['title' => 'DESC', 'author_id' => 'ASC']); 217 | * ``` 218 | * 219 | * Produces: 220 | * 221 | * `ORDER BY title DESC, author_id ASC` 222 | * 223 | * ``` 224 | * $query 225 | * ->order(['title' => $query->newExpr('DESC NULLS FIRST')]) 226 | * ->order('author_id'); 227 | * ``` 228 | * 229 | * Will generate: 230 | * 231 | * `ORDER BY title DESC NULLS FIRST, author_id` 232 | * 233 | * ``` 234 | * $expression = $query->newExpr()->add(['id % 2 = 0']); 235 | * $query->order($expression)->order(['title' => 'ASC']); 236 | * ``` 237 | * 238 | * Will become: 239 | * 240 | * `ORDER BY (id %2 = 0), title ASC` 241 | * 242 | * If you need to set complex expressions as order conditions, you 243 | * should use `orderAsc()` or `orderDesc()`. 244 | * 245 | * @param array|\Cake\Database\ExpressionInterface|\Closure|string $fields fields to be added to the list 246 | * @param bool $overwrite whether to reset order with field list or not 247 | * @return $this 248 | */ 249 | public function order($fields, $overwrite = false); 250 | 251 | /** 252 | * Set the page of results you want. 253 | * 254 | * This method provides an easier to use interface to set the limit + offset 255 | * in the record set you want as results. If empty the limit will default to 256 | * the existing limit clause, and if that too is empty, then `25` will be used. 257 | * 258 | * Pages must start at 1. 259 | * 260 | * @param int $num The page number you want. 261 | * @param int|null $limit The number of rows you want in the page. If null 262 | * the current limit clause will be used. 263 | * @return $this 264 | * @throws \InvalidArgumentException If page number < 1. 265 | */ 266 | public function page(int $num, ?int $limit = null); 267 | 268 | /** 269 | * Returns an array representation of the results after executing the query. 270 | * 271 | * @return array 272 | */ 273 | public function toArray(): array; 274 | 275 | /** 276 | * Set the default Table object that will be used by this query 277 | * and form the `FROM` clause. 278 | * 279 | * @param \Cake\Datasource\RepositoryInterface $repository The default repository object to use 280 | * @return $this 281 | */ 282 | public function repository(RepositoryInterface $repository); 283 | 284 | /** 285 | * Returns the default repository object that will be used by this query, 286 | * that is, the repository that will appear in the from clause. 287 | * 288 | * @return \Cake\Datasource\RepositoryInterface|null $repository The default repository object to use 289 | */ 290 | public function getRepository(): ?RepositoryInterface; 291 | 292 | /** 293 | * Adds a condition or set of conditions to be used in the WHERE clause for this 294 | * query. Conditions can be expressed as an array of fields as keys with 295 | * comparison operators in it, the values for the array will be used for comparing 296 | * the field to such literal. Finally, conditions can be expressed as a single 297 | * string or an array of strings. 298 | * 299 | * When using arrays, each entry will be joined to the rest of the conditions using 300 | * an AND operator. Consecutive calls to this function will also join the new 301 | * conditions specified using the AND operator. Additionally, values can be 302 | * expressed using expression objects which can include other query objects. 303 | * 304 | * Any conditions created with this methods can be used with any SELECT, UPDATE 305 | * and DELETE type of queries. 306 | * 307 | * ### Conditions using operators: 308 | * 309 | * ``` 310 | * $query->where([ 311 | * 'posted >=' => new DateTime('3 days ago'), 312 | * 'title LIKE' => 'Hello W%', 313 | * 'author_id' => 1, 314 | * ], ['posted' => 'datetime']); 315 | * ``` 316 | * 317 | * The previous example produces: 318 | * 319 | * `WHERE posted >= 2012-01-27 AND title LIKE 'Hello W%' AND author_id = 1` 320 | * 321 | * Second parameter is used to specify what type is expected for each passed 322 | * key. Valid types can be used from the mapped with Database\Type class. 323 | * 324 | * ### Nesting conditions with conjunctions: 325 | * 326 | * ``` 327 | * $query->where([ 328 | * 'author_id !=' => 1, 329 | * 'OR' => ['published' => true, 'posted <' => new DateTime('now')], 330 | * 'NOT' => ['title' => 'Hello'] 331 | * ], ['published' => boolean, 'posted' => 'datetime'] 332 | * ``` 333 | * 334 | * The previous example produces: 335 | * 336 | * `WHERE author_id = 1 AND (published = 1 OR posted < '2012-02-01') AND NOT (title = 'Hello')` 337 | * 338 | * You can nest conditions using conjunctions as much as you like. Sometimes, you 339 | * may want to define 2 different options for the same key, in that case, you can 340 | * wrap each condition inside a new array: 341 | * 342 | * `$query->where(['OR' => [['published' => false], ['published' => true]])` 343 | * 344 | * Keep in mind that every time you call where() with the third param set to false 345 | * (default), it will join the passed conditions to the previous stored list using 346 | * the AND operator. Also, using the same array key twice in consecutive calls to 347 | * this method will not override the previous value. 348 | * 349 | * ### Using expressions objects: 350 | * 351 | * ``` 352 | * $exp = $query->newExpr()->add(['id !=' => 100, 'author_id' != 1])->tieWith('OR'); 353 | * $query->where(['published' => true], ['published' => 'boolean'])->where($exp); 354 | * ``` 355 | * 356 | * The previous example produces: 357 | * 358 | * `WHERE (id != 100 OR author_id != 1) AND published = 1` 359 | * 360 | * Other Query objects that be used as conditions for any field. 361 | * 362 | * ### Adding conditions in multiple steps: 363 | * 364 | * You can use callable functions to construct complex expressions, functions 365 | * receive as first argument a new QueryExpression object and this query instance 366 | * as second argument. Functions must return an expression object, that will be 367 | * added the list of conditions for the query using the AND operator. 368 | * 369 | * ``` 370 | * $query 371 | * ->where(['title !=' => 'Hello World']) 372 | * ->where(function ($exp, $query) { 373 | * $or = $exp->or(['id' => 1]); 374 | * $and = $exp->and(['id >' => 2, 'id <' => 10]); 375 | * return $or->add($and); 376 | * }); 377 | * ``` 378 | * 379 | * * The previous example produces: 380 | * 381 | * `WHERE title != 'Hello World' AND (id = 1 OR (id > 2 AND id < 10))` 382 | * 383 | * ### Conditions as strings: 384 | * 385 | * ``` 386 | * $query->where(['articles.author_id = authors.id', 'modified IS NULL']); 387 | * ``` 388 | * 389 | * The previous example produces: 390 | * 391 | * `WHERE articles.author_id = authors.id AND modified IS NULL` 392 | * 393 | * Please note that when using the array notation or the expression objects, all 394 | * values will be correctly quoted and transformed to the correspondent database 395 | * data type automatically for you, thus securing your application from SQL injections. 396 | * If you use string conditions make sure that your values are correctly quoted. 397 | * The safest thing you can do is to never use string conditions. 398 | * 399 | * @param string|array|\Closure|null $conditions The conditions to filter on. 400 | * @param array $types associative array of type names used to bind values to query 401 | * @param bool $overwrite whether to reset conditions with passed list or not 402 | * @return $this 403 | */ 404 | public function where($conditions = null, array $types = [], bool $overwrite = false); 405 | } 406 | -------------------------------------------------------------------------------- /QueryTrait.php: -------------------------------------------------------------------------------- 1 | _repository = $repository; 96 | 97 | return $this; 98 | } 99 | 100 | /** 101 | * Returns the default table object that will be used by this query, 102 | * that is, the table that will appear in the from clause. 103 | * 104 | * @return \Cake\Datasource\RepositoryInterface 105 | */ 106 | public function getRepository(): RepositoryInterface 107 | { 108 | return $this->_repository; 109 | } 110 | 111 | /** 112 | * Set the result set for a query. 113 | * 114 | * Setting the resultset of a query will make execute() a no-op. Instead 115 | * of executing the SQL query and fetching results, the ResultSet provided to this 116 | * method will be returned. 117 | * 118 | * This method is most useful when combined with results stored in a persistent cache. 119 | * 120 | * @param iterable $results The results this query should return. 121 | * @return $this 122 | */ 123 | public function setResult(iterable $results) 124 | { 125 | $this->_results = $results; 126 | 127 | return $this; 128 | } 129 | 130 | /** 131 | * Executes this query and returns a results iterator. This function is required 132 | * for implementing the IteratorAggregate interface and allows the query to be 133 | * iterated without having to call execute() manually, thus making it look like 134 | * a result set instead of the query itself. 135 | * 136 | * @return \Cake\Datasource\ResultSetInterface 137 | * @psalm-suppress ImplementedReturnTypeMismatch 138 | */ 139 | public function getIterator() 140 | { 141 | return $this->all(); 142 | } 143 | 144 | /** 145 | * Enable result caching for this query. 146 | * 147 | * If a query has caching enabled, it will do the following when executed: 148 | * 149 | * - Check the cache for $key. If there are results no SQL will be executed. 150 | * Instead the cached results will be returned. 151 | * - When the cached data is stale/missing the result set will be cached as the query 152 | * is executed. 153 | * 154 | * ### Usage 155 | * 156 | * ``` 157 | * // Simple string key + config 158 | * $query->cache('my_key', 'db_results'); 159 | * 160 | * // Function to generate key. 161 | * $query->cache(function ($q) { 162 | * $key = serialize($q->clause('select')); 163 | * $key .= serialize($q->clause('where')); 164 | * return md5($key); 165 | * }); 166 | * 167 | * // Using a pre-built cache engine. 168 | * $query->cache('my_key', $engine); 169 | * 170 | * // Disable caching 171 | * $query->cache(false); 172 | * ``` 173 | * 174 | * @param \Closure|string|false $key Either the cache key or a function to generate the cache key. 175 | * When using a function, this query instance will be supplied as an argument. 176 | * @param string|\Psr\SimpleCache\CacheInterface $config Either the name of the cache config to use, or 177 | * a cache engine instance. 178 | * @return $this 179 | */ 180 | public function cache($key, $config = 'default') 181 | { 182 | if ($key === false) { 183 | $this->_cache = null; 184 | 185 | return $this; 186 | } 187 | $this->_cache = new QueryCacher($key, $config); 188 | 189 | return $this; 190 | } 191 | 192 | /** 193 | * Returns the current configured query `_eagerLoaded` value 194 | * 195 | * @return bool 196 | */ 197 | public function isEagerLoaded(): bool 198 | { 199 | return $this->_eagerLoaded; 200 | } 201 | 202 | /** 203 | * Sets the query instance to be an eager loaded query. If no argument is 204 | * passed, the current configured query `_eagerLoaded` value is returned. 205 | * 206 | * @param bool $value Whether or not to eager load. 207 | * @return $this 208 | */ 209 | public function eagerLoaded(bool $value) 210 | { 211 | $this->_eagerLoaded = $value; 212 | 213 | return $this; 214 | } 215 | 216 | /** 217 | * Returns a key => value array representing a single aliased field 218 | * that can be passed directly to the select() method. 219 | * The key will contain the alias and the value the actual field name. 220 | * 221 | * If the field is already aliased, then it will not be changed. 222 | * If no $alias is passed, the default table for this query will be used. 223 | * 224 | * @param string $field The field to alias 225 | * @param string|null $alias the alias used to prefix the field 226 | * @return array 227 | */ 228 | public function aliasField(string $field, ?string $alias = null): array 229 | { 230 | $namespaced = strpos($field, '.') !== false; 231 | $aliasedField = $field; 232 | 233 | if ($namespaced) { 234 | [$alias, $field] = explode('.', $field); 235 | } 236 | 237 | if (!$alias) { 238 | $alias = $this->getRepository()->getAlias(); 239 | } 240 | 241 | $key = sprintf('%s__%s', $alias, $field); 242 | if (!$namespaced) { 243 | $aliasedField = $alias . '.' . $field; 244 | } 245 | 246 | return [$key => $aliasedField]; 247 | } 248 | 249 | /** 250 | * Runs `aliasField()` for each field in the provided list and returns 251 | * the result under a single array. 252 | * 253 | * @param array $fields The fields to alias 254 | * @param string|null $defaultAlias The default alias 255 | * @return string[] 256 | */ 257 | public function aliasFields(array $fields, ?string $defaultAlias = null): array 258 | { 259 | $aliased = []; 260 | foreach ($fields as $alias => $field) { 261 | if (is_numeric($alias) && is_string($field)) { 262 | $aliased += $this->aliasField($field, $defaultAlias); 263 | continue; 264 | } 265 | $aliased[$alias] = $field; 266 | } 267 | 268 | return $aliased; 269 | } 270 | 271 | /** 272 | * Fetch the results for this query. 273 | * 274 | * Will return either the results set through setResult(), or execute this query 275 | * and return the ResultSetDecorator object ready for streaming of results. 276 | * 277 | * ResultSetDecorator is a traversable object that implements the methods found 278 | * on Cake\Collection\Collection. 279 | * 280 | * @return \Cake\Datasource\ResultSetInterface 281 | */ 282 | public function all(): ResultSetInterface 283 | { 284 | if ($this->_results !== null) { 285 | return $this->_results; 286 | } 287 | 288 | $results = null; 289 | if ($this->_cache) { 290 | $results = $this->_cache->fetch($this); 291 | } 292 | if ($results === null) { 293 | $results = $this->_decorateResults($this->_execute()); 294 | if ($this->_cache) { 295 | $this->_cache->store($this, $results); 296 | } 297 | } 298 | $this->_results = $results; 299 | 300 | return $this->_results; 301 | } 302 | 303 | /** 304 | * Returns an array representation of the results after executing the query. 305 | * 306 | * @return array 307 | */ 308 | public function toArray(): array 309 | { 310 | return $this->all()->toArray(); 311 | } 312 | 313 | /** 314 | * Register a new MapReduce routine to be executed on top of the database results 315 | * Both the mapper and caller callable should be invokable objects. 316 | * 317 | * The MapReduce routing will only be run when the query is executed and the first 318 | * result is attempted to be fetched. 319 | * 320 | * If the third argument is set to true, it will erase previous map reducers 321 | * and replace it with the arguments passed. 322 | * 323 | * @param callable|null $mapper The mapper callable. 324 | * @param callable|null $reducer The reducing function. 325 | * @param bool $overwrite Set to true to overwrite existing map + reduce functions. 326 | * @return $this 327 | * @see \Cake\Collection\Iterator\MapReduce for details on how to use emit data to the map reducer. 328 | */ 329 | public function mapReduce(?callable $mapper = null, ?callable $reducer = null, bool $overwrite = false) 330 | { 331 | if ($overwrite) { 332 | $this->_mapReduce = []; 333 | } 334 | if ($mapper === null) { 335 | if (!$overwrite) { 336 | throw new InvalidArgumentException('$mapper can be null only when $overwrite is true.'); 337 | } 338 | 339 | return $this; 340 | } 341 | $this->_mapReduce[] = compact('mapper', 'reducer'); 342 | 343 | return $this; 344 | } 345 | 346 | /** 347 | * Returns the list of previously registered map reduce routines. 348 | * 349 | * @return array 350 | */ 351 | public function getMapReducers(): array 352 | { 353 | return $this->_mapReduce; 354 | } 355 | 356 | /** 357 | * Registers a new formatter callback function that is to be executed when trying 358 | * to fetch the results from the database. 359 | * 360 | * If the second argument is set to true, it will erase previous formatters 361 | * and replace them with the passed first argument. 362 | * 363 | * Callbacks are required to return an iterator object, which will be used as 364 | * the return value for this query's result. Formatter functions are applied 365 | * after all the `MapReduce` routines for this query have been executed. 366 | * 367 | * Formatting callbacks will receive two arguments, the first one being an object 368 | * implementing `\Cake\Collection\CollectionInterface`, that can be traversed and 369 | * modified at will. The second one being the query instance on which the formatter 370 | * callback is being applied. 371 | * 372 | * Usually the query instance received by the formatter callback is the same query 373 | * instance on which the callback was attached to, except for in a joined 374 | * association, in that case the callback will be invoked on the association source 375 | * side query, and it will receive that query instance instead of the one on which 376 | * the callback was originally attached to - see the examples below! 377 | * 378 | * ### Examples: 379 | * 380 | * Return all results from the table indexed by id: 381 | * 382 | * ``` 383 | * $query->select(['id', 'name'])->formatResults(function ($results) { 384 | * return $results->indexBy('id'); 385 | * }); 386 | * ``` 387 | * 388 | * Add a new column to the ResultSet: 389 | * 390 | * ``` 391 | * $query->select(['name', 'birth_date'])->formatResults(function ($results) { 392 | * return $results->map(function ($row) { 393 | * $row['age'] = $row['birth_date']->diff(new DateTime)->y; 394 | * 395 | * return $row; 396 | * }); 397 | * }); 398 | * ``` 399 | * 400 | * Add a new column to the results with respect to the query's hydration configuration: 401 | * 402 | * ``` 403 | * $query->formatResults(function ($results, $query) { 404 | * return $results->map(function ($row) use ($query) { 405 | * $data = [ 406 | * 'bar' => 'baz', 407 | * ]; 408 | * 409 | * if ($query->isHydrationEnabled()) { 410 | * $row['foo'] = new Foo($data) 411 | * } else { 412 | * $row['foo'] = $data; 413 | * } 414 | * 415 | * return $row; 416 | * }); 417 | * }); 418 | * ``` 419 | * 420 | * Retaining access to the association target query instance of joined associations, 421 | * by inheriting the contain callback's query argument: 422 | * 423 | * ``` 424 | * // Assuming a `Articles belongsTo Authors` association that uses the join strategy 425 | * 426 | * $articlesQuery->contain('Authors', function ($authorsQuery) { 427 | * return $authorsQuery->formatResults(function ($results, $query) use ($authorsQuery) { 428 | * // Here `$authorsQuery` will always be the instance 429 | * // where the callback was attached to. 430 | * 431 | * // The instance passed to the callback in the second 432 | * // argument (`$query`), will be the one where the 433 | * // callback is actually being applied to, in this 434 | * // example that would be `$articlesQuery`. 435 | * 436 | * // ... 437 | * 438 | * return $results; 439 | * }); 440 | * }); 441 | * ``` 442 | * 443 | * @param callable|null $formatter The formatting callable. 444 | * @param int|true $mode Whether or not to overwrite, append or prepend the formatter. 445 | * @return $this 446 | * @throws \InvalidArgumentException 447 | */ 448 | public function formatResults(?callable $formatter = null, $mode = self::APPEND) 449 | { 450 | if ($mode === self::OVERWRITE) { 451 | $this->_formatters = []; 452 | } 453 | if ($formatter === null) { 454 | if ($mode !== self::OVERWRITE) { 455 | throw new InvalidArgumentException('$formatter can be null only when $mode is overwrite.'); 456 | } 457 | 458 | return $this; 459 | } 460 | 461 | if ($mode === self::PREPEND) { 462 | array_unshift($this->_formatters, $formatter); 463 | 464 | return $this; 465 | } 466 | 467 | $this->_formatters[] = $formatter; 468 | 469 | return $this; 470 | } 471 | 472 | /** 473 | * Returns the list of previously registered format routines. 474 | * 475 | * @return callable[] 476 | */ 477 | public function getResultFormatters(): array 478 | { 479 | return $this->_formatters; 480 | } 481 | 482 | /** 483 | * Returns the first result out of executing this query, if the query has not been 484 | * executed before, it will set the limit clause to 1 for performance reasons. 485 | * 486 | * ### Example: 487 | * 488 | * ``` 489 | * $singleUser = $query->select(['id', 'username'])->first(); 490 | * ``` 491 | * 492 | * @return \Cake\Datasource\EntityInterface|array|null The first result from the ResultSet. 493 | */ 494 | public function first() 495 | { 496 | if ($this->_dirty) { 497 | $this->limit(1); 498 | } 499 | 500 | return $this->all()->first(); 501 | } 502 | 503 | /** 504 | * Get the first result from the executing query or raise an exception. 505 | * 506 | * @throws \Cake\Datasource\Exception\RecordNotFoundException When there is no first record. 507 | * @return \Cake\Datasource\EntityInterface|array The first result from the ResultSet. 508 | */ 509 | public function firstOrFail() 510 | { 511 | $entity = $this->first(); 512 | if (!$entity) { 513 | $table = $this->getRepository(); 514 | throw new RecordNotFoundException(sprintf( 515 | 'Record not found in table "%s"', 516 | $table->getTable() 517 | )); 518 | } 519 | 520 | return $entity; 521 | } 522 | 523 | /** 524 | * Returns an array with the custom options that were applied to this query 525 | * and that were not already processed by another method in this class. 526 | * 527 | * ### Example: 528 | * 529 | * ``` 530 | * $query->applyOptions(['doABarrelRoll' => true, 'fields' => ['id', 'name']); 531 | * $query->getOptions(); // Returns ['doABarrelRoll' => true] 532 | * ``` 533 | * 534 | * @see \Cake\Datasource\QueryInterface::applyOptions() to read about the options that will 535 | * be processed by this class and not returned by this function 536 | * @return array 537 | * @see applyOptions() 538 | */ 539 | public function getOptions(): array 540 | { 541 | return $this->_options; 542 | } 543 | 544 | /** 545 | * Enables calling methods from the result set as if they were from this class 546 | * 547 | * @param string $method the method to call 548 | * @param array $arguments list of arguments for the method to call 549 | * @return mixed 550 | * @throws \BadMethodCallException if no such method exists in result set 551 | */ 552 | public function __call(string $method, array $arguments) 553 | { 554 | $resultSetClass = $this->_decoratorClass(); 555 | if (in_array($method, get_class_methods($resultSetClass), true)) { 556 | $results = $this->all(); 557 | 558 | return $results->$method(...$arguments); 559 | } 560 | throw new BadMethodCallException( 561 | sprintf('Unknown method "%s"', $method) 562 | ); 563 | } 564 | 565 | /** 566 | * Populates or adds parts to current query clauses using an array. 567 | * This is handy for passing all query clauses at once. 568 | * 569 | * @param array $options the options to be applied 570 | * @return $this 571 | */ 572 | abstract public function applyOptions(array $options); 573 | 574 | /** 575 | * Executes this query and returns a traversable object containing the results 576 | * 577 | * @return \Cake\Datasource\ResultSetInterface 578 | */ 579 | abstract protected function _execute(): ResultSetInterface; 580 | 581 | /** 582 | * Decorates the results iterator with MapReduce routines and formatters 583 | * 584 | * @param \Traversable $result Original results 585 | * @return \Cake\Datasource\ResultSetInterface 586 | */ 587 | protected function _decorateResults(Traversable $result): ResultSetInterface 588 | { 589 | $decorator = $this->_decoratorClass(); 590 | foreach ($this->_mapReduce as $functions) { 591 | $result = new MapReduce($result, $functions['mapper'], $functions['reducer']); 592 | } 593 | 594 | if (!empty($this->_mapReduce)) { 595 | $result = new $decorator($result); 596 | } 597 | 598 | foreach ($this->_formatters as $formatter) { 599 | $result = $formatter($result, $this); 600 | } 601 | 602 | if (!empty($this->_formatters) && !($result instanceof $decorator)) { 603 | $result = new $decorator($result); 604 | } 605 | 606 | return $result; 607 | } 608 | 609 | /** 610 | * Returns the name of the class to be used for decorating results 611 | * 612 | * @return string 613 | * @psalm-return class-string<\Cake\Datasource\ResultSetInterface> 614 | */ 615 | protected function _decoratorClass(): string 616 | { 617 | return ResultSetDecorator::class; 618 | } 619 | } 620 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Total Downloads](https://img.shields.io/packagist/dt/cakephp/datasource.svg?style=flat-square)](https://packagist.org/packages/cakephp/datasource) 2 | [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt) 3 | 4 | # CakePHP Datasource Library 5 | 6 | This library contains interfaces for implementing Repositories and Entities using any data source, 7 | a class for managing connections to datasources and traits to help you quickly implement the 8 | interfaces provided by this package. 9 | 10 | ## Repositories 11 | 12 | A repository is a class capable of interfacing with a data source using operations such as 13 | `find`, `save` and `delete` by using intermediate query objects for expressing commands to 14 | the data store and returning Entities as the single result unit of such system. 15 | 16 | In the case of a Relational database, a Repository would be a `Table`, which can be return single 17 | or multiple `Entity` objects by using a `Query`. 18 | 19 | This library exposes the following interfaces for creating a system that implements the 20 | repository pattern and is compatible with the CakePHP framework: 21 | 22 | * `RepositoryInterface` - Describes the methods for a base repository class. 23 | * `EntityInterface` - Describes the methods for a single result object. 24 | * `ResultSetInterface` - Represents the idea of a collection of Entities as a result of a query. 25 | 26 | Additionally, this package provides a few traits and classes you can use in your own implementations: 27 | 28 | * `EntityTrait` - Contains the default implementation for the `EntityInterface`. 29 | * `QueryTrait` - Exposes the methods for creating a query object capable of returning decoratable collections. 30 | * `ResultSetDecorator` - Decorates any traversable object, so it complies with `ResultSetInterface`. 31 | 32 | 33 | ## Connections 34 | 35 | This library contains a couple of utility classes meant to create and manage 36 | connection objects. Connections are typically used in repositories for 37 | interfacing with the actual data source system. 38 | 39 | The `ConnectionManager` class acts as a registry to access database connections 40 | your application has. It provides a place that other objects can get references 41 | to existing connections. Creating connections with the `ConnectionManager` is 42 | easy: 43 | 44 | ```php 45 | use Cake\Datasource\ConnectionManager; 46 | 47 | ConnectionManager::config('connection-one', [ 48 | 'className' => 'MyApp\Connections\CustomConnection', 49 | 'param1' => 'value', 50 | 'param2' => 'another value' 51 | ]); 52 | 53 | ConnectionManager::config('connection-two', [ 54 | 'className' => 'MyApp\Connections\CustomConnection', 55 | 'param1' => 'different value', 56 | 'param2' => 'another value' 57 | ]); 58 | ``` 59 | 60 | When requested, the `ConnectionManager` will instantiate 61 | `MyApp\Connections\CustomConnection` by passing `param1` and `param2` inside an 62 | array as the first argument of the constructor. 63 | 64 | Once configured connections can be fetched using `ConnectionManager::get()`. 65 | This method will construct and load a connection if it has not been built 66 | before, or return the existing known connection: 67 | 68 | ```php 69 | use Cake\Datasource\ConnectionManager; 70 | $conn = ConnectionManager::get('master'); 71 | ``` 72 | 73 | It is also possible to store connection objects by passing the instance directly to the manager: 74 | 75 | ```php 76 | use Cake\Datasource\ConnectionManager; 77 | $conn = ConnectionManager::config('other', $connectionInstance); 78 | ``` 79 | 80 | ## Documentation 81 | 82 | Please make sure you check the [official API documentation](https://api.cakephp.org/4.x/namespace-Cake.Datasource.html) 83 | -------------------------------------------------------------------------------- /RepositoryInterface.php: -------------------------------------------------------------------------------- 1 | get($id); 82 | * 83 | * $article = $articles->get($id, ['contain' => ['Comments]]); 84 | * ``` 85 | * 86 | * @param mixed $primaryKey primary key value to find 87 | * @param array $options options accepted by `Table::find()` 88 | * @throws \Cake\Datasource\Exception\RecordNotFoundException if the record with such id 89 | * could not be found 90 | * @return \Cake\Datasource\EntityInterface 91 | * @see \Cake\Datasource\RepositoryInterface::find() 92 | */ 93 | public function get($primaryKey, array $options = []): EntityInterface; 94 | 95 | /** 96 | * Creates a new Query instance for this repository 97 | * 98 | * @return \Cake\Datasource\QueryInterface 99 | */ 100 | public function query(); 101 | 102 | /** 103 | * Update all matching records. 104 | * 105 | * Sets the $fields to the provided values based on $conditions. 106 | * This method will *not* trigger beforeSave/afterSave events. If you need those 107 | * first load a collection of records and update them. 108 | * 109 | * @param string|array|\Closure|\Cake\Database\Expression\QueryExpression $fields A hash of field => new value. 110 | * @param mixed $conditions Conditions to be used, accepts anything Query::where() 111 | * can take. 112 | * @return int Count Returns the affected rows. 113 | */ 114 | public function updateAll($fields, $conditions): int; 115 | 116 | /** 117 | * Deletes all records matching the provided conditions. 118 | * 119 | * This method will *not* trigger beforeDelete/afterDelete events. If you 120 | * need those first load a collection of records and delete them. 121 | * 122 | * This method will *not* execute on associations' `cascade` attribute. You should 123 | * use database foreign keys + ON CASCADE rules if you need cascading deletes combined 124 | * with this method. 125 | * 126 | * @param mixed $conditions Conditions to be used, accepts anything Query::where() 127 | * can take. 128 | * @return int Returns the number of affected rows. 129 | * @see \Cake\Datasource\RepositoryInterface::delete() 130 | */ 131 | public function deleteAll($conditions): int; 132 | 133 | /** 134 | * Returns true if there is any record in this repository matching the specified 135 | * conditions. 136 | * 137 | * @param array $conditions list of conditions to pass to the query 138 | * @return bool 139 | */ 140 | public function exists($conditions): bool; 141 | 142 | /** 143 | * Persists an entity based on the fields that are marked as dirty and 144 | * returns the same entity after a successful save or false in case 145 | * of any error. 146 | * 147 | * @param \Cake\Datasource\EntityInterface $entity the entity to be saved 148 | * @param array|\ArrayAccess $options The options to use when saving. 149 | * @return \Cake\Datasource\EntityInterface|false 150 | */ 151 | public function save(EntityInterface $entity, $options = []); 152 | 153 | /** 154 | * Delete a single entity. 155 | * 156 | * Deletes an entity and possibly related associations from the database 157 | * based on the 'dependent' option used when defining the association. 158 | * 159 | * @param \Cake\Datasource\EntityInterface $entity The entity to remove. 160 | * @param array|\ArrayAccess $options The options for the delete. 161 | * @return bool success 162 | */ 163 | public function delete(EntityInterface $entity, $options = []): bool; 164 | 165 | /** 166 | * This creates a new entity object. 167 | * 168 | * Careful: This does not trigger any field validation. 169 | * This entity can be persisted without validation error as empty record. 170 | * Always patch in required fields before saving. 171 | * 172 | * @return \Cake\Datasource\EntityInterface 173 | */ 174 | public function newEmptyEntity(): EntityInterface; 175 | 176 | /** 177 | * Create a new entity + associated entities from an array. 178 | * 179 | * This is most useful when hydrating request data back into entities. 180 | * For example, in your controller code: 181 | * 182 | * ``` 183 | * $article = $this->Articles->newEntity($this->request->getData()); 184 | * ``` 185 | * 186 | * The hydrated entity will correctly do an insert/update based 187 | * on the primary key data existing in the database when the entity 188 | * is saved. Until the entity is saved, it will be a detached record. 189 | * 190 | * @param array $data The data to build an entity with. 191 | * @param array $options A list of options for the object hydration. 192 | * @return \Cake\Datasource\EntityInterface 193 | */ 194 | public function newEntity(array $data, array $options = []): EntityInterface; 195 | 196 | /** 197 | * Create a list of entities + associated entities from an array. 198 | * 199 | * This is most useful when hydrating request data back into entities. 200 | * For example, in your controller code: 201 | * 202 | * ``` 203 | * $articles = $this->Articles->newEntities($this->request->getData()); 204 | * ``` 205 | * 206 | * The hydrated entities can then be iterated and saved. 207 | * 208 | * @param array $data The data to build an entity with. 209 | * @param array $options A list of options for the objects hydration. 210 | * @return \Cake\Datasource\EntityInterface[] An array of hydrated records. 211 | */ 212 | public function newEntities(array $data, array $options = []): array; 213 | 214 | /** 215 | * Merges the passed `$data` into `$entity` respecting the accessible 216 | * fields configured on the entity. Returns the same entity after being 217 | * altered. 218 | * 219 | * This is most useful when editing an existing entity using request data: 220 | * 221 | * ``` 222 | * $article = $this->Articles->patchEntity($article, $this->request->getData()); 223 | * ``` 224 | * 225 | * @param \Cake\Datasource\EntityInterface $entity the entity that will get the 226 | * data merged in 227 | * @param array $data key value list of fields to be merged into the entity 228 | * @param array $options A list of options for the object hydration. 229 | * @return \Cake\Datasource\EntityInterface 230 | */ 231 | public function patchEntity(EntityInterface $entity, array $data, array $options = []): EntityInterface; 232 | 233 | /** 234 | * Merges each of the elements passed in `$data` into the entities 235 | * found in `$entities` respecting the accessible fields configured on the entities. 236 | * Merging is done by matching the primary key in each of the elements in `$data` 237 | * and `$entities`. 238 | * 239 | * This is most useful when editing a list of existing entities using request data: 240 | * 241 | * ``` 242 | * $article = $this->Articles->patchEntities($articles, $this->request->getData()); 243 | * ``` 244 | * 245 | * @param \Cake\Datasource\EntityInterface[]|\Traversable $entities the entities that will get the 246 | * data merged in 247 | * @param array $data list of arrays to be merged into the entities 248 | * @param array $options A list of options for the objects hydration. 249 | * @return \Cake\Datasource\EntityInterface[] 250 | */ 251 | public function patchEntities(iterable $entities, array $data, array $options = []): array; 252 | } 253 | -------------------------------------------------------------------------------- /ResultSetDecorator.php: -------------------------------------------------------------------------------- 1 | getInnerIterator(); 40 | if ($iterator instanceof Countable) { 41 | return $iterator->count(); 42 | } 43 | 44 | return count($this->toArray()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ResultSetInterface.php: -------------------------------------------------------------------------------- 1 | rule = $rule; 70 | $this->name = $name; 71 | $this->options = $options; 72 | } 73 | 74 | /** 75 | * Set options for the rule invocation. 76 | * 77 | * Old options will be merged with the new ones. 78 | * 79 | * @param array $options The options to set. 80 | * @return $this 81 | */ 82 | public function setOptions(array $options) 83 | { 84 | $this->options = $options + $this->options; 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * Set the rule name. 91 | * 92 | * Only truthy names will be set. 93 | * 94 | * @param string|null $name The name to set. 95 | * @return $this 96 | */ 97 | public function setName(?string $name) 98 | { 99 | if ($name) { 100 | $this->name = $name; 101 | } 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * Invoke the rule. 108 | * 109 | * @param \Cake\Datasource\EntityInterface $entity The entity the rule 110 | * should apply to. 111 | * @param array $scope The rule's scope/options. 112 | * @return bool Whether or not the rule passed. 113 | */ 114 | public function __invoke(EntityInterface $entity, array $scope): bool 115 | { 116 | $rule = $this->rule; 117 | $pass = $rule($entity, $this->options + $scope); 118 | if ($pass === true || empty($this->options['errorField'])) { 119 | return $pass === true; 120 | } 121 | 122 | $message = 'invalid'; 123 | if (isset($this->options['message'])) { 124 | $message = $this->options['message']; 125 | } 126 | if (is_string($pass)) { 127 | $message = $pass; 128 | } 129 | if ($this->name) { 130 | $message = [$this->name => $message]; 131 | } else { 132 | $message = [$message]; 133 | } 134 | $errorField = $this->options['errorField']; 135 | $entity->setError($errorField, $message); 136 | 137 | if ($entity instanceof InvalidPropertyInterface && isset($entity->{$errorField})) { 138 | $invalidValue = $entity->{$errorField}; 139 | $entity->setInvalidField($errorField, $invalidValue); 140 | } 141 | 142 | return $pass === true; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /RulesAwareTrait.php: -------------------------------------------------------------------------------- 1 | rulesChecker(); 53 | $options = $options ?: new ArrayObject(); 54 | $options = is_array($options) ? new ArrayObject($options) : $options; 55 | $hasEvents = ($this instanceof EventDispatcherInterface); 56 | 57 | if ($hasEvents) { 58 | $event = $this->dispatchEvent( 59 | 'Model.beforeRules', 60 | compact('entity', 'options', 'operation') 61 | ); 62 | if ($event->isStopped()) { 63 | return $event->getResult(); 64 | } 65 | } 66 | 67 | $result = $rules->check($entity, $operation, $options->getArrayCopy()); 68 | 69 | if ($hasEvents) { 70 | $event = $this->dispatchEvent( 71 | 'Model.afterRules', 72 | compact('entity', 'options', 'result', 'operation') 73 | ); 74 | 75 | if ($event->isStopped()) { 76 | return $event->getResult(); 77 | } 78 | } 79 | 80 | return $result; 81 | } 82 | 83 | /** 84 | * Returns the RulesChecker for this instance. 85 | * 86 | * A RulesChecker object is used to test an entity for validity 87 | * on rules that may involve complex logic or data that 88 | * needs to be fetched from relevant datasources. 89 | * 90 | * @see \Cake\Datasource\RulesChecker 91 | * @return \Cake\Datasource\RulesChecker 92 | */ 93 | public function rulesChecker(): RulesChecker 94 | { 95 | if ($this->_rulesChecker !== null) { 96 | return $this->_rulesChecker; 97 | } 98 | /** @psalm-var class-string<\Cake\Datasource\RulesChecker> $class */ 99 | $class = defined('static::RULES_CLASS') ? static::RULES_CLASS : RulesChecker::class; 100 | /** @psalm-suppress ArgumentTypeCoercion */ 101 | $this->_rulesChecker = $this->buildRules(new $class(['repository' => $this])); 102 | $this->dispatchEvent('Model.buildRules', ['rules' => $this->_rulesChecker]); 103 | 104 | return $this->_rulesChecker; 105 | } 106 | 107 | /** 108 | * Returns a RulesChecker object after modifying the one that was supplied. 109 | * 110 | * Subclasses should override this method in order to initialize the rules to be applied to 111 | * entities saved by this instance. 112 | * 113 | * @param \Cake\Datasource\RulesChecker $rules The rules object to be modified. 114 | * @return \Cake\Datasource\RulesChecker 115 | */ 116 | public function buildRules(RulesChecker $rules): RulesChecker 117 | { 118 | return $rules; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /RulesChecker.php: -------------------------------------------------------------------------------- 1 | _options = $options; 115 | $this->_useI18n = function_exists('__d'); 116 | } 117 | 118 | /** 119 | * Adds a rule that will be applied to the entity both on create and update 120 | * operations. 121 | * 122 | * ### Options 123 | * 124 | * The options array accept the following special keys: 125 | * 126 | * - `errorField`: The name of the entity field that will be marked as invalid 127 | * if the rule does not pass. 128 | * - `message`: The error message to set to `errorField` if the rule does not pass. 129 | * 130 | * @param callable $rule A callable function or object that will return whether 131 | * the entity is valid or not. 132 | * @param string|array|null $name The alias for a rule, or an array of options. 133 | * @param array $options List of extra options to pass to the rule callable as 134 | * second argument. 135 | * @return $this 136 | */ 137 | public function add(callable $rule, $name = null, array $options = []) 138 | { 139 | $this->_rules[] = $this->_addError($rule, $name, $options); 140 | 141 | return $this; 142 | } 143 | 144 | /** 145 | * Adds a rule that will be applied to the entity on create operations. 146 | * 147 | * ### Options 148 | * 149 | * The options array accept the following special keys: 150 | * 151 | * - `errorField`: The name of the entity field that will be marked as invalid 152 | * if the rule does not pass. 153 | * - `message`: The error message to set to `errorField` if the rule does not pass. 154 | * 155 | * @param callable $rule A callable function or object that will return whether 156 | * the entity is valid or not. 157 | * @param string|array|null $name The alias for a rule or an array of options. 158 | * @param array $options List of extra options to pass to the rule callable as 159 | * second argument. 160 | * @return $this 161 | */ 162 | public function addCreate(callable $rule, $name = null, array $options = []) 163 | { 164 | $this->_createRules[] = $this->_addError($rule, $name, $options); 165 | 166 | return $this; 167 | } 168 | 169 | /** 170 | * Adds a rule that will be applied to the entity on update operations. 171 | * 172 | * ### Options 173 | * 174 | * The options array accept the following special keys: 175 | * 176 | * - `errorField`: The name of the entity field that will be marked as invalid 177 | * if the rule does not pass. 178 | * - `message`: The error message to set to `errorField` if the rule does not pass. 179 | * 180 | * @param callable $rule A callable function or object that will return whether 181 | * the entity is valid or not. 182 | * @param string|array|null $name The alias for a rule, or an array of options. 183 | * @param array $options List of extra options to pass to the rule callable as 184 | * second argument. 185 | * @return $this 186 | */ 187 | public function addUpdate(callable $rule, $name = null, array $options = []) 188 | { 189 | $this->_updateRules[] = $this->_addError($rule, $name, $options); 190 | 191 | return $this; 192 | } 193 | 194 | /** 195 | * Adds a rule that will be applied to the entity on delete operations. 196 | * 197 | * ### Options 198 | * 199 | * The options array accept the following special keys: 200 | * 201 | * - `errorField`: The name of the entity field that will be marked as invalid 202 | * if the rule does not pass. 203 | * - `message`: The error message to set to `errorField` if the rule does not pass. 204 | * 205 | * @param callable $rule A callable function or object that will return whether 206 | * the entity is valid or not. 207 | * @param string|array|null $name The alias for a rule, or an array of options. 208 | * @param array $options List of extra options to pass to the rule callable as 209 | * second argument. 210 | * @return $this 211 | */ 212 | public function addDelete(callable $rule, $name = null, array $options = []) 213 | { 214 | $this->_deleteRules[] = $this->_addError($rule, $name, $options); 215 | 216 | return $this; 217 | } 218 | 219 | /** 220 | * Runs each of the rules by passing the provided entity and returns true if all 221 | * of them pass. The rules to be applied are depended on the $mode parameter which 222 | * can only be RulesChecker::CREATE, RulesChecker::UPDATE or RulesChecker::DELETE 223 | * 224 | * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity. 225 | * @param string $mode Either 'create, 'update' or 'delete'. 226 | * @param array $options Extra options to pass to checker functions. 227 | * @return bool 228 | * @throws \InvalidArgumentException if an invalid mode is passed. 229 | */ 230 | public function check(EntityInterface $entity, string $mode, array $options = []): bool 231 | { 232 | if ($mode === self::CREATE) { 233 | return $this->checkCreate($entity, $options); 234 | } 235 | 236 | if ($mode === self::UPDATE) { 237 | return $this->checkUpdate($entity, $options); 238 | } 239 | 240 | if ($mode === self::DELETE) { 241 | return $this->checkDelete($entity, $options); 242 | } 243 | 244 | throw new InvalidArgumentException('Wrong checking mode: ' . $mode); 245 | } 246 | 247 | /** 248 | * Runs each of the rules by passing the provided entity and returns true if all 249 | * of them pass. The rules selected will be only those specified to be run on 'create' 250 | * 251 | * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity. 252 | * @param array $options Extra options to pass to checker functions. 253 | * @return bool 254 | */ 255 | public function checkCreate(EntityInterface $entity, array $options = []): bool 256 | { 257 | return $this->_checkRules($entity, $options, array_merge($this->_rules, $this->_createRules)); 258 | } 259 | 260 | /** 261 | * Runs each of the rules by passing the provided entity and returns true if all 262 | * of them pass. The rules selected will be only those specified to be run on 'update' 263 | * 264 | * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity. 265 | * @param array $options Extra options to pass to checker functions. 266 | * @return bool 267 | */ 268 | public function checkUpdate(EntityInterface $entity, array $options = []): bool 269 | { 270 | return $this->_checkRules($entity, $options, array_merge($this->_rules, $this->_updateRules)); 271 | } 272 | 273 | /** 274 | * Runs each of the rules by passing the provided entity and returns true if all 275 | * of them pass. The rules selected will be only those specified to be run on 'delete' 276 | * 277 | * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity. 278 | * @param array $options Extra options to pass to checker functions. 279 | * @return bool 280 | */ 281 | public function checkDelete(EntityInterface $entity, array $options = []): bool 282 | { 283 | return $this->_checkRules($entity, $options, $this->_deleteRules); 284 | } 285 | 286 | /** 287 | * Used by top level functions checkDelete, checkCreate and checkUpdate, this function 288 | * iterates an array containing the rules to be checked and checks them all. 289 | * 290 | * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity. 291 | * @param array $options Extra options to pass to checker functions. 292 | * @param \Cake\Datasource\RuleInvoker[] $rules The list of rules that must be checked. 293 | * @return bool 294 | */ 295 | protected function _checkRules(EntityInterface $entity, array $options = [], array $rules = []): bool 296 | { 297 | $success = true; 298 | $options += $this->_options; 299 | foreach ($rules as $rule) { 300 | $success = $rule($entity, $options) && $success; 301 | } 302 | 303 | return $success; 304 | } 305 | 306 | /** 307 | * Utility method for decorating any callable so that if it returns false, the correct 308 | * property in the entity is marked as invalid. 309 | * 310 | * @param callable $rule The rule to decorate 311 | * @param string|array|null $name The alias for a rule or an array of options 312 | * @param array $options The options containing the error message and field. 313 | * @return \Cake\Datasource\RuleInvoker 314 | */ 315 | protected function _addError(callable $rule, $name = null, array $options = []): RuleInvoker 316 | { 317 | if (is_array($name)) { 318 | $options = $name; 319 | $name = null; 320 | } 321 | 322 | if (!($rule instanceof RuleInvoker)) { 323 | $rule = new RuleInvoker($rule, $name, $options); 324 | } else { 325 | $rule->setOptions($options)->setName($name); 326 | } 327 | 328 | return $rule; 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /SchemaInterface.php: -------------------------------------------------------------------------------- 1 | =7.2.0", 28 | "cakephp/core": "^4.0", 29 | "psr/log": "^1.1", 30 | "psr/simple-cache": "^1.0" 31 | }, 32 | "suggest": { 33 | "cakephp/utility": "If you decide to use EntityTrait.", 34 | "cakephp/collection": "If you decide to use ResultSetInterface.", 35 | "cakephp/cache": "If you decide to use Query caching." 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "Cake\\Datasource\\": "." 40 | } 41 | } 42 | } 43 | --------------------------------------------------------------------------------