Seditio Source
Root |
./othercms/croogo-4.0.7/vendor/admad/cakephp-sequence/src/Model/Behavior/SequenceBehavior.php
<?php
namespace ADmad\Sequence\Model\Behavior;

use
ArrayObject;
use
Cake\Database\Expression\IdentifierExpression;
use
Cake\Datasource\EntityInterface;
use
Cake\Event\Event;
use
Cake\ORM\Behavior;
use
Cake\ORM\Entity;
use
Cake\ORM\Query;

/**
 * SequenceBehavior maintains a contiguous sequence of integers (starting at 1
 * or other configurable integer) in a selected column, such as `position`, for all
 * records in a table, or groups of records identified by one or more
 * 'scope' fields, when adding, editing (including moving groups) or deleting
 * records.
 *
 * Consider the following simple example with no groups:
 * Record  Position
 * A       1
 * B       2
 * C       3
 * D       4
 * E       5
 * F       6
 * G       7
 *  - Save
 *    - If adding new record
 *      - If order not specified e.g. Record H added:
 *          Inserts H at end of list i.e. highest order + 1
 *      - If order specified e.g. Record H added at position 3:
 *          Inserts at specified order
 *          Increments order of all other records whose order >= order of
 *           inserted record i.e. D, E, F & G get incremented
 *    - If editing existing record:
 *      - If order not specified and scope not specified, or same
 *          No Action
 *      - If order not specified but scope specified and different:
 *          Decrement order of all records whose order > old order in the old
 *           scope, and change order to highest order of new scopes + 1
 *      - If order specified:
 *        - If new order < old order e.g. record E moves from 4 to 2
 *            Increments order of all other records whose order > new order and
 *             order < old order i.e. order of C & D get incremented
 *        - If new order > old order e.g. record C moves from 2 to 4
 *            Decrements order of all other records whose order > old order and
 *             <= new order i.e. order of D & E get decremented
 *        - If new order == old order
 *            No action
 *  - Delete
 *      Decrement order of all records whose order > order of deleted record
 *
 * Inspired by Neil Crooke's Sequence behavior for CakePHP 1.3. Above description
 * has been "borrowed" from it :).
 *
 * @copyright 2015-Present ADmad
 * @link https://github.com/ADmad/cakephp-sequence
 * @license MIT License - http://www.opensource.org/licenses/mit-license.php
 */
