PageRenderTime 156ms CodeModel.GetById 125ms app.highlight 24ms RepoModel.GetById 1ms app.codeStats 1ms

/src/Datasource/EntityTrait.php

http://github.com/cakephp/cakephp
PHP | 1244 lines | 543 code | 132 blank | 569 comment | 66 complexity | 6dbcccc7fb7f113acca9685dd89f6fbe MD5 | raw file
   1<?php
   2declare(strict_types=1);
   3
   4/**
   5 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
   6 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
   7 *
   8 * Licensed under The MIT License
   9 * For full copyright and license information, please see the LICENSE.txt
  10 * Redistributions of files must retain the above copyright notice.
  11 *
  12 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  13 * @link          https://cakephp.org CakePHP(tm) Project
  14 * @since         3.0.0
  15 * @license       https://opensource.org/licenses/mit-license.php MIT License
  16 */
  17namespace Cake\Datasource;
  18
  19use Cake\Collection\Collection;
  20use Cake\ORM\Entity;
  21use Cake\Utility\Hash;
  22use Cake\Utility\Inflector;
  23use InvalidArgumentException;
  24use Traversable;
  25
  26/**
  27 * An entity represents a single result row from a repository. It exposes the
  28 * methods for retrieving and storing fields associated in this row.
  29 */
  30trait EntityTrait
  31{
  32    /**
  33     * Holds all fields and their values for this entity.
  34     *
  35     * @var array
  36     */
  37    protected $_fields = [];
  38
  39    /**
  40     * Holds all fields that have been changed and their original values for this entity.
  41     *
  42     * @var array
  43     */
  44    protected $_original = [];
  45
  46    /**
  47     * List of field names that should **not** be included in JSON or Array
  48     * representations of this Entity.
  49     *
  50     * @var string[]
  51     */
  52    protected $_hidden = [];
  53
  54    /**
  55     * List of computed or virtual fields that **should** be included in JSON or array
  56     * representations of this Entity. If a field is present in both _hidden and _virtual
  57     * the field will **not** be in the array/json versions of the entity.
  58     *
  59     * @var string[]
  60     */
  61    protected $_virtual = [];
  62
  63    /**
  64     * Holds a list of the fields that were modified or added after this object
  65     * was originally created.
  66     *
  67     * @var bool[]
  68     */
  69    protected $_dirty = [];
  70
  71    /**
  72     * Holds a cached list of getters/setters per class
  73     *
  74     * @var array
  75     */
  76    protected static $_accessors = [];
  77
  78    /**
  79     * Indicates whether or not this entity is yet to be persisted.
  80     * Entities default to assuming they are new. You can use Table::persisted()
  81     * to set the new flag on an entity based on records in the database.
  82     *
  83     * @var bool
  84     */
  85    protected $_new = true;
  86
  87    /**
  88     * List of errors per field as stored in this object.
  89     *
  90     * @var array
  91     */
  92    protected $_errors = [];
  93
  94    /**
  95     * List of invalid fields and their data for errors upon validation/patching.
  96     *
  97     * @var array
  98     */
  99    protected $_invalid = [];
 100
 101    /**
 102     * Map of fields in this entity that can be safely assigned, each
 103     * field name points to a boolean indicating its status. An empty array
 104     * means no fields are accessible
 105     *
 106     * The special field '\*' can also be mapped, meaning that any other field
 107     * not defined in the map will take its value. For example, `'\*' => true`
 108     * means that any field not defined in the map will be accessible by default
 109     *
 110     * @var array
 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            if ($options['guard'] === true && !$this->isAccessible($name)) {
 239                continue;
 240            }
 241
 242            $this->setDirty($name, true);
 243
 244            if (
 245                !array_key_exists($name, $this->_original) &&
 246                array_key_exists($name, $this->_fields) &&
 247                $this->_fields[$name] !== $value
 248            ) {
 249                $this->_original[$name] = $this->_fields[$name];
 250            }
 251
 252            if (!$options['setter']) {
 253                $this->_fields[$name] = $value;
 254                continue;
 255            }
 256
 257            $setter = static::_accessor($name, 'set');
 258            if ($setter) {
 259                $value = $this->{$setter}($value);
 260            }
 261            $this->_fields[$name] = $value;
 262        }
 263
 264        return $this;
 265    }
 266
 267    /**
 268     * Returns the value of a field by name
 269     *
 270     * @param string $field the name of the field to retrieve
 271     * @return mixed
 272     * @throws \InvalidArgumentException if an empty field name is passed
 273     */
 274    public function &get(string $field)
 275    {
 276        if ($field === '') {
 277            throw new InvalidArgumentException('Cannot get an empty field');
 278        }
 279
 280        $value = null;
 281        $method = static::_accessor($field, 'get');
 282
 283        if (isset($this->_fields[$field])) {
 284            $value = &$this->_fields[$field];
 285        }
 286
 287        if ($method) {
 288            $result = $this->{$method}($value);
 289
 290            return $result;
 291        }
 292
 293        return $value;
 294    }
 295
 296    /**
 297     * Returns the value of an original field by name
 298     *
 299     * @param string $field the name of the field for which original value is retrieved.
 300     * @return mixed
 301     * @throws \InvalidArgumentException if an empty field name is passed.
 302     */
 303    public function getOriginal(string $field)
 304    {
 305        if (!strlen($field)) {
 306            throw new InvalidArgumentException('Cannot get an empty field');
 307        }
 308        if (array_key_exists($field, $this->_original)) {
 309            return $this->_original[$field];
 310        }
 311
 312        return $this->get($field);
 313    }
 314
 315    /**
 316     * Gets all original values of the entity.
 317     *
 318     * @return array
 319     */
 320    public function getOriginalValues(): array
 321    {
 322        $originals = $this->_original;
 323        $originalKeys = array_keys($originals);
 324        foreach ($this->_fields as $key => $value) {
 325            if (!in_array($key, $originalKeys, true)) {
 326                $originals[$key] = $value;
 327            }
 328        }
 329
 330        return $originals;
 331    }
 332
 333    /**
 334     * Returns whether this entity contains a field named $field
 335     * that contains a non-null value.
 336     *
 337     * ### Example:
 338     *
 339     * ```
 340     * $entity = new Entity(['id' => 1, 'name' => null]);
 341     * $entity->has('id'); // true
 342     * $entity->has('name'); // false
 343     * $entity->has('last_name'); // false
 344     * ```
 345     *
 346     * You can check multiple fields by passing an array:
 347     *
 348     * ```
 349     * $entity->has(['name', 'last_name']);
 350     * ```
 351     *
 352     * All fields must not be null to get a truthy result.
 353     *
 354     * When checking multiple fields. All fields must not be null
 355     * in order for true to be returned.
 356     *
 357     * @param string|string[] $field The field or fields to check.
 358     * @return bool
 359     */
 360    public function has($field): bool
 361    {
 362        foreach ((array)$field as $prop) {
 363            if ($this->get($prop) === null) {
 364                return false;
 365            }
 366        }
 367
 368        return true;
 369    }
 370
 371    /**
 372     * Checks that a field is empty
 373     *
 374     * This is not working like the PHP `empty()` function. The method will
 375     * return true for:
 376     *
 377     * - `''` (empty string)
 378     * - `null`
 379     * - `[]`
 380     *
 381     * and false in all other cases.
 382     *
 383     * @param string $field The field to check.
 384     * @return bool
 385     */
 386    public function isEmpty(string $field): bool
 387    {
 388        $value = $this->get($field);
 389        if (
 390            $value === null ||
 391            (
 392                is_array($value) &&
 393                empty($value) ||
 394                (
 395                    is_string($value) &&
 396                    empty($value)
 397                )
 398            )
 399        ) {
 400            return true;
 401        }
 402
 403        return false;
 404    }
 405
 406    /**
 407     * Checks tha a field has a value.
 408     *
 409     * This method will return true for
 410     *
 411     * - Non-empty strings
 412     * - Non-empty arrays
 413     * - Any object
 414     * - Integer, even `0`
 415     * - Float, even 0.0
 416     *
 417     * and false in all other cases.
 418     *
 419     * @param string $field The field to check.
 420     * @return bool
 421     */
 422    public function hasValue(string $field): bool
 423    {
 424        return !$this->isEmpty($field);
 425    }
 426
 427    /**
 428     * Removes a field or list of fields from this entity
 429     *
 430     * ### Examples:
 431     *
 432     * ```
 433     * $entity->unset('name');
 434     * $entity->unset(['name', 'last_name']);
 435     * ```
 436     *
 437     * @param string|string[] $field The field to unset.
 438     * @return $this
 439     */
 440    public function unset($field)
 441    {
 442        $field = (array)$field;
 443        foreach ($field as $p) {
 444            unset($this->_fields[$p], $this->_original[$p], $this->_dirty[$p]);
 445        }
 446
 447        return $this;
 448    }
 449
 450    /**
 451     * Removes a field or list of fields from this entity
 452     *
 453     * @deprecated 4.0.0 Use unset() instead. Will be removed in 5.0.
 454     * @param string|string[] $field The field to unset.
 455     * @return $this
 456     */
 457    public function unsetProperty($field)
 458    {
 459        return $this->unset($field);
 460    }
 461
 462    /**
 463     * Sets hidden fields.
 464     *
 465     * @param string[] $fields An array of fields to hide from array exports.
 466     * @param bool $merge Merge the new fields with the existing. By default false.
 467     * @return $this
 468     */
 469    public function setHidden(array $fields, bool $merge = false)
 470    {
 471        if ($merge === false) {
 472            $this->_hidden = $fields;
 473
 474            return $this;
 475        }
 476
 477        $fields = array_merge($this->_hidden, $fields);
 478        $this->_hidden = array_unique($fields);
 479
 480        return $this;
 481    }
 482
 483    /**
 484     * Gets the hidden fields.
 485     *
 486     * @return string[]
 487     */
 488    public function getHidden(): array
 489    {
 490        return $this->_hidden;
 491    }
 492
 493    /**
 494     * Sets the virtual fields on this entity.
 495     *
 496     * @param string[] $fields An array of fields to treat as virtual.
 497     * @param bool $merge Merge the new fields with the existing. By default false.
 498     * @return $this
 499     */
 500    public function setVirtual(array $fields, bool $merge = false)
 501    {
 502        if ($merge === false) {
 503            $this->_virtual = $fields;
 504
 505            return $this;
 506        }
 507
 508        $fields = array_merge($this->_virtual, $fields);
 509        $this->_virtual = array_unique($fields);
 510
 511        return $this;
 512    }
 513
 514    /**
 515     * Gets the virtual fields on this entity.
 516     *
 517     * @return string[]
 518     */
 519    public function getVirtual(): array
 520    {
 521        return $this->_virtual;
 522    }
 523
 524    /**
 525     * Gets the list of visible fields.
 526     *
 527     * The list of visible fields is all standard fields
 528     * plus virtual fields minus hidden fields.
 529     *
 530     * @return string[] A list of fields that are 'visible' in all
 531     *     representations.
 532     */
 533    public function getVisible(): array
 534    {
 535        $fields = array_keys($this->_fields);
 536        $fields = array_merge($fields, $this->_virtual);
 537
 538        return array_diff($fields, $this->_hidden);
 539    }
 540
 541    /**
 542     * Returns an array with all the fields that have been set
 543     * to this entity
 544     *
 545     * This method will recursively transform entities assigned to fields
 546     * into arrays as well.
 547     *
 548     * @return array
 549     */
 550    public function toArray(): array
 551    {
 552        $result = [];
 553        foreach ($this->getVisible() as $field) {
 554            $value = $this->get($field);
 555            if (is_array($value)) {
 556                $result[$field] = [];
 557                foreach ($value as $k => $entity) {
 558                    if ($entity instanceof EntityInterface) {
 559                        $result[$field][$k] = $entity->toArray();
 560                    } else {
 561                        $result[$field][$k] = $entity;
 562                    }
 563                }
 564            } elseif ($value instanceof EntityInterface) {
 565                $result[$field] = $value->toArray();
 566            } else {
 567                $result[$field] = $value;
 568            }
 569        }
 570
 571        return $result;
 572    }
 573
 574    /**
 575     * Returns the fields that will be serialized as JSON
 576     *
 577     * @return array
 578     */
 579    public function jsonSerialize(): array
 580    {
 581        return $this->extract($this->getVisible());
 582    }
 583
 584    /**
 585     * Implements isset($entity);
 586     *
 587     * @param string $offset The offset to check.
 588     * @return bool Success
 589     */
 590    public function offsetExists($offset): bool
 591    {
 592        return $this->has($offset);
 593    }
 594
 595    /**
 596     * Implements $entity[$offset];
 597     *
 598     * @param string $offset The offset to get.
 599     * @return mixed
 600     */
 601    public function &offsetGet($offset)
 602    {
 603        return $this->get($offset);
 604    }
 605
 606    /**
 607     * Implements $entity[$offset] = $value;
 608     *
 609     * @param string $offset The offset to set.
 610     * @param mixed $value The value to set.
 611     * @return void
 612     */
 613    public function offsetSet($offset, $value): void
 614    {
 615        $this->set($offset, $value);
 616    }
 617
 618    /**
 619     * Implements unset($result[$offset]);
 620     *
 621     * @param string $offset The offset to remove.
 622     * @return void
 623     */
 624    public function offsetUnset($offset): void
 625    {
 626        $this->unset($offset);
 627    }
 628
 629    /**
 630     * Fetch accessor method name
 631     * Accessor methods (available or not) are cached in $_accessors
 632     *
 633     * @param string $property the field name to derive getter name from
 634     * @param string $type the accessor type ('get' or 'set')
 635     * @return string method name or empty string (no method available)
 636     */
 637    protected static function _accessor(string $property, string $type): string
 638    {
 639        $class = static::class;
 640
 641        if (isset(static::$_accessors[$class][$type][$property])) {
 642            return static::$_accessors[$class][$type][$property];
 643        }
 644
 645        if (!empty(static::$_accessors[$class])) {
 646            return static::$_accessors[$class][$type][$property] = '';
 647        }
 648
 649        if (static::class === Entity::class) {
 650            return '';
 651        }
 652
 653        foreach (get_class_methods($class) as $method) {
 654            $prefix = substr($method, 1, 3);
 655            if ($method[0] !== '_' || ($prefix !== 'get' && $prefix !== 'set')) {
 656                continue;
 657            }
 658            $field = lcfirst(substr($method, 4));
 659            $snakeField = Inflector::underscore($field);
 660            $titleField = ucfirst($field);
 661            static::$_accessors[$class][$prefix][$snakeField] = $method;
 662            static::$_accessors[$class][$prefix][$field] = $method;
 663            static::$_accessors[$class][$prefix][$titleField] = $method;
 664        }
 665
 666        if (!isset(static::$_accessors[$class][$type][$property])) {
 667            static::$_accessors[$class][$type][$property] = '';
 668        }
 669
 670        return static::$_accessors[$class][$type][$property];
 671    }
 672
 673    /**
 674     * Returns an array with the requested fields
 675     * stored in this entity, indexed by field name
 676     *
 677     * @param string[] $fields list of fields to be returned
 678     * @param bool $onlyDirty Return the requested field only if it is dirty
 679     * @return array
 680     */
 681    public function extract(array $fields, bool $onlyDirty = false): array
 682    {
 683        $result = [];
 684        foreach ($fields as $field) {
 685            if (!$onlyDirty || $this->isDirty($field)) {
 686                $result[$field] = $this->get($field);
 687            }
 688        }
 689
 690        return $result;
 691    }
 692
 693    /**
 694     * Returns an array with the requested original fields
 695     * stored in this entity, indexed by field name.
 696     *
 697     * Fields that are unchanged from their original value will be included in the
 698     * return of this method.
 699     *
 700     * @param string[] $fields List of fields to be returned
 701     * @return array
 702     */
 703    public function extractOriginal(array $fields): array
 704    {
 705        $result = [];
 706        foreach ($fields as $field) {
 707            $result[$field] = $this->getOriginal($field);
 708        }
 709
 710        return $result;
 711    }
 712
 713    /**
 714     * Returns an array with only the original fields
 715     * stored in this entity, indexed by field name.
 716     *
 717     * This method will only return fields that have been modified since
 718     * the entity was built. Unchanged fields will be omitted.
 719     *
 720     * @param string[] $fields List of fields to be returned
 721     * @return array
 722     */
 723    public function extractOriginalChanged(array $fields): array
 724    {
 725        $result = [];
 726        foreach ($fields as $field) {
 727            $original = $this->getOriginal($field);
 728            if ($original !== $this->get($field)) {
 729                $result[$field] = $original;
 730            }
 731        }
 732
 733        return $result;
 734    }
 735
 736    /**
 737     * Sets the dirty status of a single field.
 738     *
 739     * @param string $field the field to set or check status for
 740     * @param bool $isDirty true means the field was changed, false means
 741     * it was not changed. Defaults to true.
 742     * @return $this
 743     */
 744    public function setDirty(string $field, bool $isDirty = true)
 745    {
 746        if ($isDirty === false) {
 747            unset($this->_dirty[$field]);
 748
 749            return $this;
 750        }
 751
 752        $this->_dirty[$field] = true;
 753        unset($this->_errors[$field], $this->_invalid[$field]);
 754
 755        return $this;
 756    }
 757
 758    /**
 759     * Checks if the entity is dirty or if a single field of it is dirty.
 760     *
 761     * @param string|null $field The field to check the status for. Null for the whole entity.
 762     * @return bool Whether the field was changed or not
 763     */
 764    public function isDirty(?string $field = null): bool
 765    {
 766        if ($field === null) {
 767            return !empty($this->_dirty);
 768        }
 769
 770        return isset($this->_dirty[$field]);
 771    }
 772
 773    /**
 774     * Gets the dirty fields.
 775     *
 776     * @return string[]
 777     */
 778    public function getDirty(): array
 779    {
 780        return array_keys($this->_dirty);
 781    }
 782
 783    /**
 784     * Sets the entire entity as clean, which means that it will appear as
 785     * no fields being modified or added at all. This is an useful call
 786     * for an initial object hydration
 787     *
 788     * @return void
 789     */
 790    public function clean(): void
 791    {
 792        $this->_dirty = [];
 793        $this->_errors = [];
 794        $this->_invalid = [];
 795        $this->_original = [];
 796    }
 797
 798    /**
 799     * Set the status of this entity.
 800     *
 801     * Using `true` means that the entity has not been persisted in the database,
 802     * `false` that it already is.
 803     *
 804     * @param bool $new Indicate whether or not this entity has been persisted.
 805     * @return $this
 806     */
 807    public function setNew(bool $new)
 808    {
 809        if ($new) {
 810            foreach ($this->_fields as $k => $p) {
 811                $this->_dirty[$k] = true;
 812            }
 813        }
 814
 815        $this->_new = $new;
 816
 817        return $this;
 818    }
 819
 820    /**
 821     * Returns whether or not this entity has already been persisted.
 822     *
 823     * @return bool Whether or not the entity has been persisted.
 824     */
 825    public function isNew(): bool
 826    {
 827        if (func_num_args()) {
 828            deprecationWarning('Using isNew() as setter is deprecated. Use setNew() instead.');
 829
 830            $this->setNew(func_get_arg(0));
 831        }
 832
 833        return $this->_new;
 834    }
 835
 836    /**
 837     * Returns whether this entity has errors.
 838     *
 839     * @param bool $includeNested true will check nested entities for hasErrors()
 840     * @return bool
 841     */
 842    public function hasErrors(bool $includeNested = true): bool
 843    {
 844        if (Hash::filter($this->_errors)) {
 845            return true;
 846        }
 847
 848        if ($includeNested === false) {
 849            return false;
 850        }
 851
 852        foreach ($this->_fields as $field) {
 853            if ($this->_readHasErrors($field)) {
 854                return true;
 855            }
 856        }
 857
 858        return false;
 859    }
 860
 861    /**
 862     * Returns all validation errors.
 863     *
 864     * @return array
 865     */
 866    public function getErrors(): array
 867    {
 868        $diff = array_diff_key($this->_fields, $this->_errors);
 869
 870        return $this->_errors + (new Collection($diff))
 871            ->filter(function ($value) {
 872                return is_array($value) || $value instanceof EntityInterface;
 873            })
 874            ->map(function ($value) {
 875                return $this->_readError($value);
 876            })
 877            ->filter()
 878            ->toArray();
 879    }
 880
 881    /**
 882     * Returns validation errors of a field
 883     *
 884     * @param string $field Field name to get the errors from
 885     * @return array
 886     */
 887    public function getError(string $field): array
 888    {
 889        $errors = $this->_errors[$field] ?? [];
 890        if ($errors) {
 891            return $errors;
 892        }
 893
 894        return $this->_nestedErrors($field);
 895    }
 896
 897    /**
 898     * Sets error messages to the entity
 899     *
 900     * ## Example
 901     *
 902     * ```
 903     * // Sets the error messages for multiple fields at once
 904     * $entity->setErrors(['salary' => ['message'], 'name' => ['another message']]);
 905     * ```
 906     *
 907     * @param array $errors The array of errors to set.
 908     * @param bool $overwrite Whether or not to overwrite pre-existing errors for $fields
 909     * @return $this
 910     */
 911    public function setErrors(array $errors, bool $overwrite = false)
 912    {
 913        if ($overwrite) {
 914            foreach ($errors as $f => $error) {
 915                $this->_errors[$f] = (array)$error;
 916            }
 917
 918            return $this;
 919        }
 920
 921        foreach ($errors as $f => $error) {
 922            $this->_errors += [$f => []];
 923
 924            // String messages are appended to the list,
 925            // while more complex error structures need their
 926            // keys preserved for nested validator.
 927            if (is_string($error)) {
 928                $this->_errors[$f][] = $error;
 929            } else {
 930                foreach ($error as $k => $v) {
 931                    $this->_errors[$f][$k] = $v;
 932                }
 933            }
 934        }
 935
 936        return $this;
 937    }
 938
 939    /**
 940     * Sets errors for a single field
 941     *
 942     * ### Example
 943     *
 944     * ```
 945     * // Sets the error messages for a single field
 946     * $entity->setError('salary', ['must be numeric', 'must be a positive number']);
 947     * ```
 948     *
 949     * @param string $field The field to get errors for, or the array of errors to set.
 950     * @param string|array $errors The errors to be set for $field
 951     * @param bool $overwrite Whether or not to overwrite pre-existing errors for $field
 952     * @return $this
 953     */
 954    public function setError(string $field, $errors, bool $overwrite = false)
 955    {
 956        if (is_string($errors)) {
 957            $errors = [$errors];
 958        }
 959
 960        return $this->setErrors([$field => $errors], $overwrite);
 961    }
 962
 963    /**
 964     * Auxiliary method for getting errors in nested entities
 965     *
 966     * @param string $field the field in this entity to check for errors
 967     * @return array errors in nested entity if any
 968     */
 969    protected function _nestedErrors(string $field): array
 970    {
 971        // Only one path element, check for nested entity with error.
 972        if (strpos($field, '.') === false) {
 973            return $this->_readError($this->get($field));
 974        }
 975        // Try reading the errors data with field as a simple path
 976        $error = Hash::get($this->_errors, $field);
 977        if ($error !== null) {
 978            return $error;
 979        }
 980        $path = explode('.', $field);
 981
 982        // Traverse down the related entities/arrays for
 983        // the relevant entity.
 984        $entity = $this;
 985        $len = count($path);
 986        while ($len) {
 987            $part = array_shift($path);
 988            $len = count($path);
 989            $val = null;
 990            if ($entity instanceof EntityInterface) {
 991                $val = $entity->get($part);
 992            } elseif (is_array($entity)) {
 993                $val = $entity[$part] ?? false;
 994            }
 995
 996            if (
 997                is_array($val) ||
 998                $val instanceof Traversable ||
 999                $val instanceof EntityInterface
1000            ) {
1001                $entity = $val;
1002            } else {
1003                $path[] = $part;
1004                break;
1005            }
1006        }
1007        if (count($path) <= 1) {
1008            return $this->_readError($entity, array_pop($path));
1009        }
1010
1011        return [];
1012    }
1013
1014    /**
1015     * Reads if there are errors for one or many objects.
1016     *
1017     * @param array|\Cake\Datasource\EntityInterface $object The object to read errors from.
1018     * @return bool
1019     */
1020    protected function _readHasErrors($object): bool
1021    {
1022        if ($object instanceof EntityInterface && $object->hasErrors()) {
1023            return true;
1024        }
1025
1026        if (is_array($object)) {
1027            foreach ($object as $value) {
1028                if ($this->_readHasErrors($value)) {
1029                    return true;
1030                }
1031            }
1032        }
1033
1034        return false;
1035    }
1036
1037    /**
1038     * Read the error(s) from one or many objects.
1039     *
1040     * @param iterable|\Cake\Datasource\EntityInterface $object The object to read errors from.
1041     * @param string|null $path The field name for errors.
1042     * @return array
1043     */
1044    protected function _readError($object, $path = null): array
1045    {
1046        if ($path !== null && $object instanceof EntityInterface) {
1047            return $object->getError($path);
1048        }
1049        if ($object instanceof EntityInterface) {
1050            return $object->getErrors();
1051        }
1052        if (is_iterable($object)) {
1053            $array = array_map(function ($val) {
1054                if ($val instanceof EntityInterface) {
1055                    return $val->getErrors();
1056                }
1057            }, (array)$object);
1058
1059            return array_filter($array);
1060        }
1061
1062        return [];
1063    }
1064
1065    /**
1066     * Get a list of invalid fields and their data for errors upon validation/patching
1067     *
1068     * @return array
1069     */
1070    public function getInvalid(): array
1071    {
1072        return $this->_invalid;
1073    }
1074
1075    /**
1076     * Get a single value of an invalid field. Returns null if not set.
1077     *
1078     * @param string $field The name of the field.
1079     * @return mixed|null
1080     */
1081    public function getInvalidField(string $field)
1082    {
1083        return $this->_invalid[$field] ?? null;
1084    }
1085
1086    /**
1087     * Set fields as invalid and not patchable into the entity.
1088     *
1089     * This is useful for batch operations when one needs to get the original value for an error message after patching.
1090     * This value could not be patched into the entity and is simply copied into the _invalid property for debugging
1091     * purposes or to be able to log it away.
1092     *
1093     * @param array $fields The values to set.
1094     * @param bool $overwrite Whether or not to overwrite pre-existing values for $field.
1095     * @return $this
1096     */
1097    public function setInvalid(array $fields, bool $overwrite = false)
1098    {
1099        foreach ($fields as $field => $value) {
1100            if ($overwrite === true) {
1101                $this->_invalid[$field] = $value;
1102                continue;
1103            }
1104            $this->_invalid += [$field => $value];
1105        }
1106
1107        return $this;
1108    }
1109
1110    /**
1111     * Sets a field as invalid and not patchable into the entity.
1112     *
1113     * @param string $field The value to set.
1114     * @param mixed $value The invalid value to be set for $field.
1115     * @return $this
1116     */
1117    public function setInvalidField(string $field, $value)
1118    {
1119        $this->_invalid[$field] = $value;
1120
1121        return $this;
1122    }
1123
1124    /**
1125     * Stores whether or not a field value can be changed or set in this entity.
1126     * The special field `*` can also be marked as accessible or protected, meaning
1127     * that any other field specified before will take its value. For example
1128     * `$entity->setAccess('*', true)` means that any field not specified already
1129     * will be accessible by default.
1130     *
1131     * You can also call this method with an array of fields, in which case they
1132     * will each take the accessibility value specified in the second argument.
1133     *
1134     * ### Example:
1135     *
1136     * ```
1137     * $entity->setAccess('id', true); // Mark id as not protected
1138     * $entity->setAccess('author_id', false); // Mark author_id as protected
1139     * $entity->setAccess(['id', 'user_id'], true); // Mark both fields as accessible
1140     * $entity->setAccess('*', false); // Mark all fields as protected
1141     * ```
1142     *
1143     * @param string|array $field Single or list of fields to change its accessibility
1144     * @param bool $set True marks the field as accessible, false will
1145     * mark it as protected.
1146     * @return $this
1147     */
1148    public function setAccess($field, bool $set)
1149    {
1150        if ($field === '*') {
1151            $this->_accessible = array_map(function ($p) use ($set) {
1152                return (bool)$set;
1153            }, $this->_accessible);
1154            $this->_accessible['*'] = (bool)$set;
1155
1156            return $this;
1157        }
1158
1159        foreach ((array)$field as $prop) {
1160            $this->_accessible[$prop] = (bool)$set;
1161        }
1162
1163        return $this;
1164    }
1165
1166    /**
1167     * Checks if a field is accessible
1168     *
1169     * ### Example:
1170     *
1171     * ```
1172     * $entity->isAccessible('id'); // Returns whether it can be set or not
1173     * ```
1174     *
1175     * @param string $field Field name to check
1176     * @return bool
1177     */
1178    public function isAccessible(string $field): bool
1179    {
1180        $value = $this->_accessible[$field] ??
1181            null;
1182
1183        return ($value === null && !empty($this->_accessible['*'])) || $value;
1184    }
1185
1186    /**
1187     * Returns the alias of the repository from which this entity came from.
1188     *
1189     * @return string
1190     */
1191    public function getSource(): string
1192    {
1193        return $this->_registryAlias;
1194    }
1195
1196    /**
1197     * Sets the source alias
1198     *
1199     * @param string $alias the alias of the repository
1200     * @return $this
1201     */
1202    public function setSource(string $alias)
1203    {
1204        $this->_registryAlias = $alias;
1205
1206        return $this;
1207    }
1208
1209    /**
1210     * Returns a string representation of this object in a human readable format.
1211     *
1212     * @return string
1213     */
1214    public function __toString(): string
1215    {
1216        return (string)json_encode($this, JSON_PRETTY_PRINT);
1217    }
1218
1219    /**
1220     * Returns an array that can be used to describe the internal state of this
1221     * object.
1222     *
1223     * @return array
1224     */
1225    public function __debugInfo(): array
1226    {
1227        $fields = $this->_fields;
1228        foreach ($this->_virtual as $field) {
1229            $fields[$field] = $this->$field;
1230        }
1231
1232        return $fields + [
1233            '[new]' => $this->isNew(),
1234            '[accessible]' => $this->_accessible,
1235            '[dirty]' => $this->_dirty,
1236            '[original]' => $this->_original,
1237            '[virtual]' => $this->_virtual,
1238            '[hasErrors]' => $this->hasErrors(),
1239            '[errors]' => $this->_errors,
1240            '[invalid]' => $this->_invalid,
1241            '[repository]' => $this->_registryAlias,
1242        ];
1243    }
1244}