Browse Source

Add ToggleBehavior

dereuromark 8 years ago
parent
commit
0f9f65539f

+ 176 - 0
src/Model/Behavior/ToggleBehavior.php

@@ -0,0 +1,176 @@
+<?php
+
+namespace Tools\Model\Behavior;
+
+use ArrayObject;
+use Cake\Datasource\EntityInterface;
+use Cake\Event\Event;
+use Cake\ORM\Behavior;
+use Symfony\Component\Console\Exception\LogicException;
+
+/**
+ * ToggleBehavior
+ *
+ * An implementation of a unique field toggle per table or scope.
+ * This will ensure that on a set of records only one can be a "primary" one, setting the others to false then.
+ * On delete it will give the primary status to another record if applicable.
+ *
+ * @author Mark Scherer
+ * @license MIT
+ */
+class ToggleBehavior extends Behavior {
+
+	/**
+	 * Default config
+	 *
+	 * @var array
+	 */
+	protected $_defaultConfig = [
+		'field' => 'primary',
+		'on' => 'afterSave', // afterSave (without transactions) or beforeSave (with transactions)
+		'scopeFields' => [],
+		'scope' => [],
+		'findOrder' => null // null = autodetect modified/created
+	];
+
+	/**
+	 * @param \Cake\Event\Event $event
+	 * @param \Cake\Datasource\EntityInterface $entity
+	 * @param \ArrayObject $options
+	 *
+	 * @return void
+	 */
+	public function afterDelete(Event $event, EntityInterface $entity, ArrayObject $options) {
+		$field = $this->getConfig('field');
+
+		$value = $entity->get($field);
+		if (!$value) {
+			return;
+		}
+
+		$conditions = $this->buildConditions($entity);
+
+		$order = $this->getConfig('findOrder');
+		if ($order === null) {
+			$order = [];
+			if ($this->_table->schema()->column('modified')) {
+				$order['modified'] = 'DESC';
+			}
+		}
+		$entity = $this->_table->find()->where($conditions)->order($order)->first();
+		if (!$entity) {
+			// This should be caught with a validation rule if at least one "primary" must exist
+			return;
+		}
+
+		$entity->set($field, true);
+		$this->_table->saveOrFail($entity);
+	}
+
+	/**
+	 * @param \Cake\Event\Event $event
+	 * @param \Cake\Datasource\EntityInterface $entity
+	 * @param \ArrayObject $options
+	 * @return void
+	 */
+	public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options) {
+		$field = $this->getConfig('field');
+
+		if ($entity->isNew() && !$entity->get($field)) {
+			if (!$this->getCurrent($entity)) {
+				$entity->set($field, true);
+			}
+		}
+
+		if ($this->_config['on'] !== 'beforeSave') {
+			return;
+		}
+
+		$value = $entity->get($this->getConfig('field'));
+		if (!$value && !$this->getCurrent($entity)) {
+			// This should be caught with a validation rule as this is not normal behavior
+			throw new LogicException();
+		}
+
+		$this->removeFromOthers($entity);
+	}
+
+	/**
+	 * @param \Cake\Event\Event $event
+	 * @param \Cake\Datasource\EntityInterface $entity
+	 * @param \ArrayObject $options
+	 *
+	 * @return void
+	 */
+	public function afterSave(Event $event, EntityInterface $entity, ArrayObject $options) {
+		if ($this->_config['on'] !== 'afterSave') {
+			return;
+		}
+
+		if (!$entity->dirty($this->getConfig('field'))) {
+			return;
+		}
+
+		$value = $entity->get($this->getConfig('field'));
+		if (!$value && !$this->getCurrent($entity)) {
+			// This should be caught with a validation rule as this is not normal behavior
+			throw new LogicException();
+		}
+
+		$this->removeFromOthers($entity);
+	}
+
+	/**
+	 * @param \Cake\Datasource\EntityInterface $entity
+	 *
+	 * @return mixed
+	 */
+	protected function getCurrent(EntityInterface $entity) {
+		$conditions = $this->buildConditions($entity);
+
+		return $this->_table->find()
+			->where($conditions)
+			->first();
+	}
+
+	/**
+	 * @param \Cake\Datasource\EntityInterface $entity
+	 *
+	 * @return void
+	 */
+	protected function removeFromOthers(EntityInterface $entity) {
+		$field = $this->getConfig('field');
+		$id = $entity->get('id');
+		$conditions = $this->buildConditions($entity);
+		$this->_table->updateAll([$field => false], ['id !=' => $id] + $conditions);
+	}
+
+	/**
+	 * @param \Cake\Datasource\EntityInterface $entity
+	 *
+	 * @return mixed
+	 */
+	protected function buildConditions(EntityInterface $entity) {
+		$conditions = $this->config('scope');
+		$scopeFields = (array)$this->getConfig('scopeFields');
+		foreach ($scopeFields as $scopeField) {
+			$conditions[$scopeField] = $entity->get($scopeField);
+		}
+		return $conditions;
+	}
+
+	/**
+	 * @param \Cake\Datasource\EntityInterface $entity
+	 *
+	 * @return void
+	 */
+	public function toggleField(EntityInterface $entity) {
+		$field = $this->getConfig('field');
+		$id = $entity->get('id');
+		$conditions = $this->buildConditions($entity);
+
+		$this->_table->updateAll([$field => true], ['id' => $id] + $conditions);
+		$this->_table->updateAll([$field => false], ['id !=' => $id] + $conditions);
+	}
+
+}

