• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

michalsn / codeigniter-translatable / 17217073242

25 Aug 2025 06:16PM UTC coverage: 95.279%. Remained the same
17217073242

push

github

web-flow
Bump actions/checkout from 4 to 5 (#3)

Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

222 of 233 relevant lines covered (95.28%)

8.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.05
/src/Traits/HasTranslations.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Michalsn\CodeIgniterTranslatable\Traits;
6

7
use CodeIgniter\BaseModel;
8
use Michalsn\CodeIgniterTranslatable\Config\Translatable;
9
use Michalsn\CodeIgniterTranslatable\Exceptions\TranslatableException;
10
use ReflectionException;
11
use ReflectionObject;
12

13
trait HasTranslations
14
{
15
    private BaseModel $translatableModel;
16
    private ?Translatable $translatableConfig;
17
    private array $translations       = [];
18
    private string $defaultLocale     = '';
19
    private array $supportedLocales   = [];
20
    private array $activeTranslations = [];
21
    private bool $tempUseFallbackLocale;
22
    private string $tempFallbackLocale;
23
    private bool $tempFillWithEmpty;
24
    private bool $searchInTranslations = false;
25

26
    /**
27
     * Set up model events and initialize
28
     * translatable model related stuff.
29
     */
30
    protected function initTranslations(BaseModel|string $translatableModel): void
31
    {
32
        $this->beforeInsert[]  = 'translationsBeforeInsert';
27✔
33
        $this->afterInsert[]   = 'translationsAfterInsert';
27✔
34
        $this->beforeUpdate[]  = 'translationsBeforeUpdate';
27✔
35
        $this->afterUpdate[]   = 'translationsAfterUpdate';
27✔
36
        $this->beforeFind[]    = 'translationsBeforeFind';
27✔
37
        $this->afterFind[]     = 'translationsAfterFind';
27✔
38
        $this->allowedFields[] = 'translations';
27✔
39

40
        $this->translatableModel  = $translatableModel instanceof BaseModel ? $translatableModel : model($translatableModel);
27✔
41
        $this->translatableConfig = config('Translatable');
27✔
42

43
        $this->defaultLocale    = config('App')->defaultLocale;
27✔
44
        $this->supportedLocales = config('App')->supportedLocales;
27✔
45

46
        $this->tempUseFallbackLocale = $this->translatableConfig->useFallbackLocale;
27✔
47
        $this->tempFallbackLocale    = $this->translatableConfig->fallbackLocale ?? $this->defaultLocale;
27✔
48
        $this->tempFillWithEmpty     = $this->translatableConfig->fillWithEmpty;
27✔
49

50
        helper('inflector');
27✔
51
    }
52

53
    /**
54
     * Will get all the supported translations.
55
     */
56
    public function withAllTranslations(): static
57
    {
58
        $this->activeTranslations = $this->supportedLocales;
3✔
59

60
        return $this;
3✔
61
    }
62

63
    /**
64
     * Will get only listed translations.
65
     */
66
    public function withTranslations(array $locales): static
67
    {
68
        foreach ($locales as $locale) {
3✔
69
            if (! in_array($locale, $this->supportedLocales, true)) {
3✔
70
                throw TranslatableException::forLocaleNotSupported($locale);
×
71
            }
72
        }
73

74
        $this->activeTranslations = $locales;
3✔
75

76
        return $this;
3✔
77
    }
78

79
    public function useFallbackLocale(bool $fallback = true): self
80
    {
81
        $this->tempUseFallbackLocale = $fallback;
2✔
82

83
        return $this;
2✔
84
    }
85

86
    public function setFallbackLocale(string $locale): self
87
    {
88
        if (! in_array($locale, $this->supportedLocales, true)) {
3✔
89
            throw TranslatableException::forLocaleNotSupported($locale);
1✔
90
        }
91

92
        $this->tempFallbackLocale = $locale;
2✔
93
        $this->useFallbackLocale();
2✔
94

95
        return $this;
2✔
96
    }
97

98
    public function useFillOnEmpty(bool $fill = true): self
99
    {
100
        $this->tempFillWithEmpty = $fill;
1✔
101

102
        return $this;
1✔
103
    }
104

105
    /**
106
     * Sets the default locale for current request.
107
     */
108
    private function setDefaultTranslations(): void
109
    {
110
        $this->activeTranslations = [is_cli() ? $this->defaultLocale : service('request')->getLocale()];
17✔
111
    }
112

113
    /**
114
     * Build foreign key based on current model settings.
115
     */
116
    private function buildForeignKeyField(): string
117
    {
118
        return singular($this->table) . '_id';
27✔
119
    }
120

121
    /**
122
     * Build primary key based on a translatable model.
123
     */
124
    private function buildPrimaryKeyField(): string
125
    {
126
        $refObj = new ReflectionObject($this->translatableModel);
5✔
127

128
        $refProp = $refObj->getProperty('primaryKey');
5✔
129

130
        return $refProp->getValue($this->translatableModel);
5✔
131
    }
132

133
    /**
134
     * Store translations before insert/update.
135
     */
136
    private function setTranslations(array $translations): void
137
    {
138
        foreach ($translations as $locale => $fields) {
27✔
139
            if (in_array($locale, $this->supportedLocales, true)) {
27✔
140
                $this->translations[$locale] = $fields;
27✔
141
            }
142
        }
143
    }
144

145
    /**
146
     * Before insert event.
147
     */
148
    protected function translationsBeforeInsert(array $eventData): array
149
    {
150
        if (array_key_exists('translations', $eventData['data'])) {
27✔
151
            $this->setTranslations($eventData['data']['translations']);
27✔
152
            unset($eventData['data']['translations']);
27✔
153
        }
154

155
        return $eventData;
27✔
156
    }
157

158
    /**
159
     * After insert event.
160
     */
161
    protected function translationsAfterInsert(array $eventData): array
162
    {
163
        if ($this->translations !== [] && $eventData['result']) {
27✔
164
            $foreignKeyField = $this->buildForeignKeyField();
27✔
165

166
            foreach ($this->translations as $locale => $translations) {
27✔
167
                $this->translatableModel->insert(array_merge($translations, [
27✔
168
                    $foreignKeyField => $eventData[$this->primaryKey],
27✔
169
                    'locale'         => $locale,
27✔
170
                ]));
27✔
171
            }
172

173
            $this->translations = [];
27✔
174
        }
175

176
        return $eventData;
27✔
177
    }
178

179
    /**
180
     * Before update event.
181
     */
182
    protected function translationsBeforeUpdate(array $eventData): array
183
    {
184
        if (array_key_exists('translations', $eventData['data'])) {
1✔
185
            $this->setTranslations($eventData['data']['translations']);
1✔
186
            unset($eventData['data']['translations']);
1✔
187
        }
188

189
        return $eventData;
1✔
190
    }
191

192
    /**
193
     * After update event.
194
     *
195
     * @throws ReflectionException
196
     */
197
    protected function translationsAfterUpdate(array $eventData): array
198
    {
199
        if ($this->translations !== [] && $eventData['result']) {
1✔
200
            $foreignKeyField = $this->buildForeignKeyField();
1✔
201

202
            foreach ($this->translations as $locale => $translations) {
1✔
203
                foreach ($eventData[$this->primaryKey] as $id) {
1✔
204
                    $where = [
1✔
205
                        $foreignKeyField => $id,
1✔
206
                        'locale'         => $locale,
1✔
207
                    ];
1✔
208

209
                    $found = $this->translatableModel
1✔
210
                        ->where($where)
1✔
211
                        ->countAllResults();
1✔
212

213
                    $translations = array_merge((array) $translations, $where);
1✔
214

215
                    if ($found === 1) {
1✔
216
                        $this->translatableModel
1✔
217
                            ->where($where)
1✔
218
                            ->update(null, $translations);
1✔
219
                    } else {
220
                        $this->translatableModel->insert($translations);
×
221
                    }
222
                }
223
            }
224

225
            $this->translations = [];
1✔
226
        }
227

228
        return $eventData;
1✔
229
    }
230

231
    /**
232
     * Before find event.
233
     */
234
    protected function translationsBeforeFind(array $eventData): array
235
    {
236
        if (! $this->searchInTranslations) {
23✔
237
            return $eventData;
15✔
238
        }
239

240
        if ($this->activeTranslations === []) {
8✔
241
            $this->setDefaultTranslations();
8✔
242
        }
243

244
        $locales = $this->activeTranslations;
8✔
245

246
        // Make sure the fallback locale is used
247
        if ($this->tempUseFallbackLocale && ! in_array($this->tempFallbackLocale, $locales, true)) {
8✔
248
            $locales[] = $this->tempFallbackLocale;
×
249
        }
250

251
        $this
8✔
252
            ->groupEnd()
8✔
253
            ->whereIn($this->translatableModel->getTable() . '.locale', $locales)
8✔
254
            ->select('DISTINCT ' . $this->db->prefixTable($this->table) . '.*', false)
8✔
255
            ->join(
8✔
256
                $this->translatableModel->getTable(),
8✔
257
                sprintf(
8✔
258
                    '%s.%s = %s.%s',
8✔
259
                    $this->translatableModel->getTable(),
8✔
260
                    $this->buildForeignKeyField(),
8✔
261
                    $this->table,
8✔
262
                    $this->primaryKey,
8✔
263
                ),
8✔
264
            );
8✔
265

266
        return $eventData;
8✔
267
    }
268

269
    /**
270
     * After find event.
271
     */
272
    protected function translationsAfterFind(array $eventData): array
273
    {
274
        if (empty($eventData['data'])) {
23✔
275
            return $eventData;
×
276
        }
277

278
        if ($this->activeTranslations === []) {
23✔
279
            $this->setDefaultTranslations();
9✔
280
        }
281

282
        $locales = $this->activeTranslations;
23✔
283

284
        // Make sure the fallback locale is used
285
        if ($this->tempUseFallbackLocale && ! in_array($this->tempFallbackLocale, $locales, true)) {
23✔
286
            $locales[] = $this->tempFallbackLocale;
3✔
287
        }
288

289
        if ($eventData['singleton']) {
23✔
290
            if ($this->tempReturnType === 'array') {
13✔
291
                $eventData['data']['translations'] = $this->getTranslatableById($eventData['data'][$this->primaryKey], $locales);
7✔
292
            } else {
293
                $eventData['data']->translations = $this->getTranslatableById($eventData['data']->{$this->primaryKey}, $locales);
6✔
294
            }
295
        } else {
296
            $keys         = array_column($eventData['data'], $this->primaryKey);
10✔
297
            $translations = $this->getTranslatableByIds($keys, $locales);
10✔
298

299
            foreach ($eventData['data'] as &$data) {
10✔
300
                if ($this->tempReturnType === 'array') {
10✔
301
                    $data['translations'] = $translations[$data[$this->primaryKey]] ?? [];
9✔
302
                } else {
303
                    $data->translations = $translations[$data->{$this->primaryKey}] ?? [];
1✔
304
                }
305
            }
306
        }
307

308
        $this->resetTranslations();
23✔
309

310
        return $eventData;
23✔
311
    }
312

313
    /**
314
     * Reset translations.
315
     */
316
    private function resetTranslations(): void
317
    {
318
        $this->tempUseFallbackLocale = $this->translatableConfig->useFallbackLocale;
23✔
319
        $this->tempFallbackLocale    = $this->translatableConfig->fallbackLocale ?? $this->defaultLocale;
23✔
320
        $this->tempFillWithEmpty     = $this->translatableConfig->fillWithEmpty;
23✔
321
        $this->activeTranslations    = [];
23✔
322
        $this->searchInTranslations  = false;
23✔
323
    }
324

325
    /**
326
     * @param list<string> $locales
327
     */
328
    protected function getTranslatableById(int|string $foreignKeyId, array $locales): array
329
    {
330
        $foreignKeyField = $this->buildForeignKeyField();
13✔
331
        $items           = $this->translatableModel
13✔
332
            ->where($foreignKeyField, $foreignKeyId)
13✔
333
            ->whereIn('locale', $locales)
13✔
334
            ->findAll();
13✔
335

336
        // Format
337
        $items = array_map(static function ($item) use ($foreignKeyField) {
13✔
338
            if (is_array($item)) {
10✔
339
                unset($item[$foreignKeyField], $item['locale']);
×
340
            } else {
341
                unset($item->{$foreignKeyField}, $item->locale);
10✔
342
            }
343

344
            return $item;
10✔
345
        }, array_column($items, null, 'locale'));
13✔
346

347
        // Fill missing translations
348
        if (($this->tempFillWithEmpty || $this->tempUseFallbackLocale)
13✔
349
            && ($missingLocales = array_diff($locales, array_keys($items))) !== []) {
13✔
350
            $primaryKeyField = $this->buildPrimaryKeyField();
4✔
351

352
            foreach ($missingLocales as $missing) {
4✔
353
                if ($this->tempUseFallbackLocale && isset($items[$this->tempFallbackLocale])) {
4✔
354
                    $items[$missing] = $items[$this->tempFallbackLocale] ?? null;
2✔
355
                    if (is_array($items[$missing])) {
2✔
356
                        $items[$missing][$primaryKeyField] = null;
×
357
                    } else {
358
                        $items[$missing]->{$primaryKeyField} = null;
2✔
359
                    }
360
                } else {
361
                    $items[$missing] = $this->fillEmptyTranslation($foreignKeyField, $foreignKeyId);
2✔
362
                }
363
            }
364
        }
365

366
        // Remove fallback locale if conditions are met
367
        if ($this->tempUseFallbackLocale && count($this->activeTranslations) < count($locales)) {
13✔
368
            unset($items[$this->tempFallbackLocale]);
2✔
369
        }
370

371
        return $items;
13✔
372
    }
373

374
    /**
375
     * @param list<int|string> $foreignKeyIds
376
     * @param list<string>     $locales
377
     */
378
    protected function getTranslatableByIds(array $foreignKeyIds, array $locales): array
379
    {
380
        $foreignKeyField = $this->buildForeignKeyField();
10✔
381
        $items           = $this->translatableModel
10✔
382
            ->whereIn($foreignKeyField, $foreignKeyIds)
10✔
383
            ->whereIn('locale', $locales)
10✔
384
            ->findAll();
10✔
385

386
        $results = [];
10✔
387

388
        // Format found translations
389
        foreach ($items as &$item) {
10✔
390
            if (is_array($item)) {
10✔
391
                $id     = $item[$foreignKeyField];
×
392
                $locale = $item['locale'];
×
393
                unset($item[$foreignKeyField], $item['locale']);
×
394
            } else {
395
                $id     = $item->{$foreignKeyField};
10✔
396
                $locale = $item->locale;
10✔
397
                unset($item->{$foreignKeyField}, $item->locale);
10✔
398
            }
399
            $results[$id][$locale] = $item;
10✔
400
        }
401

402
        unset($items);
10✔
403

404
        if ($this->tempFillWithEmpty || $this->tempUseFallbackLocale) {
10✔
405
            $removeFallback  = count($this->activeTranslations) < count($locales);
1✔
406
            $primaryKeyField = $this->buildPrimaryKeyField();
1✔
407

408
            foreach ($foreignKeyIds as $foreignKeyId) {
1✔
409
                // Fill missing translations
410
                if (($missingLocales = array_diff($locales, array_keys($results[$foreignKeyId] ?? []))) !== []) {
1✔
411
                    foreach ($missingLocales as $missing) {
1✔
412
                        if ($this->tempUseFallbackLocale && isset($results[$foreignKeyId][$this->tempFallbackLocale])) {
1✔
413
                            $results[$foreignKeyId][$missing] = $results[$foreignKeyId][$this->tempFallbackLocale] ?? null;
1✔
414
                            if (is_array($results[$foreignKeyId][$missing])) {
1✔
415
                                $results[$foreignKeyId][$missing][$primaryKeyField] = null;
×
416
                            } else {
417
                                $results[$foreignKeyId][$missing]->{$primaryKeyField} = null;
1✔
418
                            }
419
                        } else {
420
                            $results[$foreignKeyId][$missing] = $this->fillEmptyTranslation($foreignKeyField, $foreignKeyId);
×
421
                        }
422
                    }
423
                }
424

425
                // Remove fallback locale if conditions are met
426
                if ($this->tempUseFallbackLocale && $removeFallback) {
1✔
427
                    unset($results[$foreignKeyId][$this->tempFallbackLocale]);
1✔
428
                }
429
            }
430
        }
431

432
        return $results;
10✔
433
    }
434

435
    /**
436
     * Fill empty translations.
437
     */
438
    private function fillEmptyTranslation(string $foreignKeyField, int|string $foreignKeyId)
439
    {
440
        $refObj = new ReflectionObject($this->translatableModel);
2✔
441

442
        $refProp = $refObj->getProperty('primaryKey');
2✔
443
        $idField = $refProp->getValue($this->translatableModel);
2✔
444

445
        $refProp = $refObj->getProperty('allowedFields');
2✔
446
        $fields  = $refProp->getValue($this->translatableModel);
2✔
447

448
        array_unshift($fields, $idField);
2✔
449

450
        return array_reduce(
2✔
451
            $fields,
2✔
452
            static function ($acc, $key) use ($foreignKeyField) {
2✔
453
                if ($key === 'locale') {
2✔
454
                    return $acc;
2✔
455
                }
456
                if ($key === $foreignKeyField) {
2✔
457
                    // $acc[$key] = $foreignKeyId;
458
                    unset($acc[$key]);
2✔
459

460
                    return $acc;
2✔
461
                }
462
                $acc[$key] = $key === 'id' ? null : '';
2✔
463

464
                return $acc;
2✔
465
            },
2✔
466
            [],
2✔
467
        );
2✔
468
    }
469

470
    private function handleTranslationSearch(): void
471
    {
472
        if ($this->searchInTranslations === false) {
8✔
473
            $this->searchInTranslations = true;
8✔
474
            $this->groupStart();
8✔
475
        }
476
    }
477

478
    public function whereTranslation(string $field, int|string|null $value = null): self
479
    {
480
        $this->handleTranslationSearch();
2✔
481
        $this->where($this->translatableModel->getTable() . '.' . $field, $value);
2✔
482

483
        return $this;
2✔
484
    }
485

486
    public function orWhereTranslation(string $field, int|string|null $value = null): self
487
    {
488
        $this->handleTranslationSearch();
1✔
489
        $this->orWhere($this->translatableModel->getTable() . '.' . $field, $value);
1✔
490

491
        return $this;
1✔
492
    }
493

494
    public function whereInTranslation(string $field, array $value): self
495
    {
496
        $this->handleTranslationSearch();
1✔
497
        $this->whereIn($this->translatableModel->getTable() . '.' . $field, $value);
1✔
498

499
        return $this;
1✔
500
    }
501

502
    public function whereNotInTranslation(string $field, array $value): self
503
    {
504
        $this->handleTranslationSearch();
1✔
505
        $this->whereNotIn($this->translatableModel->getTable() . '.' . $field, $value);
1✔
506

507
        return $this;
1✔
508
    }
509

510
    public function likeTranslation(string $field, string $value, string $type = 'both'): self
511
    {
512
        $this->handleTranslationSearch();
3✔
513
        $this->like($this->translatableModel->getTable() . '.' . $field, $value, $type);
3✔
514

515
        return $this;
3✔
516
    }
517

518
    public function orLikeTranslation(string $field, string $value, string $type = 'both'): self
519
    {
520
        $this->handleTranslationSearch();
1✔
521
        $this->orLike($this->translatableModel->getTable() . '.' . $field, $value, $type);
1✔
522

523
        return $this;
1✔
524
    }
525

526
    public function notLikeTranslation(string $field, string $value, string $type = 'both'): self
527
    {
528
        $this->handleTranslationSearch();
1✔
529
        $this->notLike($this->translatableModel->getTable() . '.' . $field, $value, $type);
1✔
530

531
        return $this;
1✔
532
    }
533

534
    public function orNotLikeTranslation(string $field, string $value, string $type = 'both'): self
535
    {
536
        $this->handleTranslationSearch();
1✔
537
        $this->orNotLike($this->translatableModel->getTable() . '.' . $field, $value, $type);
1✔
538

539
        return $this;
1✔
540
    }
541
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc