<?php
declare(strict_types=1);

/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link          https://cakephp.org CakePHP(tm) Project
 * @since         3.0.0
 * @license       https://opensource.org/licenses/mit-license.php MIT License
 */
namespace Cake\ORM;

use ArrayObject;
use BadMethodCallException;
use Cake\Core\App;
use Cake\Core\Configure;
use Cake\Database\Connection;
use Cake\Database\Schema\TableSchemaInterface;
use Cake\Database\TypeFactory;
use Cake\Datasource\ConnectionManager;
use Cake\Datasource\EntityInterface;
use Cake\Datasource\Exception\InvalidPrimaryKeyException;
use Cake\Datasource\RepositoryInterface;
use Cake\Datasource\RulesAwareTrait;
use Cake\Event\EventDispatcherInterface;
use Cake\Event\EventDispatcherTrait;
use Cake\Event\EventListenerInterface;
use Cake\Event\EventManager;
use Cake\ORM\Association\BelongsTo;
use Cake\ORM\Association\BelongsToMany;
use Cake\ORM\Association\HasMany;
use Cake\ORM\Association\HasOne;
use Cake\ORM\Exception\MissingEntityException;
use Cake\ORM\Exception\PersistenceFailedException;
use Cake\ORM\Exception\RolledbackTransactionException;
use Cake\ORM\Rule\IsUnique;
use Cake\Utility\Inflector;
use Cake\Validation\ValidatorAwareInterface;
use Cake\Validation\ValidatorAwareTrait;
use Exception;
use InvalidArgumentException;
use RuntimeException;

/**
 * Represents a single database table.
 *
 * Exposes methods for retrieving data out of it, and manages the associations
 * this table has to other tables. Multiple instances of this class can be created
 * for the same database table with different aliases, this allows you to address
 * your database structure in a richer and more expressive way.
 *
 * ### Retrieving data
 *
 * The primary way to retrieve data is using Table::find(). See that method
 * for more information.
 *
 * ### Dynamic finders
 *
 * In addition to the standard find($type) finder methods, CakePHP provides dynamic
 * finder methods. These methods allow you to easily set basic conditions up. For example
 * to filter users by username you would call
 *
 * ```
 * $query = $users->findByUsername('mark');
 * ```
 *
 * You can also combine conditions on multiple fields using either `Or` or `And`:
 *
 * ```
 * $query = $users->findByUsernameOrEmail('mark', 'mark@example.org');
 * ```
 *
 * ### Bulk updates/deletes
 *
 * You can use Table::updateAll() and Table::deleteAll() to do bulk updates/deletes.
 * You should be aware that events will *not* be fired for bulk updates/deletes.
 *
 * ### Events
 *
 * Table objects emit several events during as life-cycle hooks during find, delete and save
 * operations. All events use the CakePHP event package:
 *
 * - `Model.beforeFind` Fired before each find operation. By stopping the event and
 *   supplying a return value you can bypass the find operation entirely. Any
 *   changes done to the $query instance will be retained for the rest of the find. The
 *   `$primary` parameter indicates whether this is the root query, or an
 *   associated query.
 *
 * - `Model.buildValidator` Allows listeners to modify validation rules
 *   for the provided named validator.
 *
 * - `Model.buildRules` Allows listeners to modify the rules checker by adding more rules.
 *
 * - `Model.beforeRules` Fired before an entity is validated using the rules checker.
 *   By stopping this event, you can return the final value of the rules checking operation.
 *
 * - `Model.afterRules` Fired after the rules have been checked on the entity. By
 *   stopping this event, you can return the final value of the rules checking operation.
 *
 * - `Model.beforeSave` Fired before each entity is saved. Stopping this event will
 *   abort the save operation. When the event is stopped the result of the event will be returned.
 *
 * - `Model.afterSave` Fired after an entity is saved.
 *
 * - `Model.afterSaveCommit` Fired after the transaction in which the save operation is
 *   wrapped has been committed. It’s also triggered for non atomic saves where database
 *   operations are implicitly committed. The event is triggered only for the primary
 *   table on which save() is directly called. The event is not triggered if a
 *   transaction is started before calling save.
 *
 * - `Model.beforeDelete` Fired before an entity is deleted. By stopping this
 *   event you will abort the delete operation.
 *
 * - `Model.afterDelete` Fired after an entity has been deleted.
 *
 * ### Callbacks
 *
 * You can subscribe to the events listed above in your table classes by implementing the
 * lifecycle methods below:
 *
 * - `beforeFind(EventInterface $event, Query $query, ArrayObject $options, boolean $primary)`
 * - `beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)`
 * - `afterMarshal(EventInterface $event, EntityInterface $entity, ArrayObject $options)`
 * - `buildValidator(EventInterface $event, Validator $validator, string $name)`
 * - `buildRules(RulesChecker $rules)`
 * - `beforeRules(EventInterface $event, EntityInterface $entity, ArrayObject $options, string $operation)`
 * - `afterRules(EventInterface $event, EntityInterface $entity, ArrayObject $options, bool $result, string $operation)`
 * - `beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)`
 * - `afterSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)`
 * - `afterSaveCommit(EventInterface $event, EntityInterface $entity, ArrayObject $options)`
 * - `beforeDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options)`
 * - `afterDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options)`
 * - `afterDeleteCommit(EventInterface $event, EntityInterface $entity, ArrayObject $options)`
 *
 * @see \Cake\Event\EventManager for reference on the events system.
 * @link https://book.cakephp.org/4/en/orm/table-objects.html#event-list
 */
class Table implements RepositoryInterface, EventListenerInterface, EventDispatcherInterface, ValidatorAwareInterface
{
    use EventDispatcherTrait;
    use RulesAwareTrait;
    use ValidatorAwareTrait;

    /**
     * Name of default validation set.
     *
     * @var string
     */
    public const DEFAULT_VALIDATOR = 'default';

    /**
     * The alias this object is assigned to validators as.
     *
     * @var string
     */
    public const VALIDATOR_PROVIDER_NAME = 'table';

    /**
     * The name of the event dispatched when a validator has been built.
     *
     * @var string
     */
    public const BUILD_VALIDATOR_EVENT = 'Model.buildValidator';

    /**
     * The rules class name that is used.
     *
     * @var string
     */
    public const RULES_CLASS = RulesChecker::class;

    /**
     * The IsUnique class name that is used.
     *
     * @var string
     */
    public const IS_UNIQUE_CLASS = IsUnique::class;

    /**
     * Name of the table as it can be found in the database
     *
     * @var string|null
     */
    protected $_table;

    /**
     * Human name giving to this particular instance. Multiple objects representing
     * the same database table can exist by using different aliases.
     *
     * @var string|null
     */
    protected $_alias;

    /**
     * Connection instance
     *
     * @var \Cake\Database\Connection|null
     */
    protected $_connection;

    /**
     * The schema object containing a description of this table fields
     *
     * @var \Cake\Database\Schema\TableSchemaInterface|null
     */
    protected $_schema;

    /**
     * The name of the field that represents the primary key in the table
     *
     * @var array<string>|string|null
     */
    protected $_primaryKey;

    /**
     * The name of the field that represents a human-readable representation of a row
     *
     * @var array<string>|string|null
     */
    protected $_displayField;

    /**
     * The associations container for this Table.
     *
     * @var \Cake\ORM\AssociationCollection
     */
    protected $_associations;

    /**
     * BehaviorRegistry for this table
     *
     * @var \Cake\ORM\BehaviorRegistry
     */
    protected $_behaviors;

    /**
     * The name of the class that represent a single row for this table
     *
     * @var string
     * @psalm-var class-string<\Cake\Datasource\EntityInterface>
     */
    protected $_entityClass;

    /**
     * Registry key used to create this table object
     *
     * @var string|null
     */
    protected $_registryAlias;

    /**
     * Initializes a new instance
     *
     * The $config array understands the following keys:
     *
     * - table: Name of the database table to represent
     * - alias: Alias to be assigned to this table (default to table name)
     * - connection: The connection instance to use
     * - entityClass: The fully namespaced class name of the entity class that will
     *   represent rows in this table.
     * - schema: A \Cake\Database\Schema\TableSchemaInterface object or an array that can be
     *   passed to it.
     * - eventManager: An instance of an event manager to use for internal events
     * - behaviors: A BehaviorRegistry. Generally not used outside of tests.
     * - associations: An AssociationCollection instance.
     * - validator: A Validator instance which is assigned as the "default"
     *   validation set, or an associative array, where key is the name of the
     *   validation set and value the Validator instance.
     *
     * @param array<string, mixed> $config List of options for this table
     */
    public function __construct(array $config = [])
    {
        if (!empty($config['registryAlias'])) {
            $this->setRegistryAlias($config['registryAlias']);
        }
        if (!empty($config['table'])) {
            $this->setTable($config['table']);
        }
        if (!empty($config['alias'])) {
            $this->setAlias($config['alias']);
        }
        if (!empty($config['connection'])) {
            $this->setConnection($config['connection']);
        }
        if (!empty($config['schema'])) {
            $this->setSchema($config['schema']);
        }
        if (!empty($config['entityClass'])) {
            $this->setEntityClass($config['entityClass']);
        }
        $eventManager = $behaviors = $associations = null;
        if (!empty($config['eventManager'])) {
            $eventManager = $config['eventManager'];
        }
        if (!empty($config['behaviors'])) {
            $behaviors = $config['behaviors'];
        }
        if (!empty($config['associations'])) {
            $associations = $config['associations'];
        }
        if (!empty($config['validator'])) {
            if (!is_array($config['validator'])) {
                $this->setValidator(static::DEFAULT_VALIDATOR, $config['validator']);
            } else {
                foreach ($config['validator'] as $name => $validator) {
                    $this->setValidator($name, $validator);
                }
            }
        }
        $this->_eventManager = $eventManager ?: new EventManager();
        $this->_behaviors = $behaviors ?: new BehaviorRegistry();
        $this->_behaviors->setTable($this);
        $this->_associations = $associations ?: new AssociationCollection();

        $this->initialize($config);
        $this->_eventManager->on($this);
        $this->dispatchEvent('Model.initialize');
    }

    /**
     * Get the default connection name.
     *
     * This method is used to get the fallback connection name if an
     * instance is created through the TableLocator without a connection.
     *
     * @return string
     * @see \Cake\ORM\Locator\TableLocator::get()
     */
    public static function defaultConnectionName(): string
    {
        return 'default';
    }

    /**
     * Initialize a table instance. Called after the constructor.
     *
     * You can use this method to define associations, attach behaviors
     * define validation and do any other initialization logic you need.
     *
     * ```
     *  public function initialize(array $config)
     *  {
     *      $this->belongsTo('Users');
     *      $this->belongsToMany('Tagging.Tags');
     *      $this->setPrimaryKey('something_else');
     *  }
     * ```
     *
     * @param array<string, mixed> $config Configuration options passed to the constructor
     * @return void
     */
    public function initialize(array $config): void
    {
    }

    /**
     * Sets the database table name.
     *
     * This can include the database schema name in the form 'schema.table'.
     * If the name must be quoted, enable automatic identifier quoting.
     *
     * @param string $table Table name.
     * @return $this
     */
    public function setTable(string $table)
    {
        $this->_table = $table;

        return $this;
    }

    /**
     * Returns the database table name.
     *
     * This can include the database schema name if set using `setTable()`.
     *
     * @return string
     */
    public function getTable(): string
    {
        if ($this->_table === null) {
            $table = namespaceSplit(static::class);
            $table = substr(end($table), 0, -5);
            if (!$table) {
                $table = $this->getAlias();
            }
            $this->_table = Inflector::underscore($table);
        }

        return $this->_table;
    }

