From c5701a237f6409580efbbe8faca6b5009bbea813 Mon Sep 17 00:00:00 2001 From: Sergio Mendolia Date: Sun, 10 Sep 2023 16:34:56 +0200 Subject: [PATCH 1/2] Refactor --- composer.json | 1 + composer.lock | 72 +++++- config/bundles.php | 1 + config/packages/security.yaml | 6 +- config/packages/twig.yaml | 2 + src/Command/RenameAuthorCommand.php | 60 ----- src/Command/RenameSerieCommand.php | 56 ----- src/Controller/AuthorController.php | 51 ---- .../AutocompleteGroupController.php | 3 +- src/Controller/DefaultController.php | 56 +---- src/Controller/GroupController.php | 55 +++++ src/Controller/SearchController.php | 27 --- src/Controller/SerieController.php | 54 ----- src/Controller/TagController.php | 52 ---- src/Entity/Book.php | 14 -- src/Entity/Shelf.php | 16 ++ src/Form/BookFilterType.php | 223 ++++++++++++++++++ src/Menu/MenuBuilder.php | 29 ++- src/Repository/BookRepository.php | 125 ++-------- src/Service/FilteredBookUrlGenerator.php | 68 ++++++ src/Twig/AddNewShelf.php | 35 +++ src/Twig/FilteredBookUrl.php | 37 +++ src/Twig/Search.php | 34 --- symfony.lock | 3 + templates/_menu.html.twig | 1 - templates/author/detail.html.twig | 18 -- templates/base.html.twig | 6 +- templates/book/_list.html.twig | 28 +-- templates/book/_search.html.twig | 32 --- templates/book/_teaser.html.twig | 4 +- templates/book/index.html.twig | 2 +- templates/components/AddNewShelf.html.twig | 23 +- templates/default/index.html.twig | 41 ++++ templates/group/index.html.twig | 31 ++- templates/search/index.html.twig | 8 - templates/serie/detail.html.twig | 15 -- 36 files changed, 664 insertions(+), 625 deletions(-) delete mode 100644 src/Command/RenameAuthorCommand.php delete mode 100644 src/Command/RenameSerieCommand.php delete mode 100644 src/Controller/AuthorController.php create mode 100644 src/Controller/GroupController.php delete mode 100644 src/Controller/SearchController.php delete mode 100644 src/Controller/SerieController.php delete mode 100644 src/Controller/TagController.php create mode 100644 src/Form/BookFilterType.php create mode 100644 src/Service/FilteredBookUrlGenerator.php create mode 100644 src/Twig/FilteredBookUrl.php delete mode 100644 src/Twig/Search.php delete mode 100644 templates/_menu.html.twig delete mode 100644 templates/author/detail.html.twig delete mode 100644 templates/book/_search.html.twig delete mode 100644 templates/search/index.html.twig delete mode 100644 templates/serie/detail.html.twig diff --git a/composer.json b/composer.json index 45216460..90d6aa85 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "ext-exif": "*", "ext-iconv": "*", "ext-zip": "*", + "andanteproject/page-filter-form-bundle": "^1.0", "doctrine/annotations": "^2.0", "doctrine/doctrine-bundle": "^2.9", "doctrine/doctrine-migrations-bundle": "^3.2", diff --git a/composer.lock b/composer.lock index 65de2ade..51a66bb8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,76 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5027203782f84e98244f6fc42910d068", + "content-hash": "fe8e1b02dc8d8a2ffd767b2414d31183", "packages": [ + { + "name": "andanteproject/page-filter-form-bundle", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/andanteproject/page-filter-form-bundle.git", + "reference": "8455670ba8fe3485ae14d0d76ed401cbafce2172" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/andanteproject/page-filter-form-bundle/zipball/8455670ba8fe3485ae14d0d76ed401cbafce2172", + "reference": "8455670ba8fe3485ae14d0d76ed401cbafce2172", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "symfony/form": "^4.0 || ^5.0 || ^6.0" + }, + "require-dev": { + "ext-json": "*", + "friendsofphp/php-cs-fixer": "^3.4", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-symfony": "^1.0", + "phpunit/phpunit": "^9.5", + "roave/security-advisories": "dev-master", + "symfony/framework-bundle": "^4.0 | ^5.0 | ^6.0", + "symfony/yaml": "^5.2" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Andante\\PageFilterFormBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andante Project", + "homepage": "https://github.com/andanteproject" + }, + { + "name": "Cristoforo Cervino", + "homepage": "https://github.com/cristoforocervino" + } + ], + "description": "A Symfony Bundle to simplify the handling of page filters for lists/tables in admin panels.", + "keywords": [ + "PHP7", + "admin-panel", + "filters", + "form", + "php", + "php74", + "symfony", + "symfony-bundle", + "symfony-form" + ], + "support": { + "issues": "https://github.com/andanteproject/page-filter-form-bundle/issues", + "source": "https://github.com/andanteproject/page-filter-form-bundle/tree/1.0.3" + }, + "time": "2023-08-28T15:56:25+00:00" + }, { "name": "behat/transliterator", "version": "v1.5.0", @@ -13809,5 +13877,5 @@ "ext-zip": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/config/bundles.php b/config/bundles.php index c4629831..dec2bd6f 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -22,4 +22,5 @@ Symfony\UX\LiveComponent\LiveComponentBundle::class => ['all' => true], Symfony\UX\Autocomplete\AutocompleteBundle::class => ['all' => true], Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true], + Andante\PageFilterFormBundle\AndantePageFilterFormBundle::class => ['all' => true], ]; diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 0d308bfe..7f462e15 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -11,8 +11,12 @@ security: property: username firewalls: dev: - pattern: ^/(_(profiler|wdt)|css|images|js)/ + pattern: ^/(_(profiler|wdt)|css|images|js|media)/ security: false + image_resolver: + pattern: ^/media/cache/resolve + security: + false main: lazy: true provider: app_user_provider diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index e5e109e1..ad84eeec 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -3,6 +3,8 @@ twig: globals: displayMode: "@app.display_mode_subscriber" #The id of your service + form_themes: ['bootstrap_5_layout.html.twig'] + when@test: twig: strict_variables: true diff --git a/src/Command/RenameAuthorCommand.php b/src/Command/RenameAuthorCommand.php deleted file mode 100644 index b3ac158d..00000000 --- a/src/Command/RenameAuthorCommand.php +++ /dev/null @@ -1,60 +0,0 @@ -addArgument('author', InputArgument::REQUIRED, 'Author to rename') - ->addArgument('newname', InputArgument::REQUIRED, 'new name') - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - $toRename = $input->getArgument('author'); - $newName = $input->getArgument('newname'); - - if (!is_string($toRename) || !is_string($newName)) { - throw new \Exception('Arguments must be strings'); - } - - /** @var Book[] $books */ - $books = $this->bookRepository->getByAuthorQuery($toRename)->getResult(); - - foreach ($books as $book) { - $io->writeln('Renaming '.$toRename.' to '.$newName.' in '.$book->getTitle()); - - $book->removeAuthor($toRename); - if ($newName !== '') { - $book->addAuthor($newName); - } - } - - $this->entityManager->flush(); - - return Command::SUCCESS; - } -} diff --git a/src/Command/RenameSerieCommand.php b/src/Command/RenameSerieCommand.php deleted file mode 100644 index a66f4b09..00000000 --- a/src/Command/RenameSerieCommand.php +++ /dev/null @@ -1,56 +0,0 @@ -addArgument('serie', InputArgument::REQUIRED, 'Serie to rename') - ->addArgument('newname', InputArgument::REQUIRED, 'new name') - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - $toRename = $input->getArgument('serie'); - $newName = $input->getArgument('newname'); - - if (!is_string($toRename) || !is_string($newName)) { - throw new \Exception('Arguments must be strings'); - } - /** @var Book[] $books */ - $books = $this->bookRepository->getBySerieQuery($this->slugger->slug($toRename))->getResult(); - - foreach ($books as $book) { - $io->writeln('Renaming '.$toRename.' to '.$newName.' in '.$book->getTitle()); - $book->setSerie($newName); - } - - $this->entityManager->flush(); - - return Command::SUCCESS; - } -} diff --git a/src/Controller/AuthorController.php b/src/Controller/AuthorController.php deleted file mode 100644 index 76a219e1..00000000 --- a/src/Controller/AuthorController.php +++ /dev/null @@ -1,51 +0,0 @@ - '\d+'])] - public function index(BookRepository $bookRepository, PaginatorInterface $paginator, int $page = 1): Response - { - $authors = $bookRepository->getAllAuthors(); - - $pagination = $paginator->paginate($authors, $page, 18); - - return $this->render('group/index.html.twig', [ - 'pagination' => $pagination, - 'page' => $page, - 'type' => 'authors', - ]); - } - - #[Route('/detail/{slug}/{page}', name: 'app_authors_detail', requirements: ['page' => '\d+'])] - public function detail(string $slug, BookRepository $bookRepository, PaginatorInterface $paginator, int $page = 1): Response - { - $authors = $bookRepository->getAllAuthors(); - - $slug = urldecode($slug); - $author = $authors[$slug] ?? null; - - if (null === $author) { - return $this->redirectToRoute('app_authors'); - } - - $pagination = $paginator->paginate( - $bookRepository->getByAuthorQuery($author['item']), - $page, - 18 - ); - - return $this->render('author/detail.html.twig', [ - 'pagination' => $pagination, - 'author' => $author, - ]); - } -} diff --git a/src/Controller/AutocompleteGroupController.php b/src/Controller/AutocompleteGroupController.php index 8f69d460..c234e0fc 100644 --- a/src/Controller/AutocompleteGroupController.php +++ b/src/Controller/AutocompleteGroupController.php @@ -27,6 +27,7 @@ public function index(Request $request, BookRepository $bookRepository, string $ 'serie' => $bookRepository->getAllSeries()->getResult(), 'authors' => $bookRepository->getAllAuthors(), 'tags' => $bookRepository->getAllTags(), + 'publisher' => $bookRepository->getAllPublisherss()->getResult(), default => [], }; @@ -41,7 +42,7 @@ public function index(Request $request, BookRepository $bookRepository, string $ } $json['results'][] = ['value' => $item['item'], 'text' => $item['item']]; } - if (!$exactmatch && strlen($query) > 2) { + if (!$exactmatch && strlen($query) > 2 && $request->get('create', true) === true) { $json['results'][] = ['value' => $query, 'text' => 'New: '.$query]; } diff --git a/src/Controller/DefaultController.php b/src/Controller/DefaultController.php index 254af029..cb8c9209 100644 --- a/src/Controller/DefaultController.php +++ b/src/Controller/DefaultController.php @@ -2,68 +2,36 @@ namespace App\Controller; +use Andante\PageFilterFormBundle\PageFilterFormTrait; +use App\Form\BookFilterType; use App\Repository\BookRepository; use Knp\Component\Pager\PaginatorInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class DefaultController extends AbstractController { - #[Route('/{page}', name: 'app_homepage', requirements: ['page' => '\d+'])] - public function index(BookRepository $bookRepository, PaginatorInterface $paginator, int $page = 1): Response - { - $pagination = $paginator->paginate( - $bookRepository->getAllBooksQuery(), - $page, - 18 - ); + use PageFilterFormTrait; - return $this->render('default/index.html.twig', [ - 'pagination' => $pagination, - 'page' => $page, - ]); - } - - #[Route('/favorites/{page}', name: 'app_favorites', requirements: ['page' => '\d+'])] - public function favorites(BookRepository $bookRepository, PaginatorInterface $paginator, int $page = 1): Response - { - $pagination = $paginator->paginate( - $bookRepository->getFavoriteBooksQuery(), - $page, - 18 - ); - - return $this->render('default/index.html.twig', [ - 'pagination' => $pagination, 'page' => $page, - ]); - } - - #[Route('/finished/{read}/{page}', name: 'app_read', requirements: ['page' => '\d+'])] - public function finished(BookRepository $bookRepository, PaginatorInterface $paginator, int $read, int $page = 1): Response + #[Route('/{page}', name: 'app_homepage', requirements: ['page' => '\d+'])] + public function index(Request $request, BookRepository $bookRepository, PaginatorInterface $paginator, int $page = 1): Response { - $pagination = $paginator->paginate( - $bookRepository->getBooksByReadStatus((bool) $read), - $page, - 18 - ); + $qb = $bookRepository->getAllBooksQueryBuilder(); - return $this->render('default/index.html.twig', [ - 'pagination' => $pagination, 'page' => $page, - ]); - } + $form = $this->createAndHandleFilter(BookFilterType::class, $qb, $request); - #[Route('/unverified/{page}', name: 'app_unverified', requirements: ['page' => '\d+'])] - public function unverified(BookRepository $bookRepository, PaginatorInterface $paginator, int $page = 1): Response - { $pagination = $paginator->paginate( - $bookRepository->getUnverifiedBooksQuery(), + $qb->getQuery(), $page, 18 ); return $this->render('default/index.html.twig', [ - 'pagination' => $pagination, 'page' => $page, + 'pagination' => $pagination, + 'page' => $page, + 'form' => $form->createView(), ]); } } diff --git a/src/Controller/GroupController.php b/src/Controller/GroupController.php new file mode 100644 index 00000000..a13be500 --- /dev/null +++ b/src/Controller/GroupController.php @@ -0,0 +1,55 @@ + '\d+'])] + public function groups(Request $request, string $type, int $page = 1): Response + { + $group = []; + switch ($type) { + case 'authors': + $group = $this->bookRepository->getAllAuthors(); + break; + case 'tags': + $group = $this->bookRepository->getAllTags(); + break; + case 'publisher': + $group = $this->bookRepository->getAllPublishers()->getResult(); + break; + case 'serie': + $group = $this->bookRepository->getAllSeries()->getResult(); + break; + } + + $search = $request->get('search', ''); + + if ($search !== '') { + $group = array_filter($group, static function ($item) use ($search) { + return str_contains(strtolower($item['item']), strtolower($search)); + }); + } + + $pagination = $this->paginator->paginate($group, $page, 300); + + return $this->render('group/index.html.twig', [ + 'pagination' => $pagination, + 'page' => $page, + 'type' => $type, + 'search' => $search, + ]); + } +} diff --git a/src/Controller/SearchController.php b/src/Controller/SearchController.php deleted file mode 100644 index efaa8392..00000000 --- a/src/Controller/SearchController.php +++ /dev/null @@ -1,27 +0,0 @@ -search($query, 5000); - } - - return $this->render('search/index.html.twig', [ - 'query' => $query, - 'pagination' => $paginator->paginate($books, $page, 18), - ]); - } -} diff --git a/src/Controller/SerieController.php b/src/Controller/SerieController.php deleted file mode 100644 index c7cef2ca..00000000 --- a/src/Controller/SerieController.php +++ /dev/null @@ -1,54 +0,0 @@ - '\d+'])] - public function index(BookRepository $bookRepository, PaginatorInterface $paginator, int $page = 1): Response - { - $series = $bookRepository->getAllSeries()->getResult(); - - $pagination = $paginator->paginate($series, $page, 18); - - return $this->render('group/index.html.twig', [ - 'pagination' => $pagination, - 'page' => $page, - 'type' => 'serie', - ]); - } - - #[Route('/detail/{slug}/{page}', name: 'app_serie_detail', requirements: ['page' => '\d+'])] - public function detail(string $slug, BookRepository $bookRepository, PaginatorInterface $paginator, int $page = 1): Response - { - $series = $bookRepository->getAllSeries()->getResult(); - if (!is_array($series)) { - throw $this->createNotFoundException('No series found'); - } - - $serie = array_filter($series, static fn ($serie) => $serie['slug'] === $slug); - - $serie = current($serie); - - $pagination = $paginator->paginate( - $bookRepository->getBySerieQuery($slug), - $page, - 18 - ); - /** @var Book $firstBook */ - $firstBook = current($pagination->getItems()); - - return $this->render('serie/detail.html.twig', [ - 'pagination' => $pagination, - 'serie' => $serie, - ]); - } -} diff --git a/src/Controller/TagController.php b/src/Controller/TagController.php deleted file mode 100644 index a26ec9c1..00000000 --- a/src/Controller/TagController.php +++ /dev/null @@ -1,52 +0,0 @@ - '\d+'])] - public function index(BookRepository $bookRepository, PaginatorInterface $paginator, int $page = 1): Response - { - $tags = $bookRepository->getAllTags(); - - $pagination = $paginator->paginate($tags, $page, 18); - - return $this->render('group/index.html.twig', [ - 'pagination' => $pagination, - 'page' => $page, - 'type' => 'tags', - ]); - } - - #[Route('/detail/{slug}/{page}', name: 'app_tags_detail', requirements: ['page' => '\d+'])] - public function detail(string $slug, BookRepository $bookRepository, PaginatorInterface $paginator, int $page = 1): Response - { - $tags = $bookRepository->getAllTags(); - - $slug = urldecode($slug); - - $tag = $tags[$slug] ?? null; - - if (null === $tag) { - return $this->redirectToRoute('app_tags'); - } - - $pagination = $paginator->paginate( - $bookRepository->getByTagQuery($tag['item']), - $page, - 18 - ); - - return $this->render('author/detail.html.twig', [ - 'pagination' => $pagination, - 'author' => $tag, - ]); - } -} diff --git a/src/Entity/Book.php b/src/Entity/Book.php index b22002ce..ca1d52b7 100644 --- a/src/Entity/Book.php +++ b/src/Entity/Book.php @@ -30,10 +30,6 @@ class Book #[Gedmo\Slug(fields: ['title', 'id'], style: 'lower')] private string $slug; - #[ORM\Column(length: 128, unique: false)] - #[Gedmo\Slug(fields: ['serie'], style: 'lower', unique: false)] - private string $serieSlug; - #[ORM\Column(type: Types::DATE_MUTABLE)] #[Gedmo\Timestampable(on: 'create')] private \DateTimeInterface $created; @@ -403,16 +399,6 @@ public function removeBookInteraction(BookInteraction $bookInteraction): static return $this; } - public function getSerieSlug(): string - { - return $this->serieSlug; - } - - public function setSerieSlug(string $serieSlug): void - { - $this->serieSlug = $serieSlug; - } - /** * @return array|null */ diff --git a/src/Entity/Shelf.php b/src/Entity/Shelf.php index 30024abd..0030d90b 100644 --- a/src/Entity/Shelf.php +++ b/src/Entity/Shelf.php @@ -5,6 +5,7 @@ use App\Repository\ShelfRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; @@ -33,6 +34,9 @@ class Shelf #[Gedmo\Slug(fields: ['name', 'id'], style: 'lower')] private string $slug; + #[ORM\Column(type: Types::JSON, nullable: true)] + private ?array $queryString = null; + public function __construct() { $this->books = new ArrayCollection(); @@ -107,4 +111,16 @@ public function setSlug(string $slug): void { $this->slug = $slug; } + + public function getQueryString(): ?array + { + return $this->queryString; + } + + public function setQueryString(?array $queryString): static + { + $this->queryString = $queryString; + + return $this; + } } diff --git a/src/Form/BookFilterType.php b/src/Form/BookFilterType.php new file mode 100644 index 00000000..8ff6acda --- /dev/null +++ b/src/Form/BookFilterType.php @@ -0,0 +1,223 @@ +setMethod('GET'); + + $builder->add('title', Type\SearchType::class, [ + 'required' => false, + 'target_callback' => function (QueryBuilder $qb, ?string $searchValue): void { + if ($searchValue !== null) { + $qb->andWhere($qb->expr()->like('book.title', ':title')); + $qb->setParameter('title', '%'.$searchValue.'%'); + } + }, + ]); + + $builder->add('authors', Type\TextType::class, [ + 'autocomplete' => true, + 'tom_select_options' => [ + 'create' => false, + ], + 'mapped' => false, + 'required' => false, + 'autocomplete_url' => $this->router->generate('app_autocomplete_group', ['type' => 'authors', 'create' => false]), + 'target_callback' => function (QueryBuilder $qb, ?string $searchValue): void { + if ($searchValue === null || $searchValue === '') { + return; + } + $authors = explode(',', $searchValue); + + $orModule = $qb->expr()->orX(); + + foreach ($authors as $key => $author) { + $orModule->add('JSON_CONTAINS(book.authors, :author'.$key.')=1'); + $qb->setParameter('author'.$key, json_encode([$author])); + } + $qb->andWhere($orModule); + }, + ]); + + $builder->add('tags', Type\TextType::class, [ + 'autocomplete' => true, + 'tom_select_options' => [ + 'create' => false, + ], + 'mapped' => false, + 'required' => false, + 'autocomplete_url' => $this->router->generate('app_autocomplete_group', ['type' => 'tags', 'create' => false]), + 'target_callback' => function (QueryBuilder $qb, ?string $searchValue): void { + if ($searchValue === null || $searchValue === '') { + return; + } + $tags = explode(',', $searchValue); + + $orModule = $qb->expr()->orX(); + + foreach ($tags as $key => $tag) { + $orModule->add('JSON_CONTAINS(book.tags, :tag'.$key.')=1'); + $qb->setParameter('tag'.$key, json_encode([$tag])); + } + $qb->andWhere($orModule); + }, + ]); + + $builder->add('serie', Type\TextType::class, [ + 'autocomplete' => true, + 'tom_select_options' => [ + 'create' => false, + ], + 'mapped' => false, + 'required' => false, + 'autocomplete_url' => $this->router->generate('app_autocomplete_group', ['type' => 'serie', 'create' => false]), + 'target_callback' => function (QueryBuilder $qb, ?string $searchValue): void { + if ($searchValue === null || $searchValue === '') { + return; + } + $series = explode(',', $searchValue); + + $orModule = $qb->expr()->orX(); + + foreach ($series as $key => $serie) { + $orModule->add('book.serie=:serie'.$key); + $qb->setParameter('serie'.$key, $serie); + } + $qb->andWhere($orModule); + }, + ]); + + $builder->add('publisher', Type\TextType::class, [ + 'autocomplete' => true, + 'tom_select_options' => [ + 'create' => false, + ], + 'mapped' => false, + 'required' => false, + 'autocomplete_url' => $this->router->generate('app_autocomplete_group', ['type' => 'publisher', 'create' => false]), + 'target_callback' => function (QueryBuilder $qb, ?string $searchValue): void { + if ($searchValue === null || $searchValue === '') { + return; + } + $publishers = explode(',', $searchValue); + + $orModule = $qb->expr()->orX(); + + foreach ($publishers as $key => $publisher) { + $orModule->add('book.publisher=:publisher'.$key); + $qb->setParameter('publisher'.$key, $publisher); + } + $qb->andWhere($orModule); + }, + ]); + + $builder->add('read', Type\ChoiceType::class, [ + 'choices' => [ + 'Any' => '', + 'Read' => 'read', + 'Unread' => 'unread', + ], + 'required' => false, + 'mapped' => false, + 'target_callback' => function (QueryBuilder $qb, ?string $readValue): void { + switch ($readValue) { + case 'read': + $qb->andWhere('bookInteraction.finished = true'); + break; + case 'unread': + $qb->andWhere('(bookInteraction.finished = false OR bookInteraction.finished IS NULL)'); + break; + } + }, + ]); + + $builder->add('favorite', Type\ChoiceType::class, [ + 'choices' => [ + 'Any' => '', + 'Favorite' => 'favorite', + 'Not favorite' => 'notfavorite', + ], + 'required' => false, + 'mapped' => false, + 'target_callback' => function (QueryBuilder $qb, ?string $readValue): void { + switch ($readValue) { + case 'favorite': + $qb->andWhere('bookInteraction.favorite = true'); + break; + case 'notfavorite': + $qb->andWhere('(bookInteraction.favorite = false OR bookInteraction.favorite IS NULL)'); + break; + } + }, + ]); + + $builder->add('verified', Type\ChoiceType::class, [ + 'choices' => [ + 'Any' => '', + 'Verified' => 'verified', + 'Not Verified' => 'unverified', + ], + 'required' => false, + 'mapped' => false, + 'target_callback' => function (QueryBuilder $qb, ?string $readValue): void { + switch ($readValue) { + case 'verified': + $qb->andWhere('book.verified = true'); + break; + case 'unverified': + $qb->andWhere('book.verified = false'); + break; + } + }, + ]); + + $builder->add('orderBy', Type\ChoiceType::class, [ + 'choices' => [ + 'title' => 'title', + 'id' => 'id', + 'publishDate' => 'publishDate', + 'serieIndex' => 'serieIndex', + 'created' => 'created', + ], + 'data' => 'title', + 'expanded' => true, + 'mapped' => false, + 'target_callback' => function (QueryBuilder $qb, ?string $orderByValue): void { + if ($orderByValue === null) { + $orderByValue='title'; + } + $qb->orderBy('book.'.$orderByValue, 'ASC'); + }, + ]); + + $builder->add('submit', Type\SubmitType::class, [ + 'label' => 'Filter', + 'attr' => [ + 'class' => 'btn btn-primary', + ], + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Book::class, + 'csrf_protection' => false, + ]); + } +} diff --git a/src/Menu/MenuBuilder.php b/src/Menu/MenuBuilder.php index 57c0bc00..f251d8c7 100644 --- a/src/Menu/MenuBuilder.php +++ b/src/Menu/MenuBuilder.php @@ -2,7 +2,9 @@ namespace App\Menu; +use App\Entity\Shelf; use App\Entity\User; +use App\Service\FilteredBookUrlGenerator; use Knp\Menu\FactoryInterface; use Knp\Menu\ItemInterface; use Symfony\Bundle\SecurityBundle\Security; @@ -17,7 +19,7 @@ final class MenuBuilder /** * Add any other dependency you need... */ - public function __construct(private readonly FactoryInterface $factory, private readonly Security $security) + public function __construct(private readonly FactoryInterface $factory, private readonly Security $security, private FilteredBookUrlGenerator $bookUrlGenerator) { } @@ -30,24 +32,25 @@ public function createMainMenu(array $options): ItemInterface $menu = $this->factory->createItem('root'); $menu->setChildrenAttribute('class', 'nav flex-column'); - $menu->addChild('Home', ['route' => 'app_homepage', ...$this->defaultAttr])->setExtra('icon', 'house-fill'); - $menu->addChild('Favorites', ['route' => 'app_favorites', ...$this->defaultAttr])->setExtra('icon', 'heart-fill'); - $menu->addChild('Read', ['route' => 'app_read', 'routeParameters' => ['read' => 1], ...$this->defaultAttr])->setExtra('icon', 'journal-check'); - $menu->addChild('Not read', ['route' => 'app_read', 'routeParameters' => ['read' => 0], ...$this->defaultAttr])->setExtra('icon', 'journal'); - $menu->addChild('Series', ['route' => 'app_serie', ...$this->defaultAttr])->setExtra('icon', 'list'); - $menu->addChild('Authors', ['route' => 'app_authors', ...$this->defaultAttr])->setExtra('icon', 'people-fill'); - $menu->addChild('Tags', ['route' => 'app_tags', ...$this->defaultAttr])->setExtra('icon', 'tags-fill'); - $menu->addChild('Unverified', ['route' => 'app_unverified', ...$this->defaultAttr])->setExtra('icon', 'question-circle-fill'); - $menu->addChild('setting_divider', ['label' => 'Others'])->setExtra('divider', true); - $menu->addChild('Settings', ['route' => 'admin', ...$this->defaultAttr])->setExtra('icon', 'gear-fill'); + $menu->addChild('All Books', ['route' => 'app_homepage', ...$this->defaultAttr])->setExtra('icon', 'house-fill'); + $menu->addChild('Series', ['route' => 'app_groups', 'routeParameters' =>['type'=>'serie'], ...$this->defaultAttr])->setExtra('icon', 'list'); + $menu->addChild('Authors', ['route' => 'app_groups', 'routeParameters' =>['type'=>'authors'], ...$this->defaultAttr])->setExtra('icon', 'people-fill'); + $menu->addChild('Tags', ['route' => 'app_groups', 'routeParameters' =>['type'=>'tags'], ...$this->defaultAttr])->setExtra('icon', 'tags-fill'); + $menu->addChild('Publishers', ['route' => 'app_groups', 'routeParameters' =>['type'=>'publisher'], ...$this->defaultAttr])->setExtra('icon', 'tags-fill'); $user = $this->security->getUser(); if ($user instanceof User && $user->getShelves()->count() > 0) { $menu->addChild('shelves_divider', ['label' => 'Shelves'])->setExtra('divider', true); foreach ($user->getShelves() as $shelf) { - $menu->addChild($shelf->getSlug(), ['label' => $shelf->getName(), 'route' => 'app_shelf', 'routeParameters' => ['slug' => $shelf->getSlug()], ...$this->defaultAttr]) - ->setExtra('icon', 'bookshelf'); + /** @var Shelf $shelf */ + if ($shelf->getQueryString() !== null) { + $menu->addChild($shelf->getSlug(), ['label' => $shelf->getName(), 'route' => 'app_homepage', 'routeParameters' => $shelf->getQueryString(), ...$this->defaultAttr]) + ->setExtra('icon', 'bookmark-fill'); + } else { + $menu->addChild($shelf->getSlug(), ['label' => $shelf->getName(), 'route' => 'app_shelf', 'routeParameters' => ['slug' => $shelf->getSlug()], ...$this->defaultAttr]) + ->setExtra('icon', 'bookshelf'); + } } } diff --git a/src/Repository/BookRepository.php b/src/Repository/BookRepository.php index 98803927..3cbcd1c3 100644 --- a/src/Repository/BookRepository.php +++ b/src/Repository/BookRepository.php @@ -5,6 +5,7 @@ use App\Entity\Book; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\Query; +use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; use Symfony\Bundle\SecurityBundle\Security; @@ -23,120 +24,18 @@ public function __construct(ManagerRegistry $registry, Security $security) $this->security = $security; } - public function getAllBooksQuery(): Query + public function getAllBooksQueryBuilder(): QueryBuilder { - return $this->createQueryBuilder('b') - ->select('b') - ->getQuery(); - } - - public function getFavoriteBooksQuery(): Query - { - return $this->createQueryBuilder('b') - ->select('b') - ->join('b.bookInteractions', 'bookInteraction', 'WITH', 'bookInteraction.favorite = true and bookInteraction.user=:user') - ->setParameter('user', $this->security->getUser()) - ->getQuery(); - } - - public function getUnverifiedBooksQuery(): Query - { - return $this->createQueryBuilder('b') - ->select('b') - ->where('b.verified = false') - ->getQuery(); - } - - public function getBooksByReadStatus(bool $read): Query - { - $q = $this->createQueryBuilder('b') - ->select('b') - ->leftJoin('b.bookInteractions', 'bookInteraction', 'WITH', 'bookInteraction.user=:user') - ->andWhere('bookInteraction.finished = :read'); - - if (!$read) { - $q->orWhere('bookInteraction.finished IS NULL'); - } - - $q->setParameter('user', $this->security->getUser()) - ->setParameter('read', (int) $read); - - return $q->getQuery(); - } - - public function getByAuthorQuery(string $author): Query - { - return $this->createQueryBuilder('b') - ->select('b') - ->where('JSON_CONTAINS(b.authors, :author)=1') - ->setParameter('author', json_encode([$author])) - ->getQuery(); - } - - public function getByTagQuery(string $tag): Query - { - return $this->createQueryBuilder('b') - ->select('b') - ->where('JSON_CONTAINS(b.tags, :tag)=1') - ->setParameter('tag', json_encode([$tag])) - ->getQuery(); - } - - public function getBySerieQuery(string $serieSlug): Query - { - return $this->createQueryBuilder('b') - ->select('b') - ->where('b.serieSlug = :serieSlug') - ->setParameter('serieSlug', $serieSlug) - ->addOrderBy('b.serieIndex', 'ASC') - ->getQuery(); - } - - /** - * @return array - */ - public function search(string $query, int $results = 5): array - { - $return = $this->createQueryBuilder('b') - ->select('b') - ->where('b.serie like :query') - ->orWhere('b.title like :query') - ->orWhere('JSON_CONTAINS(b.authors, :author)=1') - ->setParameter('author', json_encode([$query])) - ->setParameter('query', '%'.$query.'%') - ->setMaxResults($results) - ->addOrderBy('b.title', 'ASC') - ->getQuery()->getResult(); - if (!is_array($return)) { - return []; - } - - return $return; - } - - public function save(Book $entity, bool $flush = false): void - { - $this->getEntityManager()->persist($entity); - - if ($flush) { - $this->getEntityManager()->flush(); - } - } - - public function remove(Book $entity, bool $flush = false): void - { - $this->getEntityManager()->remove($entity); - - if ($flush) { - $this->getEntityManager()->flush(); - } + return $this->createQueryBuilder('book') + ->select('book') + ->leftJoin('book.bookInteractions', 'bookInteraction', 'WITH', 'bookInteraction.user=:user') + ->setParameter('user', $this->security->getUser()); } public function getAllSeries(): Query { return $this->createQueryBuilder('serie') ->select('serie.serie as item') - ->addSelect('serie.serieSlug as slug') ->addSelect('COUNT(serie.id) as bookCount') ->addSelect('MAX(serie.serieIndex) as lastBookIndex') ->addSelect('COUNT(bookInteraction.finished) as booksFinished') @@ -146,6 +45,18 @@ public function getAllSeries(): Query ->addGroupBy('serie.serie')->getQuery(); } + public function getAllPublishers(): Query + { + return $this->createQueryBuilder('publisher') + ->select('publisher.publisher as item') + ->addSelect('COUNT(publisher.id) as bookCount') + ->addSelect('COUNT(bookInteraction.finished) as booksFinished') + ->where('publisher.publisher IS NOT NULL') + ->leftJoin('publisher.bookInteractions', 'bookInteraction', 'WITH', 'bookInteraction.finished = true and bookInteraction.user= :user') + ->setParameter('user', $this->security->getUser()) + ->addGroupBy('publisher.publisher')->getQuery(); + } + /** * @return GroupType[] */ diff --git a/src/Service/FilteredBookUrlGenerator.php b/src/Service/FilteredBookUrlGenerator.php new file mode 100644 index 00000000..f71e0069 --- /dev/null +++ b/src/Service/FilteredBookUrlGenerator.php @@ -0,0 +1,68 @@ + '', + 'authors' => [], + 'tags' => [], + 'serie' => '', + 'publisher' => '', + 'read' => '', + 'favorite' => '', + 'verified' => '', + 'orderBy' => 'title', + 'submit' => '', + ]; + + public function __construct(private RequestStack $request) + { + } + + /** + * @return array + */ + public function getParametersArray(array $values): array + { + $params = self::FIELDS_DEFAULT_VALUE; + foreach ($values as $key => $value) { + if (!array_key_exists($key, self::FIELDS_DEFAULT_VALUE)) { + throw new \RuntimeException('Invalid key '.$key); + } + $params[$key] = $value; + } + + return $params; + } + + public function getParametersArrayForCurrent($onlyModified = false): array + { + $request = $this->request->getMainRequest(); + if ($request === null) { + return self::FIELDS_DEFAULT_VALUE; + } + $params = []; + + $queryParams = $request->query->all(); + + foreach (self::FIELDS_DEFAULT_VALUE as $key => $value) { + if (array_key_exists($key, $queryParams)) { + $value = $queryParams[$key]; + if ($key === 'authors' || $key === 'tags') { + $value = array_filter(explode(',', $value)); + } + } + + if ($onlyModified === true && $value === self::FIELDS_DEFAULT_VALUE[$key]) { + continue; + } + $params[$key] = $value; + } + + return $params; + } +} diff --git a/src/Twig/AddNewShelf.php b/src/Twig/AddNewShelf.php index 212e4ba0..eb2d2c9a 100644 --- a/src/Twig/AddNewShelf.php +++ b/src/Twig/AddNewShelf.php @@ -29,6 +29,9 @@ class AddNewShelf extends AbstractController #[LiveProp(writable: true)] public string $name = ''; + #[LiveProp()] + public ?array $currentFilters =null; + #[LiveProp()] public User $user; @@ -62,4 +65,36 @@ public function save(EntityManagerInterface $entityManager): void $this->dispatchBrowserEvent('manager:flush'); $this->isEditing = false; } + + #[LiveAction] + public function saveFilters(EntityManagerInterface $entityManager): void + { + if (null === $this->shelf) { + $this->shelf = new Shelf(); + } + + $this->name = trim($this->name); + if ('' === $this->name) { + return; + } + + $this->shelf->setName($this->name); + $this->shelf->setUser($this->user); + + foreach ($this->currentFilters as $key => $value) { + if (is_array($value)) { + $this->currentFilters[$key] = implode(',', $value); + } + } + + $this->shelf->setQueryString($this->currentFilters); + + $entityManager->persist($this->shelf); + $entityManager->flush(); + + $this->dispatchBrowserEvent('manager:flush'); + $this->isEditing = false; + } + + } diff --git a/src/Twig/FilteredBookUrl.php b/src/Twig/FilteredBookUrl.php new file mode 100644 index 00000000..0004fce7 --- /dev/null +++ b/src/Twig/FilteredBookUrl.php @@ -0,0 +1,37 @@ +filteredBookUrlGenerator->getParametersArray($params); + + return $this->router->generate('app_homepage', $params); + } + public function currentPageParams($onlyModified=false) + { + return $this->filteredBookUrlGenerator->getParametersArrayForCurrent($onlyModified); + } + +} diff --git a/src/Twig/Search.php b/src/Twig/Search.php deleted file mode 100644 index 11ceaeb3..00000000 --- a/src/Twig/Search.php +++ /dev/null @@ -1,34 +0,0 @@ - - */ - public function getBooks(): array - { - if (null === $this->query) { - return []; - } - - return $this->bookRepository->search($this->query); - } -} diff --git a/symfony.lock b/symfony.lock index d076a01d..cedee67b 100644 --- a/symfony.lock +++ b/symfony.lock @@ -1,4 +1,7 @@ { + "andanteproject/page-filter-form-bundle": { + "version": "1.0.3" + }, "doctrine/annotations": { "version": "2.0", "recipe": { diff --git a/templates/_menu.html.twig b/templates/_menu.html.twig deleted file mode 100644 index ef5027f0..00000000 --- a/templates/_menu.html.twig +++ /dev/null @@ -1 +0,0 @@ -{% extends '@KnpMenu/menu.html.twig' %} \ No newline at end of file diff --git a/templates/author/detail.html.twig b/templates/author/detail.html.twig deleted file mode 100644 index fff64451..00000000 --- a/templates/author/detail.html.twig +++ /dev/null @@ -1,18 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block title %} - {{ author.item }} -{% endblock %} - -{% block body %} - -
-
-
-

{{ author.booksFinished }} books read on a total of {{ author.bookCount }} books.

- - - {% include 'book/_pagination.html.twig' %} - - -{% endblock %} diff --git a/templates/base.html.twig b/templates/base.html.twig index 8d983fbc..ef860455 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -25,13 +25,9 @@ - - -
    +