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;
{
public function __construct(
protected ChapterRepo $chapterRepo,
- protected ReferenceFetcher $referenceFetcher
+ protected ChapterQueries $queries,
+ protected EntityQueries $entityQueries,
+ protected ReferenceFetcher $referenceFetcher,
) {
}
*/
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,
+ ]);
}
/**
'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);
*/
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();
*/
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()]));
'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);
*/
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()]));
*/
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);
*/
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);
*/
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);
*/
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]);
*/
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'));
*/
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');
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;
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');
}
*/
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');
*/
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');
*/
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');
*/
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');
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;
{
public function __construct(
protected PageRepo $pageRepo,
+ protected PageQueries $pageQueries,
+ protected EntityQueries $entityQueries,
protected ReferenceFetcher $referenceFetcher
) {
}
$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'));
* Class Chapter.
*
* @property Collection<Page> $pages
- * @property string $description
* @property ?int $default_template_id
* @property ?Page $defaultTemplate
*/
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 */
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 */
--- /dev/null
+<?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');
+ }]));
+ }
+}
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);
+ }
}
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()
--- /dev/null
+<?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;
+}
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;
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.
*/
*/
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');
}
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();
- }
}
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;
public function __construct(
protected BaseRepo $baseRepo,
protected RevisionRepo $revisionRepo,
+ protected EntityQueries $entityQueries,
protected ReferenceStore $referenceStore,
protected ReferenceUpdater $referenceUpdater
) {
*/
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');
}
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.
*/