Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.31% covered (warning)
79.31%
138 / 174
20.00% covered (danger)
20.00%
1 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
SlugsController
79.31% covered (warning)
79.31%
138 / 174
20.00% covered (danger)
20.00%
1 / 5
36.45
0.00% covered (danger)
0.00%
0 / 1
 index
84.88% covered (warning)
84.88%
73 / 86
0.00% covered (danger)
0.00%
0 / 1
13.58
 view
62.50% covered (warning)
62.50%
25 / 40
0.00% covered (danger)
0.00%
0 / 1
7.90
 add
75.86% covered (warning)
75.86%
22 / 29
0.00% covered (danger)
0.00%
0 / 1
5.35
 edit
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 delete
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
1<?php
2declare(strict_types=1);
3
4namespace App\Controller\Admin;
5
6use App\Controller\AppController;
7use Cake\Http\Response;
8use Exception;
9
10/**
11 * Slugs Controller
12 *
13 * Handles administration of URL slugs across the application.
14 * Provides CRUD operations for managing slugs and their relationships
15 * with various content types (Articles, etc.).
16 *
17 * @property \App\Model\Table\SlugsTable $Slugs
18 * @method \App\Model\Entity\Slug[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = [])
19 */
20class SlugsController extends AppController
21{
22    /**
23     * Index method
24     *
25     * Lists all slugs with filtering and search capabilities.
26     * Handles both regular and AJAX requests, displaying related content information
27     * for each slug.
28     *
29     * Features:
30     * - Search filtering by slug text
31     * - Status filtering by model type
32     * - Efficient fetching of related content to avoid N+1 query issues
33     * - AJAX support for dynamic updates
34     *
35     * @return \Cake\Http\Response|null Returns Response for AJAX requests, null otherwise
36     */
37    public function index(): ?Response
38    {
39        $statusFilter = $this->request->getQuery('status'); // This is now 'model' filter
40        $search = $this->request->getQuery('search');
41
42        // Get all unique model types from the slugs table
43        $modelTypes = $this->Slugs->find()
44            ->select(['model'])
45            ->distinct('model')
46            ->orderBy(['model' => 'ASC'])
47            ->all()
48            ->map(fn($row) => ucfirst($row->model))
49            ->toArray();
50
51        $query = $this->Slugs->find()
52            ->select([
53                'Slugs.id',
54                'Slugs.model',
55                'Slugs.foreign_key',
56                'Slugs.slug',
57                'Slugs.created',
58            ])
59            ->orderBy(['Slugs.created' => 'DESC']);
60
61        if (!empty($statusFilter)) {
62            $query->where(['Slugs.model' => $statusFilter]);
63        }
64
65        if (!empty($search)) {
66            $query->where([
67                'OR' => [
68                    'Slugs.slug LIKE' => '%' . $search . '%',
69                ],
70            ]);
71        }
72
73        // Paginate the slugs. $slugs will be a ResultSet (or similar traversable object).
74        // This is crucial for PaginatorComponent to attach pagination metadata.
75        $slugs = $this->paginate($query);
76
77        // Optimize fetching related records to avoid N+1 queries
78        $groupedSlugs = [];
79        // Iterate directly over the ResultSet returned by paginate()
80        foreach ($slugs as $slug) {
81            $groupedSlugs[$slug->model][] = $slug;
82        }
83
84        $relatedData = [];
85        foreach ($groupedSlugs as $modelName => $modelSlugs) {
86            $foreignKeys = array_column($modelSlugs, 'foreign_key');
87
88            // Skip if no foreign keys for this model (e.g., if pagination resulted in no slugs for a model)
89            if (empty($foreignKeys)) {
90                continue;
91            }
92
93            try {
94                $relatedTable = $this->fetchTable($modelName);
95
96                // Define select fields based on the model type
97                $selectFields = ['id', 'title'];
98                if ($modelName === 'Articles') {
99                    $selectFields[] = 'kind';
100                    $selectFields[] = 'is_published';
101                }
102
103                $relatedRecords = $relatedTable->find()
104                    ->select($selectFields)
105                    ->where(['id IN' => $foreignKeys])
106                    ->all()
107                    ->indexBy('id') // Index by ID for easy lookup
108                    ->toArray();
109
110                foreach ($modelSlugs as $slug) {
111                    if (isset($relatedRecords[$slug->foreign_key])) {
112                        $relatedRecord = $relatedRecords[$slug->foreign_key];
113                        $relatedData[$slug->id] = [
114                            'title' => $relatedRecord->title,
115                            'controller' => $modelName, // 'Articles', 'Tags', etc.
116                            'id' => $relatedRecord->id,
117                        ];
118
119                        // Add specific fields for Articles
120                        if ($modelName === 'Articles') {
121                            $relatedData[$slug->id]['kind'] = $relatedRecord->kind;
122                            $relatedData[$slug->id]['is_published'] = $relatedRecord->is_published;
123                        }
124                    } else {
125                        // Handle cases where the related record might have been deleted
126                        $this->log(sprintf(
127                            'Related record for slug %s (model: %s, foreign_key: %s) not found.',
128                            $slug->id,
129                            $modelName,
130                            $slug->foreign_key,
131                        ), 'warning');
132                        $relatedData[$slug->id] = [
133                            'title' => __('(Deleted)'),
134                            'controller' => $modelName,
135                            'id' => null, // Indicate that the record is missing
136                        ];
137                    }
138                }
139            } catch (Exception $e) {
140                $this->log(sprintf(
141                    'Failed to fetch related records for model %s: %s',
142                    $modelName,
143                    $e->getMessage(),
144                ), 'error');
145                // For all slugs associated with this problematic model, mark them as unretrievable
146                foreach ($modelSlugs as $slug) {
147                    $relatedData[$slug->id] = [
148                        'title' => __('(Error loading)'),
149                        'controller' => $modelName,
150                        'id' => null,
151                    ];
152                }
153            }
154        }
155
156        if ($this->request->is('ajax')) {
157            // Pass the original $slugs (ResultSet) to the view
158            $this->set(compact('slugs', 'search', 'relatedData', 'modelTypes', 'statusFilter'));
159            $this->viewBuilder()->setLayout('ajax');
160
161            return $this->render('search_results');
162        }
163
164        // Pass the original $slugs (ResultSet) to the view
165        $this->set(compact('slugs', 'relatedData', 'modelTypes', 'statusFilter'));
166
167        return null;
168    }
169
170    /**
171     * View method
172     *
173     * Displays detailed information about a specific slug and its associated content.
174     * Dynamically loads the related record based on the slug's model type.
175     *
176     * @param string|null $id The UUID of the slug to view
177     * @return void
178     * @throws \Cake\Datasource\Exception\RecordNotFoundException When slug not found
179     */
180    public function view(?string $id = null): void
181    {
182        $slug = $this->Slugs->get($id);
183
184        // Get the related record if possible
185        $relatedRecord = null;
186        if ($slug->model && $slug->foreign_key) {
187            try {
188                $relatedTable = $this->fetchTable($slug->model);
189
190                // Build the query based on the model type
191                $query = $relatedTable->find()
192                    ->where(['id' => $slug->foreign_key]);
193
194                // Add specific fields for Articles
195                if ($slug->model === 'Articles') {
196                    $query->select(['id', 'title', 'kind', 'slug', 'is_published']);
197                } else {
198                    $query->select(['id', 'title', 'slug']);
199                }
200
201                $relatedRecord = $query->first();
202
203                if (!$relatedRecord) {
204                    $this->Flash->warning(__(
205                        'The related {0} record (ID: {1}) could not be found.',
206                        $slug->model,
207                        $slug->foreign_key,
208                    ));
209                    $this->log(sprintf(
210                        'Related record not found for slug %s (model: %s, foreign_key: %s)',
211                        $slug->id,
212                        $slug->model,
213                        $slug->foreign_key,
214                    ), 'warning');
215                }
216            } catch (Exception $e) {
217                $this->Flash->error(__('Unable to load related {0} record.', $slug->model));
218                $this->log(sprintf(
219                    'Failed to fetch related record for slug %s (model: %s, foreign_key: %s): %s',
220                    $slug->id,
221                    $slug->model,
222                    $slug->foreign_key,
223                    $e->getMessage(),
224                ), 'error');
225            }
226        }
227
228        // Get all slugs for this model/foreign_key combination
229        $relatedSlugs = $this->Slugs->find()
230            ->where([
231                'model' => $slug->model,
232                'foreign_key' => $slug->foreign_key,
233                'id !=' => $slug->id,
234            ])
235            ->orderBy(['created' => 'DESC'])
236            ->all();
237
238        $this->set(compact('slug', 'relatedRecord', 'relatedSlugs'));
239    }
240
241    /**
242     * Add method
243     *
244     * Creates a new slug record with associated content relationship.
245     * Provides form with model selection and related content options.
246     *
247     * @return \Cake\Http\Response|null|void Redirects to index on success, renders view otherwise
248     */
249    public function add(): ?Response
250    {
251        $slug = $this->Slugs->newEmptyEntity();
252
253        // Get all unique model types from the slugs table
254        $modelTypes = $this->Slugs->find()
255            ->select(['model'])
256            ->distinct('model')
257            ->orderBy(['model' => 'ASC'])
258            ->all()
259            ->map(fn($row) => $row->model)
260            ->toArray();
261
262        if ($this->request->is('post')) {
263            $slug = $this->Slugs->patchEntity($slug, $this->request->getData());
264            if ($this->Slugs->save($slug)) {
265                $this->Flash->success(__('The slug has been saved.'));
266
267                return $this->redirect(['action' => 'index']);
268            }
269            $this->Flash->error(__('The slug could not be saved. Please, try again.'));
270        }
271
272        // Get the selected model (either from form data or default to first model)
273        $selectedModel = $this->request->getData('model') ?? ($modelTypes[0] ?? null);
274
275        // If we have a selected model, get its records
276        $relatedRecords = [];
277        if ($selectedModel) {
278            try {
279                $relatedRecords = $this->fetchTable($selectedModel)
280                    ->find('list', limit: 200)
281                    ->all();
282            } catch (Exception $e) {
283                $this->Flash->error(__('Unable to load related records for {0}.', $selectedModel));
284                $this->log(sprintf(
285                    'Failed to fetch related records for model %s: %s',
286                    $selectedModel,
287                    $e->getMessage(),
288                ), 'error');
289            }
290        }
291
292        $this->set(compact('slug', 'modelTypes', 'relatedRecords', 'selectedModel'));
293
294        return null;
295    }
296
297    /**
298     * Edit method
299     *
300     * Modifies an existing slug record and its relationships.
301     * Provides form with current values and content selection options.
302     *
303     * @param string|null $id The UUID of the slug to edit
304     * @return \Cake\Http\Response|null|void Redirects to index on success, renders view otherwise
305     * @throws \Cake\Datasource\Exception\RecordNotFoundException When slug not found
306     */
307    public function edit(?string $id = null): ?Response
308    {
309        $slug = $this->Slugs->find()
310            ->where(['id' => $id])
311            ->firstOrFail();
312
313        if ($this->request->is(['patch', 'post', 'put'])) {
314            $slug = $this->Slugs->patchEntity($slug, $this->request->getData());
315            if ($this->Slugs->save($slug)) {
316                $this->Flash->success(__('The slug has been saved.'));
317
318                return $this->redirect(['action' => 'index']);
319            }
320            $this->Flash->error(__('The slug could not be saved. Please, try again.'));
321        }
322
323        // Get related records based on the model type
324        $relatedRecords = $this->fetchTable($slug->model)->find('list', limit: 200)->all();
325        $this->set(compact('relatedRecords'));
326
327        $this->set(compact('slug'));
328
329        return null;
330    }
331
332    /**
333     * Delete method
334     *
335     * @param string|null $id Slug id.
336     * @return \Cake\Http\Response|null Redirects to index.
337     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
338     */
339    public function delete(?string $id = null): ?Response
340    {
341        $this->request->allowMethod(['post', 'delete']);
342        $slug = $this->Slugs->get($id);
343        if ($this->Slugs->delete($slug)) {
344            $this->Flash->success(__('The slug has been deleted.'));
345        } else {
346            $this->Flash->error(__('The slug could not be deleted. Please, try again.'));
347        }
348
349        return $this->redirect(['prefix' => 'Admin', 'controller' => 'slugs', 'action' => 'index']);
350    }
351}