Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.91% covered (success)
90.91%
60 / 66
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
SlugsTable
90.91% covered (success)
90.91%
60 / 66
80.00% covered (warning)
80.00%
4 / 5
8.05
0.00% covered (danger)
0.00%
0 / 1
 initialize
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 setupAssociations
72.73% covered (warning)
72.73%
16 / 22
0.00% covered (danger)
0.00%
0 / 1
4.32
 validationDefault
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
1
 buildRules
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 findBySlugAndModel
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3
4namespace App\Model\Table;
5
6use Cake\Core\App;
7use Cake\Log\LogTrait;
8use Cake\ORM\Query\SelectQuery;
9use Cake\ORM\RulesChecker;
10use Cake\ORM\Table;
11use Cake\Validation\Validator;
12use Exception;
13
14/**
15 * Slugs Model
16 *
17 * @method \App\Model\Entity\Slug newEmptyEntity()
18 * @method \App\Model\Entity\Slug newEntity(array $data, array $options = [])
19 * @method array<\App\Model\Entity\Slug> newEntities(array $data, array $options = [])
20 * @method \App\Model\Entity\Slug get(mixed $primaryKey, array|string $finder = 'all', \Psr\SimpleCache\CacheInterface|string|null $cache = null, \Closure|string|null $cacheKey = null, mixed ...$args)
21 * @method \App\Model\Entity\Slug findOrCreate($search, ?callable $callback = null, array $options = [])
22 * @method \App\Model\Entity\Slug patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = [])
23 * @method array<\App\Model\Entity\Slug> patchEntities(iterable $entities, array $data, array $options = [])
24 * @method \App\Model\Entity\Slug|false save(\Cake\Datasource\EntityInterface $entity, array $options = [])
25 * @method \App\Model\Entity\Slug saveOrFail(\Cake\Datasource\EntityInterface $entity, array $options = [])
26 * @method iterable<\App\Model\Entity\Slug>|\Cake\Datasource\ResultSetInterface<\App\Model\Entity\Slug>|false saveMany(iterable $entities, array $options = [])
27 * @method iterable<\App\Model\Entity\Slug>|\Cake\Datasource\ResultSetInterface<\App\Model\Entity\Slug> saveManyOrFail(iterable $entities, array $options = [])
28 * @method iterable<\App\Model\Entity\Slug>|\Cake\Datasource\ResultSetInterface<\App\Model\Entity\Slug>|false deleteMany(iterable $entities, array $options = [])
29 * @method iterable<\App\Model\Entity\Slug>|\Cake\Datasource\ResultSetInterface<\App\Model\Entity\Slug> deleteManyOrFail(iterable $entities, array $options = [])
30 * @mixin \Cake\ORM\Behavior\TimestampBehavior
31 */
32class SlugsTable extends Table
33{
34    use LogTrait;
35
36    /**
37     * Initialize method
38     *
39     * @param array<string, mixed> $config The configuration for the Table.
40     * @return void
41     */
42    public function initialize(array $config): void
43    {
44        parent::initialize($config);
45
46        $this->setTable('slugs');
47        $this->setDisplayField('slug');
48        $this->setPrimaryKey('id');
49
50        $this->addBehavior('Timestamp', [
51            'events' => [
52                'Model.beforeSave' => [
53                    'created' => 'new',
54                ],
55            ],
56        ]);
57
58        // Set up dynamic associations based on existing slugs
59        $this->setupAssociations();
60    }
61
62    /**
63     * Sets up dynamic associations based on the unique model values in the slugs table.
64     * Uses cache to improve performance.
65     *
66     * @return void
67     */
68    protected function setupAssociations(): void
69    {
70        $models = $this->find()
71            ->select(['model'])
72            ->distinct()
73            ->disableHydration()
74            ->all()
75            ->extract('model')
76            ->toArray();
77
78        foreach ($models as $model) {
79            try {
80                $className = App::className($model, 'Model/Table', 'Table');
81                if ($className) {
82                    $this->belongsTo($model, [
83                        'className' => $className,
84                        'foreignKey' => 'foreign_key',
85                        'conditions' => [$this->getAlias() . '.model' => $model],
86                        'joinType' => 'LEFT',
87                    ]);
88                }
89            } catch (Exception $e) {
90                $this->log(sprintf(
91                    'Failed to setup association for model %s: %s',
92                    $model,
93                    $e->getMessage(),
94                ), 'error');
95            }
96        }
97    }
98
99    /**
100     * Default validation rules.
101     *
102     * @param \Cake\Validation\Validator $validator Validator instance.
103     * @return \Cake\Validation\Validator
104     */
105    public function validationDefault(Validator $validator): Validator
106    {
107        $validator
108            ->uuid('id')
109            ->allowEmptyString('id', 'create');
110
111        $validator
112            ->scalar('model')
113            ->maxLength('model', 20)
114            ->requirePresence('model', 'create')
115            ->notEmptyString('model');
116
117        $validator
118            ->uuid('foreign_key')
119            ->requirePresence('foreign_key', 'create')
120            ->notEmptyString('foreign_key');
121
122        $validator
123            ->scalar('slug')
124            ->maxLength('slug', 255)
125            ->requirePresence('slug', 'create')
126            ->notEmptyString('slug')
127            ->regex(
128                'slug',
129                '/^[a-z0-9-]+$/',
130                __('The slug must be URL-safe (only lowercase letters, numbers, and hyphens)'),
131            );
132
133        return $validator;
134    }
135
136    /**
137     * Returns a rules checker object that will be used for validating
138     * application integrity.
139     *
140     * @param \Cake\ORM\RulesChecker $rules The rules object to be modified.
141     * @return \Cake\ORM\RulesChecker
142     */
143    public function buildRules(RulesChecker $rules): RulesChecker
144    {
145        $rules->add($rules->isUnique(
146            ['slug', 'model'],
147            __('This slug is already in use for this model type.'),
148        ));
149
150        return $rules;
151    }
152
153    /**
154     * Find by slug and model.
155     *
156     * @param \Cake\ORM\Query\SelectQuery $query The query to modify
157     * @param array $options The options containing slug and model
158     * @return \Cake\ORM\Query\SelectQuery
159     */
160    public function findBySlugAndModel(SelectQuery $query, array $options): SelectQuery
161    {
162        return $query->where([
163            $this->getAlias() . '.slug' => $options['slug'],
164            $this->getAlias() . '.model' => $options['model'],
165        ]);
166    }
167}