Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.26% covered (success)
97.26%
71 / 73
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImageGalleriesImagesTable
97.26% covered (success)
97.26%
71 / 73
90.00% covered (success)
90.00%
9 / 10
14
0.00% covered (danger)
0.00%
0 / 1
 initialize
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 validationDefault
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
 buildRules
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 afterSave
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 afterDelete
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 reorderImages
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
 getNextPosition
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 findOrdered
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 queuePreviewRegeneration
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 clearGalleryCache
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3
4namespace App\Model\Table;
5
6use ArrayObject;
7use Cake\Cache\Cache;
8use Cake\Datasource\EntityInterface;
9use Cake\Datasource\FactoryLocator;
10use Cake\Event\EventInterface;
11use Cake\Log\LogTrait;
12use Cake\ORM\Query\SelectQuery;
13use Cake\ORM\RulesChecker;
14use Cake\ORM\Table;
15use Cake\Validation\Validator;
16
17/**
18 * ImageGalleriesImages Model
19 *
20 * @property \App\Model\Table\ImageGalleriesTable&\Cake\ORM\Association\BelongsTo $ImageGalleries
21 * @property \App\Model\Table\ImagesTable&\Cake\ORM\Association\BelongsTo $Images
22 * @method \App\Model\Entity\ImageGalleriesImage newEmptyEntity()
23 * @method \App\Model\Entity\ImageGalleriesImage newEntity(array $data, array $options = [])
24 * @method array<\App\Model\Entity\ImageGalleriesImage> newEntities(array $data, array $options = [])
25 * @method \App\Model\Entity\ImageGalleriesImage get(mixed $primaryKey, array|string $finder = 'all', \Psr\SimpleCache\CacheInterface|string|null $cache = null, \Closure|string|null $cacheKey = null, mixed ...$args)
26 * @method \App\Model\Entity\ImageGalleriesImage findOrCreate($search, ?callable $callback = null, array $options = [])
27 * @method \App\Model\Entity\ImageGalleriesImage patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = [])
28 * @method array<\App\Model\Entity\ImageGalleriesImage> patchEntities(iterable $entities, array $data, array $options = [])
29 * @method \App\Model\Entity\ImageGalleriesImage|false save(\Cake\Datasource\EntityInterface $entity, array $options = [])
30 * @method \App\Model\Entity\ImageGalleriesImage saveOrFail(\Cake\Datasource\EntityInterface $entity, array $options = [])
31 * @method iterable<\App\Model\Entity\ImageGalleriesImage>|\Cake\Datasource\ResultSetInterface<\App\Model\Entity\ImageGalleriesImage>|false saveMany(iterable $entities, array $options = [])
32 * @method iterable<\App\Model\Entity\ImageGalleriesImage>|\Cake\Datasource\ResultSetInterface<\App\Model\Entity\ImageGalleriesImage> saveManyOrFail(iterable $entities, array $options = [])
33 * @method iterable<\App\Model\Entity\ImageGalleriesImage>|\Cake\Datasource\ResultSetInterface<\App\Model\Entity\ImageGalleriesImage>|false deleteMany(iterable $entities, array $options = [])
34 * @method iterable<\App\Model\Entity\ImageGalleriesImage>|\Cake\Datasource\ResultSetInterface<\App\Model\Entity\ImageGalleriesImage> deleteManyOrFail(iterable $entities, array $options = [])
35 * @mixin \Cake\ORM\Behavior\TimestampBehavior
36 */
37class ImageGalleriesImagesTable extends Table
38{
39    use LogTrait;
40
41    /**
42     * Initialize method
43     *
44     * @param array<string, mixed> $config The configuration for the Table.
45     * @return void
46     */
47    public function initialize(array $config): void
48    {
49        parent::initialize($config);
50
51        $this->setTable('image_galleries_images');
52        $this->setDisplayField('id');
53        $this->setPrimaryKey('id');
54
55        $this->addBehavior('Timestamp');
56
57        $this->belongsTo('ImageGalleries', [
58            'foreignKey' => 'image_gallery_id',
59            'joinType' => 'INNER',
60        ]);
61        $this->belongsTo('Images', [
62            'foreignKey' => 'image_id',
63            'joinType' => 'INNER',
64        ]);
65    }
66
67    /**
68     * Default validation rules.
69     *
70     * @param \Cake\Validation\Validator $validator Validator instance.
71     * @return \Cake\Validation\Validator
72     */
73    public function validationDefault(Validator $validator): Validator
74    {
75        $validator
76            ->uuid('image_gallery_id')
77            ->requirePresence('image_gallery_id', 'create')
78            ->notEmptyString('image_gallery_id');
79
80        $validator
81            ->uuid('image_id')
82            ->requirePresence('image_id', 'create')
83            ->notEmptyString('image_id');
84
85        $validator
86            ->integer('position')
87            ->requirePresence('position', 'create')
88            ->notEmptyString('position');
89
90        $validator
91            ->scalar('caption')
92            ->allowEmptyString('caption');
93
94        return $validator;
95    }
96
97    /**
98     * Returns a rules checker object that will be used for validating
99     * application integrity.
100     *
101     * @param \Cake\ORM\RulesChecker $rules The rules object to be modified.
102     * @return \Cake\ORM\RulesChecker
103     */
104    public function buildRules(RulesChecker $rules): RulesChecker
105    {
106        $rules->add($rules->existsIn(['image_gallery_id'], 'ImageGalleries'), ['errorField' => 'image_gallery_id']);
107        $rules->add($rules->existsIn(['image_id'], 'Images'), ['errorField' => 'image_id']);
108
109        return $rules;
110    }
111
112    /**
113     * afterSave callback - Queue preview regeneration when images are added to gallery
114     *
115     * @param \Cake\Event\EventInterface $event The event object
116     * @param \Cake\Datasource\EntityInterface $entity The gallery-image association
117     * @param \ArrayObject $options Options for the save operation
118     * @return void
119     */
120    public function afterSave(EventInterface $event, EntityInterface $entity, ArrayObject $options): void
121    {
122        // Queue preview regeneration for the gallery when images are added
123        $this->log(sprintf(
124            'ImageGalleriesImagesTable::afterSave triggered for gallery %s, image %s',
125            $entity->image_gallery_id,
126            $entity->image_id,
127        ), 'info', ['group_name' => 'ImageGalleriesImagesTable']);
128        $this->queuePreviewRegeneration($entity->image_gallery_id);
129        $this->clearGalleryCache($entity->image_gallery_id);
130    }
131
132    /**
133     * afterDelete callback - Queue preview regeneration when images are removed from gallery
134     *
135     * @param \Cake\Event\EventInterface $event The event object
136     * @param \Cake\Datasource\EntityInterface $entity The gallery-image association
137     * @param \ArrayObject $options Options for the delete operation
138     * @return void
139     */
140    public function afterDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options): void
141    {
142        // Queue preview regeneration for the gallery when images are removed
143        $this->queuePreviewRegeneration($entity->image_gallery_id);
144        $this->clearGalleryCache($entity->image_gallery_id);
145    }
146
147    /**
148     * Reorder images within a gallery
149     *
150     * @param string $galleryId The gallery ID
151     * @param array $imageIds Array of image IDs in the desired order
152     * @return bool Success
153     */
154    public function reorderImages(string $galleryId, array $imageIds): bool
155    {
156        $connection = $this->getConnection();
157
158        $result = $connection->transactional(function () use ($galleryId, $imageIds) {
159            foreach ($imageIds as $position => $imageId) {
160                $this->updateAll(
161                    ['position' => $position],
162                    [
163                        'image_gallery_id' => $galleryId,
164                        'image_id' => $imageId,
165                    ],
166                );
167            }
168
169            return true;
170        });
171
172        // Queue preview regeneration and clear gallery cache after reordering
173        if ($result) {
174            $this->queuePreviewRegeneration($galleryId);
175            $this->clearGalleryCache($galleryId);
176        }
177
178        return $result;
179    }
180
181    /**
182     * Get the next available position for a gallery
183     *
184     * @param string $galleryId The gallery ID
185     * @return int The next position
186     */
187    public function getNextPosition(string $galleryId): int
188    {
189        $query = $this->find()
190            ->where(['image_gallery_id' => $galleryId])
191            ->select(['max_position' => $this->find()->func()->max('position')]);
192
193        $result = $query->first();
194
195        return $result && $result->max_position !== null ? (int)$result->max_position + 1 : 0;
196    }
197
198    /**
199     * Custom finder to get images ordered by position
200     *
201     * @param \Cake\ORM\Query\SelectQuery $query The query object
202     * @param array $options Options array
203     * @return \Cake\ORM\Query\SelectQuery
204     */
205    public function findOrdered(SelectQuery $query, array $options): SelectQuery
206    {
207        return $query->orderBy(['ImageGalleriesImages.position' => 'ASC']);
208    }
209
210    /**
211     * Queue preview regeneration for a gallery
212     *
213     * @param string $galleryId Gallery ID
214     * @return void
215     */
216    private function queuePreviewRegeneration(string $galleryId): void
217    {
218        // Get the ImageGalleries table and call its preview generation method
219        $imageGalleriesTable = FactoryLocator::get('Table')->get('ImageGalleries');
220        $imageGalleriesTable->queuePreviewGeneration($galleryId);
221    }
222
223    /**
224     * Clear gallery placeholder cache for both admin and public contexts
225     *
226     * @param string $galleryId Gallery ID
227     * @return void
228     */
229    private function clearGalleryCache(string $galleryId): void
230    {
231        // Clear both public and admin gallery caches
232        Cache::delete("gallery_placeholder_{$galleryId}", 'default');
233        Cache::delete("gallery_placeholder_admin_{$galleryId}", 'default');
234
235        // Clear article cache to update articles containing this gallery
236        // This ensures articles immediately reflect gallery image changes
237        Cache::clear('content');
238        $this->log(
239            sprintf('Cleared article cache due to gallery %s image changes', $galleryId),
240            'info',
241            ['group_name' => 'ImageGalleriesImagesTable'],
242        );
243    }
244}