]> BookStack Code Mirror - bookstack/blob - app/Entities/Managers/BookContents.php
Entity Repo & Controller Refactor (#1690)
[bookstack] / app / Entities / Managers / BookContents.php
1 <?php namespace BookStack\Entities\Managers;
2
3 use BookStack\Entities\Book;
4 use BookStack\Entities\BookChild;
5 use BookStack\Entities\Chapter;
6 use BookStack\Entities\Entity;
7 use BookStack\Entities\Page;
8 use BookStack\Exceptions\SortOperationException;
9 use Illuminate\Support\Collection;
10
11 class BookContents
12 {
13
14     /**
15      * @var Book
16      */
17     protected $book;
18
19     /**
20      * BookContents constructor.
21      * @param $book
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         return max($maxChapter, $maxPage, 1);
40     }
41
42     /**
43      * Get the contents as a sorted collection tree.
44      * TODO - Support $renderPages option
45      */
46     public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
47     {
48         $pages = $this->getPages($showDrafts);
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('pages', collect($pages)->sortBy($this->bookChildSortFunc()));
58             } else {
59                 $lonePages = $lonePages->concat($pages);
60             }
61         });
62
63         $all->each(function (Entity $entity) {
64             $entity->setRelation('book', $this->book);
65         });
66
67         return collect($chapters)->concat($lonePages)->sortBy($this->bookChildSortFunc());
68     }
69
70     /**
71      * Function for providing a sorting score for an entity in relation to the
72      * other items within the book.
73      */
74     protected function bookChildSortFunc(): callable
75     {
76         return function (Entity $entity) {
77             if (isset($entity['draft']) && $entity['draft']) {
78                 return -100;
79             }
80             return $entity['priority'] ?? 0;
81         };
82     }
83
84     /**
85      * Get the visible pages within this book.
86      */
87     protected function getPages(bool $showDrafts = false): Collection
88     {
89         $query = Page::visible()->where('book_id', '=', $this->book->id);
90
91         if (!$showDrafts) {
92             $query->where('draft', '=', false);
93         }
94
95         return $query->get();
96     }
97
98     /**
99      * Sort the books content using the given map.
100      * The map is a single-dimension collection of objects in the following format:
101      *   {
102      *     +"id": "294" (ID of item)
103      *     +"sort": 1 (Sort order index)
104      *     +"parentChapter": false (ID of parent chapter, as string, or false)
105      *     +"type": "page" (Entity type of item)
106      *     +"book": "1" (Id of book to place item in)
107      *   }
108      *
109      * Returns a list of books that were involved in the operation.
110      * @throws SortOperationException
111      */
112     public function sortUsingMap(Collection $sortMap): Collection
113     {
114         // Load models into map
115         $this->loadModelsIntoSortMap($sortMap);
116         $booksInvolved = $this->getBooksInvolvedInSort($sortMap);
117
118         // Perform the sort
119         $sortMap->each(function ($mapItem) {
120             $this->applySortUpdates($mapItem);
121         });
122
123         // Update permissions and activity.
124         $booksInvolved->each(function (Book $book) {
125             $book->rebuildPermissions();
126         });
127
128         return $booksInvolved;
129     }
130
131     /**
132      * Using the given sort map item, detect changes for the related model
133      * and update it if required.
134      */
135     protected function applySortUpdates(\stdClass $sortMapItem)
136     {
137         /** @var BookChild $model */
138         $model = $sortMapItem->model;
139
140         $priorityChanged = intval($model->priority) !== intval($sortMapItem->sort);
141         $bookChanged = intval($model->book_id) !== intval($sortMapItem->book);
142         $chapterChanged = ($sortMapItem->type === 'page') && intval($model->chapter_id) !== $sortMapItem->parentChapter;
143
144         if ($bookChanged) {
145             $model->changeBook($sortMapItem->book);
146         }
147
148         if ($chapterChanged) {
149             $model->chapter_id = intval($sortMapItem->parentChapter);
150             $model->save();
151         }
152
153         if ($priorityChanged) {
154             $model->priority = intval($sortMapItem->sort);
155             $model->save();
156         }
157     }
158
159     /**
160      * Load models from the database into the given sort map.
161      */
162     protected function loadModelsIntoSortMap(Collection $sortMap): void
163     {
164         $keyMap = $sortMap->keyBy(function (\stdClass $sortMapItem) {
165             return  $sortMapItem->type . ':' . $sortMapItem->id;
166         });
167         $pageIds = $sortMap->where('type', '=', 'page')->pluck('id');
168         $chapterIds = $sortMap->where('type', '=', 'chapter')->pluck('id');
169
170         $pages = Page::visible()->whereIn('id', $pageIds)->get();
171         $chapters = Chapter::visible()->whereIn('id', $chapterIds)->get();
172
173         foreach ($pages as $page) {
174             $sortItem = $keyMap->get('page:' . $page->id);
175             $sortItem->model = $page;
176         }
177
178         foreach ($chapters as $chapter) {
179             $sortItem = $keyMap->get('chapter:' . $chapter->id);
180             $sortItem->model = $chapter;
181         }
182     }
183
184     /**
185      * Get the books involved in a sort.
186      * The given sort map should have its models loaded first.
187      * @throws SortOperationException
188      */
189     protected function getBooksInvolvedInSort(Collection $sortMap): Collection
190     {
191         $bookIdsInvolved = collect([$this->book->id]);
192         $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('book'));
193         $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('model.book_id'));
194         $bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
195
196         $books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get();
197
198         if (count($books) !== count($bookIdsInvolved)) {
199             throw new SortOperationException("Could not find all books requested in sort operation");
200         }
201
202         return $books;
203     }
204 }