]> BookStack Code Mirror - bookstack/blob - app/Api/ApiEntityListFormatter.php
2fd9b7c5507b4ce8e8713280c78d62dd26c9171e
[bookstack] / app / Api / ApiEntityListFormatter.php
1 <?php
2
3 namespace BookStack\Api;
4
5 use BookStack\Entities\Models\Entity;
6 use BookStack\Entities\Models\Page;
7
8 class ApiEntityListFormatter
9 {
10     /**
11      * The list to be formatted.
12      * @var Entity[]
13      */
14     protected array $list = [];
15
16     /**
17      * Whether to include related titles in the response.
18      */
19     protected bool $includeRelatedTitles = false;
20
21     /**
22      * The fields to show in the formatted data.
23      * Can be a plain string array item for a direct model field (If existing on model).
24      * If the key is a string, with a callable value, the return value of the callable
25      * will be used for the resultant value. A null return value will omit the property.
26      * @var array<string|int, string|callable>
27      */
28     protected array $fields = [
29         'id',
30         'name',
31         'slug',
32         'book_id',
33         'chapter_id',
34         'draft',
35         'template',
36         'priority',
37         'created_at',
38         'updated_at',
39     ];
40
41     public function __construct(array $list)
42     {
43         $this->list = $list;
44
45         // Default dynamic fields
46         $this->withField('url', fn(Entity $entity) => $entity->getUrl());
47     }
48
49     /**
50      * Add a field to be used in the formatter, with the property using the given
51      * name and value being the return type of the given callback.
52      */
53     public function withField(string $property, callable $callback): self
54     {
55         $this->fields[$property] = $callback;
56         return $this;
57     }
58
59     /**
60      * Show the 'type' property in the response reflecting the entity type.
61      * EG: page, chapter, bookshelf, book
62      * To be included in results with non-pre-determined types.
63      */
64     public function withType(): self
65     {
66         $this->withField('type', fn(Entity $entity) => $entity->getType());
67         return $this;
68     }
69
70     /**
71      * Include tags in the formatted data.
72      */
73     public function withTags(): self
74     {
75         $this->withField('tags', fn(Entity $entity) => $entity->tags);
76         return $this;
77     }
78
79     /**
80      * Enable the inclusion of related book and chapter titles in the response.
81      */
82     public function withRelatedTitles(): self
83     {
84         $this->includeRelatedTitles = true;
85
86         $this->withField('book_title', function (Entity $entity) {
87             if (method_exists($entity, 'book')) {
88                 return $entity->book?->name;
89             }
90             return null;
91         });
92
93         $this->withField('chapter_title', function (Entity $entity) {
94             if ($entity instanceof Page && $entity->chapter_id) {
95                 return optional($entity->getAttribute('chapter'))->name;
96             }
97             return null;
98         });
99
100         return $this;
101     }
102
103     /**
104      * Format the data and return an array of formatted content.
105      * @return array[]
106      */
107     public function format(): array
108     {
109         if ($this->includeRelatedTitles) {
110             $this->loadRelatedTitles();
111         }
112
113         $results = [];
114
115         foreach ($this->list as $item) {
116             $results[] = $this->formatSingle($item);
117         }
118
119         return $results;
120     }
121
122     /**
123      * Eager load the related book and chapter data when needed.
124      */
125     protected function loadRelatedTitles(): void
126     {
127         $pages = collect($this->list)->filter(fn($item) => $item instanceof Page);
128
129         foreach ($this->list as $entity) {
130             if (method_exists($entity, 'book')) {
131                 $entity->load('book');
132             }
133             if ($entity instanceof Page && $entity->chapter_id) {
134                 $entity->load('chapter');
135             }
136         }
137     }
138
139     /**
140      * Format a single entity item to a plain array.
141      */
142     protected function formatSingle(Entity $entity): array
143     {
144         $result = [];
145         $values = (clone $entity)->toArray();
146
147         foreach ($this->fields as $field => $callback) {
148             if (is_string($callback)) {
149                 $field = $callback;
150                 if (!isset($values[$field])) {
151                     continue;
152                 }
153                 $value = $values[$field];
154             } else {
155                 $value = $callback($entity);
156                 if (is_null($value)) {
157                     continue;
158                 }
159             }
160
161             $result[$field] = $value;
162         }
163
164         return $result;
165     }
166 }