Passed
Push — develop ( dcff1a...e70357 )
by Nikolay
19:46 queued 14s
created

ModelsBase::makeCacheKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 4
rs 10
cc 1
nc 1
nop 2
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright (C) 2017-2020 Alexey Portnov and Nikolay Beketov
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along with this program.
17
 * If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
namespace MikoPBX\Common\Models;
21
22
use MikoPBX\Common\Providers\BeanstalkConnectionModelsProvider;
23
use MikoPBX\Common\Providers\CDRDatabaseProvider;
24
use MikoPBX\Common\Providers\ManagedCacheProvider;
25
use MikoPBX\Common\Providers\ModelsCacheProvider;
26
use MikoPBX\Common\Providers\ModelsMetadataProvider;
27
use MikoPBX\Common\Providers\TranslationProvider;
28
use MikoPBX\Core\Providers\EventsLogDatabaseProvider;
29
use MikoPBX\Modules\PbxExtensionUtils;
30
use Phalcon\Db\Adapter\AdapterInterface;
31
use Phalcon\Di;
32
use Phalcon\Messages\Message;
33
use Phalcon\Messages\MessageInterface;
34
use Phalcon\Mvc\Model;
35
use Phalcon\Mvc\Model\Relation;
36
use Phalcon\Mvc\Model\Resultset;
37
use Phalcon\Mvc\Model\Resultset\Simple;
38
use Phalcon\Mvc\Model\ResultsetInterface;
39
use Phalcon\Text;
40
use Phalcon\Url;
41
42
/**
43
 * Class ModelsBase
44
 *
45
 * @method static mixed findFirstById(array|string|int $parameters = null)
46
 * @method static mixed findFirstByKey(string|null $parameters)
47
 * @method static mixed findFirstByUniqid(array|string|int $parameters = null)
48
 * @method static mixed findFirst(array|string|int $parameters = null)
49
 * @method static ResultsetInterface find(array|string|int $parameters = null)
50
 * @method static mixed count(array $parameters = null)
51
 * @method  bool create()
52
 * @method  bool delete()
53
 * @method  bool save()
54
 * @method  bool update()
55
 * @method  array|MessageInterface[] getMessages(mixed $filter = null)
56
 * @method static AdapterInterface getReadConnection()
57
 * @method  Simple|false getRelated(string $alias, $arguments = null)
58
 *
59
 * @property \Phalcon\Mvc\Model\Manager _modelsManager
60
 * @property \Phalcon\Di                di
61
 *
62
 * @package MikoPBX\Common\Models
63
 */