    /**
     * Sets the table alias.
     *
     * @param string $alias Table alias
     * @return $this
     */
    public function setAlias(string $alias)
    {
        $this->_alias = $alias;

        return $this;
    }

    /**
     * Returns the table alias.
     *
     * @return string
     */
    public function getAlias(): string
    {
        if ($this->_alias === null) {
            $alias = namespaceSplit(static::class);
            $alias = substr(end($alias), 0, -5) ?: $this->getTable();
            $this->_alias = $alias;
        }

        return $this->_alias;
    }

    /**
     * Alias a field with the table's current alias.
     *
     * If field is already aliased it will result in no-op.
     *
     * @param string $field The field to alias.
     * @return string The field prefixed with the table alias.
     */
    public function aliasField(string $field): string
    {
        if (strpos($field, '.') !== false) {
            return $field;
        }

        return $this->getAlias() . '.' . $field;
    }

    /**
     * Sets the table registry key used to create this table instance.
     *
     * @param string $registryAlias The key used to access this object.
     * @return $this
     */
    public function setRegistryAlias(string $registryAlias)
    {
        $this->_registryAlias = $registryAlias;

        return $this;
    }

    /**
     * Returns the table registry key used to create this table instance.
     *
     * @return string
     */
    public function getRegistryAlias(): string
    {
        if ($this->_registryAlias === null) {
            $this->_registryAlias = $this->getAlias();
        }

        return $this->_registryAlias;
    }

    /**
     * Sets the connection instance.
     *
     * @param \Cake\Database\Connection $connection The connection instance
     * @return $this
     */
    public function setConnection(Connection $connection)
    {
        $this->_connection = $connection;

        return $this;
    }

    /**
     * Returns the connection instance.
     *
     * @return \Cake\Database\Connection
     */
    public function getConnection(): Connection
    {
        if (!$this->_connection) {
            /** @var \Cake\Database\Connection $connection */
            $connection = ConnectionManager::get(static::defaultConnectionName());
            $this->_connection = $connection;
        }

        return $this->_connection;
    }

    /**
     * Returns the schema table object describing this table's properties.
     *
     * @return \Cake\Database\Schema\TableSchemaInterface
     */
    public function getSchema(): TableSchemaInterface
    {
        if ($this->_schema === null) {
            $this->_schema = $this->_initializeSchema(
                $this->getConnection()
                    ->getSchemaCollection()
                    ->describe($this->getTable())
            );
            if (Configure::read('debug')) {
                $this->checkAliasLengths();
            }
        }

        return $this->_schema;
    }

    /**
     * Sets the schema table object describing this table's properties.
     *
     * If an array is passed, a new TableSchemaInterface will be constructed
     * out of it and used as the schema for this table.
     *
     * @param \Cake\Database\Schema\TableSchemaInterface|array $schema Schema to be used for this table
     * @return $this
     */
    public function setSchema($schema)
    {
        if (is_array($schema)) {
            $constraints = [];

            if (isset($schema['_constraints'])) {
                $constraints = $schema['_constraints'];
                unset($schema['_constraints']);
            }

            $schema = $this->getConnection()->getDriver()->newTableSchema($this->getTable(), $schema);

            foreach ($constraints as $name => $value) {
                $schema->addConstraint($name, $value);
            }
        }

        $this->_schema = $schema;
        if (Configure::read('debug')) {
            $this->checkAliasLengths();
        }

        return $this;
    }

    /**
     * Checks if all table name + column name combinations used for
     * queries fit into the max length allowed by database driver.
     *
     * @return void
     * @throws \RuntimeException When an alias combination is too long
     */
    protected function checkAliasLengths(): void
    {
        if ($this->_schema === null) {
            throw new RuntimeException("Unable to check max alias lengths for  `{$this->getAlias()}` without schema.");
        }

        $maxLength = null;
        if (method_exists($this->getConnection()->getDriver(), 'getMaxAliasLength')) {
            $maxLength = $this->getConnection()->getDriver()->getMaxAliasLength();
        }
        if ($maxLength === null) {
            return;
        }

        $table = $this->getAlias();
        foreach ($this->_schema->columns() as $name) {
            if (strlen($table . '__' . $name) > $maxLength) {
                $nameLength = $maxLength - 2;
                throw new RuntimeException(
                    'ORM queries generate field aliases using the table name/alias and column name. ' .
                    "The table alias `{$table}` and column `{$name}` create an alias longer than ({$nameLength}). " .
                    'You must change the table schema in the database and shorten either the table or column ' .
                    'identifier so they fit within the database alias limits.'
                );
            }
        }
    }

    /**
     * Override this function in order to alter the schema used by this table.
     * This function is only called after fetching the schema out of the database.
     * If you wish to provide your own schema to this table without touching the
     * database, you can override schema() or inject the definitions though that
     * method.
     *
     * ### Example:
     *
     * ```
     * protected function _initializeSchema(\Cake\Database\Schema\TableSchemaInterface $schema) {
     *  $schema->setColumnType('preferences', 'json');
     *  return $schema;
     * }
     * ```
     *
     * @param \Cake\Database\Schema\TableSchemaInterface $schema The table definition fetched from database.
     * @return \Cake\Database\Schema\TableSchemaInterface the altered schema
     */
    protected function _initializeSchema(TableSchemaInterface $schema): TableSchemaInterface
    {
        return $schema;
    }

    /**
     * Test to see if a Table has a specific field/column.
     *
     * Delegates to the schema object and checks for column presence
     * using the Schema\Table instance.
     *
     * @param string $field The field to check for.
     * @return bool True if the field exists, false if it does not.
     */
    public function hasField(string $field): bool
    {
        $schema = $this->getSchema();

        return $schema->getColumn($field) !== null;
    }

    /**
     * Sets the primary key field name.
     *
     * @param array<string>|string $key Sets a new name to be used as primary key
     * @return $this
     */
    public function setPrimaryKey($key)
    {
        $this->_primaryKey = $key;

        return $this;
    }

    /**
     * Returns the primary key field name.
     *
     * @return array<string>|string
     */
    public function getPrimaryKey()
    {
        if ($this->_primaryKey === null) {
            $key = $this->getSchema()->getPrimaryKey();
            if (count($key) === 1) {
                $key = $key[0];
            }
            $this->_primaryKey = $key;
        }

        return $this->_primaryKey;
    }

    /**
     * Sets the display field.
     *
     * @param array<string>|string $field Name to be used as display field.
     * @return $this
     */
    public function setDisplayField($field)
    {
        $this->_displayField = $field;

        return $this;
    }

    /**
     * Returns the display field.
     *
     * @return array<string>|string|null
     */
    public function getDisplayField()
    {
        if ($this->_displayField === null) {
            $schema = $this->getSchema();
            $this->_displayField = $this->getPrimaryKey();
            foreach (['title', 'name', 'label'] as $field) {
                if ($schema->hasColumn($field)) {
                    $this->_displayField = $field;
                    break;
                }
            }
        }

        return $this->_displayField;
    }

    /**
     * Returns the class used to hydrate rows for this table.
     *
     * @return string
     * @psalm-return class-string<\Cake\Datasource\EntityInterface>
     */
    public function getEntityClass(): string
    {
        if (!$this->_entityClass) {
            $default = Entity::class;
            $self = static::class;
            $parts = explode('\\', $self);

            if ($self === self::class || count($parts) < 3) {
                return $this->_entityClass = $default;
            }

            $alias = Inflector::classify(Inflector::underscore(substr(array_pop($parts), 0, -5)));
            $name = implode('\\', array_slice($parts, 0, -1)) . '\\Entity\\' . $alias;
            if (!class_exists($name)) {
                return $this->_entityClass = $default;
            }

            /** @var class-string<\Cake\Datasource\EntityInterface>|null $class */
            $class = App::className($name, 'Model/Entity');
            if (!$class) {
                throw new MissingEntityException([$name]);
            }

            $this->_entityClass = $class;
        }

        return $this->_entityClass;
    }

    /**
     * Sets the class used to hydrate rows for this table.
     *
     * @param string $name The name of the class to use
     * @throws \Cake\ORM\Exception\MissingEntityException when the entity class cannot be found
     * @return $this
     */
    public function setEntityClass(string $name)
    {
        /** @psalm-var class-string<\Cake\Datasource\EntityInterface>|null */
        $class = App::className($name, 'Model/Entity');
        if ($class === null) {
            throw new MissingEntityException([$name]);
        }

        $this->_entityClass = $class;

        return $this;
    }

    /**
     * Add a behavior.
     *
     * Adds a behavior to this table's behavior collection. Behaviors
     * provide an easy way to create horizontally re-usable features
     * that can provide trait like functionality, and allow for events
     * to be listened to.
     *
     * Example:
     *
     * Load a behavior, with some settings.
     *
     * ```
     * $this->addBehavior('Tree', ['parent' => 'parentId']);
     * ```
     *
     * Behaviors are generally loaded during Table::initialize().
     *
     * @param string $name The name of the behavior. Can be a short class reference.
     * @param array<string, mixed> $options The options for the behavior to use.
     * @return $this
     * @throws \RuntimeException If a behavior is being reloaded.
     * @see \Cake\ORM\Behavior
     */
    public function addBehavior(string $name, array $options = [])
    {
        $this->_behaviors->load($name, $options);

        return $this;
    }

    /**
     * Adds an array of behaviors to the table's behavior collection.
     *
     * Example:
     *
     * ```
     * $this->addBehaviors([
     *      'Timestamp',
     *      'Tree' => ['level' => 'level'],
     * ]);
     * ```
     *
     * @param array $behaviors All the behaviors to load.
     * @return $this
     * @throws \RuntimeException If a behavior is being reloaded.
     */
    public function addBehaviors(array $behaviors)
    {
        foreach ($behaviors as $name => $options) {
            if (is_int($name)) {
                $name = $options;
                $options = [];
            }

            $this->addBehavior($name, $options);
        }

        return $this;
    }

    /**
     * Removes a behavior from this table's behavior registry.
     *
     * Example:
     *
     * Remove a behavior from this table.
     *
     * ```
     * $this->removeBehavior('Tree');
     * ```
     *
     * @param string $name The alias that the behavior was added with.
     * @return $this
     * @see \Cake\ORM\Behavior
     */
    public function removeBehavior(string $name)
    {
        $this->_behaviors->unload($name);

        return $this;
    }

    /**
     * Returns the behavior registry for this table.
     *
     * @return \Cake\ORM\BehaviorRegistry The BehaviorRegistry instance.
     */
    public function behaviors(): BehaviorRegistry
    {
        return $this->_behaviors;
    }

    /**
     * Get a behavior from the registry.
     *
     * @param string $name The behavior alias to get from the registry.
     * @return \Cake\ORM\Behavior
     * @throws \InvalidArgumentException If the behavior does not exist.
     */
    public function getBehavior(string $name): Behavior
    {
        if (!$this->_behaviors->has($name)) {
            throw new InvalidArgumentException(sprintf(
                'The %s behavior is not defined on %s.',
                $name,
                static::class
            ));
        }

        $behavior = $this->_behaviors->get($name);

        return $behavior;
    }

    /**
     * Check if a behavior with the given alias has been loaded.
     *
     * @param string $name The behavior alias to check.
     * @return bool Whether the behavior exists.
     */
    public function hasBehavior(string $name): bool
    {
        return $this->_behaviors->has($name);
    }

