]> BookStack Code Mirror - bookstack/commitdiff
Queries: Extracted chapter repo queries to class
authorDan Brown <redacted>
Mon, 5 Feb 2024 15:59:20 +0000 (15:59 +0000)
committerDan Brown <redacted>
Mon, 5 Feb 2024 15:59:20 +0000 (15:59 +0000)
Updated query classes to align to interface for common aligned
operations.
Extracted repeated string-identifier-based finding from page/chapter
repos to shared higher-level entity queries.

12 files changed:
app/Entities/Controllers/ChapterController.php
app/Entities/Controllers/ChapterExportController.php
app/Entities/Controllers/PageController.php
app/Entities/Models/Chapter.php
app/Entities/Queries/BookQueries.php
app/Entities/Queries/BookshelfQueries.php
app/Entities/Queries/ChapterQueries.php [new file with mode: 0644]
app/Entities/Queries/EntityQueries.php
app/Entities/Queries/PageQueries.php
app/Entities/Queries/ProvidesEntityQueries.php [new file with mode: 0644]
app/Entities/Repos/ChapterRepo.php
app/Entities/Repos/PageRepo.php

index 00616888a701c7aea91cc8f9f8517d9a3e1426c7..4b0a525eb371b38cc641814ef1a6f1f006fae93e 100644 (file)
@@ -5,6 +5,8 @@ namespace BookStack\Entities\Controllers;
 use BookStack\Activity\Models\View;
 use BookStack\Activity\Tools\UserEntityWatchOptions;
 use BookStack\Entities\Models\Book;
+use BookStack\Entities\Queries\ChapterQueries;
+use BookStack\Entities\Queries\EntityQueries;
 use BookStack\Entities\Repos\ChapterRepo;
 use BookStack\Entities\Tools\BookContents;
 use BookStack\Entities\Tools\Cloner;
