]> BookStack Code Mirror - bookstack/blob - app/Entities/Tools/BookContents.php
Refactored sort system a little
[bookstack] / app / Entities / Tools / BookContents.php
1 <?php
2
3 namespace BookStack\Entities\Tools;
4
5 use BookStack\Entities\Models\Book;
6 use BookStack\Entities\Models\BookChild;
7 use BookStack\Entities\Models\Chapter;
8 use BookStack\Entities\Models\Entity;
9 use BookStack\Entities\Models\Page;
10 use BookStack\Exceptions\SortOperationException;
11 use Illuminate\Support\Collection;
12
13 class BookContents
14 {
15     /**
16      * @var Book
17      */
18     protected $book;
19
20     /**
21      * BookContents constructor.
22      */
23     public function __construct(Book $book)
24     {
25         $this->book = $book;
26     }
27
28     /**
29      * Get the current priority of the last item
30      * at the top-level of the book.
31      */
32     public function getLastPriority(): int
33     {
34         $maxPage = Page::visible()->where('book_id', '=', $this->book->id)
35             ->where('draft', '=', false)
36             ->where('chapter_id', '=', 0)->max('priority');
37         $maxChapter = Chapter::visible()->where('book_id', '=', $this->book->id)
38             ->max('priority');
39
40         return max($maxChapter, $maxPage, 1);
41     }
42
43     /**
44      * Get the contents as a sorted collection tree.
45      */
46     public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
47     {
48         $pages = $this->getPages($showDrafts, $renderPages);
49         $chapters = Chapter::visible()->where('book_id', '=', $this->book->id)->get();
50         $all = collect()->concat($pages)->concat($chapters);
51         $chapterMap = $chapters->keyBy('id');
52         $lonePages = collect();
53
54         $pages->groupBy('chapter_id')->each(function ($pages, $chapter_id) use ($chapterMap, &$lonePages) {
55             $chapter = $chapterMap->get($chapter_id);
56             if ($chapter) {
57                 $chapter->setAttribute('visible_pages', collect($pages)->sortBy($this->bookChildSortFunc()));
58             } else {
59                 $lonePages = $lonePages->concat($pages);
60             }
61         });
62
63         $chapters->whereNull('visible_pages')->each(function (Chapter $chapter) {
64             $chapter->setAttribute('visible_pages', collect([]));
65         });
66
67         $all->each(function (Entity $entity) use ($renderPages) {
68             $entity->setRelation('book', $this->book);
69
70             if ($renderPages && $entity instanceof Page) {
71                 $entity->html = (new PageContent($entity))->render();
72             }
73         });
74
75         return collect($chapters)->concat($lonePages)->sortBy($this->bookChildSortFunc());
76     }
77
78     /**
79      * Function for providing a sorting score for an entity in relation to the
80      * other items within the book.
81      */
82     protected function bookChildSortFunc(): callable
83     {
84         return function (Entity $entity) {
85             if (isset($entity['draft']) && $entity['draft']) {
86                 return -100;
87             }
88
89             return $entity['priority'] ?? 0;
90         };
91     }
92
93     /**
94      * Get the visible pages within this book.
95      */
96     protected function getPages(bool $showDrafts = false, bool $getPageContent = false): Collection
97     {
98         $query = Page::visible()
99             ->select($getPageContent ? Page::$contentAttributes : Page::$listAttributes)
100             ->where('book_id', '=', $this->book->id);
101
102         if (!$showDrafts) {
103             $query->where('draft', '=', false);
104         }
105
106         return $query->get();
107     }
108
109     /**
110      * Sort the books content using the given sort map.
111      * Returns a list of books that were involved in the operation.
112      *
113      * @throws SortOperationException
114      */
115     public function sortUsingMap(BookSortMap $sortMap): Collection
116     {
117         // Load models into map
118         $this->loadModelsIntoSortMap($sortMap);
119         $booksInvolved = $this->getBooksInvolvedInSort($sortMap);
120
121         // Perform the sort
122         foreach ($sortMap->all() as $item) {
123             $this->applySortUpdates($item);
124         }
125
126         // Update permissions and activity.
127         $booksInvolved->each(function (Book $book) {
128             $book->rebuildPermissions();
129         });
130
131         return $booksInvolved;
132     }
133
134     /**
135      * Using the given sort map item, detect changes for the related model
136      * and update it if required.
137      */
138     protected function applySortUpdates(BookSortMapItem $sortMapItem): void
139     {
140         $model = $sortMapItem->model;
141         if (!$model) {
142             return;
143         }
144
145         $priorityChanged = $model->priority !== $sortMapItem->sort;
146         $bookChanged = $model->book_id !== $sortMapItem->parentBookId;
147         $chapterChanged = ($model instanceof Page) && $model->chapter_id !== $sortMapItem->parentChapterId;
148
149         if ($bookChanged) {
150             $model->changeBook($sortMapItem->parentBookId);
151         }
152
153         if ($chapterChanged) {
154             $model->chapter_id = intval($sortMapItem->parentChapterId);
155             $model->save();
156         }
157
158         if ($priorityChanged) {
159             $model->priority = $sortMapItem->sort;
160             $model->save();
161         }
162     }
163
164     /**
165      * Load models from the database into the given sort map.
166      */
167     protected function loadModelsIntoSortMap(BookSortMap $sortMap): void
168     {
169         $collection = collect($sortMap->all());
170
171         $keyMap = $collection->keyBy(function (BookSortMapItem $sortMapItem) {
172             return  $sortMapItem->type . ':' . $sortMapItem->id;
173         });
174
175         $pageIds = $collection->where('type', '=', 'page')->pluck('id');
176         $chapterIds = $collection->where('type', '=', 'chapter')->pluck('id');
177
178         $pages = Page::visible()->whereIn('id', $pageIds)->get();
179         $chapters = Chapter::visible()->whereIn('id', $chapterIds)->get();
180
181         foreach ($pages as $page) {
182             /** @var BookSortMapItem $sortItem */
183             $sortItem = $keyMap->get('page:' . $page->id);
184             $sortItem->model = $page;
185         }
186
187         foreach ($chapters as $chapter) {
188             /** @var BookSortMapItem $sortItem */
189             $sortItem = $keyMap->get('chapter:' . $chapter->id);
190             $sortItem->model = $chapter;
191         }
192     }
193
194     /**
195      * Get the books involved in a sort.
196      * The given sort map should have its models loaded first.
197      *
198      * @throws SortOperationException
199      */
200     protected function getBooksInvolvedInSort(BookSortMap $sortMap): Collection
201     {
202         $collection = collect($sortMap->all());
203
204         $bookIdsInvolved = array_unique(array_merge(
205             [$this->book->id],
206             $collection->pluck('parentBookId')->values()->all(),
207             $collection->pluck('model.book_id')->values()->all(),
208         ));
209         
210         $books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get();
211
212         if (count($books) !== count($bookIdsInvolved)) {
213             throw new SortOperationException('Could not find all books requested in sort operation');
214         }
215
216         return $books;
217     }
218 }