    /**
     * Returns an association object configured for the specified alias.
     *
     * The name argument also supports dot syntax to access deeper associations.
     *
     * ```
     * $users = $this->getAssociation('Articles.Comments.Users');
     * ```
     *
     * Note that this method requires the association to be present or otherwise
     * throws an exception.
     * If you are not sure, use hasAssociation() before calling this method.
     *
     * @param string $name The alias used for the association.
     * @return \Cake\ORM\Association The association.
     * @throws \InvalidArgumentException
     */
    public function getAssociation(string $name): Association
    {
        $association = $this->findAssociation($name);
        if (!$association) {
            $assocations = $this->associations()->keys();

            $message = "The `{$name}` association is not defined on `{$this->getAlias()}`.";
            if ($assocations) {
                $message .= "\nValid associations are: " . implode(', ', $assocations);
            }
            throw new InvalidArgumentException($message);
        }

        return $association;
    }

    /**
     * Checks whether a specific association exists on this Table instance.
     *
     * The name argument also supports dot syntax to access deeper associations.
     *
     * ```
     * $hasUsers = $this->hasAssociation('Articles.Comments.Users');
     * ```
     *
     * @param string $name The alias used for the association.
     * @return bool
     */
    public function hasAssociation(string $name): bool
    {
        return $this->findAssociation($name) !== null;
    }

    /**
     * Returns an association object configured for the specified alias if any.
     *
     * The name argument also supports dot syntax to access deeper associations.
     *
     * ```
     * $users = $this->getAssociation('Articles.Comments.Users');
     * ```
     *
     * @param string $name The alias used for the association.
     * @return \Cake\ORM\Association|null Either the association or null.
     */
    protected function findAssociation(string $name): ?Association
    {
        if (strpos($name, '.') === false) {
            return $this->_associations->get($name);
        }

        $result = null;
        [$name, $next] = array_pad(explode('.', $name, 2), 2, null);
        if ($name !== null) {
            $result = $this->_associations->get($name);
        }

        if ($result !== null && $next !== null) {
            $result = $result->getTarget()->getAssociation($next);
        }

        return $result;
    }

    /**
     * Get the associations collection for this table.
     *
     * @return \Cake\ORM\AssociationCollection The collection of association objects.
     */
    public function associations(): AssociationCollection
    {
        return $this->_associations;
    }

    /**
     * Setup multiple associations.
     *
     * It takes an array containing set of table names indexed by association type
     * as argument:
     *
     * ```
     * $this->Posts->addAssociations([
     *   'belongsTo' => [
     *     'Users' => ['className' => 'App\Model\Table\UsersTable']
     *   ],
     *   'hasMany' => ['Comments'],
     *   'belongsToMany' => ['Tags']
     * ]);
     * ```
     *
     * Each association type accepts multiple associations where the keys
     * are the aliases, and the values are association config data. If numeric
     * keys are used the values will be treated as association aliases.
     *
     * @param array $params Set of associations to bind (indexed by association type)
     * @return $this
     * @see \Cake\ORM\Table::belongsTo()
     * @see \Cake\ORM\Table::hasOne()
     * @see \Cake\ORM\Table::hasMany()
     * @see \Cake\ORM\Table::belongsToMany()
     */
    public function addAssociations(array $params)
    {
        foreach ($params as $assocType => $tables) {
            foreach ($tables as $associated => $options) {
                if (is_numeric($associated)) {
                    $associated = $options;
                    $options = [];
                }
                $this->{$assocType}($associated, $options);
            }
        }

        return $this;
    }

    /**
     * Creates a new BelongsTo association between this table and a target
     * table. A "belongs to" association is a N-1 relationship where this table
     * is the N side, and where there is a single associated record in the target
     * table for each one in this table.
     *
     * Target table can be inferred by its name, which is provided in the
     * first argument, or you can either pass the to be instantiated or
     * an instance of it directly.
     *
     * The options array accept the following keys:
     *
     * - className: The class name of the target table object
     * - targetTable: An instance of a table object to be used as the target table
     * - foreignKey: The name of the field to use as foreign key, if false none
     *   will be used
     * - conditions: array with a list of conditions to filter the join with
     * - joinType: The type of join to be used (e.g. INNER)
     * - strategy: The loading strategy to use. 'join' and 'select' are supported.
     * - finder: The finder method to use when loading records from this association.
     *   Defaults to 'all'. When the strategy is 'join', only the fields, containments,
     *   and where conditions will be used from the finder.
     *
     * This method will return the association object that was built.
     *
     * @param string $associated the alias for the target table. This is used to
     * uniquely identify the association
     * @param array<string, mixed> $options list of options to configure the association definition
     * @return \Cake\ORM\Association\BelongsTo
     */
    public function belongsTo(string $associated, array $options = []): BelongsTo
    {
        $options += ['sourceTable' => $this];

        /** @var \Cake\ORM\Association\BelongsTo $association */
        $association = $this->_associations->load(BelongsTo::class, $associated, $options);

        return $association;
    }

    /**
     * Creates a new HasOne association between this table and a target
     * table. A "has one" association is a 1-1 relationship.
     *
     * Target table can be inferred by its name, which is provided in the
     * first argument, or you can either pass the class name to be instantiated or
     * an instance of it directly.
     *
     * The options array accept the following keys:
     *
     * - className: The class name of the target table object
     * - targetTable: An instance of a table object to be used as the target table
     * - foreignKey: The name of the field to use as foreign key, if false none
     *   will be used
     * - dependent: Set to true if you want CakePHP to cascade deletes to the
     *   associated table when an entity is removed on this table. The delete operation
     *   on the associated table will not cascade further. To get recursive cascades enable
     *   `cascadeCallbacks` as well. Set to false if you don't want CakePHP to remove
     *   associated data, or when you are using database constraints.
     * - cascadeCallbacks: Set to true if you want CakePHP to fire callbacks on
     *   cascaded deletes. If false the ORM will use deleteAll() to remove data.
     *   When true records will be loaded and then deleted.
     * - conditions: array with a list of conditions to filter the join with
     * - joinType: The type of join to be used (e.g. LEFT)
     * - strategy: The loading strategy to use. 'join' and 'select' are supported.
     * - finder: The finder method to use when loading records from this association.
     *   Defaults to 'all'. When the strategy is 'join', only the fields, containments,
     *   and where conditions will be used from the finder.
     *
     * This method will return the association object that was built.
     *
     * @param string $associated the alias for the target table. This is used to
     * uniquely identify the association
     * @param array<string, mixed> $options list of options to configure the association definition
     * @return \Cake\ORM\Association\HasOne
     */
    public function hasOne(string $associated, array $options = []): HasOne
    {
        $options += ['sourceTable' => $this];

        /** @var \Cake\ORM\Association\HasOne $association */
        $association = $this->_associations->load(HasOne::class, $associated, $options);

        return $association;
    }

    /**
     * Creates a new HasMany association between this table and a target
     * table. A "has many" association is a 1-N relationship.
     *
     * Target table can be inferred by its name, which is provided in the
     * first argument, or you can either pass the class name to be instantiated or
     * an instance of it directly.
     *
     * The options array accept the following keys:
     *
     * - className: The class name of the target table object
     * - targetTable: An instance of a table object to be used as the target table
     * - foreignKey: The name of the field to use as foreign key, if false none
     *   will be used
     * - dependent: Set to true if you want CakePHP to cascade deletes to the
     *   associated table when an entity is removed on this table. The delete operation
     *   on the associated table will not cascade further. To get recursive cascades enable
     *   `cascadeCallbacks` as well. Set to false if you don't want CakePHP to remove
     *   associated data, or when you are using database constraints.
     * - cascadeCallbacks: Set to true if you want CakePHP to fire callbacks on
     *   cascaded deletes. If false the ORM will use deleteAll() to remove data.
     *   When true records will be loaded and then deleted.
     * - conditions: array with a list of conditions to filter the join with
     * - sort: The order in which results for this association should be returned
     * - saveStrategy: Either 'append' or 'replace'. When 'append' the current records
     *   are appended to any records in the database. When 'replace' associated records
     *   not in the current set will be removed. If the foreign key is a null able column
     *   or if `dependent` is true records will be orphaned.
     * - strategy: The strategy to be used for selecting results Either 'select'
     *   or 'subquery'. If subquery is selected the query used to return results
     *   in the source table will be used as conditions for getting rows in the
     *   target table.
     * - finder: The finder method to use when loading records from this association.
     *   Defaults to 'all'.
     *
     * This method will return the association object that was built.
     *
     * @param string $associated the alias for the target table. This is used to
     * uniquely identify the association
     * @param array<string, mixed> $options list of options to configure the association definition
     * @return \Cake\ORM\Association\HasMany
     */
    public function hasMany(string $associated, array $options = []): HasMany
    {
        $options += ['sourceTable' => $this];

        /** @var \Cake\ORM\Association\HasMany $association */
        $association = $this->_associations->load(HasMany::class, $associated, $options);

        return $association;
    }

    /**
     * Creates a new BelongsToMany association between this table and a target
     * table. A "belongs to many" association is a M-N relationship.
     *
     * Target table can be inferred by its name, which is provided in the
     * first argument, or you can either pass the class name to be instantiated or
     * an instance of it directly.
     *
     * The options array accept the following keys:
     *
     * - className: The class name of the target table object.
     * - targetTable: An instance of a table object to be used as the target table.
     * - foreignKey: The name of the field to use as foreign key.
     * - targetForeignKey: The name of the field to use as the target foreign key.
     * - joinTable: The name of the table representing the link between the two
     * - through: If you choose to use an already instantiated link table, set this
     *   key to a configured Table instance containing associations to both the source
     *   and target tables in this association.
     * - dependent: Set to false, if you do not want junction table records removed
     *   when an owning record is removed.
     * - cascadeCallbacks: Set to true if you want CakePHP to fire callbacks on
     *   cascaded deletes. If false the ORM will use deleteAll() to remove data.
     *   When true join/junction table records will be loaded and then deleted.
     * - conditions: array with a list of conditions to filter the join with.
     * - sort: The order in which results for this association should be returned.
     * - strategy: The strategy to be used for selecting results Either 'select'
     *   or 'subquery'. If subquery is selected the query used to return results
     *   in the source table will be used as conditions for getting rows in the
     *   target table.
     * - saveStrategy: Either 'append' or 'replace'. Indicates the mode to be used
     *   for saving associated entities. The former will only create new links
     *   between both side of the relation and the latter will do a wipe and
     *   replace to create the links between the passed entities when saving.
     * - strategy: The loading strategy to use. 'select' and 'subquery' are supported.
     * - finder: The finder method to use when loading records from this association.
     *   Defaults to 'all'.
     *
     * This method will return the association object that was built.
     *
     * @param string $associated the alias for the target table. This is used to
     * uniquely identify the association
     * @param array<string, mixed> $options list of options to configure the association definition
     * @return \Cake\ORM\Association\BelongsToMany
     */
    public function belongsToMany(string $associated, array $options = []): BelongsToMany
    {
        $options += ['sourceTable' => $this];

        /** @var \Cake\ORM\Association\BelongsToMany $association */
        $association = $this->_associations->load(BelongsToMany::class, $associated, $options);

        return $association;
    }