@@ -24,7 +26,9 @@ class ChapterController extends Controller
 {
     public function __construct(
         protected ChapterRepo $chapterRepo,
-        protected ReferenceFetcher $referenceFetcher
+        protected ChapterQueries $queries,
+        protected EntityQueries $entityQueries,
+        protected ReferenceFetcher $referenceFetcher,
     ) {
     }
 
@@ -33,12 +37,15 @@ class ChapterController extends Controller
      */
     public function create(string $bookSlug)
     {
-        $book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
+        $book = $this->entityQueries->books->findVisibleBySlug($bookSlug);
         $this->checkOwnablePermission('chapter-create', $book);
 
         $this->setPageTitle(trans('entities.chapters_create'));
 
-        return view('chapters.create', ['book' => $book, 'current' => $book]);
+        return view('chapters.create', [
+            'book' => $book,
+            'current' => $book,
+        ]);
     }
 
     /**
@@ -55,7 +62,7 @@ class ChapterController extends Controller
             'default_template_id' => ['nullable', 'integer'],
         ]);
 
-        $book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
+        $book = $this->entityQueries->books->findVisibleBySlug($bookSlug);
         $this->checkOwnablePermission('chapter-create', $book);
 
         $chapter = $this->chapterRepo->create($validated, $book);
@@ -68,7 +75,7 @@ class ChapterController extends Controller
      */
     public function show(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-view', $chapter);
 
         $sidebarTree = (new BookContents($chapter->book))->getTree();
@@ -96,7 +103,7 @@ class ChapterController extends Controller
      */
     public function edit(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-update', $chapter);
 
         $this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
@@ -118,7 +125,7 @@ class ChapterController extends Controller
             'default_template_id' => ['nullable', 'integer'],
         ]);
 
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-update', $chapter);
 
         $this->chapterRepo->update($chapter, $validated);
@@ -133,7 +140,7 @@ class ChapterController extends Controller
      */
     public function showDelete(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-delete', $chapter);
 
         $this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
@@ -149,7 +156,7 @@ class ChapterController extends Controller
      */
     public function destroy(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-delete', $chapter);
 
         $this->chapterRepo->destroy($chapter);
@@ -164,7 +171,7 @@ class ChapterController extends Controller
      */
     public function showMove(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug);
         $this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
         $this->checkOwnablePermission('chapter-update', $chapter);
         $this->checkOwnablePermission('chapter-delete', $chapter);
@@ -182,7 +189,7 @@ class ChapterController extends Controller
      */
     public function move(Request $request, string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-update', $chapter);
         $this->checkOwnablePermission('chapter-delete', $chapter);
 
@@ -211,7 +218,7 @@ class ChapterController extends Controller
      */
     public function showCopy(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-view', $chapter);
 
         session()->flashInput(['name' => $chapter->name]);
@@ -230,13 +237,13 @@ class ChapterController extends Controller
      */
     public function copy(Request $request, Cloner $cloner, string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-view', $chapter);
 
         $entitySelection = $request->get('entity_selection') ?: null;
-        $newParentBook = $entitySelection ? $this->chapterRepo->findParentByIdentifier($entitySelection) : $chapter->getParent();
+        $newParentBook = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $chapter->getParent();
 
-        if (is_null($newParentBook)) {
+        if (!$newParentBook instanceof Book) {
             $this->showErrorNotification(trans('errors.selected_book_not_found'));
 
             return redirect($chapter->getUrl('/copy'));
@@ -256,7 +263,7 @@ class ChapterController extends Controller
      */
     public function convertToBook(HierarchyTransformer $transformer, string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-update', $chapter);
         $this->checkOwnablePermission('chapter-delete', $chapter);
         $this->checkPermission('book-create-all');
index b67ec9b373b0d29adaf356471e6ab38e31b32a59..0e071fc82bd49da3ad212749042dd21ed89b6cc8 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace BookStack\Entities\Controllers;
 
-use BookStack\Entities\Repos\ChapterRepo;
+use BookStack\Entities\Queries\ChapterQueries;
 use BookStack\Entities\Tools\ExportFormatter;
 use BookStack\Exceptions\NotFoundException;
 use BookStack\Http\Controller;
@@ -10,16 +10,10 @@ use Throwable;
 
 class ChapterExportController extends Controller
 {
-    protected $chapterRepo;
-    protected $exportFormatter;
-
-    /**
-     * ChapterExportController constructor.
-     */
-    public function __construct(ChapterRepo $chapterRepo, ExportFormatter $exportFormatter)
-    {
-        $this->chapterRepo = $chapterRepo;
-        $this->exportFormatter = $exportFormatter;
+    public function __construct(
+        protected ChapterQueries $queries,
+        protected ExportFormatter $exportFormatter,
+    ) {
         $this->middleware('can:content-export');
     }
 
@@ -31,7 +25,7 @@ class ChapterExportController extends Controller
      */
     public function pdf(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug);
         $pdfContent = $this->exportFormatter->chapterToPdf($chapter);
 
         return $this->download()->directly($pdfContent, $chapterSlug . '.pdf');
@@ -45,7 +39,7 @@ class ChapterExportController extends Controller
      */
     public function html(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug);
         $containedHtml = $this->exportFormatter->chapterToContainedHtml($chapter);
 
         return $this->download()->directly($containedHtml, $chapterSlug . '.html');
@@ -58,7 +52,7 @@ class ChapterExportController extends Controller
      */
     public function plainText(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug);
         $chapterText = $this->exportFormatter->chapterToPlainText($chapter);
 
         return $this->download()->directly($chapterText, $chapterSlug . '.txt');
@@ -71,7 +65,7 @@ class ChapterExportController extends Controller
      */
     public function markdown(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug);
         $chapterText = $this->exportFormatter->chapterToMarkdown($chapter);
 
         return $this->download()->directly($chapterText, $chapterSlug . '.md');
index eaad3c0b79de3303d1544b45d018cbef42fd75b1..f2cd729c6767daf4e3fe857c0768e16f5be2accb 100644 (file)
@@ -8,6 +8,8 @@ use BookStack\Activity\Tools\UserEntityWatchOptions;
 use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
+use BookStack\Entities\Queries\PageQueries;
 use BookStack\Entities\Repos\PageRepo;
 use BookStack\Entities\Tools\BookContents;
 use BookStack\Entities\Tools\Cloner;
@@ -29,6 +31,8 @@ class PageController extends Controller
 {
     public function __construct(
         protected PageRepo $pageRepo,
+        protected PageQueries $pageQueries,
+        protected EntityQueries $entityQueries,
         protected ReferenceFetcher $referenceFetcher
     ) {
     }
@@ -435,9 +439,9 @@ class PageController extends Controller
         $this->checkOwnablePermission('page-view', $page);
 
         $entitySelection = $request->get('entity_selection') ?: null;
-        $newParent = $entitySelection ? $this->pageRepo->findParentByIdentifier($entitySelection) : $page->getParent();
+        $newParent = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $page->getParent();
 
-        if (is_null($newParent)) {
+        if (!$newParent instanceof Book && !$newParent instanceof Chapter) {
             $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
 
             return redirect($page->getUrl('/copy'));
index d3a7101116ba5e36341773b2190e08a899deba86..422c82442fe8d203ed63cffd404eefa03af96c92 100644 (file)
@@ -11,7 +11,6 @@ use Illuminate\Support\Collection;
  * Class Chapter.
  *
  * @property Collection<Page> $pages
- * @property string           $description
  * @property ?int             $default_template_id
  * @property ?Page            $defaultTemplate
  */
index 6de28f0c2640dc96c131ac69946b0615f152a260..5e85523cf67ac94265931e46afa7cb293321989c 100644 (file)
@@ -6,13 +6,18 @@ use BookStack\Entities\Models\Book;
 use BookStack\Exceptions\NotFoundException;
 use Illuminate\Database\Eloquent\Builder;
 
-class BookQueries
+class BookQueries implements ProvidesEntityQueries
 {
     public function start(): Builder
     {
         return Book::query();
     }
 
+    public function findVisibleById(int $id): ?Book
+    {
+        return $this->start()->scopes('visible')->find($id);
+    }
+
     public function findVisibleBySlug(string $slug): Book
     {
         /** @var ?Book $book */
index 7edff45b958a5e7a5ee23878a4f608143d62e9fa..52e123087ce5980e1d0920214ed0ba9202628e99 100644 (file)
@@ -6,13 +6,18 @@ use BookStack\Entities\Models\Bookshelf;
 use BookStack\Exceptions\NotFoundException;
 use Illuminate\Database\Eloquent\Builder;
 
-class BookshelfQueries
+class BookshelfQueries implements ProvidesEntityQueries
 {
     public function start(): Builder
     {
         return Bookshelf::query();
     }
 
+    public function findVisibleById(int $id): ?Bookshelf
+    {
+        return $this->start()->scopes('visible')->find($id);
+    }
+
     public function findVisibleBySlug(string $slug): Bookshelf
     {
         /** @var ?Bookshelf $shelf */
diff --git a/app/Entities/Queries/ChapterQueries.php b/app/Entities/Queries/ChapterQueries.php
new file mode 100644 (file)
index 0000000..dcfc4aa
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+
+namespace BookStack\Entities\Queries;
+
+use BookStack\Entities\Models\Chapter;
+use BookStack\Exceptions\NotFoundException;
+use Illuminate\Database\Eloquent\Builder;
+
+class ChapterQueries implements ProvidesEntityQueries
+{
+    protected static array $listAttributes = [
+        'id', 'slug', 'name', 'description', 'priority',
+        'created_at', 'updated_at',
+        'created_by', 'updated_by', 'owned_by',
+    ];
+
+    public function start(): Builder
+    {
+        return Chapter::query();
+    }
+
+    public function findVisibleById(int $id): ?Chapter
+    {
+        return $this->start()->scopes('visible')->find($id);
+    }
+
+    public function findVisibleBySlugs(string $bookSlug, string $chapterSlug): Chapter
+    {
+        /** @var ?Chapter $chapter */
+        $chapter = $this->start()->with('book')
+            ->whereHas('book', function (Builder $query) use ($bookSlug) {
+                $query->where('slug', '=', $bookSlug);
+            })
+            ->where('slug', '=', $chapterSlug)
+            ->first();
+
+        if ($chapter === null) {
+            throw new NotFoundException(trans('errors.chapter_not_found'));
+        }
+
+        return $chapter;
+    }
+
+    public function visibleForList(): Builder
+    {
+        return $this->start()
+            ->select(array_merge(static::$listAttributes, ['book_slug' => function ($builder) {
+                $builder->select('slug')
+                    ->from('books')
+                    ->whereColumn('books.id', '=', 'chapters.book_id');
+            }]));
+    }
+}
index b1973b0ea4386202e98f68bd10656a305999cb06..39a21c9138cb684fa644ed93a41cdd6faa6b34c3 100644 (file)
@@ -2,12 +2,42 @@
 
 namespace BookStack\Entities\Queries;
 
+use BookStack\Entities\Models\Entity;
+
 class EntityQueries
 {
     public function __construct(
         public BookshelfQueries $shelves,
         public BookQueries $books,
+        public ChapterQueries $chapters,
         public PageQueries $pages,
     ) {
     }
+
+    /**
+     * Find an entity via an identifier string in the format:
+     * {type}:{id}
+     * Example: (book:5).
+     */
+    public function findVisibleByStringIdentifier(string $identifier): ?Entity
+    {
+        $explodedId = explode(':', $identifier);
+        $entityType = $explodedId[0];
+        $entityId = intval($explodedId[1]);
+
+        /** @var ?ProvidesEntityQueries $queries */
+        $queries = match ($entityType) {
+            'page' => $this->pages,
+            'chapter' => $this->chapters,
+            'book' => $this->books,
+            'bookshelf' => $this->shelves,
+            default => null,
+        };
+
+        if (is_null($queries)) {
+            return null;
+        }
+
+        return $queries->findVisibleById($entityId);
+    }
 }
index b9c64f63ce81962c3d96c6aeb179abdfccd017b1..f1991626f07390dd1332f8d2870fffe443694c83 100644 (file)
@@ -5,13 +5,18 @@ namespace BookStack\Entities\Queries;
 use BookStack\Entities\Models\Page;
 use Illuminate\Database\Eloquent\Builder;
 
-class PageQueries
+class PageQueries implements ProvidesEntityQueries
 {
     public function start(): Builder
     {
         return Page::query();
     }
 
+    public function findVisibleById(int $id): ?Page
+    {
+        return $this->start()->scopes('visible')->find($id);
+    }
+
     public function visibleForList(): Builder
     {
         return $this->start()
diff --git a/app/Entities/Queries/ProvidesEntityQueries.php b/app/Entities/Queries/ProvidesEntityQueries.php
new file mode 100644 (file)
index 0000000..5c37b02
--- /dev/null
@@ -0,0 +1,12 @@
+<?php
+
+namespace BookStack\Entities\Queries;
+
+use BookStack\Entities\Models\Entity;
+use Illuminate\Database\Eloquent\Builder;
+
+interface ProvidesEntityQueries
+{
+    public function start(): Builder;
+    public function findVisibleById(int $id): ?Entity;
+}
index 50b554d68dc585b4d1f3b42b16c8021936721c8e..cacca93f6e9f2968ee22fe57969b3bff61e5003a 100644 (file)
@@ -4,12 +4,11 @@ namespace BookStack\Entities\Repos;
 
 use BookStack\Activity\ActivityType;
 use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Page;
 use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Queries\EntityQueries;
 use BookStack\Entities\Tools\BookContents;
 use BookStack\Entities\Tools\TrashCan;
 use BookStack\Exceptions\MoveOperationException;
-use BookStack\Exceptions\NotFoundException;
 use BookStack\Exceptions\PermissionsException;
 use BookStack\Facades\Activity;
 use Exception;
@@ -17,26 +16,11 @@ use Exception;
 class ChapterRepo
 {
     public function __construct(
-        protected BaseRepo $baseRepo
+        protected BaseRepo $baseRepo,
+        protected EntityQueries $entityQueries,
     ) {
     }
 
-    /**
-     * Get a chapter via the slug.
-     *
-     * @throws NotFoundException
-     */
-    public function getBySlug(string $bookSlug, string $chapterSlug): Chapter
-    {
-        $chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->first();
-
-        if ($chapter === null) {
-            throw new NotFoundException(trans('errors.chapter_not_found'));
-        }
-
-        return $chapter;
-    }
-
     /**
      * Create a new chapter in the system.
      */
@@ -91,8 +75,8 @@ class ChapterRepo
      */
     public function move(Chapter $chapter, string $parentIdentifier): Book
     {
-        $parent = $this->findParentByIdentifier($parentIdentifier);
-        if (is_null($parent)) {
+        $parent = $this->entityQueries->findVisibleByStringIdentifier($parentIdentifier);
+        if (!$parent instanceof Book) {
             throw new MoveOperationException('Book to move chapter into not found');
         }
 
@@ -106,24 +90,4 @@ class ChapterRepo
 
         return $parent;
     }
-
-    /**
-     * Find a page parent entity via an identifier string in the format:
-     * {type}:{id}
-     * Example: (book:5).
-     *
-     * @throws MoveOperationException
-     */
-    public function findParentByIdentifier(string $identifier): ?Book
-    {
-        $stringExploded = explode(':', $identifier);
-        $entityType = $stringExploded[0];
-        $entityId = intval($stringExploded[1]);
-
-        if ($entityType !== 'book') {
-            throw new MoveOperationException('Chapters can only be in books');
-        }
-
-        return Book::visible()->where('id', '=', $entityId)->first();
-    }
 }
index 85237a75219f62784160f388fa91601bcaa338db..929de528d6265f99dc437d7dffddb6bdc76c3882 100644 (file)
@@ -8,6 +8,7 @@ use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Entity;
 use BookStack\Entities\Models\Page;
 use BookStack\Entities\Models\PageRevision;
+use BookStack\Entities\Queries\EntityQueries;
 use BookStack\Entities\Tools\BookContents;
 use BookStack\Entities\Tools\PageContent;
 use BookStack\Entities\Tools\PageEditorData;
@@ -26,6 +27,7 @@ class PageRepo
     public function __construct(
         protected BaseRepo $baseRepo,
         protected RevisionRepo $revisionRepo,
+        protected EntityQueries $entityQueries,
         protected ReferenceStore $referenceStore,
         protected ReferenceUpdater $referenceUpdater
     ) {
@@ -324,8 +326,8 @@ class PageRepo
      */
     public function move(Page $page, string $parentIdentifier): Entity
     {
-        $parent = $this->findParentByIdentifier($parentIdentifier);
-        if (is_null($parent)) {
+        $parent = $this->entityQueries->findVisibleByStringIdentifier($parentIdentifier);
+        if (!$parent instanceof Chapter && !$parent instanceof Book) {
             throw new MoveOperationException('Book or chapter to move page into not found');
         }
 
@@ -343,28 +345,6 @@ class PageRepo
         return $parent;
     }
 
-    /**
-     * Find a page parent entity via an identifier string in the format:
-     * {type}:{id}
-     * Example: (book:5).
-     *
-     * @throws MoveOperationException
-     */
-    public function findParentByIdentifier(string $identifier): ?Entity
-    {
-        $stringExploded = explode(':', $identifier);
-        $entityType = $stringExploded[0];
-        $entityId = intval($stringExploded[1]);
-
-        if ($entityType !== 'book' && $entityType !== 'chapter') {
-            throw new MoveOperationException('Pages can only be in books or chapters');
-        }
-
-        $parentClass = $entityType === 'book' ? Book::class : Chapter::class;
-
-        return $parentClass::visible()->where('id', '=', $entityId)->first();
-    }
-
     /**
      * Get a new priority for a page.
      */