diff --git a/src/Utilities/Repository.php b/src/Utilities/Repository.php index 65c07a9..b9d48b5 100644 --- a/src/Utilities/Repository.php +++ b/src/Utilities/Repository.php @@ -10,7 +10,6 @@ namespace Meritoo\Common\Utilities; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\QueryBuilder; -use ReflectionException; /** * Useful methods for repository @@ -20,73 +19,143 @@ use ReflectionException; */ class Repository { + /** + * Name of key responsible for sorting/position of an item in array + * + * @var string + */ + const POSITION_KEY = 'position'; + /** * Replenishes positions of given items * - * @param array $items The items - * @param bool $asLast (optional) If is set to true, items are placed at the end (default behaviour). Otherwise - * - at top. + * @param array $items Objects who have "getPosition()" and "setPosition()" methods or arrays + * @param bool $asLast (optional) If is set to true, items are placed at the end (default behaviour). Otherwise - + * at top. * @param bool $force (optional) If is set to true, positions are set even there is no extreme position. - * Otherwise - if extreme position is not found (is null) replenishment is stopped / skipped + * Otherwise - if extreme position is unknown (is null) replenishment is stopped / skipped * (default behaviour). - * - * @throws ReflectionException */ - public static function replenishPositions($items, $asLast = true, $force = false) + public static function replenishPositions(array &$items, $asLast = true, $force = false) { $position = self::getExtremePosition($items, $asLast); + /* + * Extreme position is unknown, but it's required? + * Use 0 as default/start value + */ if (null === $position && $force) { $position = 0; } - if (null !== $position && !empty($items)) { - foreach ($items as $item) { - if (method_exists($item, 'getPosition')) { - if (null === $item->getPosition()) { - if ($asLast) { - ++$position; - } else { - --$position; - } + /* + * Extreme position is unknown or there are no items to sort? + * Nothing to do + */ + if (null === $position || empty($items)) { + return; + } - if (method_exists($item, 'setPosition')) { - $item->setPosition($position); - } - } - } + foreach ($items as &$item) { + /* + * The item is not sortable? + */ + if (!self::isSortable($item)) { + continue; } + + /* + * Position has been set? + * Nothing to do + */ + if (self::isSorted($item)) { + continue; + } + + /* + * Calculate position + */ + if ($asLast) { + ++$position; + } else { + --$position; + } + + /* + * It's an object? + * Use proper method to set position + */ + if (is_object($item)) { + $item->setPosition($position); + continue; + } + + /* + * It's an array + * Use proper key to set position + */ + $item[static::POSITION_KEY] = $position; } } /** * Returns extreme position (max or min) of given items * - * @param array $items The items + * @param array $items Objects who have "getPosition()" and "setPosition()" methods or arrays * @param bool $max (optional) If is set to true, maximum value is returned. Otherwise - minimum. * @return int - * - * @throws ReflectionException */ - public static function getExtremePosition($items, $max = true) + public static function getExtremePosition(array $items, $max = true) { + /* + * No items? + * Nothing to do + */ + if (empty($items)) { + return null; + } + $extreme = null; - if (!empty($items)) { - foreach ($items as $item) { - if (Reflection::hasMethod($item, 'getPosition')) { - $position = $item->getPosition(); + foreach ($items as $item) { + /* + * The item is not sortable? + */ + if (!self::isSortable($item)) { + continue; + } - if ($max) { - if ($position > $extreme) { - $extreme = $position; - } - } else { - if ($position < $extreme) { - $extreme = $position; - } - } + $position = null; + + /* + * Let's grab the position + */ + if (is_object($item)) { + $position = $item->getPosition(); + } elseif (array_key_exists(static::POSITION_KEY, $item)) { + $position = $item[static::POSITION_KEY]; + } + + /* + * Maximum value is expected? + */ + if ($max) { + /* + * Position was found and it's larger than previously found position (the extreme position)? + */ + if (null === $extreme || (null !== $position && $position > $extreme)) { + $extreme = $position; } + + continue; + } + + /* + * Minimum value is expected here. + * Position was found and it's smaller than previously found position (the extreme position)? + */ + if (null === $extreme || (null !== $position && $position < $extreme)) { + $extreme = $position; } } @@ -113,4 +182,54 @@ class Repository ->createQueryBuilder($alias) ->orderBy(sprintf('%s.%s', $alias, $property), $direction); } + + /** + * Returns information if given item is sortable + * + * Sortable means it's an: + * - array + * or + * - object and has getPosition() and setPosition() + * + * @param mixed $item An item to verify (object who has "getPosition()" and "setPosition()" methods or an array) + * @return bool + */ + private static function isSortable($item) + { + return is_array($item) + || + ( + is_object($item) + && + Reflection::hasMethod($item, 'getPosition') + && + Reflection::hasMethod($item, 'setPosition') + ); + } + + /** + * Returns information if given item is sorted (position has been set) + * + * @param mixed $item An item to verify (object who has "getPosition()" and "setPosition()" methods or an array) + * @return bool + */ + private static function isSorted($item) + { + /* + * Given item is not sortable? + */ + if (!self::isSortable($item)) { + return false; + } + + /* + * It's an object or it's an array + * and position has been set? + */ + + return + (is_object($item) && null !== $item->getPosition()) + || + (is_array($item) && isset($item[static::POSITION_KEY])); + } } diff --git a/tests/Utilities/Repository/Sortable.php b/tests/Utilities/Repository/Sortable.php new file mode 100644 index 0000000..0e4f2db --- /dev/null +++ b/tests/Utilities/Repository/Sortable.php @@ -0,0 +1,66 @@ + + * @copyright Meritoo.pl + */ +class Sortable +{ + /** + * Position used while sorting + * + * @var int + */ + private $position; + + /** + * Class constructor + * + * @param int $position (optional) Position used while sorting + */ + public function __construct($position = null) + { + $this->position = $position; + } + + /** + * Returns position used while sorting + * + * @return int + */ + public function getPosition() + { + return $this->position; + } + + /** + * Sets position used while sorting + * + * @param int $position Position used while sorting + */ + public function setPosition($position) + { + $this->position = $position; + } + + /** + * Returns representation of object as string + * + * @return string + */ + public function __toString() + { + return sprintf('%s (position: %d)', self::class, $this->getPosition()); + } +} diff --git a/tests/Utilities/RepositoryTest.php b/tests/Utilities/RepositoryTest.php new file mode 100644 index 0000000..3739a30 --- /dev/null +++ b/tests/Utilities/RepositoryTest.php @@ -0,0 +1,603 @@ + + * @copyright Meritoo.pl + */ +class RepositoryTest extends BaseTestCase +{ + public function testConstructor() + { + static::assertHasNoConstructor(Repository::class); + } + + public function testReplenishPositionsWithoutItems() + { + $items = []; + Repository::replenishPositions($items); + + static::assertEquals([], $items); + } + + public function testReplenishPositionsUsingNotSortableObjects() + { + $before = [ + new stdClass(), + new stdClass(), + new stdClass(), + ]; + + $after = [ + new stdClass(), + new stdClass(), + new stdClass(), + ]; + + /* + * Using defaults + */ + Repository::replenishPositions($before); + static::assertEquals($before, $after); + + /* + * Place items at the top + */ + Repository::replenishPositions($before, false); + static::assertEquals($before, $after); + + /* + * Set positions even there is no extreme position (at the end) + */ + Repository::replenishPositions($before, true, true); + static::assertEquals($before, $after); + + /* + * Set positions even there is no extreme position (at the top) + */ + Repository::replenishPositions($before, false, true); + static::assertEquals($before, $after); + } + + /** + * @param array $items Objects who have "getPosition()" and "setPosition()" methods or arrays + * @dataProvider provideArraysWithoutExtremePosition + */ + public function testReplenishPositionsUsingArraysWithoutExtremePosition(array $items) + { + Repository::replenishPositions($items); + static::assertEquals($items, $items); + + Repository::replenishPositions($items, false); + static::assertEquals($items, $items); + } + + /** + * @param array $items Objects who have "getPosition()" and "setPosition()" methods or arrays + * @param bool $asLast If is set to true, items are placed at the end (default behaviour). Otherwise - at top. + * @param array $expected Items with replenished positions + * + * @dataProvider provideArraysWithoutExtremePosition + */ + public function testReplenishPositionsUsingArraysWithoutExtremePositionForce(array $items, $asLast, array $expected) + { + Repository::replenishPositions($items, $asLast, true); + static::assertEquals($expected, $items); + } + + /** + * @param array $items Objects who have "getPosition()" and "setPosition()" methods or arrays + * @param bool $asLast If is set to true, items are placed at the end (default behaviour). Otherwise - at top. + * @param array $expected Items with replenished positions + * + * @dataProvider provideArraysWithExtremePosition + */ + public function testReplenishPositionsUsingArraysWithExtremePositionForce(array $items, $asLast, array $expected) + { + Repository::replenishPositions($items, $asLast, true); + static::assertEquals($expected, $items); + } + + /** + * @param array $items Objects who have "getPosition()" and "setPosition()" methods or arrays + * @dataProvider provideObjectsWithoutExtremePosition + */ + public function testReplenishPositionsUsingObjectsWithoutExtremePosition(array $items) + { + Repository::replenishPositions($items); + static::assertEquals($items, $items); + + Repository::replenishPositions($items, false); + static::assertEquals($items, $items); + } + + /** + * @param array $items Objects who have "getPosition()" and "setPosition()" methods or arrays + * @param bool $asLast If is set to true, items are placed at the end (default behaviour). Otherwise - at top. + * @param array $expected Items with replenished positions + * + * @dataProvider provideObjectsWithoutExtremePosition + */ + public function testReplenishPositionsUsingObjectsWithoutExtremePositionForce(array $items, $asLast, array $expected) + { + Repository::replenishPositions($items, $asLast, true); + static::assertEquals($expected, $items); + } + + /** + * @param array $items Objects who have "getPosition()" and "setPosition()" methods or arrays + * @param bool $asLast If is set to true, items are placed at the end (default behaviour). Otherwise - at top. + * @param array $expected Items with replenished positions + * + * @dataProvider provideObjectsWithExtremePosition + */ + public function testReplenishPositionsUsingObjectsWithExtremePositionForce(array $items, $asLast, array $expected) + { + Repository::replenishPositions($items, $asLast, true); + static::assertEquals($expected, $items); + } + + public function testGetExtremePositionWithoutItems() + { + static::assertNull(Repository::getExtremePosition([])); + static::assertNull(Repository::getExtremePosition([], false)); + } + + /** + * @param array $items Objects who have "getPosition()" and "setPosition()" methods or arrays + * @param bool $max (optional) If is set to true, maximum value is returned. Otherwise - minimum. + * @param int $expected Extreme position (max or min) of given items + * + * @dataProvider provideArraysWithoutExtremePositionToGetExtremePosition + */ + public function testGetExtremePositionUsingArraysWithoutExtremePosition(array $items, $max, $expected) + { + static::assertEquals($expected, Repository::getExtremePosition($items, $max)); + } + + public function testGetExtremePositionUsingObjects() + { + } + + public function testGetEntityOrderedQueryBuilder() + { + } + + /** + * Provides arrays without extreme position used to replenish positions of them + * + * @return Generator + */ + public function provideArraysWithoutExtremePosition() + { + yield[ + [ + [], + [], + ], + true, + [ + [ + Repository::POSITION_KEY => 1, + ], + [ + Repository::POSITION_KEY => 2, + ], + ], + ]; + + yield[ + [ + [], + [], + ], + false, + [ + [ + Repository::POSITION_KEY => -1, + ], + [ + Repository::POSITION_KEY => -2, + ], + ], + ]; + + yield[ + [ + [ + 'lorem' => 'ipsum', + 'dolor', + 'sit' => 1, + ], + [ + 'abc' => 'def', + 'ghi' => null, + 'jkl' => 10, + ], + ], + true, + [ + [ + 'lorem' => 'ipsum', + 'dolor', + 'sit' => 1, + Repository::POSITION_KEY => 1, + ], + [ + 'abc' => 'def', + 'ghi' => null, + 'jkl' => 10, + Repository::POSITION_KEY => 2, + ], + ], + ]; + + yield[ + [ + [ + 'lorem' => 'ipsum', + 'dolor', + 'sit' => 1, + ], + [ + 'abc' => 'def', + 'ghi' => null, + 'jkl' => 10, + ], + ], + false, + [ + [ + 'lorem' => 'ipsum', + 'dolor', + 'sit' => 1, + Repository::POSITION_KEY => -1, + ], + [ + 'abc' => 'def', + 'ghi' => null, + 'jkl' => 10, + Repository::POSITION_KEY => -2, + ], + ], + ]; + } + + /** + * Provides arrays with extreme position used to replenish positions of them + * + * @return Generator + */ + public function provideArraysWithExtremePosition() + { + yield[ + [ + [ + Repository::POSITION_KEY => 1, + ], + [], + [], + ], + true, + [ + [ + Repository::POSITION_KEY => 1, + ], + [ + Repository::POSITION_KEY => 2, + ], + [ + Repository::POSITION_KEY => 3, + ], + ], + ]; + + yield[ + [ + [], + [], + [ + Repository::POSITION_KEY => 1, + ], + ], + true, + [ + [ + Repository::POSITION_KEY => 2, + ], + [ + Repository::POSITION_KEY => 3, + ], + [ + Repository::POSITION_KEY => 1, + ], + ], + ]; + + yield[ + [ + [ + Repository::POSITION_KEY => 1, + ], + [], + [], + ], + false, + [ + [ + Repository::POSITION_KEY => 1, + ], + [ + Repository::POSITION_KEY => 0, + ], + [ + Repository::POSITION_KEY => -1, + ], + ], + ]; + + yield[ + [ + + [], + [], + [ + Repository::POSITION_KEY => 1, + ], + ], + false, + [ + [ + Repository::POSITION_KEY => 0, + ], + [ + Repository::POSITION_KEY => -1, + ], + [ + Repository::POSITION_KEY => 1, + ], + ], + ]; + } + + /** + * Provides objects without extreme position used to replenish positions of them + * + * @return Generator + */ + public function provideObjectsWithoutExtremePosition() + { + yield[ + [ + new Sortable(), + new Sortable(), + new Sortable(), + ], + true, + [ + new Sortable(1), + new Sortable(2), + new Sortable(3), + ], + ]; + + yield[ + [ + new Sortable(), + new Sortable(), + new Sortable(), + ], + false, + [ + new Sortable(-1), + new Sortable(-2), + new Sortable(-3), + ], + ]; + } + + /** + * Provides objects with extreme position used to replenish positions of them + * + * @return Generator + */ + public function provideObjectsWithExtremePosition() + { + yield[ + [ + new Sortable(1), + new Sortable(), + new Sortable(), + ], + true, + [ + new Sortable(1), + new Sortable(2), + new Sortable(3), + ], + ]; + + yield[ + [ + new Sortable(), + new Sortable(1), + new Sortable(), + ], + true, + [ + new Sortable(2), + new Sortable(1), + new Sortable(3), + ], + ]; + + yield[ + [ + new Sortable(1), + new Sortable(), + new Sortable(), + ], + false, + [ + new Sortable(1), + new Sortable(0), + new Sortable(-1), + ], + ]; + } + + /** + * Provides arrays without extreme position used to get extreme position + * + * @return Generator + */ + public function provideArraysWithoutExtremePositionToGetExtremePosition() + { + yield[ + [], + false, + null, + ]; + + yield[ + [], + true, + null, + ]; + + yield[ + [ + [ + 'lorem' => 'ipsum', + 'dolor', + 'sit' => 1, + ], + [ + 'abc' => 'def', + 'ghi' => null, + 'jkl' => 10, + ], + ], + true, + null, + ]; + + yield[ + [ + [ + 'lorem' => 'ipsum', + 'dolor', + 'sit' => 1, + ], + [ + 'abc' => 'def', + 'ghi' => null, + 'jkl' => 10, + ], + ], + false, + null, + ]; + } + + /** + * Provides arrays with extreme position used to get extreme position + * + * @return Generator + */ + public function provideArraysWithExtremePositionToGetExtremePosition() + { + yield[ + [ + [ + Repository::POSITION_KEY => 1, + ], + [], + [], + ], + true, + 1, + ]; + + yield[ + [ + [ + Repository::POSITION_KEY => 1, + ], + [], + [], + ], + false, + 1, + ]; + + yield[ + [ + [], + [], + [ + Repository::POSITION_KEY => 1, + ], + ], + true, + 1, + ]; + + yield[ + [ + [], + [], + [ + Repository::POSITION_KEY => 1, + ], + ], + false, + 1, + ]; + + yield[ + [ + [ + Repository::POSITION_KEY => 1, + ], + [], + [ + Repository::POSITION_KEY => 2, + ], + [], + ], + true, + 1, + ]; + + yield[ + [ + [ + Repository::POSITION_KEY => 1, + ], + [], + [ + Repository::POSITION_KEY => 2, + ], + [], + ], + false, + 2, + ]; + } +}