    /**
     * Creates a new Query for this repository and applies some defaults based on the
     * type of search that was selected.
     *
     * ### Model.beforeFind event
     *
     * Each find() will trigger a `Model.beforeFind` event for all attached
     * listeners. Any listener can set a valid result set using $query
     *
     * By default, `$options` will recognize the following keys:
     *
     * - fields
     * - conditions
     * - order
     * - limit
     * - offset
     * - page
     * - group
     * - having
     * - contain
     * - join
     *
     * ### Usage
     *
     * Using the options array:
     *
     * ```
     * $query = $articles->find('all', [
     *   'conditions' => ['published' => 1],
     *   'limit' => 10,
     *   'contain' => ['Users', 'Comments']
     * ]);
     * ```
     *
     * Using the builder interface:
     *
     * ```
     * $query = $articles->find()
     *   ->where(['published' => 1])
     *   ->limit(10)
     *   ->contain(['Users', 'Comments']);
     * ```
     *
     * ### Calling finders
     *
     * The find() method is the entry point for custom finder methods.
     * You can invoke a finder by specifying the type:
     *
     * ```
     * $query = $articles->find('published');
     * ```
     *
     * Would invoke the `findPublished` method.
     *
     * @param string $type the type of query to perform
     * @param array<string, mixed> $options An array that will be passed to Query::applyOptions()
     * @return \Cake\ORM\Query The query builder
     */
    public function find(string $type = 'all', array $options = []): Query
    {
        return $this->callFinder($type, $this->query()->select(), $options);
    }

    /**
     * Returns the query as passed.
     *
     * By default findAll() applies no conditions, you
     * can override this method in subclasses to modify how `find('all')` works.
     *
     * @param \Cake\ORM\Query $query The query to find with
     * @param array<string, mixed> $options The options to use for the find
     * @return \Cake\ORM\Query The query builder
     */
    public function findAll(Query $query, array $options): Query
    {
        return $query;
    }

    /**
     * Sets up a query object so results appear as an indexed array, useful for any
     * place where you would want a list such as for populating input select boxes.
     *
     * When calling this finder, the fields passed are used to determine what should
     * be used as the array key, value and optionally what to group the results by.
     * By default, the primary key for the model is used for the key, and the display
     * field as value.
     *
     * The results of this finder will be in the following form:
     *
     * ```
     * [
     *  1 => 'value for id 1',
     *  2 => 'value for id 2',
     *  4 => 'value for id 4'
     * ]
     * ```
     *
     * You can specify which property will be used as the key and which as value
     * by using the `$options` array, when not specified, it will use the results
     * of calling `primaryKey` and `displayField` respectively in this table:
     *
     * ```
     * $table->find('list', [
     *  'keyField' => 'name',
     *  'valueField' => 'age'
     * ]);
     * ```
     *
     * The `valueField` can also be an array, in which case you can also specify
     * the `valueSeparator` option to control how the values will be concatenated:
     *
     * ```
     * $table->find('list', [
     *  'valueField' => ['first_name', 'last_name'],
     *  'valueSeparator' => ' | ',
     * ]);
     * ```
     *
     * The results of this finder will be in the following form:
     *
     * ```
     * [
     *  1 => 'John | Doe',
     *  2 => 'Steve | Smith'
     * ]
     * ```
     *
     * Results can be put together in bigger groups when they share a property, you
     * can customize the property to use for grouping by setting `groupField`:
     *
     * ```
     * $table->find('list', [
     *  'groupField' => 'category_id',
     * ]);
     * ```
     *
     * When using a `groupField` results will be returned in this format:
     *
     * ```
     * [
     *  'group_1' => [
     *      1 => 'value for id 1',
     *      2 => 'value for id 2',
     *  ]
     *  'group_2' => [
     *      4 => 'value for id 4'
     *  ]
     * ]
     * ```
     *
     * @param \Cake\ORM\Query $query The query to find with
     * @param array<string, mixed> $options The options for the find
     * @return \Cake\ORM\Query The query builder
     */
    public function findList(Query $query, array $options): Query
    {
        $options += [
            'keyField' => $this->getPrimaryKey(),
            'valueField' => $this->getDisplayField(),
            'groupField' => null,
            'valueSeparator' => ';',
        ];

        if (
            !$query->clause('select') &&
            !is_object($options['keyField']) &&
            !is_object($options['valueField']) &&
            !is_object($options['groupField'])
        ) {
            $fields = array_merge(
                (array)$options['keyField'],
                (array)$options['valueField'],
                (array)$options['groupField']
            );
            $columns = $this->getSchema()->columns();
            if (count($fields) === count(array_intersect($fields, $columns))) {
                $query->select($fields);
            }
        }

        $options = $this->_setFieldMatchers(
            $options,
            ['keyField', 'valueField', 'groupField']
        );

        return $query->formatResults(function ($results) use ($options) {
            /** @var \Cake\Collection\CollectionInterface $results */
            return $results->combine(
                $options['keyField'],
                $options['valueField'],
                $options['groupField']
            );
        });
    }

    /**
     * Results for this finder will be a nested array, and is appropriate if you want
     * to use the parent_id field of your model data to build nested results.
     *
     * Values belonging to a parent row based on their parent_id value will be
     * recursively nested inside the parent row values using the `children` property
     *
     * You can customize what fields are used for nesting results, by default the
     * primary key and the `parent_id` fields are used. If you wish to change
     * these defaults you need to provide the keys `keyField`, `parentField` or `nestingKey` in
     * `$options`:
     *
     * ```
     * $table->find('threaded', [
     *  'keyField' => 'id',
     *  'parentField' => 'ancestor_id',
     *  'nestingKey' => 'children'
     * ]);
     * ```
     *
     * @param \Cake\ORM\Query $query The query to find with
     * @param array<string, mixed> $options The options to find with
     * @return \Cake\ORM\Query The query builder
     */
    public function findThreaded(Query $query, array $options): Query
    {
        $options += [
            'keyField' => $this->getPrimaryKey(),
            'parentField' => 'parent_id',
            'nestingKey' => 'children',
        ];

        $options = $this->_setFieldMatchers($options, ['keyField', 'parentField']);

        return $query->formatResults(function ($results) use ($options) {
            /** @var \Cake\Collection\CollectionInterface $results */
            return $results->nest($options['keyField'], $options['parentField'], $options['nestingKey']);
        });
    }

    /**
     * Out of an options array, check if the keys described in `$keys` are arrays
     * and change the values for closures that will concatenate the each of the
     * properties in the value array when passed a row.
     *
     * This is an auxiliary function used for result formatters that can accept
     * composite keys when comparing values.
     *
     * @param array<string, mixed> $options the original options passed to a finder
     * @param array<string> $keys the keys to check in $options to build matchers from
     * the associated value
     * @return array
     */
    protected function _setFieldMatchers(array $options, array $keys): array
    {
        foreach ($keys as $field) {
            if (!is_array($options[$field])) {
                continue;
            }

            if (count($options[$field]) === 1) {
                $options[$field] = current($options[$field]);
                continue;
            }

            $fields = $options[$field];
            $glue = in_array($field, ['keyField', 'parentField'], true) ? ';' : $options['valueSeparator'];
            $options[$field] = function ($row) use ($fields, $glue) {
                $matches = [];
                foreach ($fields as $field) {
                    $matches[] = $row[$field];
                }

                return implode($glue, $matches);
            };
        }

        return $options;
    }

    /**
     * {@inheritDoc}
     *
     * ### Usage
     *
     * Get an article and some relationships:
     *
     * ```
     * $article = $articles->get(1, ['contain' => ['Users', 'Comments']]);
     * ```
     *
     * @param mixed $primaryKey primary key value to find
     * @param array<string, mixed> $options options accepted by `Table::find()`
     * @return \Cake\Datasource\EntityInterface
     * @throws \Cake\Datasource\Exception\RecordNotFoundException if the record with such id
     * could not be found
     * @throws \Cake\Datasource\Exception\InvalidPrimaryKeyException When $primaryKey has an
     *      incorrect number of elements.
     * @see \Cake\Datasource\RepositoryInterface::find()
     * @psalm-suppress InvalidReturnType
     */
    public function get($primaryKey, array $options = []): EntityInterface
    {
        $key = (array)$this->getPrimaryKey();
        $alias = $this->getAlias();
        foreach ($key as $index => $keyname) {
            $key[$index] = $alias . '.' . $keyname;
        }
        $primaryKey = (array)$primaryKey;
        if (count($key) !== count($primaryKey)) {
            $primaryKey = $primaryKey ?: [null];
            $primaryKey = array_map(function ($key) {
                return var_export($key, true);
            }, $primaryKey);

            throw new InvalidPrimaryKeyException(sprintf(
                'Record not found in table "%s" with primary key [%s]',
                $this->getTable(),
                implode(', ', $primaryKey)
            ));
        }
        $conditions = array_combine($key, $primaryKey);

        $cacheConfig = $options['cache'] ?? false;
        $cacheKey = $options['key'] ?? false;
        $finder = $options['finder'] ?? 'all';
        unset($options['key'], $options['cache'], $options['finder']);

        $query = $this->find($finder, $options)->where($conditions);

        if ($cacheConfig) {
            if (!$cacheKey) {
                $cacheKey = sprintf(
                    'get-%s-%s-%s',
                    $this->getConnection()->configName(),
                    $this->getTable(),
                    json_encode($primaryKey)
                );
            }
            $query->cache($cacheKey, $cacheConfig);
        }

        /** @psalm-suppress InvalidReturnStatement */
        return $query->firstOrFail();
    }

    /**
     * Handles the logic executing of a worker inside a transaction.
     *
     * @param callable $worker The worker that will run inside the transaction.
     * @param bool $atomic Whether to execute the worker inside a database transaction.
     * @return mixed
     */
    protected function _executeTransaction(callable $worker, bool $atomic = true)
    {
        if ($atomic) {
            return $this->getConnection()->transactional(function () use ($worker) {
                return $worker();
            });
        }

        return $worker();
    }

    /**
     * Checks if the caller would have executed a commit on a transaction.
     *
     * @param bool $atomic True if an atomic transaction was used.
     * @param bool $primary True if a primary was used.
     * @return bool Returns true if a transaction was committed.
     */
    protected function _transactionCommitted(bool $atomic, bool $primary): bool
    {
        return !$this->getConnection()->inTransaction() && ($atomic || $primary);
    }

    /**
     * Finds an existing record or creates a new one.
     *
     * A find() will be done to locate an existing record using the attributes
     * defined in $search. If records matches the conditions, the first record
     * will be returned.
     *
     * If no record can be found, a new entity will be created
     * with the $search properties. If a callback is provided, it will be
     * called allowing you to define additional default values. The new
     * entity will be saved and returned.
     *
     * If your find conditions require custom order, associations or conditions, then the $search
     * parameter can be a callable that takes the Query as the argument, or a \Cake\ORM\Query object passed
     * as the $search parameter. Allowing you to customize the find results.
     *
     * ### Options
     *
     * The options array is passed to the save method with exception to the following keys:
     *
     * - atomic: Whether to execute the methods for find, save and callbacks inside a database
     *   transaction (default: true)
     * - defaults: Whether to use the search criteria as default values for the new entity (default: true)
     *
     * @param \Cake\ORM\Query|callable|array $search The criteria to find existing
     *   records by. Note that when you pass a query object you'll have to use
     *   the 2nd arg of the method to modify the entity data before saving.
     * @param callable|null $callback A callback that will be invoked for newly
     *   created entities. This callback will be called *before* the entity
     *   is persisted.
     * @param array<string, mixed> $options The options to use when saving.
     * @return \Cake\Datasource\EntityInterface An entity.
     * @throws \Cake\ORM\Exception\PersistenceFailedException When the entity couldn't be saved
     */
    public function findOrCreate($search, ?callable $callback = null, $options = []): EntityInterface
    {
        $options = new ArrayObject($options + [
            'atomic' => true,
            'defaults' => true,
        ]);

        $entity = $this->_executeTransaction(function () use ($search, $callback, $options) {
            return $this->_processFindOrCreate($search, $callback, $options->getArrayCopy());
        }, $options['atomic']);

        if ($entity && $this->_transactionCommitted($options['atomic'], true)) {
            $this->dispatchEvent('Model.afterSaveCommit', compact('entity', 'options'));
        }

        return $entity;
    }

