diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 135bc56..917e3c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +10,10 @@ on: jobs: testsuite: - uses: ADmad/.github/.github/workflows/testsuite-with-db.yml@master + uses: ADmad/.github/.github/workflows/testsuite-with-db.yml@cake6 secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} cs-stan: - uses: ADmad/.github/.github/workflows/cs-stan.yml@master + uses: ADmad/.github/.github/workflows/cs-stan.yml@cake6 secrets: inherit diff --git a/README.md b/README.md index 2d3482d..754f43a 100644 --- a/README.md +++ b/README.md @@ -55,25 +55,35 @@ query to order by the position field if an order clause has not already been set ### Methods +CakePHP 6 no longer forwards behavior methods through the table. Call the +behavior directly using `$modelObject->getBehavior('Sequence')`. When migrating +from CakePHP 5, update calls to `moveUp()`, `moveDown()`, and `setOrder()` as shown +below. Automatic ordering on find, save, and delete continues to work by adding +the behavior to the table. + #### moveUp(\Cake\Datasource\EntityInterface $entity) Move up record by one position: ```php -$modelObject->moveUp($entity); +$modelObject->getBehavior('Sequence')->moveUp($entity); ``` #### moveDown(\Cake\Datasource\EntityInterface $entity) Move down record by one position: ```php -$modelObject->moveDown($entity); +$modelObject->getBehavior('Sequence')->moveDown($entity); ``` -#### setOrder(array $record) +#### setOrder(array $records) 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]`. +```php +$modelObject->getBehavior('Sequence')->setOrder([3, 1, 2]); +``` + ## Acknowledgement Shout out to @neilcrookes for his wonderful Sequence Behavior for CakePHP 1.3 diff --git a/composer.json b/composer.json index 671f4ad..4a0913e 100644 --- a/composer.json +++ b/composer.json @@ -5,11 +5,12 @@ "type": "cakephp-plugin", "license": "MIT", "require": { - "cakephp/orm": "^5.0" + "php": ">=8.5", + "cakephp/orm": "^6.0@dev" }, "require-dev": { - "cakephp/cakephp": "^5.0", - "phpunit/phpunit": "^10.5.58 || ^11.5.3 || ^12.4" + "cakephp/cakephp": "^6.0@dev", + "phpunit/phpunit": "^13.0" }, "autoload": { "psr-4": { @@ -28,5 +29,7 @@ "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true } - } + }, + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 02bedfb..04d4057 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,11 @@ - + ./tests/TestCase diff --git a/src/Model/Behavior/SequenceBehavior.php b/src/Model/Behavior/SequenceBehavior.php index 11970ac..be6303c 100644 --- a/src/Model/Behavior/SequenceBehavior.php +++ b/src/Model/Behavior/SequenceBehavior.php @@ -67,7 +67,7 @@ class SequenceBehavior extends Behavior * * @var array */ - protected array $_defaultConfig = [ + protected array $defaultConfig = [ 'sequenceField' => 'position', 'scope' => [], 'startAt' => 1, @@ -78,7 +78,7 @@ class SequenceBehavior extends Behavior * * @var array|null */ - protected ?array $_oldValues = null; + protected ?array $oldValues = null; /** * Normalize config options. @@ -95,12 +95,12 @@ class SequenceBehavior extends Behavior */ public function initialize(array $config): void { - if (!$this->_config['scope']) { + if (!$this->config['scope']) { return; } - if (is_string($this->_config['scope'])) { - $this->_config['scope'] = [$this->_config['scope']]; + if (is_string($this->config['scope'])) { + $this->config['scope'] = [$this->config['scope']]; } } @@ -115,7 +115,7 @@ public function initialize(array $config): void public function beforeFind(EventInterface $event, SelectQuery $query, ArrayObject $options): void { if (!$query->clause('order')) { - $query->orderBy([$this->_table->aliasField($this->_config['sequenceField']) => 'ASC']); + $query->orderBy([$this->table->aliasField($this->config['sequenceField']) => 'ASC']); } } @@ -129,7 +129,7 @@ public function beforeFind(EventInterface $event, SelectQuery $query, ArrayObjec */ public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options): void { - $newScope = $this->_getScope($entity); + $newScope = $this->getScope($entity); if ($newScope === false) { return; } @@ -142,12 +142,12 @@ public function beforeSave(EventInterface $event, EntityInterface $entity, Array // Order not specified if ($newOrder === null) { // Insert at end of list - $entity->set($orderField, $this->_getHighestOrder($newScope) + 1); + $entity->set($orderField, $this->getHighestOrder($newScope) + 1); // Order specified } else { // Increment order of records it's inserted before - $this->_sync( - [$orderField => $this->_getUpdateExpression('+')], + $this->sync( + [$orderField => $this->getUpdateExpression('+')], [$orderField . ' >=' => $newOrder], $newScope, ); @@ -160,7 +160,7 @@ public function beforeSave(EventInterface $event, EntityInterface $entity, Array return; } - [$oldOrder, $oldScope] = $this->_getOldValues($entity); + [$oldOrder, $oldScope] = $this->getOldValues($entity); // No action if new and old scope and order same if ( @@ -173,8 +173,8 @@ public function beforeSave(EventInterface $event, EntityInterface $entity, Array // 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('-')], + $this->sync( + [$orderField => $this->getUpdateExpression('-')], [$orderField . ' >' => $oldOrder], $oldScope, ); @@ -184,14 +184,14 @@ public function beforeSave(EventInterface $event, EntityInterface $entity, Array // Insert at end of new scope $entity->set( $orderField, - $this->_getHighestOrder($newScope) + 1, + $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('+')], + $this->sync( + [$orderField => $this->getUpdateExpression('+')], [$orderField . ' >=' => $newOrder], $newScope, ); @@ -201,8 +201,8 @@ public function beforeSave(EventInterface $event, EntityInterface $entity, Array // If moving up if ($newOrder < $oldOrder) { // Increment order of those in between - $this->_sync( - [$orderField => $this->_getUpdateExpression('+')], + $this->sync( + [$orderField => $this->getUpdateExpression('+')], [ $orderField . ' >=' => $newOrder, $orderField . ' <' => $oldOrder, @@ -213,8 +213,8 @@ public function beforeSave(EventInterface $event, EntityInterface $entity, Array // Moving down } else { // Decrement order of those in between - $this->_sync( - [$orderField => $this->_getUpdateExpression('-')], + $this->sync( + [$orderField => $this->getUpdateExpression('-')], [ $orderField . ' >' => $oldOrder, $orderField . ' <=' => $newOrder, @@ -239,7 +239,7 @@ public function beforeSave(EventInterface $event, EntityInterface $entity, Array */ public function beforeDelete(EventInterface $event, EntityInterface $entity): void { - $this->_oldValues = $this->_getOldValues($entity); + $this->oldValues = $this->getOldValues($entity); } /** @@ -252,20 +252,20 @@ public function beforeDelete(EventInterface $event, EntityInterface $entity): vo */ public function afterDelete(EventInterface $event, EntityInterface $entity): void { - if (!$this->_oldValues) { + if (!$this->oldValues) { return; } - $orderField = $this->_config['sequenceField']; - [$order, $scope] = $this->_oldValues; + $orderField = $this->config['sequenceField']; + [$order, $scope] = $this->oldValues; - $this->_sync( - [$orderField => $this->_getUpdateExpression('-')], + $this->sync( + [$orderField => $this->getUpdateExpression('-')], [$orderField . ' >' => $order], $scope, ); - $this->_oldValues = null; + $this->oldValues = null; } /** @@ -278,7 +278,7 @@ public function afterDelete(EventInterface $event, EntityInterface $entity): voi */ public function moveUp(EntityInterface $entity): bool { - return $this->_movePosition($entity, '-'); + return $this->movePosition($entity, '-'); } /** @@ -291,7 +291,7 @@ public function moveUp(EntityInterface $entity): bool */ public function moveDown(EntityInterface $entity): bool { - return $this->_movePosition($entity, '+'); + return $this->movePosition($entity, '+'); } /** @@ -304,19 +304,19 @@ public function moveDown(EntityInterface $entity): bool * @param string $direction Whether to increment or decrement the field. * @return bool */ - protected function _movePosition(EntityInterface $entity, string $direction = '+'): bool + protected function movePosition(EntityInterface $entity, string $direction = '+'): bool { if ($entity->isNew()) { return false; } - $scope = $this->_getScope($entity); + $scope = $this->getScope($entity); if ($scope === false) { return false; } $config = $this->getConfig(); - $table = $this->_table; + $table = $this->table; $table->removeBehavior('Sequence'); @@ -367,20 +367,20 @@ function ($connection) use ($table, $entity, $config, $scope, $direction) { * arrays like `[['id' => 1], ['id' => 2]]` or array of primary key values * like `[1, 2]`. * - * @param array $records Records. + * @param array<\Cake\Datasource\EntityInterface|array|int|string> $records Records. * @return bool */ public function setOrder(array $records): bool { $config = $this->getConfig(); - $table = $this->_table; + $table = $this->table; $table->removeBehavior('Sequence'); $return = $table->getConnection()->transactional( function ($connection) use ($table, $records) { - $order = $this->_config['startAt']; - $field = $this->_config['sequenceField']; + $order = $this->config['startAt']; + $field = $this->config['sequenceField']; /** @var string $primaryKeyField */ $primaryKeyField = $table->getPrimaryKey(); @@ -393,7 +393,7 @@ function ($connection) use ($table, $records) { $record = $table->newEntity($record, [ 'fields' => array_keys($record), 'validate' => false, - 'accessibleFields' => [ + 'patchableFields' => [ $primaryKeyField => true, ], ]); @@ -401,7 +401,7 @@ function ($connection) use ($table, $records) { $record->setDirty($primaryKeyField, false); } - $record->setAccess($field, true); + $record->setPatchable($field, true); $record->set($field, $order++); $r = $table->save( @@ -428,7 +428,7 @@ function ($connection) use ($table, $records) { * @param \Cake\Datasource\EntityInterface $entity Entity. * @return array */ - protected function _getOldValues(EntityInterface $entity): array + protected function getOldValues(EntityInterface $entity): array { $config = $this->getConfig(); $fields = array_merge($config['scope'], [$config['sequenceField']]); @@ -447,8 +447,8 @@ protected function _getOldValues(EntityInterface $entity): array * @psalm-suppress PossiblyInvalidArgument * @phpstan-ignore-next-line */ - $primaryKey = $entity->get($this->_table->getPrimaryKey()); - $entity = $this->_table->get($primaryKey, fields: $fields); + $primaryKey = $entity->get($this->table->getPrimaryKey()); + $entity = $this->table->get($primaryKey, fields: $fields); $values = $entity->extract($fields); } @@ -471,7 +471,7 @@ protected function _getOldValues(EntityInterface $entity): array * @param \Cake\Datasource\EntityInterface $entity Entity. * @return array|false */ - protected function _getScope(EntityInterface $entity): array|false + protected function getScope(EntityInterface $entity): array|false { $scope = []; $config = $this->getConfig(); @@ -504,12 +504,12 @@ protected function _getScope(EntityInterface $entity): array|false * @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): int + protected function getHighestOrder(array $scope): int { - $orderField = $this->_config['sequenceField']; + $orderField = $this->config['sequenceField']; // Find the last record in the set - $last = $this->_table->find() + $last = $this->table->find() ->select([$orderField]) ->where($scope) ->orderBy([$orderField => 'DESC']) @@ -523,7 +523,7 @@ protected function _getHighestOrder(array $scope): int } // If there isn't any records in the set, return the start number minus 1 - return (int)$this->_config['startAt'] - 1; + return (int)$this->config['startAt'] - 1; } /** @@ -535,13 +535,13 @@ protected function _getHighestOrder(array $scope): int * @param array $scope Grouping scope that will be added to coditions. * @return int Count of rows updated. */ - protected function _sync(array $fields, array $conditions, ?array $scope = null): int + protected function sync(array $fields, array $conditions, ?array $scope = null): int { if ($scope) { $conditions = array_merge($conditions, $scope); } - return $this->_table->updateAll($fields, $conditions); + return $this->table->updateAll($fields, $conditions); } /** @@ -550,11 +550,11 @@ protected function _sync(array $fields, array $conditions, ?array $scope = null) * @param string $direction Whether to increment or decrement the field. * @return \Cake\Database\Expression\QueryExpression QueryExpression to modify the order field */ - protected function _getUpdateExpression(string $direction = '+'): QueryExpression + protected function getUpdateExpression(string $direction = '+'): QueryExpression { - $field = $this->_config['sequenceField']; + $field = $this->config['sequenceField']; - return $this->_table->selectQuery()->expr() + return $this->table->selectQuery()->expr() ->add(new IdentifierExpression($field)) ->add('1') ->setConjunction($direction); diff --git a/tests/TestCase/Model/Behavior/SequenceBehaviorTest.php b/tests/TestCase/Model/Behavior/SequenceBehaviorTest.php index 5fef156..ab8fb48 100644 --- a/tests/TestCase/Model/Behavior/SequenceBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/SequenceBehaviorTest.php @@ -5,6 +5,7 @@ use Cake\ORM\Entity; use Cake\TestSuite\TestCase; +use TestApp\Model\Entity\Item; use TestApp\Model\Table\GroupedItemsTable; use TestApp\Model\Table\ItemsTable; use TestApp\Model\Table\KeywordItemsTable; @@ -19,6 +20,38 @@ class SequenceBehaviorTest extends TestCase 'plugin.ADmad/Sequence.UniqueItems', ]; + public function testFindOrdering(): void + { + $Items = $this->getTableLocator()->get('Items', [ + 'className' => ItemsTable::class, + ]); + $Items->updateAll(['position' => -1], ['id' => 5]); + + $ids = $Items->find()->all()->extract('id')->toList(); + $this->assertSame([5, 1, 2, 3, 4], $ids); + + $ids = $Items->find()->orderBy(['id' => 'DESC'])->all()->extract('id')->toList(); + $this->assertSame([5, 4, 3, 2, 1], $ids); + } + + public function testSetOrderWithGuardedSequenceField(): void + { + $Items = $this->getTableLocator()->get('Items', [ + 'className' => ItemsTable::class, + ]); + $entity = $Items->get(5); + $this->assertInstanceOf(Item::class, $entity); + $this->assertFalse($entity->isPatchable('id')); + $entity->setPatchable('position', false); + + $result = $Items->getBehavior('Sequence')->setOrder([$entity]); + + $this->assertTrue($result); + $this->assertTrue($entity->isPatchable('position')); + $this->assertSame(0, $Items->get(5)->position); + $this->assertTrue($Items->hasBehavior('Sequence')); + } + /** * [testSave description]. * @@ -241,7 +274,7 @@ public function testSetOrder() $Items->getValidator()->requirePresence('name'); // Array of arrays - $result = $Items->setOrder([ + $result = $Items->getBehavior('Sequence')->setOrder([ ['id' => 4], ['id' => 3], ['id' => 2], @@ -252,7 +285,7 @@ public function testSetOrder() $this->assertOrder([4, 3, 2, 1, 5], $Items); // Array of ids - $result = $Items->setOrder([5, 4, 2, 1, 3]); + $result = $Items->getBehavior('Sequence')->setOrder([5, 4, 2, 1, 3]); $this->assertTrue($result); $this->assertOrder([5, 4, 2, 1, 3], $Items); @@ -267,12 +300,12 @@ public function testSetOrder() ['id' => 1], ['id' => 5], ], - ['accessibleFields' => ['id' => true]], + ['patchableFields' => ['id' => true]], ); foreach ($entities as &$entity) { $entity->setNew(false); } - $result = $Items->setOrder($entities); + $result = $Items->getBehavior('Sequence')->setOrder($entities); $this->assertTrue($result); $this->assertOrder([4, 3, 2, 1, 5], $Items); @@ -282,7 +315,7 @@ public function testSetOrder() 'className' => GroupedItemsTable::class, ]); - $result = $GroupedItems->setOrder([ + $result = $GroupedItems->getBehavior('Sequence')->setOrder([ ['id' => 4], ['id' => 3], ['id' => 2], @@ -307,14 +340,14 @@ public function testMoveUp() ]); $entity = $Items->get(4); - $result = $Items->moveUp($entity); + $result = $Items->getBehavior('Sequence')->moveUp($entity); $this->assertTrue($result); $this->assertOrder([1, 2, 4, 3, 5], $Items); // Move up entity already at first position $entity = $Items->get(1); - $result = $Items->moveUp($entity); + $result = $Items->getBehavior('Sequence')->moveUp($entity); $this->assertTrue($result); $this->assertOrder([1, 2, 4, 3, 5], $Items); @@ -326,14 +359,14 @@ public function testMoveUp() ]); $entity = $GroupedItems->get(4); - $result = $GroupedItems->moveUp($entity); + $result = $GroupedItems->getBehavior('Sequence')->moveUp($entity); $this->assertTrue($result); $this->assertOrder([1, 2, 4, 3, 5], $GroupedItems, ['group_field' => 1]); // Move up entity already at first position $entity = $GroupedItems->get(1); - $result = $GroupedItems->moveUp($entity); + $result = $GroupedItems->getBehavior('Sequence')->moveUp($entity); $this->assertTrue($result); $this->assertOrder([1, 2, 4, 3, 5], $GroupedItems, ['group_field' => 1]); @@ -351,14 +384,14 @@ public function testMoveDown() ]); $entity = $Items->get(2); - $result = $Items->moveDown($entity); + $result = $Items->getBehavior('Sequence')->moveDown($entity); $this->assertTrue($result); $this->assertOrder([1, 3, 2, 4, 5], $Items); // Move down entity already at last position $entity = $Items->get(5); - $result = $Items->moveDown($entity); + $result = $Items->getBehavior('Sequence')->moveDown($entity); $this->assertTrue($result); $this->assertOrder([1, 3, 2, 4, 5], $Items); @@ -370,14 +403,14 @@ public function testMoveDown() ]); $entity = $GroupedItems->get(2); - $result = $GroupedItems->moveDown($entity); + $result = $GroupedItems->getBehavior('Sequence')->moveDown($entity); $this->assertTrue($result); $this->assertOrder([1, 3, 2, 4, 5], $GroupedItems, ['group_field' => 1]); // Move down entity already at last position $entity = $GroupedItems->get(5); - $result = $GroupedItems->moveDown($entity); + $result = $GroupedItems->getBehavior('Sequence')->moveDown($entity); $this->assertTrue($result); $this->assertOrder([1, 3, 2, 4, 5], $GroupedItems, ['group_field' => 1]); diff --git a/tests/test_app/src/Model/Entity/Item.php b/tests/test_app/src/Model/Entity/Item.php index ef026ad..33e2385 100644 --- a/tests/test_app/src/Model/Entity/Item.php +++ b/tests/test_app/src/Model/Entity/Item.php @@ -7,7 +7,7 @@ class Item extends Entity { - protected array $_accessible = ['*' => true, 'id' => false]; + protected array $patchable = ['*' => true, 'id' => false]; - protected array $_virtual = ['virutal_name']; + protected array $virtual = ['virutal_name']; } diff --git a/tests/test_app/src/Model/Table/ItemsTable.php b/tests/test_app/src/Model/Table/ItemsTable.php index bfd53cc..1ef8005 100644 --- a/tests/test_app/src/Model/Table/ItemsTable.php +++ b/tests/test_app/src/Model/Table/ItemsTable.php @@ -8,7 +8,7 @@ class ItemsTable extends Table { - protected ?string $_entityClass = Item::class; + protected ?string $entityClass = Item::class; public function initialize(array $config): void {