├── .github ├── dependabot.yml └── workflows │ └── build.yaml ├── LICENSE ├── composer.json ├── ecs.php └── src ├── Batch ├── Batch.php ├── BatchInterface.php ├── CollectionBatch.php ├── CollectionBatchInterface.php ├── RangeBatch.php └── RangeBatchInterface.php ├── Batcher ├── Batcher.php ├── BatcherInterface.php ├── Collection │ ├── CollectionBatcher.php │ ├── CollectionBatcherInterface.php │ ├── IdCollectionBatcher.php │ └── ObjectCollectionBatcher.php └── Range │ ├── IdRangeBatcher.php │ ├── NaiveIdRangeBatcher.php │ ├── NaiveIdRangeBatcherInterface.php │ ├── RangeBatcher.php │ └── RangeBatcherInterface.php ├── Exception ├── ExceptionInterface.php ├── LowerBoundIsGreaterThanUpperBoundException.php └── NoManagerException.php ├── Factory ├── BatcherFactory.php └── BatcherFactoryInterface.php └── Query ├── QueryRebuilder.php └── QueryRebuilderInterface.php /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | 5 | updates: 6 | - allow: 7 | - dependency-type: "development" 8 | commit-message: 9 | include: "scope" 10 | prefix: "composer" 11 | directory: "/" 12 | package-ecosystem: "composer" 13 | schedule: 14 | interval: "weekly" 15 | versioning-strategy: "increase" 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: "build" 2 | on: 3 | push: 4 | branches: 5 | - "0.6.x" 6 | paths-ignore: 7 | - "*.md" 8 | pull_request: 9 | paths-ignore: 10 | - "*.md" 11 | workflow_dispatch: ~ 12 | 13 | jobs: 14 | coding-standards: 15 | name: "Coding Standards" 16 | 17 | runs-on: "ubuntu-latest" 18 | 19 | strategy: 20 | matrix: 21 | php-version: 22 | - "7.4" 23 | 24 | dependencies: 25 | - "highest" 26 | 27 | steps: 28 | - name: "Checkout" 29 | uses: "actions/checkout@v2" 30 | 31 | - name: "Setup PHP, with composer and extensions" 32 | uses: "shivammathur/setup-php@v2" 33 | with: 34 | php-version: "${{ matrix.php-version }}" 35 | extensions: "${{ env.PHP_EXTENSIONS }}" 36 | coverage: "none" 37 | 38 | - name: "Install composer dependencies" 39 | uses: "ramsey/composer-install@v3" 40 | with: 41 | dependency-versions: "${{ matrix.dependencies }}" 42 | 43 | - name: "Validate composer" 44 | run: "composer validate --strict || true" 45 | 46 | - name: "Check composer normalized" 47 | run: "composer normalize --dry-run" 48 | 49 | - name: "Check style" 50 | run: "composer check-style" 51 | 52 | dependency-analysis: 53 | name: "Dependency Analysis" 54 | 55 | runs-on: "ubuntu-latest" 56 | 57 | strategy: 58 | matrix: 59 | php-version: 60 | - "7.4" 61 | - "8.0" 62 | - "8.1" 63 | - "8.2" 64 | 65 | dependencies: 66 | - "highest" 67 | 68 | steps: 69 | - name: "Checkout" 70 | uses: "actions/checkout@v2" 71 | 72 | - name: "Setup PHP, with composer and extensions" 73 | uses: "shivammathur/setup-php@v2" 74 | with: 75 | coverage: "none" 76 | extensions: "${{ env.PHP_EXTENSIONS }}" 77 | php-version: "${{ matrix.php-version }}" 78 | tools: "composer-require-checker, composer-unused" 79 | 80 | - name: "Install composer dependencies" 81 | uses: "ramsey/composer-install@v3" 82 | with: 83 | dependency-versions: "${{ matrix.dependencies }}" 84 | 85 | - name: "Run maglnet/composer-require-checker" 86 | run: "composer-require-checker check" 87 | 88 | - name: "Run composer-unused/composer-unused" 89 | run: "composer-unused" 90 | 91 | static-code-analysis: 92 | name: "Static Code Analysis" 93 | 94 | runs-on: "ubuntu-latest" 95 | 96 | strategy: 97 | matrix: 98 | php-version: 99 | - "7.4" 100 | - "8.0" 101 | - "8.1" 102 | - "8.2" 103 | 104 | dependencies: 105 | - "highest" 106 | 107 | steps: 108 | - name: "Checkout" 109 | uses: "actions/checkout@v4" 110 | 111 | - name: "Setup PHP, with composer and extensions" 112 | uses: "shivammathur/setup-php@v2" 113 | with: 114 | php-version: "${{ matrix.php-version }}" 115 | extensions: "${{ env.PHP_EXTENSIONS }}" 116 | coverage: "none" 117 | 118 | - name: "Install composer dependencies" 119 | uses: "ramsey/composer-install@v3" 120 | with: 121 | dependency-versions: "${{ matrix.dependencies }}" 122 | 123 | - name: "Static analysis" 124 | run: "composer analyse -- --php-version=${{ matrix.php-version }}" 125 | 126 | unit-tests: 127 | name: "Unit tests" 128 | 129 | runs-on: "ubuntu-latest" 130 | 131 | strategy: 132 | matrix: 133 | php-version: 134 | - "7.4" 135 | - "8.0" 136 | - "8.1" 137 | - "8.2" 138 | 139 | dependencies: 140 | - "lowest" 141 | - "highest" 142 | 143 | steps: 144 | - name: "Checkout" 145 | uses: "actions/checkout@v4" 146 | 147 | - name: "Setup PHP, with composer and extensions" 148 | uses: "shivammathur/setup-php@v2" 149 | with: 150 | php-version: "${{ matrix.php-version }}" 151 | extensions: "${{ env.PHP_EXTENSIONS }}" 152 | coverage: "none" 153 | 154 | - name: "Install composer dependencies" 155 | uses: "ramsey/composer-install@v3" 156 | with: 157 | dependency-versions: "${{ matrix.dependencies }}" 158 | 159 | - name: "Run phpunit" 160 | run: "composer phpunit" 161 | 162 | code-coverage: 163 | name: "Code Coverage" 164 | 165 | runs-on: "ubuntu-latest" 166 | 167 | strategy: 168 | matrix: 169 | php-version: 170 | - "7.4" 171 | - "8.0" 172 | 173 | dependencies: 174 | - "highest" 175 | 176 | steps: 177 | - name: "Checkout" 178 | uses: "actions/checkout@v4" 179 | 180 | - name: "Setup PHP, with composer and extensions" 181 | uses: "shivammathur/setup-php@v2" 182 | with: 183 | coverage: "pcov" 184 | extensions: "${{ env.PHP_EXTENSIONS }}" 185 | php-version: "${{ matrix.php-version }}" 186 | 187 | - name: "Set up problem matchers for phpunit/phpunit" 188 | run: "echo \"::add-matcher::${{ runner.tool_cache }}/phpunit.json\"" 189 | 190 | - name: "Install composer dependencies" 191 | uses: "ramsey/composer-install@v3" 192 | with: 193 | dependency-versions: "${{ matrix.dependencies }}" 194 | 195 | - name: "Collect code coverage with pcov and phpunit/phpunit" 196 | run: "vendor/bin/phpunit --coverage-clover=.build/logs/clover.xml" 197 | 198 | - name: "Send code coverage report to Codecov.io" 199 | env: 200 | CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}" 201 | run: "bash <(curl -s https://codecov.io/bash)" 202 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Setono 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "setono/doctrine-orm-batcher", 3 | "description": "A library for processing large collections in Doctrine", 4 | "license": "MIT", 5 | "type": "library", 6 | "authors": [ 7 | { 8 | "name": "Joachim Løvgaard", 9 | "email": "joachim@loevgaard.dk" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7.4", 14 | "doctrine/collections": "^1.6 || ^2.0", 15 | "doctrine/orm": "^2.8 || ^3.0", 16 | "doctrine/persistence": "^1.3 || ^2.1 || ^3.0", 17 | "symfony/property-access": "^5.4 || ^6.0", 18 | "webmozart/assert": "^1.10" 19 | }, 20 | "require-dev": { 21 | "doctrine/annotations": "^2.0", 22 | "doctrine/data-fixtures": "^1.5", 23 | "phpunit/phpunit": "^9.5.10", 24 | "psalm/plugin-phpunit": "^0.19.0", 25 | "setono/code-quality-pack": "^2.1.3", 26 | "symfony/cache": "^5.4 || ^6.0", 27 | "weirdan/doctrine-psalm-plugin": "^2.0" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Setono\\DoctrineORMBatcher\\": "src/" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Tests\\Setono\\DoctrineORMBatcher\\": "tests/" 37 | } 38 | }, 39 | "config": { 40 | "allow-plugins": { 41 | "dealerdirect/phpcodesniffer-composer-installer": true, 42 | "ergebnis/composer-normalize": true 43 | }, 44 | "sort-packages": true 45 | }, 46 | "extra": { 47 | "branch-alias": { 48 | "dev-master": "1.0-dev" 49 | } 50 | }, 51 | "scripts": { 52 | "analyse": "psalm", 53 | "check-style": "ecs check", 54 | "fix-style": "ecs check --fix", 55 | "phpunit": "phpunit" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | import('vendor/sylius-labs/coding-standard/ecs.php'); 12 | $containerConfigurator->parameters()->set(Option::PATHS, [ 13 | 'src', 'tests' 14 | ]); 15 | }; 16 | } 17 | 18 | return static function (ECSConfig $containerConfigurator): void { 19 | $containerConfigurator->import('vendor/sylius-labs/coding-standard/ecs.php'); 20 | $containerConfigurator->paths(['src', 'tests']); 21 | }; 22 | -------------------------------------------------------------------------------- /src/Batch/Batch.php: -------------------------------------------------------------------------------- 1 | getRootEntities(); 21 | if (0 === count($rootEntities)) { 22 | throw new InvalidArgumentException('The number of root entities on the query builder must be one or more'); 23 | } 24 | 25 | $this->class = $rootEntities[0]; 26 | $this->dql = $qb->getDQL(); 27 | $this->parameters = $qb->getParameters()->toArray(); 28 | } 29 | 30 | public function getClass(): string 31 | { 32 | return $this->class; 33 | } 34 | 35 | public function getDql(): string 36 | { 37 | return $this->dql; 38 | } 39 | 40 | public function getParameters(): array 41 | { 42 | return $this->parameters; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Batch/BatchInterface.php: -------------------------------------------------------------------------------- 1 | collection = $collection; 18 | 19 | $qb->setParameter(self::PARAMETER_COLLECTION, $this->getCollection()); 20 | 21 | /** @psalm-suppress ImpureMethodCall */ 22 | parent::__construct($qb); 23 | } 24 | 25 | public function getCollection(): array 26 | { 27 | return $this->collection; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Batch/CollectionBatchInterface.php: -------------------------------------------------------------------------------- 1 | $upperBound) { 23 | throw new LowerBoundIsGreaterThanUpperBoundException($lowerBound, $upperBound); 24 | } 25 | 26 | $this->lowerBound = $lowerBound; 27 | $this->upperBound = $upperBound; 28 | 29 | $qb->setParameter(self::PARAMETER_LOWER_BOUND, $this->getLowerBound()); 30 | $qb->setParameter(self::PARAMETER_UPPER_BOUND, $this->getUpperBound()); 31 | 32 | /** @psalm-suppress ImpureMethodCall */ 33 | parent::__construct($qb); 34 | } 35 | 36 | public function getLowerBound(): int 37 | { 38 | return $this->lowerBound; 39 | } 40 | 41 | public function getUpperBound(): int 42 | { 43 | return $this->upperBound; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Batch/RangeBatchInterface.php: -------------------------------------------------------------------------------- 1 | qb = clone $qb; 40 | $this->identifier = $identifier; 41 | $this->clearOnBatch = $clearOnBatch; 42 | 43 | $rootAliases = $this->qb->getRootAliases(); 44 | if (1 !== count($rootAliases)) { 45 | throw new InvalidArgumentException('The query builder must have exactly one root alias'); // todo better exception 46 | } 47 | 48 | $this->alias = $rootAliases[0]; 49 | 50 | $this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() 51 | ->enableExceptionOnInvalidIndex() 52 | ->enableExceptionOnInvalidPropertyPath() 53 | ->getPropertyAccessor() 54 | ; 55 | } 56 | 57 | public function getBatchCount(int $batchSize = 100): int 58 | { 59 | return (int) ceil($this->getCount() / $batchSize); 60 | } 61 | 62 | /** 63 | * Notice that the $select must include the identifier in some way. 64 | * If the $select is null the original select statement will be used. 65 | * 66 | * @psalm-suppress ReservedWord 67 | */ 68 | protected function getResult(string $select = null, int $batchSize = 100): iterable 69 | { 70 | $qb = $this->getQueryBuilder(); 71 | 72 | if (null !== $select) { 73 | $qb->select($select); 74 | } 75 | 76 | $qb->orderBy(sprintf('%s.%s', $this->alias, $this->identifier), 'ASC') 77 | ->andWhere(sprintf('%s.%s > :lastId', $this->alias, $this->identifier)) 78 | ->setMaxResults($batchSize) 79 | ; 80 | 81 | $lastId = 0; 82 | 83 | while (true) { 84 | $this->clear(); 85 | 86 | $qb->setParameter('lastId', $lastId); 87 | $result = $qb->getQuery()->getResult(); 88 | 89 | if (0 === count($result)) { 90 | break; 91 | } 92 | 93 | $lastRow = $result[count($result) - 1]; 94 | 95 | $propertyPath = is_array($lastRow) ? sprintf('[%s]', $this->identifier) : $this->identifier; 96 | $lastId = $this->propertyAccessor->getValue($lastRow, $propertyPath); 97 | 98 | yield $result; 99 | } 100 | 101 | $this->clear(); 102 | } 103 | 104 | private function clear(): void 105 | { 106 | if (!$this->clearOnBatch) { 107 | return; 108 | } 109 | 110 | $this->qb->getEntityManager()->clear(); 111 | } 112 | 113 | /** 114 | * This is made to avoid side effects by passing around the query builder object. 115 | */ 116 | protected function getQueryBuilder(): QueryBuilder 117 | { 118 | return clone $this->qb; 119 | } 120 | 121 | /** 122 | * This will return a query builder where the constraints for the respective batcher are added. 123 | */ 124 | abstract protected function getBatchableQueryBuilder(): QueryBuilder; 125 | 126 | protected function getMin(): int 127 | { 128 | if (null === $this->min) { 129 | $this->initMinMax(); 130 | } 131 | 132 | Assert::notNull($this->min); 133 | 134 | return $this->min; 135 | } 136 | 137 | protected function getMax(): int 138 | { 139 | if (null === $this->max) { 140 | $this->initMinMax(); 141 | } 142 | 143 | Assert::notNull($this->max); 144 | 145 | return $this->max; 146 | } 147 | 148 | protected function getCount(): int 149 | { 150 | if (null === $this->count) { 151 | $this->initCount(); 152 | } 153 | 154 | Assert::notNull($this->count); 155 | 156 | return $this->count; 157 | } 158 | 159 | private function initMinMax(): void 160 | { 161 | $qb = $this->getQueryBuilder(); 162 | 163 | $qb->select(sprintf('MIN(%s.%s) as min, MAX(%s.%s) as max', $this->alias, $this->identifier, $this->alias, $this->identifier)); 164 | 165 | $res = $qb->getQuery()->getScalarResult(); 166 | if (count($res) < 1) { 167 | throw new NoResultException(); 168 | } 169 | 170 | $row = $res[0]; 171 | 172 | if (null === $row['min'] || null === $row['max']) { 173 | throw new NoResultException(); 174 | } 175 | 176 | $this->min = (int) $row['min']; 177 | $this->max = (int) $row['max']; 178 | } 179 | 180 | private function initCount(): void 181 | { 182 | $qb = $this->getQueryBuilder(); 183 | $qb->select(sprintf('COUNT(%s) as c', $this->alias)); 184 | 185 | $this->count = (int) $qb->getQuery()->getSingleScalarResult(); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Batcher/BatcherInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function getBatches(int $batchSize = 100): iterable; 15 | 16 | /** 17 | * Returns the number of batches that will be returned. 18 | */ 19 | public function getBatchCount(int $batchSize = 100): int; 20 | } 21 | -------------------------------------------------------------------------------- /src/Batcher/Collection/CollectionBatcher.php: -------------------------------------------------------------------------------- 1 | getQueryBuilder(); 16 | $qb->andWhere(sprintf('%s.%s IN(:%s)', $this->alias, $this->identifier, CollectionBatch::PARAMETER_COLLECTION)); 17 | 18 | return $qb; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Batcher/Collection/CollectionBatcherInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function getBatches(int $batchSize = 100): iterable; 16 | } 17 | -------------------------------------------------------------------------------- /src/Batcher/Collection/IdCollectionBatcher.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function getBatches(int $batchSize = 100): iterable 16 | { 17 | $result = $this->getResult(sprintf('%s.%s', $this->alias, $this->identifier), $batchSize); 18 | 19 | foreach ($result as $ids) { 20 | $flattened = array_map(function ($elm) { 21 | return $elm[$this->identifier]; 22 | }, $ids); 23 | 24 | yield new CollectionBatch($flattened, $this->getBatchableQueryBuilder()); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Batcher/Collection/ObjectCollectionBatcher.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function getBatches(int $batchSize = 100): iterable 16 | { 17 | $result = $this->getResult(null, $batchSize); 18 | 19 | foreach ($result as $objects) { 20 | yield new CollectionBatch($objects, $this->getBatchableQueryBuilder()); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Batcher/Range/IdRangeBatcher.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function getBatches(int $batchSize = 100): iterable 16 | { 17 | $result = $this->getResult(sprintf('%s.%s', $this->alias, $this->identifier), $batchSize); 18 | 19 | foreach ($result as $ids) { 20 | // because we order the result set by id asc we know that the lowest number is on index 0 and the highest is on the last index 21 | yield new RangeBatch($ids[0]['id'], $ids[count($ids) - 1]['id'], $this->getBatchableQueryBuilder()); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Batcher/Range/NaiveIdRangeBatcher.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public function getBatches(int $batchSize = 100): iterable 19 | { 20 | try { 21 | $min = $this->getMin(); 22 | $max = $this->getMax(); 23 | } catch (NoResultException $e) { 24 | return; 25 | } 26 | 27 | $batches = (int) ceil((($max - $min) + 1) / $batchSize); 28 | 29 | for ($batch = 0; $batch < $batches; ++$batch) { 30 | $lastBatch = ($batch + 1 === $batches); 31 | 32 | $firstNumber = $batch * $batchSize + $min; 33 | $lastNumber = $lastBatch ? $max : ($firstNumber + $batchSize) - 1; 34 | 35 | yield new RangeBatch($firstNumber, $lastNumber, $this->getBatchableQueryBuilder()); 36 | } 37 | } 38 | 39 | /** 40 | * If the lowest id is 30 and the highest id is 190 the maximum number of rows is (190 - 30) + 1 = 161 41 | * If the number of rows is 145, then the sparseness is (161 - 145) / 161 * 100 = 9.94% and this method will return 10 in that case. 42 | */ 43 | public function getSparseness(): int 44 | { 45 | try { 46 | $bestPossibleCount = ($this->getMax() - $this->getMin()) + 1; 47 | } catch (NoResultException $e) { 48 | return 0; 49 | } 50 | 51 | return (int) round(($bestPossibleCount - $this->getCount()) / $bestPossibleCount * 100); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Batcher/Range/NaiveIdRangeBatcherInterface.php: -------------------------------------------------------------------------------- 1 | getQueryBuilder(); 16 | $qb->andWhere(sprintf('%s.%s >= :%s', $this->alias, $this->identifier, RangeBatch::PARAMETER_LOWER_BOUND)); 17 | $qb->andWhere(sprintf('%s.%s <= :%s', $this->alias, $this->identifier, RangeBatch::PARAMETER_UPPER_BOUND)); 18 | 19 | return $qb; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Batcher/Range/RangeBatcherInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function getBatches(int $batchSize = 100): iterable; 16 | } 17 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | lowerBound = $lowerBound; 20 | $this->upperBound = $upperBound; 21 | } 22 | 23 | public function getLowerBound(): int 24 | { 25 | return $this->lowerBound; 26 | } 27 | 28 | public function getUpperBound(): int 29 | { 30 | return $this->upperBound; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Exception/NoManagerException.php: -------------------------------------------------------------------------------- 1 | class = $class; 18 | } 19 | 20 | public function getClass(): string 21 | { 22 | return $this->class; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Factory/BatcherFactory.php: -------------------------------------------------------------------------------- 1 | objectCollectionBatcherClass = $objectCollectionBatcherClass; 29 | $this->idCollectionBatcherClass = $idCollectionBatcherClass; 30 | $this->naiveIdRangeBatcherClass = $naiveIdRangeBatcherClass; 31 | $this->idRangeBatcherClass = $idRangeBatcherClass; 32 | } 33 | 34 | public function createObjectCollectionBatcher( 35 | QueryBuilder $qb, 36 | string $identifier = 'id', 37 | bool $clearOnBatch = true 38 | ): CollectionBatcherInterface { 39 | return new $this->objectCollectionBatcherClass($qb, $identifier, $clearOnBatch); 40 | } 41 | 42 | public function createIdCollectionBatcher( 43 | QueryBuilder $qb, 44 | string $identifier = 'id', 45 | bool $clearOnBatch = true 46 | ): CollectionBatcherInterface { 47 | return new $this->idCollectionBatcherClass($qb, $identifier, $clearOnBatch); 48 | } 49 | 50 | public function createIdRangeBatcher( 51 | QueryBuilder $qb, 52 | string $identifier = 'id', 53 | bool $clearOnBatch = true, 54 | int $sparsenessThreshold = 5 55 | ): RangeBatcherInterface { 56 | /** @var NaiveIdRangeBatcherInterface $naiveIdBatcher */ 57 | $naiveIdBatcher = new $this->naiveIdRangeBatcherClass($qb, $identifier, $clearOnBatch); 58 | if ($naiveIdBatcher->getSparseness() <= $sparsenessThreshold) { 59 | return $naiveIdBatcher; 60 | } 61 | 62 | return new $this->idRangeBatcherClass($qb, $identifier, $clearOnBatch); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Factory/BatcherFactoryInterface.php: -------------------------------------------------------------------------------- 1 | managerRegistry = $managerRegistry; 21 | } 22 | 23 | public function rebuild(BatchInterface $batch): Query 24 | { 25 | $manager = $this->getManager($batch->getClass()); 26 | 27 | $q = $manager->createQuery($batch->getDql()); 28 | $q->setParameters(new ArrayCollection($batch->getParameters())); 29 | 30 | return $q; 31 | } 32 | 33 | private function getManager(string $class): EntityManagerInterface 34 | { 35 | /** @var EntityManagerInterface|null $manager */ 36 | $manager = $this->managerRegistry->getManagerForClass($class); 37 | if (null === $manager) { 38 | throw new NoManagerException($class); 39 | } 40 | 41 | return $manager; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Query/QueryRebuilderInterface.php: -------------------------------------------------------------------------------- 1 |