    /**
     * Performs the actual find and/or create of an entity based on the passed options.
     *
     * @param \Cake\ORM\Query|callable|array $search The criteria to find an existing record by, or a callable tha will
     *   customize the find query.
     * @param callable|null $callback A callback that will be invoked for newly
     *   created entities. This callback will be called *before* the entity
     *   is persisted.
     * @param array<string, mixed> $options The options to use when saving.
     * @return \Cake\Datasource\EntityInterface|array An entity.
     * @throws \Cake\ORM\Exception\PersistenceFailedException When the entity couldn't be saved
     * @throws \InvalidArgumentException
     */
    protected function _processFindOrCreate($search, ?callable $callback = null, $options = [])
    {
        $query = $this->_getFindOrCreateQuery($search);

        $row = $query->first();
        if ($row !== null) {
            return $row;
        }

        $entity = $this->newEmptyEntity();
        if ($options['defaults'] && is_array($search)) {
            $accessibleFields = array_combine(array_keys($search), array_fill(0, count($search), true));
            $entity = $this->patchEntity($entity, $search, ['accessibleFields' => $accessibleFields]);
        }
        if ($callback !== null) {
            $entity = $callback($entity) ?: $entity;
        }
        unset($options['defaults']);

        $result = $this->save($entity, $options);

        if ($result === false) {
            throw new PersistenceFailedException($entity, ['findOrCreate']);
        }

        return $entity;
    }

    /**
     * Gets the query object for findOrCreate().
     *
     * @param \Cake\ORM\Query|callable|array $search The criteria to find existing records by.
     * @return \Cake\ORM\Query
     */
    protected function _getFindOrCreateQuery($search): Query
    {
        if (is_callable($search)) {
            $query = $this->find();
            $search($query);
        } elseif (is_array($search)) {
            $query = $this->find()->where($search);
        } elseif ($search instanceof Query) {
            $query = $search;
        } else {
            throw new InvalidArgumentException(sprintf(
                'Search criteria must be an array, callable or Query. Got "%s"',
                getTypeName($search)
            ));
        }

        return $query;
    }

    /**
     * Creates a new Query instance for a table.
     *
     * @return \Cake\ORM\Query
     */
    public function query(): Query
    {
        return new Query($this->getConnection(), $this);
    }

    /**
     * Creates a new Query::subquery() instance for a table.
     *
     * @return \Cake\ORM\Query
     * @see \Cake\ORM\Query::subquery()
     */
    public function subquery(): Query
    {
        return Query::subquery($this);
    }

    /**
     * @inheritDoc
     */
    public function updateAll($fields, $conditions): int
    {
        $statement = $this->query()
            ->update()
            ->set($fields)
            ->where($conditions)
            ->execute();
        $statement->closeCursor();

        return $statement->rowCount();
    }

    /**
     * @inheritDoc
     */
    public function deleteAll($conditions): int
    {
        $statement = $this->query()
            ->delete()
            ->where($conditions)
            ->execute();
        $statement->closeCursor();

        return $statement->rowCount();
    }

    /**
     * @inheritDoc
     */
    public function exists($conditions): bool
    {
        return (bool)count(
            $this->find('all')
            ->select(['existing' => 1])
            ->where($conditions)
            ->limit(1)
            ->disableHydration()
            ->toArray()
        );
    }

    /**
     * {@inheritDoc}
     *
     * ### Options
     *
     * The options array accepts the following keys:
     *
     * - atomic: Whether to execute the save and callbacks inside a database
     *   transaction (default: true)
     * - checkRules: Whether to check the rules on entity before saving, if the checking
     *   fails, it will abort the save operation. (default:true)
     * - associated: If `true` it will save 1st level associated entities as they are found
     *   in the passed `$entity` whenever the property defined for the association
     *   is marked as dirty. If an array, it will be interpreted as the list of associations
     *   to be saved. It is possible to provide different options for saving on associated
     *   table objects using this key by making the custom options the array value.
     *   If `false` no associated records will be saved. (default: `true`)
     * - checkExisting: Whether to check if the entity already exists, assuming that the
     *   entity is marked as not new, and the primary key has been set.
     *
     * ### Events
     *
     * When saving, this method will trigger four events:
     *
     * - Model.beforeRules: Will be triggered right before any rule checking is done
     *   for the passed entity if the `checkRules` key in $options is not set to false.
     *   Listeners will receive as arguments the entity, options array and the operation type.
     *   If the event is stopped the rules check result will be set to the result of the event itself.
     * - Model.afterRules: Will be triggered right after the `checkRules()` method is
     *   called for the entity. Listeners will receive as arguments the entity,
     *   options array, the result of checking the rules and the operation type.
     *   If the event is stopped the checking result will be set to the result of
     *   the event itself.
     * - Model.beforeSave: Will be triggered just before the list of fields to be
     *   persisted is calculated. It receives both the entity and the options as
     *   arguments. The options array is passed as an ArrayObject, so any changes in
     *   it will be reflected in every listener and remembered at the end of the event
     *   so it can be used for the rest of the save operation. Returning false in any
     *   of the listeners will abort the saving process. If the event is stopped
     *   using the event API, the event object's `result` property will be returned.
     *   This can be useful when having your own saving strategy implemented inside a
     *   listener.
     * - Model.afterSave: Will be triggered after a successful insert or save,
     *   listeners will receive the entity and the options array as arguments. The type
     *   of operation performed (insert or update) can be determined by checking the
     *   entity's method `isNew`, true meaning an insert and false an update.
     * - Model.afterSaveCommit: Will be triggered after the transaction is committed
     *   for atomic save, listeners will receive the entity and the options array
     *   as arguments.
     *
     * This method will determine whether the passed entity needs to be
     * inserted or updated in the database. It does that by checking the `isNew`
     * method on the entity. If the entity to be saved returns a non-empty value from
     * its `errors()` method, it will not be saved.
     *
     * ### Saving on associated tables
     *
     * This method will by default persist entities belonging to associated tables,
     * whenever a dirty property matching the name of the property name set for an
     * association in this table. It is possible to control what associations will
     * be saved and to pass additional option for saving them.
     *
     * ```
     * // Only save the comments association
     * $articles->save($entity, ['associated' => ['Comments']]);
     *
     * // Save the company, the employees and related addresses for each of them.
     * // For employees do not check the entity rules
     * $companies->save($entity, [
     *   'associated' => [
     *     'Employees' => [
     *       'associated' => ['Addresses'],
     *       'checkRules' => false
     *     ]
     *   ]
     * ]);
     *
     * // Save no associations
     * $articles->save($entity, ['associated' => false]);
     * ```
     *
     * @param \Cake\Datasource\EntityInterface $entity the entity to be saved
     * @param \Cake\ORM\SaveOptionsBuilder|\ArrayAccess|array $options The options to use when saving.
     * @return \Cake\Datasource\EntityInterface|false
     * @throws \Cake\ORM\Exception\RolledbackTransactionException If the transaction is aborted in the afterSave event.
     */
    public function save(EntityInterface $entity, $options = [])
    {
        if ($options instanceof SaveOptionsBuilder) {
            $options = $options->toArray();
        }

        $options = new ArrayObject((array)$options + [
            'atomic' => true,
            'associated' => true,
            'checkRules' => true,
            'checkExisting' => true,
            '_primary' => true,
        ]);

        if ($entity->hasErrors((bool)$options['associated'])) {
            return false;
        }

        if ($entity->isNew() === false && !$entity->isDirty()) {
            return $entity;
        }

        $success = $this->_executeTransaction(function () use ($entity, $options) {
            return $this->_processSave($entity, $options);
        }, $options['atomic']);

        if ($success) {
            if ($this->_transactionCommitted($options['atomic'], $options['_primary'])) {
                $this->dispatchEvent('Model.afterSaveCommit', compact('entity', 'options'));
            }
            if ($options['atomic'] || $options['_primary']) {
                $entity->clean();
                $entity->setNew(false);
                $entity->setSource($this->getRegistryAlias());
            }
        }

        return $success;
    }

    /**
     * Try to save an entity or throw a PersistenceFailedException if the application rules checks failed,
     * the entity contains errors or the save was aborted by a callback.
     *
     * @param \Cake\Datasource\EntityInterface $entity the entity to be saved
     * @param \ArrayAccess|array $options The options to use when saving.
     * @return \Cake\Datasource\EntityInterface
     * @throws \Cake\ORM\Exception\PersistenceFailedException When the entity couldn't be saved
     * @see \Cake\ORM\Table::save()
     */
    public function saveOrFail(EntityInterface $entity, $options = []): EntityInterface
    {
        $saved = $this->save($entity, $options);
        if ($saved === false) {
            throw new PersistenceFailedException($entity, ['save']);
        }

        return $saved;
    }

    /**
     * Performs the actual saving of an entity based on the passed options.
     *
     * @param \Cake\Datasource\EntityInterface $entity the entity to be saved
     * @param \ArrayObject $options the options to use for the save operation
     * @return \Cake\Datasource\EntityInterface|false
     * @throws \RuntimeException When an entity is missing some of the primary keys.
     * @throws \Cake\ORM\Exception\RolledbackTransactionException If the transaction
     *   is aborted in the afterSave event.
     */
    protected function _processSave(EntityInterface $entity, ArrayObject $options)
    {
        $primaryColumns = (array)$this->getPrimaryKey();

        if ($options['checkExisting'] && $primaryColumns && $entity->isNew() && $entity->has($primaryColumns)) {
            $alias = $this->getAlias();
            $conditions = [];
            foreach ($entity->extract($primaryColumns) as $k => $v) {
                $conditions["$alias.$k"] = $v;
            }
            $entity->setNew(!$this->exists($conditions));
        }

        $mode = $entity->isNew() ? RulesChecker::CREATE : RulesChecker::UPDATE;
        if ($options['checkRules'] && !$this->checkRules($entity, $mode, $options)) {
            return false;
        }

        $options['associated'] = $this->_associations->normalizeKeys($options['associated']);
        $event = $this->dispatchEvent('Model.beforeSave', compact('entity', 'options'));

        if ($event->isStopped()) {
            $result = $event->getResult();
            if ($result === null) {
                return false;
            }

            if ($result !== false && !($result instanceof EntityInterface)) {
                throw new RuntimeException(sprintf(
                    'The beforeSave callback must return `false` or `EntityInterface` instance. Got `%s` instead.',
                    getTypeName($result)
                ));
            }

            return $result;
        }

        $saved = $this->_associations->saveParents(
            $this,
            $entity,
            $options['associated'],
            ['_primary' => false] + $options->getArrayCopy()
        );

        if (!$saved && $options['atomic']) {
            return false;
        }

        $data = $entity->extract($this->getSchema()->columns(), true);
        $isNew = $entity->isNew();

        if ($isNew) {
            $success = $this->_insert($entity, $data);
        } else {
            $success = $this->_update($entity, $data);
        }

        if ($success) {
            $success = $this->_onSaveSuccess($entity, $options);
        }

        if (!$success && $isNew) {
            $entity->unset($this->getPrimaryKey());
            $entity->setNew(true);
        }

        return $success ? $entity : false;
    }