+ 41 - 0
tests/Fixture/ToggleAddressesFixture.php

@@ -0,0 +1,41 @@
+<?php
+namespace Tools\Test\Fixture;
+
+use Cake\TestSuite\Fixture\TestFixture;
+
+class ToggleAddressesFixture extends TestFixture {
+
+	/**
+	 * Fields
+	 *
+	 * @var array
+	 */
+	public $fields = [
+		'id' => ['type' => 'integer', 'length' => 10, 'unsigned' => true, 'null' => false, 'default' => null, 'autoIncrement' => true, 'precision' => null],
+		'category_id' => ['type' => 'integer', 'length' => 10, 'unsigned' => true, 'null' => true, 'default' => null, 'precision' => null, 'autoIncrement' => null],
+		'name' => ['type' => 'string', 'length' => 60, 'null' => false, 'default' => null, 'precision' => null, 'fixed' => null],
+		'primary' => ['type' => 'boolean', 'length' => null, 'null' => false, 'default' => '0', 'precision' => null],
+		'created' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'precision' => null],
+		'modified' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'precision' => null],
+		'_constraints' => [
+			'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []],
+		],
+	];
+
+	/**
+	 * Records
+	 *
+	 * @var array
+	 */
+	public $records = [
+		[
+			'id' => 1,
+			'category_id' => 1,
+			'name' => 'Foo',
+			'primary' => 1,
+			'created' => '2017-04-02 15:45:33',
+			'modified' => '2017-04-02 15:45:33',
+		],
+	];
+
+}

+ 139 - 0
tests/TestCase/Model/Behavior/ToggleBehaviorTest.php

@@ -0,0 +1,139 @@
+<?php
+
+namespace Tools\Test\TestCase\Model\Behavior;
+
+use Cake\ORM\TableRegistry;
+use Tools\TestSuite\TestCase;
+
+class ToggleBehaviorTest extends TestCase {
+
+	/**
+	 * @var array
+	 */
+	public $fixtures = [
+		'plugin.tools.toggle_addresses'
+	];
+
+	/**
+	 * @var \Tools\Model\Table\Table|\Tools\Model\Behavior\ToggleBehavior
+	 */
+	public $Addresses;
+
+	/**
+	 * @return void
+	 */
+	public function setUp() {
+		parent::setUp();
+
+		$this->Addresses = TableRegistry::get('ToggleAddresses');
+		$this->Addresses->addBehavior('Tools.Toggle', ['scopeFields' => 'category_id']);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testSaveBasic() {
+		$data = [
+			'name' => 'Foo Bar',
+			'category_id' => 2,
+		];
+
+		$entity = $this->Addresses->newEntity($data);
+		$address = $this->Addresses->save($entity);
+		$this->assertTrue((bool)$address);
+		$this->assertTrue($address->primary);
+
+		$data = [
+			'name' => 'Foo Bar Ext',
+			'category_id' => 2,
+		];
+
+		$entity2 = $this->Addresses->newEntity($data);
+		$address = $this->Addresses->save($entity2);
+		$this->assertTrue((bool)$address);
+
+		$address = $this->Addresses->get($entity2->id);
+		$this->assertFalse($address->primary);
+
+		$address->primary = true;
+		$address = $this->Addresses->save($address);
+		$this->assertTrue((bool)$address);
+
+		$address = $this->Addresses->get($address->id);
+		$this->assertTrue($address->primary);
+
+		$address = $this->Addresses->get($entity->id);
+		$this->assertFalse($address->primary);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testDelete() {
+		$data = [
+			'name' => 'Foo Bar',
+			'category_id' => 2,
+		];
+		$entity = $this->Addresses->newEntity($data);
+		$address = $this->Addresses->save($entity);
+		$this->assertTrue((bool)$address);
+		$this->assertTrue($address->primary);
+
+		$data = [
+			'name' => 'Foo Bar Ext',
+			'category_id' => 2,
+			'primary' => true,
+		];
+		$address2 = $this->Addresses->newEntity($data);
+		$address2 = $this->Addresses->save($address2);
+		$this->assertTrue((bool)$address2);
+
+		$address = $this->Addresses->get($address->id);
+		$this->assertFalse($address->primary);
+
+		$address2 = $this->Addresses->get($address2->id);
+		$this->assertTrue($address2->primary);
+
+		$this->Addresses->delete($address2);
+		$address = $this->Addresses->get($address->id);
+		$this->assertTrue($address->primary);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testToggleField() {
+		$data = [
+			'name' => 'Foo Bar',
+			'category_id' => 2,
+		];
+		$entity = $this->Addresses->newEntity($data);
+		$address = $this->Addresses->save($entity);
+		$this->assertTrue((bool)$address);
+		$this->assertTrue($address->primary);
+
+		$data = [
+			'name' => 'Foo Bar Ext',
+			'category_id' => 2,
+			'primary' => true,
+		];
+		$address2 = $this->Addresses->newEntity($data);
+		$address2 = $this->Addresses->save($address2);
+		$this->assertTrue((bool)$address2);
+
+		$address = $this->Addresses->get($address->id);
+		$this->assertFalse($address->primary);
+
+		$address2 = $this->Addresses->get($address2->id);
+		$this->assertTrue($address2->primary);
+
+		$this->Addresses->toggleField($address);
+
+		$address = $this->Addresses->get($address->id);
+		$this->assertTrue($address->primary);
+
+		$address2 = $this->Addresses->get($address2->id);
+		$this->assertFalse($address2->primary);
+	}
+
+}