diff --git a/CHANGELOG.md b/CHANGELOG.md index 3974932..8f30ab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ * refactor: remove `RefreshableCount` interface * refactor: `MinimalReadableRecollection` now extends `Countable` * refactor: cleanup count traits +* refactor: refactor counting, change default strategy to + `ConditionalDelegatedCountStrategy` ## 0.6.2 diff --git a/packages/collections-common/src/Configuration.php b/packages/collections-common/src/Configuration.php index 7b479d8..9ecde0b 100644 --- a/packages/collections-common/src/Configuration.php +++ b/packages/collections-common/src/Configuration.php @@ -14,6 +14,8 @@ namespace Rekalogika\Domain\Collections\Common; use Doctrine\Common\Collections\Order; +use Rekalogika\Domain\Collections\Common\Count\ConditionalDelegatedCountStrategy; +use Rekalogika\Domain\Collections\Common\Count\CountStrategy; final class Configuration { @@ -39,4 +41,28 @@ final class Configuration * @var non-empty-array */ public static array $defaultOrderBy = ['id' => Order::Descending]; + + /** + * @var null|\Closure(): CountStrategy + */ + private static ?\Closure $defaultCountStrategy = null; + + /** + * @param \Closure(): CountStrategy $defaultCountStrategy + */ + public static function setDefaultCountStrategy(\Closure $defaultCountStrategy): void + { + self::$defaultCountStrategy = $defaultCountStrategy; + } + + public static function getDefaultCountStrategy(): CountStrategy + { + if (self::$defaultCountStrategy === null) { + $countStrategy = fn (): CountStrategy => new ConditionalDelegatedCountStrategy(); + } else { + $countStrategy = self::$defaultCountStrategy; + } + + return $countStrategy(); + } } diff --git a/packages/collections-common/src/Count/ConditionalDelegatedCountStrategy.php b/packages/collections-common/src/Count/ConditionalDelegatedCountStrategy.php new file mode 100644 index 0000000..8d4ef66 --- /dev/null +++ b/packages/collections-common/src/Count/ConditionalDelegatedCountStrategy.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Domain\Collections\Common\Count; + +use Rekalogika\Domain\Collections\Common\Exception\GettingCountUnsupportedException; +use Rekalogika\Domain\Collections\Common\Exception\SettingCountUnsupportedException; + +class ConditionalDelegatedCountStrategy implements CountStrategy +{ + public function __construct( + private ?int $softLimit = 5000, + private ?int $hardLimit = null, + private ?float $durationThreshold = 1, + ) { + } + + public function getCount(?\Countable $underlyingObject): int + { + if ($underlyingObject === null) { + throw new GettingCountUnsupportedException('The underlying object is not provided'); + } + + $start = microtime(true); + $result = \count($underlyingObject); + $duration = microtime(true) - $start; + + if ($this->hardLimit !== null && $result > $this->hardLimit) { + throw new GettingCountUnsupportedException(sprintf('The count exceeds the threshold of %d. You should refactor and use other counting strategy. Count duration: %d s', $this->hardLimit, $duration)); + } elseif ($this->softLimit !== null && $result > $this->softLimit) { + @trigger_error(sprintf('The count exceeds the warning threshold of %d. As it might impact performance, you should refactor and use other counting strategy. Count duration: %d s.', $this->softLimit, $duration), \E_USER_DEPRECATED); + } elseif ($duration > $this->durationThreshold) { + @trigger_error(sprintf('The count duration is %d s. You should consider refactoring and using other counting strategy.', $duration), \E_USER_DEPRECATED); + } + + return $result; + } + + public function setCount(?\Countable $underlyingObject, int $count): void + { + throw new SettingCountUnsupportedException('Setting count is disabled'); + } +} diff --git a/packages/collections-common/src/Trait/CountableTrait.php b/packages/collections-common/src/Trait/CountableTrait.php index cb7750f..049a513 100644 --- a/packages/collections-common/src/Trait/CountableTrait.php +++ b/packages/collections-common/src/Trait/CountableTrait.php @@ -13,12 +13,13 @@ namespace Rekalogika\Domain\Collections\Common\Trait; +use Rekalogika\Domain\Collections\Common\Configuration; use Rekalogika\Domain\Collections\Common\Count\CountStrategy; use Rekalogika\Domain\Collections\Common\Exception\InvalidCountException; trait CountableTrait { - abstract private function getCountStrategy(): CountStrategy; + abstract private function getCountStrategy(): ?CountStrategy; abstract private function getUnderlyingCountable(): ?\Countable; /** @@ -26,7 +27,9 @@ abstract private function getUnderlyingCountable(): ?\Countable; */ final public function count(): int { - $result = $this->getCountStrategy()->getCount($this->getUnderlyingCountable()); + $countStrategy = $this->getCountStrategy() ?? Configuration::getDefaultCountStrategy(); + + $result = $countStrategy->getCount($this->getUnderlyingCountable()); if ($result >= 0) { return $result; @@ -37,7 +40,9 @@ final public function count(): int final public function refreshCount(): void { + $countStrategy = $this->getCountStrategy() ?? Configuration::getDefaultCountStrategy(); + $realCount = \count($this->getUnderlyingCountable()); - $this->getCountStrategy()->setCount($this->getUnderlyingCountable(), $realCount); + $countStrategy->setCount($this->getUnderlyingCountable(), $realCount); } } diff --git a/packages/collections-domain/src/CriteriaPageable.php b/packages/collections-domain/src/CriteriaPageable.php index adc5397..bcc23eb 100644 --- a/packages/collections-domain/src/CriteriaPageable.php +++ b/packages/collections-domain/src/CriteriaPageable.php @@ -20,7 +20,7 @@ use Rekalogika\Contracts\Collections\Exception\UnexpectedValueException; use Rekalogika\Contracts\Rekapager\PageableInterface; use Rekalogika\Domain\Collections\Common\Count\CountStrategy; -use Rekalogika\Domain\Collections\Common\Count\RestrictedCountStrategy; +use Rekalogika\Domain\Collections\Common\Trait\CountableTrait; use Rekalogika\Domain\Collections\Common\Trait\PageableTrait; use Rekalogika\Domain\Collections\Trait\RecollectionPageableTrait; @@ -29,7 +29,7 @@ * @template T * @implements PageableInterface */ -class CriteriaPageable implements PageableInterface +class CriteriaPageable implements PageableInterface, \Countable { /** @use RecollectionPageableTrait */ use RecollectionPageableTrait; @@ -37,6 +37,8 @@ class CriteriaPageable implements PageableInterface /** @use PageableTrait */ use PageableTrait; + use CountableTrait; + /** * @var null|\WeakMap>> */ @@ -48,7 +50,6 @@ class CriteriaPageable implements PageableInterface private readonly Selectable $collection; private readonly Criteria $criteria; - private readonly CountStrategy $count; /** * @param ReadableCollection|Selectable $collection @@ -59,7 +60,7 @@ final private function __construct( ?Criteria $criteria = null, private readonly ?string $indexBy = null, private readonly int $itemsPerPage = 50, - ?CountStrategy $count = null, + private readonly ?CountStrategy $count = null, ) { // save collection @@ -78,10 +79,6 @@ final private function __construct( } $this->criteria = $criteria; - - // save count strategy - - $this->count = $count ?? new RestrictedCountStrategy(); } /** @@ -158,4 +155,9 @@ private function getUnderlyingCountable(): \Countable { return $this->collection->matching($this->criteria); } + + private function getCountStrategy(): ?CountStrategy + { + return $this->count; + } } diff --git a/packages/collections-domain/src/CriteriaRecollection.php b/packages/collections-domain/src/CriteriaRecollection.php index 7d3cd7c..47eb9da 100644 --- a/packages/collections-domain/src/CriteriaRecollection.php +++ b/packages/collections-domain/src/CriteriaRecollection.php @@ -20,7 +20,6 @@ use Rekalogika\Contracts\Collections\Exception\UnexpectedValueException; use Rekalogika\Contracts\Collections\ReadableRecollection; use Rekalogika\Domain\Collections\Common\Count\CountStrategy; -use Rekalogika\Domain\Collections\Common\Count\RestrictedCountStrategy; use Rekalogika\Domain\Collections\Common\Trait\ReadableRecollectionTrait; use Rekalogika\Domain\Collections\Common\Trait\SafeCollectionTrait; use Rekalogika\Domain\Collections\Trait\CriteriaReadableTrait; @@ -65,7 +64,6 @@ class CriteriaRecollection implements ReadableRecollection private readonly Selectable $collection; private readonly Criteria $criteria; - private readonly CountStrategy $count; /** * @param ReadableCollection|Selectable $collection @@ -78,7 +76,7 @@ final private function __construct( ?Criteria $criteria = null, private readonly ?string $indexBy = null, private readonly int $itemsPerPage = 50, - ?CountStrategy $count = null, + private readonly ?CountStrategy $count = null, private readonly ?int $softLimit = null, private readonly ?int $hardLimit = null, ) { @@ -99,10 +97,6 @@ final private function __construct( } $this->criteria = $criteria; - - // save count strategy - - $this->count = $count ?? new RestrictedCountStrategy(); } /** @@ -168,7 +162,7 @@ final public static function create( return $newInstance; } - private function getCountStrategy(): CountStrategy + private function getCountStrategy(): ?CountStrategy { return $this->count; } diff --git a/packages/collections-domain/src/MinimalCriteriaRecollection.php b/packages/collections-domain/src/MinimalCriteriaRecollection.php index 6216338..5761acf 100644 --- a/packages/collections-domain/src/MinimalCriteriaRecollection.php +++ b/packages/collections-domain/src/MinimalCriteriaRecollection.php @@ -20,7 +20,6 @@ use Rekalogika\Contracts\Collections\Exception\UnexpectedValueException; use Rekalogika\Contracts\Collections\MinimalReadableRecollection; use Rekalogika\Domain\Collections\Common\Count\CountStrategy; -use Rekalogika\Domain\Collections\Common\Count\RestrictedCountStrategy; use Rekalogika\Domain\Collections\Common\Trait\MinimalReadableRecollectionTrait; use Rekalogika\Domain\Collections\Common\Trait\SafeCollectionTrait; use Rekalogika\Domain\Collections\Trait\RecollectionPageableTrait; @@ -52,7 +51,6 @@ class MinimalCriteriaRecollection implements MinimalReadableRecollection, \Count private readonly Selectable $collection; private readonly Criteria $criteria; - private readonly CountStrategy $count; /** * @param ReadableCollection|Selectable $collection @@ -63,7 +61,7 @@ final private function __construct( ?Criteria $criteria = null, private readonly ?string $indexBy = null, private readonly int $itemsPerPage = 50, - ?CountStrategy $count = null, + private readonly ?CountStrategy $count = null, ) { // save collection @@ -82,10 +80,6 @@ final private function __construct( } $this->criteria = $criteria; - - // save count strategy - - $this->count = $count ?? new RestrictedCountStrategy(); } /** @@ -163,7 +157,7 @@ final public static function create( return $newInstance; } - private function getCountStrategy(): CountStrategy + private function getCountStrategy(): ?CountStrategy { return $this->count; } diff --git a/packages/collections-domain/src/MinimalRecollectionDecorator.php b/packages/collections-domain/src/MinimalRecollectionDecorator.php index a09fe72..94d56f0 100644 --- a/packages/collections-domain/src/MinimalRecollectionDecorator.php +++ b/packages/collections-domain/src/MinimalRecollectionDecorator.php @@ -22,7 +22,6 @@ use Rekalogika\Contracts\Rekapager\PageableInterface; use Rekalogika\Domain\Collections\Common\Configuration; use Rekalogika\Domain\Collections\Common\Count\CountStrategy; -use Rekalogika\Domain\Collections\Common\Count\RestrictedCountStrategy; use Rekalogika\Domain\Collections\Common\Internal\OrderByUtil; use Rekalogika\Domain\Collections\Common\Trait\MinimalRecollectionTrait; use Rekalogika\Domain\Collections\Trait\RecollectionPageableTrait; @@ -56,7 +55,6 @@ class MinimalRecollectionDecorator implements MinimalRecollection private readonly array $orderBy; private readonly Criteria $criteria; - private readonly CountStrategy $count; /** * @param Collection $collection @@ -68,7 +66,7 @@ final private function __construct( array|string|null $orderBy = null, private readonly ?string $indexBy = null, private readonly int $itemsPerPage = 50, - ?CountStrategy $count = null, + private readonly ?CountStrategy $count = null, ) { // handle collection @@ -86,10 +84,6 @@ final private function __construct( ); $this->criteria = Criteria::create()->orderBy($this->orderBy); - - // handle count strategy - - $this->count = $count ?? new RestrictedCountStrategy(); } /** @@ -149,7 +143,7 @@ final public static function create( return $newInstance; } - private function getCountStrategy(): CountStrategy + private function getCountStrategy(): ?CountStrategy { return $this->count; } diff --git a/packages/collections-domain/src/RecollectionDecorator.php b/packages/collections-domain/src/RecollectionDecorator.php index 901c559..ed5d901 100644 --- a/packages/collections-domain/src/RecollectionDecorator.php +++ b/packages/collections-domain/src/RecollectionDecorator.php @@ -21,7 +21,6 @@ use Rekalogika\Contracts\Collections\Recollection; use Rekalogika\Domain\Collections\Common\Configuration; use Rekalogika\Domain\Collections\Common\Count\CountStrategy; -use Rekalogika\Domain\Collections\Common\Count\RestrictedCountStrategy; use Rekalogika\Domain\Collections\Common\Internal\OrderByUtil; use Rekalogika\Domain\Collections\Common\Trait\RecollectionTrait; use Rekalogika\Domain\Collections\Common\Trait\SafeCollectionTrait; @@ -76,7 +75,6 @@ class RecollectionDecorator implements Recollection private readonly array $orderBy; private readonly Criteria $criteria; - private readonly CountStrategy $count; /** * @param Collection $collection @@ -90,7 +88,7 @@ final private function __construct( array|string|null $orderBy = null, private readonly ?string $indexBy = null, private readonly int $itemsPerPage = 50, - ?CountStrategy $count = null, + private readonly ?CountStrategy $count = null, private readonly ?int $softLimit = null, private readonly ?int $hardLimit = null, ) { @@ -110,10 +108,6 @@ final private function __construct( ); $this->criteria = Criteria::create()->orderBy($this->orderBy); - - // handle count strategy - - $this->count = $count ?? new RestrictedCountStrategy(); } /** @@ -179,7 +173,7 @@ final public static function create( return $newInstance; } - private function getCountStrategy(): CountStrategy + private function getCountStrategy(): ?CountStrategy { return $this->count; } diff --git a/packages/collections-domain/src/Trait/RecollectionPageableTrait.php b/packages/collections-domain/src/Trait/RecollectionPageableTrait.php index c45f6f6..ad0950f 100644 --- a/packages/collections-domain/src/Trait/RecollectionPageableTrait.php +++ b/packages/collections-domain/src/Trait/RecollectionPageableTrait.php @@ -48,7 +48,7 @@ private function getPageable(): PageableInterface $count = function (): int|bool { try { - return $this->count->getCount($this->getUnderlyingCountable()); + return $this->count(); } catch (GettingCountUnsupportedException) { return false; } diff --git a/packages/collections-orm/src/AbstractMinimalRepository.php b/packages/collections-orm/src/AbstractMinimalRepository.php index f81417f..5d95b03 100644 --- a/packages/collections-orm/src/AbstractMinimalRepository.php +++ b/packages/collections-orm/src/AbstractMinimalRepository.php @@ -51,7 +51,7 @@ abstract class AbstractMinimalRepository implements MinimalRepository */ private int $itemsPerPage; - private readonly CountStrategy $count; + private readonly ?CountStrategy $count; private readonly QueryBuilder $queryBuilder; private readonly ?string $indexBy; @@ -96,7 +96,7 @@ private function getClass(): string return $this->class; } - private function getCountStrategy(): CountStrategy + private function getCountStrategy(): ?CountStrategy { return $this->count; } diff --git a/packages/collections-orm/src/AbstractRepository.php b/packages/collections-orm/src/AbstractRepository.php index 6770fdb..cc2a288 100644 --- a/packages/collections-orm/src/AbstractRepository.php +++ b/packages/collections-orm/src/AbstractRepository.php @@ -57,7 +57,7 @@ abstract class AbstractRepository implements Repository */ private int $itemsPerPage; - private readonly CountStrategy $count; + private readonly ?CountStrategy $count; private readonly QueryBuilder $queryBuilder; private readonly ?string $indexBy; @@ -105,7 +105,7 @@ final public function __construct( */ abstract protected function configure(): RepositoryConfiguration; - private function getCountStrategy(): CountStrategy + private function getCountStrategy(): ?CountStrategy { return $this->count; } diff --git a/packages/collections-orm/src/Configuration/MinimalRepositoryConfiguration.php b/packages/collections-orm/src/Configuration/MinimalRepositoryConfiguration.php index 3089680..d158845 100644 --- a/packages/collections-orm/src/Configuration/MinimalRepositoryConfiguration.php +++ b/packages/collections-orm/src/Configuration/MinimalRepositoryConfiguration.php @@ -15,7 +15,6 @@ use Doctrine\Common\Collections\Order; use Rekalogika\Domain\Collections\Common\Count\CountStrategy; -use Rekalogika\Domain\Collections\Common\Count\RestrictedCountStrategy; use Rekalogika\Domain\Collections\Common\Internal\OrderByUtil; /** @@ -27,7 +26,6 @@ class MinimalRepositoryConfiguration * @var non-empty-array */ private readonly array $orderBy; - private readonly CountStrategy $count; /** * @param class-string $class @@ -39,11 +37,9 @@ public function __construct( private readonly string $indexBy = 'id', array|string|null $orderBy = null, private readonly int $itemsPerPage = 50, - ?CountStrategy $count = null, + private readonly ?CountStrategy $count = null, ) { $this->orderBy = OrderByUtil::normalizeOrderBy($orderBy); - - $this->count = $count ?? new RestrictedCountStrategy(); } public function getIndexBy(): string @@ -75,7 +71,7 @@ public function getItemsPerPage(): int return $this->itemsPerPage; } - public function getCountStrategy(): CountStrategy + public function getCountStrategy(): ?CountStrategy { return $this->count; } diff --git a/packages/collections-orm/src/QueryCollection.php b/packages/collections-orm/src/QueryCollection.php index f6244c1..ad62afe 100644 --- a/packages/collections-orm/src/QueryCollection.php +++ b/packages/collections-orm/src/QueryCollection.php @@ -18,7 +18,6 @@ use Rekalogika\Contracts\Collections\ReadableRecollection; use Rekalogika\Contracts\Rekapager\PageableInterface; use Rekalogika\Domain\Collections\Common\Count\CountStrategy; -use Rekalogika\Domain\Collections\Common\Count\RestrictedCountStrategy; use Rekalogika\Domain\Collections\Common\Trait\PageableTrait; use Rekalogika\Domain\Collections\Common\Trait\ReadableCollectionTrait; use Rekalogika\Domain\Collections\Common\Trait\ReadableRecollectionTrait; @@ -46,8 +45,6 @@ class QueryCollection implements ReadableRecollection /** @use ReadableRecollectionTrait */ use ReadableRecollectionTrait; - private readonly CountStrategy $count; - /** * @param int<1,max> $itemsPerPage * @param null|int<1,max> $softLimit @@ -57,15 +54,14 @@ final public function __construct( private QueryBuilder $queryBuilder, private readonly int $itemsPerPage = 50, private readonly ?string $indexBy = null, - ?CountStrategy $count = null, + private readonly ?CountStrategy $count = null, private readonly ?int $softLimit = null, private readonly ?int $hardLimit = null, ) { - $this->count = $count ?? new RestrictedCountStrategy(); } - private function getCountStrategy(): CountStrategy + private function getCountStrategy(): ?CountStrategy { return $this->count; } diff --git a/packages/collections-orm/src/QueryPageable.php b/packages/collections-orm/src/QueryPageable.php index b3ebc20..e4b6fcc 100644 --- a/packages/collections-orm/src/QueryPageable.php +++ b/packages/collections-orm/src/QueryPageable.php @@ -17,7 +17,7 @@ use Rekalogika\Collections\ORM\Trait\QueryBuilderPageableTrait; use Rekalogika\Contracts\Rekapager\PageableInterface; use Rekalogika\Domain\Collections\Common\Count\CountStrategy; -use Rekalogika\Domain\Collections\Common\Count\RestrictedCountStrategy; +use Rekalogika\Domain\Collections\Common\Trait\CountableTrait; use Rekalogika\Domain\Collections\Common\Trait\PageableTrait; /** @@ -25,7 +25,7 @@ * @template T * @implements PageableInterface */ -class QueryPageable implements PageableInterface +class QueryPageable implements PageableInterface, \Countable { /** @use QueryBuilderPageableTrait */ use QueryBuilderPageableTrait; @@ -33,7 +33,7 @@ class QueryPageable implements PageableInterface /** @use PageableTrait */ use PageableTrait; - private readonly CountStrategy $count; + use CountableTrait; /** * @param int<1,max> $itemsPerPage @@ -42,9 +42,8 @@ final public function __construct( private QueryBuilder $queryBuilder, private readonly int $itemsPerPage = 50, private readonly ?string $indexBy = null, - ?CountStrategy $count = null, + private readonly ?CountStrategy $count = null, ) { - $this->count = $count ?? new RestrictedCountStrategy(); } /** @@ -104,4 +103,9 @@ final protected function createQueryPageable( count: $count, ); } + + private function getCountStrategy(): ?CountStrategy + { + return $this->count; + } } diff --git a/packages/collections-orm/src/Trait/QueryBuilderPageableTrait.php b/packages/collections-orm/src/Trait/QueryBuilderPageableTrait.php index a259102..a12e199 100644 --- a/packages/collections-orm/src/Trait/QueryBuilderPageableTrait.php +++ b/packages/collections-orm/src/Trait/QueryBuilderPageableTrait.php @@ -48,7 +48,7 @@ private function getPageable(): PageableInterface $count = function (): int|bool { try { - return $this->count->getCount($this->getUnderlyingCountable()); + return $this->count(); } catch (GettingCountUnsupportedException) { return false; } diff --git a/tests/src/UnitTests/Collections/CountTest.php b/tests/src/UnitTests/Collections/CountTest.php index 531eac8..9a2ac90 100644 --- a/tests/src/UnitTests/Collections/CountTest.php +++ b/tests/src/UnitTests/Collections/CountTest.php @@ -33,8 +33,8 @@ public function testDefaultCount(): void ]) ); - $this->expectException(GettingCountUnsupportedException::class); - $foo = \count($collection); + $count = \count($collection); + $this->assertEquals(3, $count); } public function testRestrictedCount(): void