    /**
     * Handles the saving of children associations and executing the afterSave logic
     * once the entity for this table has been saved successfully.
     *
     * @param \Cake\Datasource\EntityInterface $entity the entity to be saved
     * @param \ArrayObject $options the options to use for the save operation
     * @return bool True on success
     * @throws \Cake\ORM\Exception\RolledbackTransactionException If the transaction
     *   is aborted in the afterSave event.
     */
    protected function _onSaveSuccess(EntityInterface $entity, ArrayObject $options): bool
    {
        $success = $this->_associations->saveChildren(
            $this,
            $entity,
            $options['associated'],
            ['_primary' => false] + $options->getArrayCopy()
        );

        if (!$success && $options['atomic']) {
            return false;
        }

        $this->dispatchEvent('Model.afterSave', compact('entity', 'options'));

        if ($options['atomic'] && !$this->getConnection()->inTransaction()) {
            throw new RolledbackTransactionException(['table' => static::class]);
        }

        if (!$options['atomic'] && !$options['_primary']) {
            $entity->clean();
            $entity->setNew(false);
            $entity->setSource($this->getRegistryAlias());
        }

        return true;
    }

    /**
     * Auxiliary function to handle the insert of an entity's data in the table
     *
     * @param \Cake\Datasource\EntityInterface $entity the subject entity from were $data was extracted
     * @param array $data The actual data that needs to be saved
     * @return \Cake\Datasource\EntityInterface|false
     * @throws \RuntimeException if not all the primary keys where supplied or could
     * be generated when the table has composite primary keys. Or when the table has no primary key.
     */
    protected function _insert(EntityInterface $entity, array $data)
    {
        $primary = (array)$this->getPrimaryKey();
        if (empty($primary)) {
            $msg = sprintf(
                'Cannot insert row in "%s" table, it has no primary key.',
                $this->getTable()
            );
            throw new RuntimeException($msg);
        }
        $keys = array_fill(0, count($primary), null);
        $id = (array)$this->_newId($primary) + $keys;

        // Generate primary keys preferring values in $data.
        $primary = array_combine($primary, $id) ?: [];
        $primary = array_intersect_key($data, $primary) + $primary;

        $filteredKeys = array_filter($primary, function ($v) {
            return $v !== null;
        });
        $data += $filteredKeys;

        if (count($primary) > 1) {
            $schema = $this->getSchema();
            foreach ($primary as $k => $v) {
                if (!isset($data[$k]) && empty($schema->getColumn($k)['autoIncrement'])) {
                    $msg = 'Cannot insert row, some of the primary key values are missing. ';
                    $msg .= sprintf(
                        'Got (%s), expecting (%s)',
                        implode(', ', $filteredKeys + $entity->extract(array_keys($primary))),
                        implode(', ', array_keys($primary))
                    );
                    throw new RuntimeException($msg);
                }
            }
        }

        if (empty($data)) {
            return false;
        }

        $statement = $this->query()->insert(array_keys($data))
            ->values($data)
            ->execute();

        $success = false;
        if ($statement->rowCount() !== 0) {
            $success = $entity;
            $entity->set($filteredKeys, ['guard' => false]);
            $schema = $this->getSchema();
            $driver = $this->getConnection()->getDriver();
            foreach ($primary as $key => $v) {
                if (!isset($data[$key])) {
                    $id = $statement->lastInsertId($this->getTable(), $key);
                    /** @var string $type */
                    $type = $schema->getColumnType($key);
                    $entity->set($key, TypeFactory::build($type)->toPHP($id, $driver));
                    break;
                }
            }
        }
        $statement->closeCursor();

        return $success;
    }

    /**
     * Generate a primary key value for a new record.
     *
     * By default, this uses the type system to generate a new primary key
     * value if possible. You can override this method if you have specific requirements
     * for id generation.
     *
     * Note: The ORM will not generate primary key values for composite primary keys.
     * You can overwrite _newId() in your table class.
     *
     * @param array<string> $primary The primary key columns to get a new ID for.
     * @return string|null Either null or the primary key value or a list of primary key values.
     */
    protected function _newId(array $primary)
    {
        if (!$primary || count($primary) > 1) {
            return null;
        }
        /** @var string $typeName */
        $typeName = $this->getSchema()->getColumnType($primary[0]);
        $type = TypeFactory::build($typeName);

        return $type->newId();
    }

    /**
     * Auxiliary function to handle the update of an entity's data in the table
     *
     * @param \Cake\Datasource\EntityInterface $entity the subject entity from were $data was extracted
     * @param array $data The actual data that needs to be saved
     * @return \Cake\Datasource\EntityInterface|false
     * @throws \InvalidArgumentException When primary key data is missing.
     */
    protected function _update(EntityInterface $entity, array $data)
    {
        $primaryColumns = (array)$this->getPrimaryKey();
        $primaryKey = $entity->extract($primaryColumns);

        $data = array_diff_key($data, $primaryKey);
        if (empty($data)) {
            return $entity;
        }

        if (count($primaryColumns) === 0) {
            $entityClass = get_class($entity);
            $table = $this->getTable();
            $message = "Cannot update `$entityClass`. The `$table` has no primary key.";
            throw new InvalidArgumentException($message);
        }

        if (!$entity->has($primaryColumns)) {
            $message = 'All primary key value(s) are needed for updating, ';
            $message .= get_class($entity) . ' is missing ' . implode(', ', $primaryColumns);
            throw new InvalidArgumentException($message);
        }

        $statement = $this->query()
            ->update()
            ->set($data)
            ->where($primaryKey)
            ->execute();

        $success = $statement->errorCode() === '00000' ? $entity : false;
        $statement->closeCursor();

        return $success;
    }

    /**
     * Persists multiple entities of a table.
     *
     * The records will be saved in a transaction which will be rolled back if
     * any one of the records fails to save due to failed validation or database
     * error.
     *
     * @param \Cake\Datasource\ResultSetInterface|array<\Cake\Datasource\EntityInterface> $entities Entities to save.
     * @param \Cake\ORM\SaveOptionsBuilder|\ArrayAccess|array $options Options used when calling Table::save() for each entity.
     * @return \Cake\Datasource\ResultSetInterface|array<\Cake\Datasource\EntityInterface>|false False on failure, entities list on success.
     * @throws \Exception
     */
    public function saveMany(iterable $entities, $options = [])
    {
        try {
            return $this->_saveMany($entities, $options);
        } catch (PersistenceFailedException $exception) {
            return false;
        }
    }

    /**
     * Persists multiple entities of a table.
     *
     * The records will be saved in a transaction which will be rolled back if
     * any one of the records fails to save due to failed validation or database
     * error.
     *
     * @param \Cake\Datasource\ResultSetInterface|array<\Cake\Datasource\EntityInterface> $entities Entities to save.
     * @param \ArrayAccess|array $options Options used when calling Table::save() for each entity.
     * @return \Cake\Datasource\ResultSetInterface|array<\Cake\Datasource\EntityInterface> Entities list.
     * @throws \Exception
     * @throws \Cake\ORM\Exception\PersistenceFailedException If an entity couldn't be saved.
     */
    public function saveManyOrFail(iterable $entities, $options = []): iterable
    {
        return $this->_saveMany($entities, $options);
    }

    /**
     * @param \Cake\Datasource\ResultSetInterface|array<\Cake\Datasource\EntityInterface> $entities Entities to save.
     * @param \Cake\ORM\SaveOptionsBuilder|\ArrayAccess|array $options Options used when calling Table::save() for each entity.
     * @throws \Cake\ORM\Exception\PersistenceFailedException If an entity couldn't be saved.
     * @throws \Exception If an entity couldn't be saved.
     * @return \Cake\Datasource\ResultSetInterface|array<\Cake\Datasource\EntityInterface> Entities list.
     */
    protected function _saveMany(iterable $entities, $options = []): iterable
    {
        $options = new ArrayObject(
            (array)$options + [
                'atomic' => true,
                'checkRules' => true,
                '_primary' => true,
            ]
        );

        /** @var array<bool> $isNew */
        $isNew = [];
        $cleanup = function ($entities) use (&$isNew): void {
            /** @var array<\Cake\Datasource\EntityInterface> $entities */
            foreach ($entities as $key => $entity) {
                if (isset($isNew[$key]) && $isNew[$key]) {
                    $entity->unset($this->getPrimaryKey());
                    $entity->setNew(true);
                }
            }
        };

        /** @var \Cake\Datasource\EntityInterface|null $failed */
        $failed = null;
        try {
            $this->getConnection()
                ->transactional(function () use ($entities, $options, &$isNew, &$failed) {
                    foreach ($entities as $key => $entity) {
                        $isNew[$key] = $entity->isNew();
                        if ($this->save($entity, $options) === false) {
                            $failed = $entity;

                            return false;
                        }
                    }
                });
        } catch (Exception $e) {
            $cleanup($entities);

            throw $e;
        }

        if ($failed !== null) {
            $cleanup($entities);

            throw new PersistenceFailedException($failed, ['saveMany']);
        }

        if ($this->_transactionCommitted($options['atomic'], $options['_primary'])) {
            foreach ($entities as $entity) {
                $this->dispatchEvent('Model.afterSaveCommit', compact('entity', 'options'));
            }
        }

        return $entities;
    }

    /**
     * {@inheritDoc}
     *
     * For HasMany and HasOne associations records will be removed based on
     * the dependent option. Join table records in BelongsToMany associations
     * will always be removed. You can use the `cascadeCallbacks` option
     * when defining associations to change how associated data is deleted.
     *
     * ### Options
     *
     * - `atomic` Defaults to true. When true the deletion happens within a transaction.
     * - `checkRules` Defaults to true. Check deletion rules before deleting the record.
     *
     * ### Events
     *
     * - `Model.beforeDelete` Fired before the delete occurs. If stopped the delete
     *   will be aborted. Receives the event, entity, and options.
     * - `Model.afterDelete` Fired after the delete has been successful. Receives
     *   the event, entity, and options.
     * - `Model.afterDeleteCommit` Fired after the transaction is committed for
     *   an atomic delete. Receives the event, entity, and options.
     *
     * The options argument will be converted into an \ArrayObject instance
     * for the duration of the callbacks, this allows listeners to modify
     * the options used in the delete operation.
     *
     * @param \Cake\Datasource\EntityInterface $entity The entity to remove.
     * @param \ArrayAccess|array $options The options for the delete.
     * @return bool success
     */
    public function delete(EntityInterface $entity, $options = []): bool
    {
        $options = new ArrayObject((array)$options + [
            'atomic' => true,
            'checkRules' => true,
            '_primary' => true,
        ]);

        $success = $this->_executeTransaction(function () use ($entity, $options) {
            return $this->_processDelete($entity, $options);
        }, $options['atomic']);

        if ($success && $this->_transactionCommitted($options['atomic'], $options['_primary'])) {
            $this->dispatchEvent('Model.afterDeleteCommit', [
                'entity' => $entity,
                'options' => $options,
            ]);
        }

        return $success;
    }