64
class ModelsBase extends Model
65
{
66
    /**
67
     * All models with lover than this version in module.json won't be attached as children
68
     */
69
    public const MIN_MODULE_MODEL_VER = '2020.2.468';
70
71
    public function initialize(): void
72
    {
73
        self::setup(['orm.events' => true]);
74
        $this->keepSnapshots(true);
75
        $this->addExtensionModulesRelations();
76
    }
77
78
    /**
79
     * Attaches model's relationships from modules models classes
80
     */
81
    private function addExtensionModulesRelations()
82
    {
83
        $modules = PbxExtensionModules::getEnabledModulesArray();
84
        foreach ($modules as $module) {
85
            $moduleDir = PbxExtensionUtils::getModuleDir($module['uniqid']);
86
87
            $moduleJson = "{$moduleDir}/module.json";
88
            if ( ! file_exists($moduleJson)) {
89
                continue;
90
            }
91
            $jsonString            = file_get_contents($moduleJson);
92
            $jsonModuleDescription = json_decode($jsonString, true);
93
            $minPBXVersion         = $jsonModuleDescription['min_pbx_version'] ?? '1.0.0';
94
            if (version_compare($minPBXVersion, self::MIN_MODULE_MODEL_VER, '<')) {
95
                continue;
96
            }
97
98
            $moduleModelsDir = "{$moduleDir}/Models";
99
            $results         = glob($moduleModelsDir . '/*.php', GLOB_NOSORT);
100
            foreach ($results as $file) {
101
                $className        = pathinfo($file)['filename'];
102
                $moduleModelClass = "\\Modules\\{$module['uniqid']}\\Models\\{$className}";
103
104
                if (class_exists($moduleModelClass)
105
                    && method_exists($moduleModelClass, 'getDynamicRelations')) {
106
                    $moduleModelClass::getDynamicRelations($this);
107
                }
108
            }
109
        }
110
    }
111
112
113
    /**
114
     * Обработчик ошибок валидации, обычно сюда попадаем если неправильно
115
     * сохраняются или удаляютмя модели или неправильно настроены зависимости между ними.
116
     * Эта функция формирует список ссылок на объект который мы пытаемся удалить
117
     *
118
     */
119
    public function onValidationFails(): void
120
    {
121
        $errorMessages = $this->getMessages();
122
        foreach ($errorMessages as $errorMessage) {
123
            if ($errorMessage->getType() === 'ConstraintViolation') {
124
                $arrMessageParts = explode('Common\\Models\\', $errorMessage->getMessage());
125
                if (count($arrMessageParts) === 2) {
126
                    $relatedModel = $arrMessageParts[1];
127
                } else {
128
                    $relatedModel = $errorMessage->getMessage();
129
                }
130
                $relatedRecords  = $this->getRelated($relatedModel);
131
                $newErrorMessage = $this->t('ConstraintViolation');
132
                $newErrorMessage .= "<ul class='list'>";
133
                if ($relatedRecords === false) {
134
                    throw new Model\Exception('Error on models relationship ' . $errorMessage);
135
                }
136
                if ($relatedRecords instanceof Resultset) {
137
                    foreach ($relatedRecords as $item) {
138
                        if ($item instanceof ModelsBase) {
139
                            $newErrorMessage .= '<li>' . $item->getRepresent(true) . '</li>';
140
                        }
141
                    }
142
                } elseif ($relatedRecords instanceof ModelsBase) {
143
                    $newErrorMessage .= '<li>' . $relatedRecords->getRepresent(true) . '</li>';
144
                } else {
145
                    $newErrorMessage .= '<li>Unknown object</li>';
146
                }
147
                $newErrorMessage .= '</ul>';
148
                $errorMessage->setMessage($newErrorMessage);
149
                break;
150
            }
151
        }
152
    }
153
154
    /**
155
     * Функция для доступа к массиву переводов из моделей, используется для
156
     * сообщений на понятном пользователю языке
157
     *
158
     * @param       $message
159
     * @param array $parameters
160
     *
161
     * @return mixed
162
     */
163
    public function t($message, $parameters = [])
164
    {
165
        return $this->getDI()->getShared(TranslationProvider::SERVICE_NAME)->t($message, $parameters);
166
    }
167
168
    /**
169
     * Returns a model's element representation
170
     *
171
     * @param bool $needLink add link to element
172
     *
173
     * @return string
174
     */
175
    public function getRepresent($needLink = false): string
176
    {
177
        switch (static::class) {
178
            case AsteriskManagerUsers::class:
179
                $name = '<i class="asterisk icon"></i> ';
180
                if (empty($this->id)) {
181
                    $name .= $this->t('mo_NewElementAsteriskManagerUsers');
182
                } else {
183
                    $name .= $this->t('repAsteriskManagerUsers', ['represent' => $this->username]);
184
                }
185
                break;
186
            case CallQueueMembers::class:
187
                $name = $this->Extensions->getRepresent();
188
                break;
189
            case CallQueues::class:
190
                $name = '<i class="users icon"></i> ';
191
                if (empty($this->id)) {
192
                    $name .= $this->t('mo_NewElementCallQueues');
193
                } else {
194
                    $name .= $this->t('mo_CallQueueShort4Dropdown') . ': ' . $this->name;
195
                }
196
                break;
197
            case ConferenceRooms::class:
198
                $name = '<i class="phone volume icon"></i> ';
199
                if (empty($this->id)) {
200
                    $name .= $this->t('mo_NewElementConferenceRooms');
201
                } else {
202
                    $name .= $this->t('mo_ConferenceRoomsShort4Dropdown') . ': ' . $this->name;
203
                }
204
                break;
205
            case CustomFiles::class:
206
                $name = "<i class='file icon'></i> {$this->filepath}";
207
                break;
208
            case DialplanApplications::class:
209
                $name = '<i class="php icon"></i> ';
210
                if (empty($this->id)) {
211
                    $name .= $this->t('mo_NewElementDialplanApplications');
212
                } else {
213
                    $name .= $this->t('mo_ApplicationShort4Dropdown') . ': ' . $this->name;
214
                }
215
                break;
216
            case ExtensionForwardingRights::class:
217
                $name = $this->Extensions->getRepresent();
218
                break;
219
            case Extensions::class:
220
                // Для внутреннего номера бывают разные представления
221
                if ($this->type === Extensions::TYPE_EXTERNAL) {
222
                    $icon = '<i class="icons"><i class="user outline icon"></i><i class="top right corner alternate mobile icon"></i></i>';
223
                } else {
224
                    $icon = '<i class="icons"><i class="user outline icon"></i></i>';
225
                }
226
                if (empty($this->id)) {
227
                    $name = "{$icon} {$this->t('mo_NewElementExtensions')}";
228
                } elseif ($this->userid > 0) {
229
                    $name = '';
230
                    if (isset($this->Users->username)) {
231
                        $name = $this->trimName($this->Users->username);
232
                    }
233
                    $name = "{$icon} {$name} <{$this->number}>";
234
                } else {
235
                    switch (strtoupper($this->type)) {
236
                        case Extensions::TYPE_CONFERENCE:
237
                            $name = $this->ConferenceRooms->getRepresent();
238
                            break;
239
                        case Extensions::TYPE_QUEUE:
240
                            $name = $this->CallQueues->getRepresent();
241
                            break;
242
                        case Extensions::TYPE_DIALPLAN_APPLICATION:
243
                            $name = $this->DialplanApplications->getRepresent();
244
                            break;
245
                        case Extensions::TYPE_IVR_MENU:
246
                            $name = $this->IvrMenu->getRepresent();
247
                            break;
248
                        case Extensions::TYPE_MODULES:
249
                            $name = '<i class="puzzle piece icon"></i> '
250
                                . $this->t('mo_ModuleShort4Dropdown')
251
                                . ': '
252
                                . $this->callerid;
253
                            break;
254
                        case Extensions::TYPE_EXTERNAL:
255
                        case Extensions::TYPE_SIP:
256
                        default:
257
                            $name = "{$this->callerid} <{$this->number}>";
258
                    }
259
                }
260
                break;
261
            case ExternalPhones::class:
262
                $name = $this->Extensions->getRepresent();
263
                break;
264
            case Fail2BanRules::class:
265
                $name = '';
266
                break;
267
            case FirewallRules::class:
268
                $name = $this->category;
269
                break;
270
            case Iax::class:
271
                $name = '<i class="server icon"></i> ';
272
                if (empty($this->id)) {
273
                    $name .= $this->t('mo_NewElementIax');
274
                } elseif ($this->disabled === '1') {
275
                    $name .= "{$this->description} ({$this->t( 'mo_Disabled' )})";
276
                } else {
277
                    $name .= $this->description;
278
                }
279
                break;
280
            case IvrMenu::class:
281
                $name = '<i class="sitemap icon"></i> ';
282
                if (empty($this->id)) {
283
                    $name .= $this->t('mo_NewElementIvrMenu');
284
                } else {
285
                    $name .= $this->t('mo_IVRMenuShort4Dropdown') . ': ' . $this->name;
286
                }
287
                break;
288
            case IvrMenuActions::class:
289
                $name = $this->IvrMenu->name;
290
                break;
291
            case Codecs::class:
292
                $name = $this->name;
293
                break;
294
            case IncomingRoutingTable::class:
295
                $name = '<i class="map signs icon"></i> ';
296
                if (empty($this->id)) {
297
                    $name .= $this->t('mo_NewElementIncomingRoutingTable');
298
                } elseif ( ! empty($this->note)) {
299
                    $name .= $this->t('repIncomingRoutingTable', ['represent' => $this->note]);
300
                } else {
301
                    $name .= $this->t('repIncomingRoutingTableNumber', ['represent' => $this->id]);
302
                }
303
                break;
304
            case LanInterfaces::class:
305
                // LanInterfaces
306
                $name = $this->name;
307
                break;
308
            case NetworkFilters::class:
309
                $name = '<i class="globe icon"></i> ';
310
                if (empty($this->id)) {
311
                    $name .= $this->t('mo_NewElementNetworkFilters');
312
                } else {
313
                    $name .= $this->description . '('
314
                        . $this->t('fw_PermitNetwork') . ': ' . $this->permit
315
                        . ')';
316
                }
317
                break;
318
            case OutgoingRoutingTable::class:
319
                $name = '<i class="random icon"></i> ';
320
                if (empty($this->id)) {
321
                    $name .= $this->t('mo_NewElementOutgoingRoutingTable');
322
                } elseif ( ! empty($this->rulename)) {
323
                    $name .= $this->t('repOutgoingRoutingTable', ['represent' => $this->rulename]);
324
                } else {
325
                    $name .= $this->t('repOutgoingRoutingTableNumber', ['represent' => $this->id]);
326
                }
327
                break;
328
            case OutWorkTimes::class:
329
                $name = '<i class="time icon"></i> ';
330
                if (empty($this->id)) {
331
                    $name .= $this->t('mo_NewElementOutWorkTimes');
332
                } elseif ( ! empty($this->description)) {
333
                    $name .= $this->t('repOutWorkTimes', ['represent' => $this->description]);
334
                } else {
335
                    $represent = '';
336
                    if (is_numeric($this->date_from)) {
337
                        $represent .= date("d/m/Y", $this->date_from) . '-';
338
                    }
339
                    if (is_numeric($this->date_to)) {
340
                        $represent .= date("d/m/Y", $this->date_to) . ' ';
341
                    }
342
                    if (isset($this->weekday_from)) {
343
                        $represent .= $this->t(date('D', strtotime("Sunday +{$this->weekday_from} days"))) . '-';
344
                    }
345
                    if (isset($this->weekday_to)) {
346
                        $represent .= $this->t(date('D', strtotime("Sunday +{$this->weekday_to} days"))) . ' ';
347
                    }
348
                    if (isset($this->time_from) || isset($this->time_to)) {
349
                        $represent .= $this->time_from . ' - ' . $this->time_to . ' ';
350
                    }
351
                    $name .= $this->t('repOutWorkTimes', ['represent' => $represent]);
352
                }
353
                break;
354
            case Providers::class:
355
                if ($this->type === "IAX") {
356
                    $name = $this->Iax->getRepresent();
357
                } else {
358
                    $name = $this->Sip->getRepresent();
359
                }
360
                break;
361
            case PbxSettings::class:
362
                $name = $this->key;
363
                break;
364
            case PbxExtensionModules::class:
365
                $name = '<i class="puzzle piece icon"></i> '
366
                    . $this->t('mo_ModuleShort4Dropdown') . ': '
367
                    . $this->name;
368
                break;
369
            case Sip::class:
370
                $name = '<i class="server icon"></i> ';
371
                if (empty($this->id)) {
372
                    $name .= $this->t('mo_NewElementSip');
373
                } elseif ($this->disabled === '1') {
374
                    $name .= "{$this->description} ({$this->t( 'mo_Disabled' )})";
375
                } else {
376
                    $name .= $this->description;
377
                }
378
379
                break;
380
            case Users::class:
381
                $name = '<i class="user outline icon"></i> ' . $this->username;
382
                break;
383
            case SoundFiles::class:
384
                $name = '<i class="file audio outline icon"></i> ';
385
                if (empty($this->id)) {
386
                    $name .= $this->t('mo_NewElementSoundFiles');
387
                } else {
388
                    $name .= $this->t('repSoundFiles', ['represent' => $this->name]);
389
                }
390
391
                break;
392
            default:
393
                $name = 'Unknown';
394
        }
395
396
        if ($needLink) {
397
            $link     = $this->getWebInterfaceLink();
398
            $category = explode('\\', static::class)[3];
399
            $result   = $this->t(
400
                'rep' . $category,
401
                [
402
                    'represent' => "<a href='{$link}'>{$name}</a>",
403
                ]
404
            );
405
        } else {
406
            $result = $name;
407
        }
408
409
        return $result;
410
    }
411
412
    /**
413
     * Укорачивает длинные имена
414
     *
415
     * @param $s
416
     *
417
     * @return string
418
     */
419
    private function trimName($s): string
420
    {
421
        $max_length = 64;
422
423
        if (strlen($s) > $max_length) {
424
            $offset = ($max_length - 3) - strlen($s);
425
            $s      = substr($s, 0, strrpos($s, ' ', $offset)) . '...';
426
        }
427
428
        return $s;
429
    }
430
431
    /**
432
     * Return link on database record in web interface
433
     *
434
     * @return string
435
     */
436
    public function getWebInterfaceLink(): string
437
    {
438
        $url = new Url();
439
440
        $baseUri = $this->di->getShared('config')->path('adminApplication.baseUri');
441
        $link    = '#';
442
        switch (static::class) {
443
            case AsteriskManagerUsers::class:
444
                $link = $url->get('asterisk-managers/modify/' . $this->id, null, null, $baseUri);
445
                break;
446
            case CallQueueMembers::class:
447
                $link = $url->get('call-queues/modify/' . $this->CallQueues->uniqid, null, null, $baseUri);
448
                break;
449
            case CallQueues::class:
450
                $link = $url->get('call-queues/modify/' . $this->uniqid, null, null, $baseUri);
451
                break;
452
            case ConferenceRooms::class:
453
                $link = $url->get('conference-rooms/modify/' . $this->uniqid, null, null, $baseUri);
454
                break;
455
            case CustomFiles::class:
456
                $link = $url->get('custom-files/modify/' . $this->id, null, null, $baseUri);
457
                break;
458
            case DialplanApplications::class:
459
                $link = $url->get('dialplan-applications/modify/' . $this->uniqid, null, null, $baseUri);
460
                break;
461
            case ExtensionForwardingRights::class:
462
463
                break;
464
            case Extensions::class:
465
                $link = $url->get('extensions/modify/' . $this->id, null, null, $baseUri);
466
                break;
467
            case ExternalPhones::class:
468
                if ($this->Extensions->is_general_user_number === "1") {
469
                    $parameters    = [
470
                        'conditions' => 'is_general_user_number="1" AND type="' . Extensions::TYPE_EXTERNAL . '" AND userid=:userid:',
471
                        'bind'       => [
472
                            'userid' => $this->Extensions->userid,
473
                        ],
474
                    ];
475
                    $needExtension = Extensions::findFirst($parameters);
476
                    $link          = $url->get('extensions/modify/' . $needExtension->id, null, null, $baseUri);
477
                } else {
478
                    $link = '#';//TODO сделать если будет раздел для допоплнинельных номеров пользователя
479
                }
480
                break;
481
            case Fail2BanRules::class:
482
                $link = '#';//TODO сделать если будет fail2ban
483
                break;
484
            case FirewallRules::class:
485
                $link = $url->get('firewall/modify/' . $this->NetworkFilters->id, null, null, $baseUri);
486
                break;
487
            case Iax::class:
488
                $link = $url->get('providers/modifyiax/' . $this->Providers->id, null, null, $baseUri);
489
                break;
490
            case IvrMenu::class:
491
                $link = $url->get('ivr-menu/modify/' . $this->uniqid, null, null, $baseUri);
492
                break;
493
            case IvrMenuActions::class:
494
                $link = $url->get('ivr-menu/modify/' . $this->IvrMenu->uniqid, null, null, $baseUri);
495
                break;
496
            case Codecs::class:
497
                break;
498
            case IncomingRoutingTable::class:
499
                $link = $url->get('incoming-routes/modify/' . $this->id, null, null, $baseUri);
500
                break;
501
            case LanInterfaces::class:
502
                $link = $url->get('network/index/', null, null, $baseUri);
503
                break;
504
            case NetworkFilters::class:
505
                $link = $url->get('firewall/modify/' . $this->id, null, null, $baseUri);
506
                break;
507
            case OutgoingRoutingTable::class:
508
                $link = $url->get('outbound-routes/modify/' . $this->id, null, null, $baseUri);
509
                break;
510
            case OutWorkTimes::class:
511
                $link = $url->get('out-off-work-time/modify/' . $this->id, null, null, $baseUri);
512
                break;
513
            case Providers::class:
514
                if ($this->type === "IAX") {
515
                    $link = $url->get('providers/modifyiax/' . $this->uniqid, null, null, $baseUri);
516
                } else {
517
                    $link = $url->get('providers/modifysip/' . $this->uniqid, null, null, $baseUri);
518
                }
519
                break;
520
            case PbxSettings::class:
521
                $link = $url->get('general-settings/index');
522
                break;
523
            case PbxExtensionModules::class:
524
                $link = $url->get(Text::uncamelize($this->uniqid), null, null, $baseUri);
525
                break;
526
            case Sip::class:
527
                if ($this->Extensions) { // Это внутренний номер?
528
                    if ($this->Extensions->is_general_user_number === "1") {
529
                        $link = $url->get('extensions/modify/' . $this->Extensions->id, null, null, $baseUri);
530
                    } else {
531
                        $link = '#';//TODO сделать если будет раздел для допоплнинельных номеров пользователя
532
                    }
533
                } elseif ($this->Providers) { // Это провайдер
534
                    $link = $url->get('providers/modifysip/' . $this->Providers->id, null, null, $baseUri);
535
                }
536
                break;
537
            case Users::class:
538
                $parameters    = [
539
                    'conditions' => 'userid=:userid:',
540
                    'bind'       => [
541
                        'userid' => $this->id,
542
                    ],
543
                ];
544
                $needExtension = Extensions::findFirst($parameters);
545
                if ($needExtension === null) {
546
                    $link = $url->get('extensions/index/', null, null, $baseUri);
547
                } else {
548
                    $link = $url->get('extensions/modify/' . $needExtension->id, null, null, $baseUri);
549
                }
550
                break;
551
            case SoundFiles::class:
552
                $link = $url->get('sound-files/modify/' . $this->id, null, null, $baseUri);
553
                break;
554
            default:
555
        }
556
557
        return $link;
558
    }
559
560
    /**
561
     * Fill default values from annotations
562
     */
563
    public function beforeValidationOnCreate(): void
564
    {
565
        $metaData      = $this->di->get(ModelsMetadataProvider::SERVICE_NAME);
566
        $defaultValues = $metaData->getDefaultValues($this);
567
        foreach ($defaultValues as $field => $value) {
568
            if ( ! isset($this->{$field})) {
569
                $this->{$field} = $value;
570
            }
571
        }
572
    }
573
574
    /**
575
     * Функция позволяет вывести список зависимостей с сылками,
576
     * которые мешают удалению текущей сущности
577
     *
578
     * @return bool
579
     */
580
    public function beforeDelete(): bool
581
    {
582
        return $this->checkRelationsSatisfaction($this, $this);
583
    }
584
585
    /**
586
     *  Check whether this object has unsatisfied relations or not
587
     *
588
     * @param $theFirstDeleteRecord
589
     * @param $currentDeleteRecord
590
     *
591
     * @return bool
592
     */
593
    private function checkRelationsSatisfaction($theFirstDeleteRecord, $currentDeleteRecord): bool
594
    {
595
        $result = true;
596
        $relations
597
                = $currentDeleteRecord->_modelsManager->getRelations(get_class($currentDeleteRecord));
598
        foreach ($relations as $relation) {
599
            $foreignKey = $relation->getOption('foreignKey');
600
            if ( ! array_key_exists('action', $foreignKey)) {
601
                continue;
602
            }
603
            // Check if there are some record which restrict delete current record
604
            $relatedModel             = $relation->getReferencedModel();
605
            $mappedFields             = $relation->getFields();
606
            $mappedFields             = is_array($mappedFields)
607
                ? $mappedFields : [$mappedFields];
608
            $referencedFields         = $relation->getReferencedFields();
609
            $referencedFields         = is_array($referencedFields)
610
                ? $referencedFields : [$referencedFields];
611
            $parameters['conditions'] = '';
612
            $parameters['bind']       = [];
613
            foreach ($referencedFields as $index => $referencedField) {
614
                $parameters['conditions']             .= $index > 0
615
                    ? ' OR ' : '';
616
                $parameters['conditions']             .= $referencedField
617
                    . '= :field'
618
                    . $index . ':';
619
                $bindField
620
                                                      = $mappedFields[$index];
621
                $parameters['bind']['field' . $index] = $currentDeleteRecord->$bindField;
622
            }
623
            $relatedRecords = $relatedModel::find($parameters);
624
            switch ($foreignKey['action']) {
625
                case Relation::ACTION_RESTRICT: // Restrict deletion and add message about unsatisfied undeleted links
626
                    foreach ($relatedRecords as $relatedRecord) {
627
                        if (serialize($relatedRecord) === serialize($theFirstDeleteRecord)
628
                            || serialize($relatedRecord) === serialize($currentDeleteRecord)
629
                        ) {
630
                            continue; // It is checked object
631
                        }
632
                        $message = new Message(
633
                            $theFirstDeleteRecord->t(
634
                                'mo_BeforeDeleteFirst',
635
                                [
636
                                    'represent' => $relatedRecord->getRepresent(true),
637
                                ]
638
                            )
639
                        );
640
                        $theFirstDeleteRecord->appendMessage($message);
641
                        $result = false;
642
                    }
643
                    break;
644
                case Relation::ACTION_CASCADE: // Удалим все зависимые записи
645
                    foreach ($relatedRecords as $relatedRecord) {
646
                        if (serialize($relatedRecord) === serialize($theFirstDeleteRecord)
647
                            || serialize($relatedRecord) === serialize($currentDeleteRecord)
648
                        ) {
649
                            continue; // It is checked object
650
                        }
651
                        $result = $result && $relatedRecord->checkRelationsSatisfaction(
652
                                $theFirstDeleteRecord,
653
                                $relatedRecord
654
                            );
655
                        if ($result) {
656
                            $result = $relatedRecord->delete();
657
                        }
658
                        if ($result === false) {
659
                            $messages = $relatedRecord->getMessages();
660
                            foreach ($messages as $message) {
661
                                $theFirstDeleteRecord->appendMessage($message);
662
                            }
663
                        }
664
                    }
665
                    break;
666
                case Relation::NO_ACTION: // Clear all refs
667
                    break;
668
                default:
669
                    break;
670
            }
671
        }
672
673
        return $result;
674
    }
675
676
    /**
677
     * After save processor
678
     */
679
    public function afterSave(): void
680
    {
681
        $this->processSettingsChanges('afterSave');
682
        self::clearCache(static::class);
683
    }
684
685
    /**
686
     * Sends changed fields and settings to backend worker WorkerModelsEvents
687
     *
688
     * @param $action string may be afterSave or afterDelete
689
     */
690
    private function processSettingsChanges(string $action): void
691
    {
692
        $doNotTrackThisDB = [
693
            CDRDatabaseProvider::SERVICE_NAME,
694
            EventsLogDatabaseProvider::SERVICE_NAME
695
        ];
696
697
        if (in_array($this->getReadConnectionService(), $doNotTrackThisDB)) {
698
            return;
699
        }
700
701
        if ( ! $this->hasSnapshotData()) {
702
            return;
703
        } // nothing changed
704
705
        $changedFields = $this->getUpdatedFields();
706
        if (empty($changedFields) && $action === 'afterSave') {
707
            return;
708
        }
709
        $this->sendChangesToBackend($action, $changedFields);
710
    }
711
712
    /**
713
     * Sends changed fields and class to WorkerModelsEvents
714
     *
715
     * @param $action
716
     * @param $changedFields
717
     */
718
    private function sendChangesToBackend($action, $changedFields): void
719
    {
720
        // Add changed fields set to Beanstalkd queue
721
        $queue = $this->di->getShared(BeanstalkConnectionModelsProvider::SERVICE_NAME);
722
        if ($queue === null) {
723
            return;
724
        }
725
        if ($this instanceof PbxSettings) {
726
            $idProperty = 'key';
727
        } else {
728
            $idProperty = 'id';
729
        }
730
        $id      = $this->$idProperty;
731
        $jobData = json_encode(
732
            [
733
                'model'         => get_class($this),
734
                'recordId'      => $id,
735
                'action'        => $action,
736
                'changedFields' => $changedFields,
737
            ]
738
        );
739
        $queue->publish($jobData);
740
    }
741
742
    /**
743
     * Invalidates cached records contains model name in cache key value
744
     *
745
     * @param      $calledClass string full model class name
746
     *
747
     */
748
    public static function clearCache(string $calledClass): void
749
    {
750
        $di = Di::getDefault();
751
        if ($di === null) {
752
            return;
753
        }
754
755
        if ($di->has(ManagedCacheProvider::SERVICE_NAME)) {
756
            $managedCache = $di->get(ManagedCacheProvider::SERVICE_NAME);
757
            $category     = explode('\\', $calledClass)[3];
758
            $keys         = $managedCache->getKeys($category);
759
            $prefix      = $managedCache->getPrefix();
760
            // Delete all items from the managed cache
761
            foreach ($keys as $key)
762
            {
763
                $cacheKey = str_ireplace($prefix, '', $key);
764
                $managedCache->delete($cacheKey);
765
            }
766
        }
767
        if ($di->has(ModelsCacheProvider::SERVICE_NAME)) {
768
            $modelsCache = $di->getShared(ModelsCacheProvider::SERVICE_NAME);
769
            $category    = explode('\\', $calledClass)[3];
770
            $keys        = $modelsCache->getKeys($category);
771
            $prefix      = $modelsCache->getPrefix();
772
            // Delete all items from the models cache
773
            foreach ($keys as $key)
774
            {
775
                $cacheKey = str_ireplace($prefix, '', $key);
776
                $modelsCache->delete($cacheKey);
777
            }
778
        }
779
780
    }
781
782
    /**
783
     * После удаления данных любой модели
784
     */
785
    public function afterDelete(): void
786
    {
787
        $this->processSettingsChanges('afterDelete');
788
        self::clearCache(static::class);
789
    }
790
791
    /**
792
     * Returns Identity field name for current model
793
     *
794
     * @return string
795
     */
796
    public function getIdentityFieldName(): string
797
    {
798
        $metaData = $this->di->get(ModelsMetadataProvider::SERVICE_NAME);
799
800
        return $metaData->getIdentityField($this);
801
    }
802
803
    /**
804
     * Returns Cache key for the models cache service
805
     *
806
     * @param string $modelClass
807
     * @param string $keyName
808
     *
809
     * @return string
810
     */
811
    public static function makeCacheKey(string $modelClass, string $keyName):string
812
    {
813
        $category    = explode('\\', $modelClass)[3];
814
        return "{$category}:{$keyName}";
815
    }
816
}