diff --git a/.github/workflows/repository_pack.yml b/.github/workflows/repository_pack.yml new file mode 100644 index 0000000..546101d --- /dev/null +++ b/.github/workflows/repository_pack.yml @@ -0,0 +1,61 @@ +name: Repository Pack + +on: + push: + branches-ignore: + - main + release: + types: [ created ] + schedule: + - + cron: "0 1 * * 6" # Run at 1am every Saturday + workflow_dispatch: + +permissions: + actions: write + contents: write + pull-requests: write + +jobs: + cqrs-pack: + runs-on: ubuntu-latest + + name: "Tests (PHP ${{ matrix.php }})" + + strategy: + fail-fast: false + matrix: + php: ["8.3"] + + steps: + - uses: actions/checkout@v4 + + - name: "Setup PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php }}" + ini-values: date.timezone=Europe/Warsaw + extensions: intl, gd, mysql, pdo_mysql, :xdebug + tools: symfony + coverage: none + + - name: "Get Composer cache directory" + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: "Setup cache" + uses: actions/cache@v3 + with: + path: | + ${{ steps.composer-cache.outputs.dir }} + key: ${{ github.run_id }}-${{ runner.os }}-${{ hashFiles('composer.json') }}-symfony-${{ matrix.symfony }} + + - name: "Create test application" + run: | + git config --global user.email "you@example.com" + git config --global user.name "Your Name" + (cd tests/Cqrs && make create-test-application) + id: end-of-setup + + - name: "Lint container" + run: (cd tests/Cqrs/app && bin/console lint:container) diff --git a/monofony/repository-meta/0.1/manifest.json b/monofony/repository-meta/0.1/manifest.json new file mode 100644 index 0000000..f3145ed --- /dev/null +++ b/monofony/repository-meta/0.1/manifest.json @@ -0,0 +1,5 @@ +{ + "copy-from-recipe": { + "src/": "%SRC_DIR%/" + } +} diff --git a/monofony/repository-meta/0.1/src/Infrastructure/Doctrine/DoctrinePaginator.php b/monofony/repository-meta/0.1/src/Infrastructure/Doctrine/DoctrinePaginator.php new file mode 100644 index 0000000..2b3f8ae --- /dev/null +++ b/monofony/repository-meta/0.1/src/Infrastructure/Doctrine/DoctrinePaginator.php @@ -0,0 +1,74 @@ + + */ +final readonly class DoctrinePaginator implements PaginatorInterface +{ + private int $firstResult; + private int $maxResults; + + /** + * @param Paginator $paginator + */ + public function __construct( + private Paginator $paginator, + ) { + $firstResult = $paginator->getQuery()->getFirstResult(); + $maxResults = $paginator->getQuery()->getMaxResults(); + + if (null === $maxResults) { + throw new \InvalidArgumentException('Missing maxResults from the query.'); + } + + $this->firstResult = $firstResult; + $this->maxResults = $maxResults; + } + + public function getItemsPerPage(): int + { + return $this->maxResults; + } + + public function getCurrentPage(): int + { + if (0 >= $this->maxResults) { + return 1; + } + + return (int) (1 + floor($this->firstResult / $this->maxResults)); + } + + public function getLastPage(): int + { + if (0 >= $this->maxResults) { + return 1; + } + + return (int) (ceil($this->getTotalItems() / $this->maxResults) ?: 1); + } + + public function getTotalItems(): int + { + return count($this->paginator); + } + + public function count(): int + { + return iterator_count($this->getIterator()); + } + + public function getIterator(): \Traversable + { + return $this->paginator->getIterator(); + } +} diff --git a/monofony/repository-meta/0.1/src/Infrastructure/Doctrine/DoctrineRepository.php b/monofony/repository-meta/0.1/src/Infrastructure/Doctrine/DoctrineRepository.php new file mode 100644 index 0000000..ae2830a --- /dev/null +++ b/monofony/repository-meta/0.1/src/Infrastructure/Doctrine/DoctrineRepository.php @@ -0,0 +1,116 @@ + + */ +abstract class DoctrineRepository implements RepositoryInterface +{ + private int|null $page = null; + private int|null $itemsPerPage = null; + + private QueryBuilder $queryBuilder; + + public function __construct( + protected EntityManagerInterface $em, + string $entityClass, + string $alias, + ) { + $this->queryBuilder = $this->em->createQueryBuilder() + ->select($alias) + ->from($entityClass, $alias); + } + + public function getIterator(): \Iterator + { + if (null !== $paginator = $this->paginator()) { + yield from $paginator; + + return; + } + + yield from $this->queryBuilder->getQuery()->getResult(); + } + + public function count(): int + { + $paginator = $this->paginator() ?? new Paginator(clone $this->queryBuilder); + + return $paginator->count(); + } + + public function paginator(): PaginatorInterface|null + { + if (null === $this->page || null === $this->itemsPerPage) { + return null; + } + + $firstResult = ($this->page - 1) * $this->itemsPerPage; + $maxResults = $this->itemsPerPage; + + $repository = $this->filter(static function (QueryBuilder $qb) use ($firstResult, $maxResults) { + $qb->setFirstResult($firstResult)->setMaxResults($maxResults); + }); + + /** @var Paginator $paginator */ + $paginator = new Paginator($repository->queryBuilder->getQuery()); + + return new DoctrinePaginator($paginator); + } + + public function withoutPagination(): static + { + $cloned = clone $this; + $cloned->page = null; + $cloned->itemsPerPage = null; + + return $cloned; + } + + public function withPagination(int $page, int $itemsPerPage): static + { + Assert::positiveInteger($page); + Assert::positiveInteger($itemsPerPage); + + $cloned = clone $this; + $cloned->page = $page; + $cloned->itemsPerPage = $itemsPerPage; + + return $cloned; + } + + /** + * @return static + * + * @phpstan-ignore-next-line + */ + protected function filter(callable $filter): static + { + $cloned = clone $this; + $filter($cloned->queryBuilder); + + return $cloned; + } + + protected function query(): QueryBuilder + { + return clone $this->queryBuilder; + } + + protected function __clone() + { + $this->queryBuilder = clone $this->queryBuilder; + } +} diff --git a/monofony/repository-meta/0.1/src/Infrastructure/Pagerfanta/PagerfantaPaginator.php b/monofony/repository-meta/0.1/src/Infrastructure/Pagerfanta/PagerfantaPaginator.php new file mode 100644 index 0000000..6115482 --- /dev/null +++ b/monofony/repository-meta/0.1/src/Infrastructure/Pagerfanta/PagerfantaPaginator.php @@ -0,0 +1,46 @@ +pagerfanta->getIterator(); + } + + public function count(): int + { + return iterator_count($this->pagerfanta->getIterator()); + } + + public function getCurrentPage(): int + { + return $this->pagerfanta->getCurrentPage(); + } + + public function getItemsPerPage(): int + { + return $this->pagerfanta->getMaxPerPage(); + } + + public function getLastPage(): int + { + return $this->pagerfanta->getNbPages(); + } + + public function getTotalItems(): int + { + return $this->pagerfanta->getNbResults(); + } +} diff --git a/monofony/repository-meta/0.1/src/Shared/Domain/Repository/PaginatorInterface.php b/monofony/repository-meta/0.1/src/Shared/Domain/Repository/PaginatorInterface.php new file mode 100644 index 0000000..b9ce557 --- /dev/null +++ b/monofony/repository-meta/0.1/src/Shared/Domain/Repository/PaginatorInterface.php @@ -0,0 +1,21 @@ + + */ +interface PaginatorInterface extends \IteratorAggregate, \Countable +{ + public function getCurrentPage(): int; + + public function getItemsPerPage(): int; + + public function getLastPage(): int; + + public function getTotalItems(): int; +} diff --git a/monofony/repository-meta/0.1/src/Shared/Domain/Repository/RepositoryInterface.php b/monofony/repository-meta/0.1/src/Shared/Domain/Repository/RepositoryInterface.php new file mode 100644 index 0000000..d94eca5 --- /dev/null +++ b/monofony/repository-meta/0.1/src/Shared/Domain/Repository/RepositoryInterface.php @@ -0,0 +1,32 @@ + + */ +interface RepositoryInterface extends \IteratorAggregate, \Countable +{ + /** + * @return \Iterator + */ + public function getIterator(): \Iterator; + + public function count(): int; + + /** + * @return PaginatorInterface|null + */ + public function paginator(): PaginatorInterface|null; + + /** + * @return static + * + * @phpstan-ignore-next-line + */ + public function withPagination(int $page, int $itemsPerPage): static; +} diff --git a/src/meta/repository-meta/composer.json b/src/meta/repository-meta/composer.json new file mode 100644 index 0000000..fac04e0 --- /dev/null +++ b/src/meta/repository-meta/composer.json @@ -0,0 +1,6 @@ +{ + "name": "monofony/repository-meta", + "type": "metapackage", + "license": "MIT", + "description": "A meta package providing recipes for repositories" +} diff --git a/src/pack/cqrs-pack/composer.json b/src/pack/cqrs-pack/composer.json index 69b2da3..b7d9759 100644 --- a/src/pack/cqrs-pack/composer.json +++ b/src/pack/cqrs-pack/composer.json @@ -5,7 +5,7 @@ "license": "MIT", "require": { "php": "^8.3", - "monofony/cqrs-meta": "^0.1.4", + "monofony/cqrs-meta": "*", "symfony/messenger": "*" } } diff --git a/src/pack/repository-pack/composer.json b/src/pack/repository-pack/composer.json new file mode 100644 index 0000000..af4e480 --- /dev/null +++ b/src/pack/repository-pack/composer.json @@ -0,0 +1,12 @@ +{ + "name": "monofony/repository-pack", + "description": "Pack for repositories", + "type": "symfony-pack", + "license": "MIT", + "require": { + "php": "^8.3", + "doctrine/orm": "*", + "monofony/repository-meta": "*", + "pagerfanta/core": "*" + } +} diff --git a/tests/Repository/.gitignore b/tests/Repository/.gitignore new file mode 100644 index 0000000..b80f0bd --- /dev/null +++ b/tests/Repository/.gitignore @@ -0,0 +1 @@ +app diff --git a/tests/Repository/Makefile b/tests/Repository/Makefile new file mode 100644 index 0000000..4b8e96b --- /dev/null +++ b/tests/Repository/Makefile @@ -0,0 +1,23 @@ +TEST_APP_BASENAME=app +TEST_APP_DIR?=${TEST_APP_BASENAME} + +.PHONY: create-test-application +create-test-application: remove-test-application install-skeleton add-requirements copy-recipe + +.PHONY: remove-test-application +remove-test-application: + rm -rf ${TEST_APP_BASENAME} + +.PHONY: install-skeleton +install-skeleton: + symfony new ${TEST_APP_BASENAME} + +.PHONY: add-requirements +add-requirements: + (cd ${TEST_APP_BASENAME} && composer require doctrine/orm) + (cd ${TEST_APP_BASENAME} && composer require pagerfanta/core) + (cd ${TEST_APP_BASENAME} && composer require --dev phpunit/phpunit) + +.PHONY: copy-recipe +copy-recipe: + cp -R ./../../monofony/repository-meta/0.1/* ${TEST_APP_DIR}