    /**
     * Deletes multiple entities of a table.
     *
     * The records will be deleted in a transaction which will be rolled back if
     * any one of the records fails to delete due to failed validation or database
     * error.
     *
     * @param \Cake\Datasource\ResultSetInterface|array<\Cake\Datasource\EntityInterface> $entities Entities to delete.
     * @param \ArrayAccess|array $options Options used when calling Table::save() for each entity.
     * @return \Cake\Datasource\ResultSetInterface|array<\Cake\Datasource\EntityInterface>|false Entities list
     *   on success, false on failure.
     * @see \Cake\ORM\Table::delete() for options and events related to this method.
     */
    public function deleteMany(iterable $entities, $options = [])
    {
        $failed = $this->_deleteMany($entities, $options);

        if ($failed !== null) {
            return false;
        }

        return $entities;
    }

    /**
     * Deletes multiple entities of a table.
     *
     * The records will be deleted in a transaction which will be rolled back if
     * any one of the records fails to delete due to failed validation or database
     * error.
     *
     * @param \Cake\Datasource\ResultSetInterface|array<\Cake\Datasource\EntityInterface> $entities Entities to delete.
     * @param \ArrayAccess|array $options Options used when calling Table::save() for each entity.
     * @return \Cake\Datasource\ResultSetInterface|array<\Cake\Datasource\EntityInterface> Entities list.
     * @throws \Cake\ORM\Exception\PersistenceFailedException
     * @see \Cake\ORM\Table::delete() for options and events related to this method.
     */
    public function deleteManyOrFail(iterable $entities, $options = []): iterable
    {
        $failed = $this->_deleteMany($entities, $options);

        if ($failed !== null) {
            throw new PersistenceFailedException($failed, ['deleteMany']);
        }

        return $entities;
    }

    /**
     * @param \Cake\Datasource\ResultSetInterface|array<\Cake\Datasource\EntityInterface> $entities Entities to delete.
     * @param \ArrayAccess|array $options Options used.
     * @return \Cake\Datasource\EntityInterface|null
     */
    protected function _deleteMany(iterable $entities, $options = []): ?EntityInterface
    {
        $options = new ArrayObject((array)$options + [
                'atomic' => true,
                'checkRules' => true,
                '_primary' => true,
            ]);

        $failed = $this->_executeTransaction(function () use ($entities, $options) {
            foreach ($entities as $entity) {
                if (!$this->_processDelete($entity, $options)) {
                    return $entity;
                }
            }

            return null;
        }, $options['atomic']);

        if ($failed === null && $this->_transactionCommitted($options['atomic'], $options['_primary'])) {
            foreach ($entities as $entity) {
                $this->dispatchEvent('Model.afterDeleteCommit', [
                    'entity' => $entity,
                    'options' => $options,
                ]);
            }
        }

        return $failed;
    }

    /**
     * Try to delete an entity or throw a PersistenceFailedException if the entity is new,
     * has no primary key value, application rules checks failed or the delete was aborted by a callback.
     *
     * @param \Cake\Datasource\EntityInterface $entity The entity to remove.
     * @param \ArrayAccess|array $options The options for the delete.
     * @return true
     * @throws \Cake\ORM\Exception\PersistenceFailedException
     * @see \Cake\ORM\Table::delete()
     */
    public function deleteOrFail(EntityInterface $entity, $options = []): bool
    {
        $deleted = $this->delete($entity, $options);
        if ($deleted === false) {
            throw new PersistenceFailedException($entity, ['delete']);
        }

        return $deleted;
    }

    /**
     * Perform the delete operation.
     *
     * Will delete the entity provided. Will remove rows from any
     * dependent associations, and clear out join tables for BelongsToMany associations.
     *
     * @param \Cake\Datasource\EntityInterface $entity The entity to delete.
     * @param \ArrayObject $options The options for the delete.
     * @throws \InvalidArgumentException if there are no primary key values of the
     * passed entity
     * @return bool success
     */
    protected function _processDelete(EntityInterface $entity, ArrayObject $options): bool
    {
        if ($entity->isNew()) {
            return false;
        }

        $primaryKey = (array)$this->getPrimaryKey();
        if (!$entity->has($primaryKey)) {
            $msg = 'Deleting requires all primary key values.';
            throw new InvalidArgumentException($msg);
        }

        if ($options['checkRules'] && !$this->checkRules($entity, RulesChecker::DELETE, $options)) {
            return false;
        }

        $event = $this->dispatchEvent('Model.beforeDelete', [
            'entity' => $entity,
            'options' => $options,
        ]);

        if ($event->isStopped()) {
            return (bool)$event->getResult();
        }

        $success = $this->_associations->cascadeDelete(
            $entity,
            ['_primary' => false] + $options->getArrayCopy()
        );
        if (!$success) {
            return $success;
        }

        $statement = $this->query()
            ->delete()
            ->where($entity->extract($primaryKey))
            ->execute();

        if ($statement->rowCount() < 1) {
            return false;
        }

        $this->dispatchEvent('Model.afterDelete', [
            'entity' => $entity,
            'options' => $options,
        ]);

        return true;
    }

    /**
     * Returns true if the finder exists for the table
     *
     * @param string $type name of finder to check
     * @return bool
     */
    public function hasFinder(string $type): bool
    {
        $finder = 'find' . $type;

        return method_exists($this, $finder) || $this->_behaviors->hasFinder($type);
    }

    /**
     * Calls a finder method and applies it to the passed query.
     *
     * @param string $type Name of the finder to be called.
     * @param \Cake\ORM\Query $query The query object to apply the finder options to.
     * @param array<string, mixed> $options List of options to pass to the finder.
     * @return \Cake\ORM\Query
     * @throws \BadMethodCallException
     * @uses findAll()
     * @uses findList()
     * @uses findThreaded()
     */
    public function callFinder(string $type, Query $query, array $options = []): Query
    {
        $query->applyOptions($options);
        $options = $query->getOptions();
        $finder = 'find' . $type;
        if (method_exists($this, $finder)) {
            return $this->{$finder}($query, $options);
        }

        if ($this->_behaviors->hasFinder($type)) {
            return $this->_behaviors->callFinder($type, [$query, $options]);
        }

        throw new BadMethodCallException(sprintf(
            'Unknown finder method "%s" on %s.',
            $type,
            static::class
        ));
    }

    /**
     * Provides the dynamic findBy and findAllBy methods.
     *
     * @param string $method The method name that was fired.
     * @param array $args List of arguments passed to the function.
     * @return \Cake\ORM\Query
     * @throws \BadMethodCallException when there are missing arguments, or when
     *  and & or are combined.
     */
    protected function _dynamicFinder(string $method, array $args)
    {
        $method = Inflector::underscore($method);
        preg_match('/^find_([\w]+)_by_/', $method, $matches);
        if (empty($matches)) {
            // find_by_ is 8 characters.
            $fields = substr($method, 8);
            $findType = 'all';
        } else {
            $fields = substr($method, strlen($matches[0]));
            $findType = Inflector::variable($matches[1]);
        }
        $hasOr = strpos($fields, '_or_');
        $hasAnd = strpos($fields, '_and_');

        $makeConditions = function ($fields, $args) {
            $conditions = [];
            if (count($args) < count($fields)) {
                throw new BadMethodCallException(sprintf(
                    'Not enough arguments for magic finder. Got %s required %s',
                    count($args),
                    count($fields)
                ));
            }
            foreach ($fields as $field) {
                $conditions[$this->aliasField($field)] = array_shift($args);
            }

            return $conditions;
        };

        if ($hasOr !== false && $hasAnd !== false) {
            throw new BadMethodCallException(
                'Cannot mix "and" & "or" in a magic finder. Use find() instead.'
            );
        }

        if ($hasOr === false && $hasAnd === false) {
            $conditions = $makeConditions([$fields], $args);
        } elseif ($hasOr !== false) {
            $fields = explode('_or_', $fields);
            $conditions = [
                'OR' => $makeConditions($fields, $args),
            ];
        } else {
            $fields = explode('_and_', $fields);
            $conditions = $makeConditions($fields, $args);
        }

        return $this->find($findType, [
            'conditions' => $conditions,
        ]);
    }

    /**
     * Handles behavior delegation + dynamic finders.
     *
     * If your Table uses any behaviors you can call them as if
     * they were on the table object.
     *
     * @param string $method name of the method to be invoked
     * @param array $args List of arguments passed to the function
     * @return mixed
     * @throws \BadMethodCallException
     */
    public function __call($method, $args)
    {
        if ($this->_behaviors->hasMethod($method)) {
            return $this->_behaviors->call($method, $args);
        }
        if (preg_match('/^find(?:\w+)?By/', $method) > 0) {
            return $this->_dynamicFinder($method, $args);
        }

        throw new BadMethodCallException(
            sprintf('Unknown method "%s" called on %s', $method, static::class)
        );
    }

    /**
     * Returns the association named after the passed value if exists, otherwise
     * throws an exception.
     *
     * @param string $property the association name
     * @return \Cake\ORM\Association
     * @throws \RuntimeException if no association with such name exists
     */
    public function __get($property)
    {
        $association = $this->_associations->get($property);
        if (!$association) {
            throw new RuntimeException(sprintf(
                'Undefined property `%s`. ' .
                'You have not defined the `%s` association on `%s`.',
                $property,
                $property,
                static::class
            ));
        }

        return $association;
    }

    /**
     * Returns whether an association named after the passed value
     * exists for this table.
     *
     * @param string $property the association name
     * @return bool
     */
    public function __isset($property)
    {
        return $this->_associations->has($property);
    }

    /**
     * Get the object used to marshal/convert array data into objects.
     *
     * Override this method if you want a table object to use custom
     * marshalling logic.
     *
     * @return \Cake\ORM\Marshaller
     * @see \Cake\ORM\Marshaller
     */
    public function marshaller(): Marshaller
    {
        return new Marshaller($this);
    }

    /**
     * {@inheritDoc}
     *
     * @return \Cake\Datasource\EntityInterface
     */
    public function newEmptyEntity(): EntityInterface
    {
        $class = $this->getEntityClass();

        return new $class([], ['source' => $this->getRegistryAlias()]);
    }

    /**
     * {@inheritDoc}
     *
     * By default all the associations on this table will be hydrated. You can
     * limit which associations are built, or include deeper associations
     * using the options parameter:
     *
     * ```
     * $article = $this->Articles->newEntity(
     *   $this->request->getData(),
     *   ['associated' => ['Tags', 'Comments.Users']]
     * );
     * ```
     *
     * You can limit fields that will be present in the constructed entity by
     * passing the `fields` option, which is also accepted for associations:
     *
     * ```
     * $article = $this->Articles->newEntity($this->request->getData(), [
     *  'fields' => ['title', 'body', 'tags', 'comments'],
     *  'associated' => ['Tags', 'Comments.Users' => ['fields' => 'username']]
     * ]
     * );
     * ```
     *
     * The `fields` option lets remove or restrict input data from ending up in
     * the entity. If you'd like to relax the entity's default accessible fields,
     * you can use the `accessibleFields` option:
     *
     * ```
     * $article = $this->Articles->newEntity(
     *   $this->request->getData(),
     *   ['accessibleFields' => ['protected_field' => true]]
     * );
     * ```
     *
     * By default, the data is validated before being passed to the new entity. In
     * the case of invalid fields, those will not be present in the resulting object.
     * The `validate` option can be used to disable validation on the passed data:
     *
     * ```
     * $article = $this->Articles->newEntity(
     *   $this->request->getData(),
     *   ['validate' => false]
     * );
     * ```
     *
     * You can also pass the name of the validator to use in the `validate` option.
     * If `null` is passed to the first param of this function, no validation will
     * be performed.
     *
     * You can use the `Model.beforeMarshal` event to modify request data
     * before it is converted into entities.
     *
     * @param array $data The data to build an entity with.
     * @param array<string, mixed> $options A list of options for the object hydration.
     * @return \Cake\Datasource\EntityInterface
     * @see \Cake\ORM\Marshaller::one()
     */
    public function newEntity(array $data, array $options = []): EntityInterface
    {
        $options['associated'] = $options['associated'] ?? $this->_associations->keys();
        $marshaller = $this->marshaller();

        return $marshaller->one($data, $options);
    }