class SequenceBehavior extends Behavior
{
   
/**
     * Default settings.
     *
     * @var array
     */
   
protected $_defaultConfig = [
       
'order' => 'position',
       
'scope' => [],
       
'start' => 1,
    ];

   
/**
     * Old values for the entity being deleted
     *
     * @var array|null
     */
   
protected $_oldValues;

   
/**
     * Normalize config options.
     *
     * @param array $config Configuration options include:
     * - order : The field name that stores the sequence number.
     *   Defaults is "position".
     * - scope : Array of field names that identify a single group of records
     *   that need to form a contiguous sequence.
     *   Default is empty array, i.e. no scope fields.
     * - start : You can start your sequence numbers at 0 or 1 or any other.
     *   Defaults is 1.
     * @return void
     */
   
public function initialize(array $config)
    {
        if (!
$this->_config['scope']) {
            return;
        }

        if (
is_string($this->_config['scope'])) {
           
$this->_config['scope'] = [$this->_config['scope']];
        }
    }

   
/**
     * Adds order value if not already set in query.
     *
     * @param \Cake\Event\Event $event The beforeFind event that was fired.
     * @param \Cake\ORM\Query $query The query object.
     * @param \ArrayObject $options The options passed to the find method.
     * @return void
     */
   
public function beforeFind(Event $event, Query $query, ArrayObject $options)
    {
        if (!
$query->clause('order')) {
           
$query->order([$this->_table->aliasField($this->_config['order']) => 'ASC']);
        }
    }

   
/**
     * Sets entity's order and updates order of other records when necessary.
     *
     * @param \Cake\Event\Event $event The beforeSave event that was fired.
     * @param \Cake\ORM\Entity $entity The entity that is going to be saved.
     * @param \ArrayObject $options The options passed to the save method.
     * @return void
     */
   
public function beforeSave(Event $event, Entity $entity, ArrayObject $options)
    {
       
$config = $this->getConfig();

       
$newOrder = null;
       
$newScope = $this->_getScope($entity);
        if (
$newScope === false) {
            return;
        }

       
$orderField = $config['order'];
       
$newOrder = $entity->get($orderField);

       
// Adding
       
if ($entity->isNew()) {
           
// Order not specified
           
if ($newOrder === null) {
               
// Insert at end of list
               
$entity->set($orderField, $this->_getHighestOrder($newScope) + 1);
               
// Order specified
           
} else {
               
// Increment order of records it's inserted before
               
$this->_sync(
                    [
$orderField => $this->_getUpdateExpression('+')],
                    [
$orderField . ' >=' => $newOrder],
                   
$newScope
               
);
            }

           
// Editing
       
} else {
           
// No action if no new order or scope specified
           
if ($newOrder === null && !$newScope) {
                return;
            }

            list(
$oldOrder, $oldScope) = $this->_getOldValues($entity);

           
// No action if new and old scope and order same
           
if (
               
$newOrder == $oldOrder &&
               
$newScope == $oldScope
           
) {
                return;
            }

           
// If changing scope
           
if ($newScope && $newScope != $oldScope) {
               
// Decrement records in old scope with higher order than moved record old order
               
$this->_sync(
                    [
$orderField => $this->_getUpdateExpression('-')],
                    [
$orderField . ' >' => $oldOrder],
                   
$oldScope
               
);

               
// Order not specified
               
if ($newOrder === null) {
                   
// Insert at end of new scope
                   
$entity->set(
                       
$orderField,
                       
$this->_getHighestOrder($newScope) + 1
                   
);

                   
// Order specified
               
} else {
                   
// Increment records in new scope with higher order than moved record new order
                   
$this->_sync(
                        [
$orderField => $this->_getUpdateExpression('+')],
                        [
$orderField . ' >=' => $newOrder],
                       
$newScope
                   
);
                }
               
// Same scope
           
} else {
               
// If moving up
               
if ($newOrder < $oldOrder) {
                   
// Increment order of those in between
                   
$this->_sync(
                        [
$orderField => $this->_getUpdateExpression('+')],
                        [
                           
$orderField . ' >=' => $newOrder,
                           
$orderField . ' <' => $oldOrder,
                        ],
                       
$newScope
                   
);

               
// Moving down
               
} else {
                   
// Decrement order of those in between
                   
$this->_sync(
                        [
$orderField => $this->_getUpdateExpression('-')],
                        [
                           
$orderField . ' >' => $oldOrder,
                           
$orderField . ' <=' => $newOrder,
                        ],
                       
$newScope
                   
);
                }
            }
        }
    }

   
/**
     * When you delete a record from a set, you need to decrement the order of all
     * records that were after it in the set.
     *
     * This hook just stores all required values from the entity. Actual order
     * updation is done in "afterDelete" hook.
     *
     * @param \Cake\Event\Event $event The beforeDelete event that was fired.
     * @param \Cake\ORM\Entity $entity The entity that is going to be deleted.
     * @return void
     */
   
public function beforeDelete(Event $event, Entity $entity)
    {
       
$this->_oldValues = $this->_getOldValues($entity);
    }

   
/**
     * When you delete a record from a set, you need to decrement the order of all
     * records that were after it in the set.
     *
     * @param \Cake\Event\Event $event The afterDelete event that was fired.
     * @param \Cake\ORM\Entity $entity The entity that has been deleted.
     * @return void
     */
   
public function afterDelete(Event $event, Entity $entity)
    {
        if (!
$this->_oldValues) {
            return;
        }

       
$orderField = $this->_config['order'];
        list(
$order, $scope) = $this->_oldValues;

       
$this->_sync(
            [
$orderField => $this->_getUpdateExpression('-')],
            [
$orderField . ' >' => $order],
           
$scope
       
);
       
$this->_oldValues = null;
    }

   
/**
     * Decrease the position of the entity on the list
     *
     * If a "higher" entity exists, this will also swap positions with it
     *
     * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved.
     * @return bool
     */
   
public function moveUp(EntityInterface $entity)
    {
        return
$this->_movePosition($entity, '-');
    }

   
/**
     * Increase the position of the entity on the list
     *
     * If a "lower" entity exists, this will also swap positions with it
     *
     * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved.
     * @return bool
     */
   
public function moveDown(EntityInterface $entity)
    {
        return
$this->_movePosition($entity, '+');
    }

   
/**
     * Change the position of the entity on the list by a single position
     *
     * If an entity that conflicts with the new position already exists, this
     * will also swap positions with it
     *
     * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved.
     * @param string $direction Whether to increment or decrement the field.
     * @return bool
     */
   
protected function _movePosition(EntityInterface $entity, $direction = '+')
    {
        if (
$entity->isNew()) {
            return
false;
        }

       
$scope = $this->_getScope($entity);
        if (
$scope === false) {
            return
false;
        }

       
$config = $this->getConfig();
       
$table = $this->_table;

       
$table->removeBehavior('Sequence');

       
$return = $table->getConnection()->transactional(
            function (
$connection) use ($table, $entity, $config, $scope, $direction) {
               
$orderField = $config['order'];
               
// Nothing to do if trying to move up entity already at first position
               
if ($direction === '-' && $entity->get($orderField) === $config['start']) {
                    return
true;
                }

               
$oldOrder = $entity->get($orderField);
               
$newOrder = $entity->get($orderField) - 1;
                if (
$direction === '+') {
                   
$newOrder = $entity->get($orderField) + 1;
                }

               
$previousEntity = $table->find()
                    ->
where(array_merge($scope, [$orderField => $newOrder]))
                    ->
first();

                if (!empty(
$previousEntity)) {
                   
$previousEntity->set($orderField, $oldOrder);
                    if (!
$table->save($previousEntity, ['atomic' => false, 'checkRules' => false])) {
                        return
false;
                    }
               
// Nothing to do if trying to move down entity already at last position
               
} elseif ($direction === '+') {
                    return
true;
                }

               
$entity->set($orderField, $newOrder);

                return
$table->save($entity, ['atomic' => false, 'checkRules' => false]);
            }
        );

       
$table->addBehavior('ADmad/Sequence.Sequence', $config);

        return (bool)
$return;
    }

   
/**
     * Set order for list of records provided.
     *
     * Records can be provided as array of entities or array of associative
     * arrays like `[['id' => 1], ['id' => 2]]` or array of primary key values
     * like `[1, 2]`.
     *
     * @param array $records Records.
     * @return bool
     */
   
public function setOrder(array $records)
    {
       
$config = $this->getConfig();
       
$table = $this->_table;

       
$table->removeBehavior('Sequence');

       
$return = $table->getConnection()->transactional(
            function (
$connection) use ($table, $records) {
               
$order = $this->_config['start'];
               
$field = $this->_config['order'];

               
/** @var string $primaryKeyField */
               
$primaryKeyField = $table->getPrimaryKey();
                foreach (
$records as $record) {
                    if (
is_scalar($record)) {
                       
$record = [$primaryKeyField => $record];
                    }

                    if (
is_array($record)) {
                       
$record = $table->newEntity($record, [
                           
'fields' => array_keys($record),
                           
'validate' => false,
                           
'accessibleFields' => [
                               
$primaryKeyField => true,
                            ],
                        ]);
                       
$record->isNew(false);
                       
$record->setDirty($primaryKeyField, false);
                    }

                   
$record->setAccess($field, true);
                   
$record->set($field, $order++);

                   
$r = $table->save(
                       
$record,
                        [
'atomic' => false, 'checkRules' => false]
                    );
                    if (
$r === false) {
                        return
false;
                    }
                }

                return
true;
            }
        );

       
$table->addBehavior('ADmad/Sequence.Sequence', $config);

        return
$return;
    }

   
/**
     * Get old order and scope values.
     *
     * @param \Cake\ORM\Entity $entity Entity.
     * @return array
     */
   
protected function _getOldValues(Entity $entity)
    {
       
$config = $this->getConfig();
       
$fields = array_merge($config['scope'], [$config['order']]);

       
$values = [];
        foreach (
$fields as $field) {
            if (
$entity->isDirty($field)) {
               
$values[$field] = $entity->getOriginal($field);
            } elseif (
$entity->has($field)) {
               
$values[$field] = $entity->get($field);
            }
        }

        if (
count($fields) != count($values)) {
           
$primaryKey = $entity->get($this->_table->getPrimaryKey());
           
$entity = $this->_table->get($primaryKey, ['fields' => $fields]);
           
$values = $entity->extract($fields);
        }

       
$order = $values[$config['order']];
        unset(
$values[$config['order']]);

        return [
$order, $values];
    }

   
/**
     * Get scope values.
     *
     * @param \Cake\Datasource\EntityInterface $entity Entity.
     * @return array|bool
     */
   
protected function _getScope(EntityInterface $entity)
    {
       
$scope = [];
       
$config = $this->getConfig();

       
// If scope are specified and data for all scope fields is not
        // provided we cannot calculate new order
       
if ($config['scope']) {
           
$scope = $entity->extract($config['scope']);
            if (
count($scope) !== count($config['scope'])) {
                return
false;
            }

           
// Modify where clauses when NULL values are used
           
foreach ($scope as $field => $value) {
                if (
is_null($value)) {
                   
$scope[$field . ' IS'] = $value;
                    unset(
$scope[$field]);
                }
            }
        }

        return
$scope;
    }

   
/**
     * Returns the current highest order of all records in the set. When a new
     * record is added to the set, it is added at the current highest order, plus
     * one.
     *
     * @param array $scope Array with scope field => scope values, used for conditions.
     * @return int Value of order field of last record in set
     */
   
protected function _getHighestOrder(array $scope = [])
    {
       
$orderField = $this->_config['order'];

       
// Find the last record in the set
       
$last = $this->_table->find()
            ->
select([$orderField])
            ->
where($scope)
            ->
order([$orderField => 'DESC'])
            ->
limit(1)
            ->
enableHydration(false)
            ->
first();

       
// If there is a last record (i.e. any) in the set, return the it's order
       
if ($last) {
            return
$last[$orderField];
        }

       
// If there isn't any records in the set, return the start number minus 1
       
return (int)$this->_config['start'] - 1;
    }

   
/**
     * Auxiliary function used to alter the value of order fields by a certain
     * amount that match the passed conditions.
     *
     * @param array $fields Fields to update.
     * @param array $conditions Conditions for matching rows.
     * @param array $scope Grouping scope that will be added to coditions.
     * @return int Count of rows updated.
     */
   
protected function _sync($fields, $conditions, $scope = null)
    {
        if (
$scope) {
           
$conditions = array_merge($conditions, $scope);
        }

        return
$this->_table->updateAll($fields, $conditions);
    }

   
/**
     * Returns the update expression for the order field.
     *
     * @param string $direction Whether to increment or decrement the field.
     * @return \Cake\Database\Expression\QueryExpression QueryExpression to modify the order field
     */
   
protected function _getUpdateExpression($direction = '+')
    {
       
$field = $this->_config['order'];

        return
$this->_table->query()->newExpr()
            ->
add(new IdentifierExpression($field))
            ->
add('1')
            ->
setConjunction($direction);
    }
}