    /**
     * {@inheritDoc}
     *
     * By default all the associations on this table will be hydrated. You can
     * limit which associations are built, or include deeper associations
     * using the options parameter:
     *
     * ```
     * $articles = $this->Articles->newEntities(
     *   $this->request->getData(),
     *   ['associated' => ['Tags', 'Comments.Users']]
     * );
     * ```
     *
     * You can limit fields that will be present in the constructed entities by
     * passing the `fields` option, which is also accepted for associations:
     *
     * ```
     * $articles = $this->Articles->newEntities($this->request->getData(), [
     *  'fields' => ['title', 'body', 'tags', 'comments'],
     *  'associated' => ['Tags', 'Comments.Users' => ['fields' => 'username']]
     *  ]
     * );
     * ```
     *
     * You can use the `Model.beforeMarshal` event to modify request data
     * before it is converted into entities.
     *
     * @param array $data The data to build an entity with.
     * @param array<string, mixed> $options A list of options for the objects hydration.
     * @return array<\Cake\Datasource\EntityInterface> An array of hydrated records.
     */
    public function newEntities(array $data, array $options = []): array
    {
        $options['associated'] = $options['associated'] ?? $this->_associations->keys();
        $marshaller = $this->marshaller();

        return $marshaller->many($data, $options);
    }

    /**
     * {@inheritDoc}
     *
     * When merging HasMany or BelongsToMany associations, all the entities in the
     * `$data` array will appear, those that can be matched by primary key will get
     * the data merged, but those that cannot, will be discarded.
     *
     * You can limit fields that will be present in the merged entity by
     * passing the `fields` option, which is also accepted for associations:
     *
     * ```
     * $article = $this->Articles->patchEntity($article, $this->request->getData(), [
     *  'fields' => ['title', 'body', 'tags', 'comments'],
     *  'associated' => ['Tags', 'Comments.Users' => ['fields' => 'username']]
     *  ]
     * );
     * ```
     *
     * ```
     * $article = $this->Articles->patchEntity($article, $this->request->getData(), [
     *   'associated' => [
     *     'Tags' => ['accessibleFields' => ['*' => true]]
     *   ]
     * ]);
     * ```
     *
     * By default, the data is validated before being passed to the entity. In
     * the case of invalid fields, those will not be assigned to the entity.
     * The `validate` option can be used to disable validation on the passed data:
     *
     * ```
     * $article = $this->patchEntity($article, $this->request->getData(),[
     *  'validate' => false
     * ]);
     * ```
     *
     * You can use the `Model.beforeMarshal` event to modify request data
     * before it is converted into entities.
     *
     * When patching scalar values (null/booleans/string/integer/float), if the property
     * presently has an identical value, the setter will not be called, and the
     * property will not be marked as dirty. This is an optimization to prevent unnecessary field
     * updates when persisting entities.
     *
     * @param \Cake\Datasource\EntityInterface $entity the entity that will get the
     * data merged in
     * @param array $data key value list of fields to be merged into the entity
     * @param array<string, mixed> $options A list of options for the object hydration.
     * @return \Cake\Datasource\EntityInterface
     * @see \Cake\ORM\Marshaller::merge()
     */
    public function patchEntity(EntityInterface $entity, array $data, array $options = []): EntityInterface
    {
        $options['associated'] = $options['associated'] ?? $this->_associations->keys();
        $marshaller = $this->marshaller();

        return $marshaller->merge($entity, $data, $options);
    }

    /**
     * {@inheritDoc}
     *
     * Those entries in `$entities` that cannot be matched to any record in
     * `$data` will be discarded. Records in `$data` that could not be matched will
     * be marshalled as a new entity.
     *
     * When merging HasMany or BelongsToMany associations, all the entities in the
     * `$data` array will appear, those that can be matched by primary key will get
     * the data merged, but those that cannot, will be discarded.
     *
     * You can limit fields that will be present in the merged entities by
     * passing the `fields` option, which is also accepted for associations:
     *
     * ```
     * $articles = $this->Articles->patchEntities($articles, $this->request->getData(), [
     *  'fields' => ['title', 'body', 'tags', 'comments'],
     *  'associated' => ['Tags', 'Comments.Users' => ['fields' => 'username']]
     *  ]
     * );
     * ```
     *
     * You can use the `Model.beforeMarshal` event to modify request data
     * before it is converted into entities.
     *
     * @param \Traversable|array<\Cake\Datasource\EntityInterface> $entities the entities that will get the
     * data merged in
     * @param array $data list of arrays to be merged into the entities
     * @param array<string, mixed> $options A list of options for the objects hydration.
     * @return array<\Cake\Datasource\EntityInterface>
     */
    public function patchEntities(iterable $entities, array $data, array $options = []): array
    {
        $options['associated'] = $options['associated'] ?? $this->_associations->keys();
        $marshaller = $this->marshaller();

        return $marshaller->mergeMany($entities, $data, $options);
    }

    /**
     * Validator method used to check the uniqueness of a value for a column.
     * This is meant to be used with the validation API and not to be called
     * directly.
     *
     * ### Example:
     *
     * ```
     * $validator->add('email', [
     *  'unique' => ['rule' => 'validateUnique', 'provider' => 'table']
     * ])
     * ```
     *
     * Unique validation can be scoped to the value of another column:
     *
     * ```
     * $validator->add('email', [
     *  'unique' => [
     *      'rule' => ['validateUnique', ['scope' => 'site_id']],
     *      'provider' => 'table'
     *  ]
     * ]);
     * ```
     *
     * In the above example, the email uniqueness will be scoped to only rows having
     * the same site_id. Scoping will only be used if the scoping field is present in
     * the data to be validated.
     *
     * @param mixed $value The value of column to be checked for uniqueness.
     * @param array<string, mixed> $options The options array, optionally containing the 'scope' key.
     *   May also be the validation context, if there are no options.
     * @param array|null $context Either the validation context or null.
     * @return bool True if the value is unique, or false if a non-scalar, non-unique value was given.
     */
    public function validateUnique($value, array $options, ?array $context = null): bool
    {
        if ($context === null) {
            $context = $options;
        }
        $entity = new Entity(
            $context['data'],
            [
                'useSetters' => false,
                'markNew' => $context['newRecord'],
                'source' => $this->getRegistryAlias(),
            ]
        );
        $fields = array_merge(
            [$context['field']],
            isset($options['scope']) ? (array)$options['scope'] : []
        );
        $values = $entity->extract($fields);
        foreach ($values as $field) {
            if ($field !== null && !is_scalar($field)) {
                return false;
            }
        }
        $class = static::IS_UNIQUE_CLASS;
        /** @var \Cake\ORM\Rule\IsUnique $rule */
        $rule = new $class($fields, $options);

        return $rule($entity, ['repository' => $this]);
    }

    /**
     * Get the Model callbacks this table is interested in.
     *
     * By implementing the conventional methods a table class is assumed
     * to be interested in the related event.
     *
     * Override this method if you need to add non-conventional event listeners.
     * Or if you want you table to listen to non-standard events.
     *
     * The conventional method map is:
     *
     * - Model.beforeMarshal => beforeMarshal
     * - Model.afterMarshal => afterMarshal
     * - Model.buildValidator => buildValidator
     * - Model.beforeFind => beforeFind
     * - Model.beforeSave => beforeSave
     * - Model.afterSave => afterSave
     * - Model.afterSaveCommit => afterSaveCommit
     * - Model.beforeDelete => beforeDelete
     * - Model.afterDelete => afterDelete
     * - Model.afterDeleteCommit => afterDeleteCommit
     * - Model.beforeRules => beforeRules
     * - Model.afterRules => afterRules
     *
     * @return array<string, mixed>
     */
    public function implementedEvents(): array
    {
        $eventMap = [
            'Model.beforeMarshal' => 'beforeMarshal',
            'Model.afterMarshal' => 'afterMarshal',
            'Model.buildValidator' => 'buildValidator',
            'Model.beforeFind' => 'beforeFind',
            'Model.beforeSave' => 'beforeSave',
            'Model.afterSave' => 'afterSave',
            'Model.afterSaveCommit' => 'afterSaveCommit',
            'Model.beforeDelete' => 'beforeDelete',
            'Model.afterDelete' => 'afterDelete',
            'Model.afterDeleteCommit' => 'afterDeleteCommit',
            'Model.beforeRules' => 'beforeRules',
            'Model.afterRules' => 'afterRules',
        ];
        $events = [];

        foreach ($eventMap as $event => $method) {
            if (!method_exists($this, $method)) {
                continue;
            }
            $events[$event] = $method;
        }

        return $events;
    }

    /**
     * {@inheritDoc}
     *
     * @param \Cake\ORM\RulesChecker $rules The rules object to be modified.
     * @return \Cake\ORM\RulesChecker
     */
    public function buildRules(RulesChecker $rules): RulesChecker
    {
        return $rules;
    }

    /**
     * Gets a SaveOptionsBuilder instance.
     *
     * @param array<string, mixed> $options Options to parse by the builder.
     * @return \Cake\ORM\SaveOptionsBuilder
     */
    public function getSaveOptionsBuilder(array $options = []): SaveOptionsBuilder
    {
        return new SaveOptionsBuilder($this, $options);
    }

    /**
     * Loads the specified associations in the passed entity or list of entities
     * by executing extra queries in the database and merging the results in the
     * appropriate properties.
     *
     * ### Example:
     *
     * ```
     * $user = $usersTable->get(1);
     * $user = $usersTable->loadInto($user, ['Articles.Tags', 'Articles.Comments']);
     * echo $user->articles[0]->title;
     * ```
     *
     * You can also load associations for multiple entities at once
     *
     * ### Example:
     *
     * ```
     * $users = $usersTable->find()->where([...])->toList();
     * $users = $usersTable->loadInto($users, ['Articles.Tags', 'Articles.Comments']);
     * echo $user[1]->articles[0]->title;
     * ```
     *
     * The properties for the associations to be loaded will be overwritten on each entity.
     *
     * @param \Cake\Datasource\EntityInterface|array<\Cake\Datasource\EntityInterface> $entities a single entity or list of entities
     * @param array $contain A `contain()` compatible array.
     * @see \Cake\ORM\Query::contain()
     * @return \Cake\Datasource\EntityInterface|array<\Cake\Datasource\EntityInterface>
     */
    public function loadInto($entities, array $contain)
    {
        return (new LazyEagerLoader())->loadInto($entities, $contain, $this);
    }

    /**
     * @inheritDoc
     */
    protected function validationMethodExists(string $name): bool
    {
        return method_exists($this, $name) || $this->behaviors()->hasMethod($name);
    }

    /**
     * Returns an array that can be used to describe the internal state of this
     * object.
     *
     * @return array
     */
    public function __debugInfo()
    {
        $conn = $this->getConnection();

        return [
            'registryAlias' => $this->getRegistryAlias(),
            'table' => $this->getTable(),
            'alias' => $this->getAlias(),
            'entityClass' => $this->getEntityClass(),
            'associations' => $this->_associations->keys(),
            'behaviors' => $this->_behaviors->loaded(),
            'defaultConnection' => static::defaultConnectionName(),
            'connectionName' => $conn->configName(),
        ];
    }
}
