| Total Complexity | 135 |
| Total Lines | 631 |
| Duplicated Lines | 0 % |
| Changes | 0 | ||
Complex classes like ModelsBase often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ModelsBase, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 39 | abstract class ModelsBase extends Model |
||
| 40 | { |
||
| 41 | |||
| 42 | public function initialize(): void |
||
| 43 | { |
||
| 44 | self::setup(['orm.events' => true]); |
||
| 45 | $this->keepSnapshots(true); |
||
| 46 | |||
| 47 | // Пройдемся по модулям и подключим их отношения к текущей модели, если они описаны |
||
| 48 | $cacheKey = explode('\\', static::class)[3]; |
||
| 49 | $modulesDir = $this->di->getConfig()->core->modulesDir; |
||
|
|
|||
| 50 | $parameters = [ |
||
| 51 | 'conditions' => 'disabled=0', |
||
| 52 | 'cache' => [ |
||
| 53 | 'key' => $cacheKey, |
||
| 54 | 'lifetime' => 5, //seconds |
||
| 55 | ], |
||
| 56 | ]; |
||
| 57 | |||
| 58 | $modules = PbxExtensionModules::find($parameters)->toArray(); |
||
| 59 | foreach ($modules as $module) { |
||
| 60 | $moduleModelsDir = "{$modulesDir}/{$module['uniqid']}/Models"; |
||
| 61 | $results = glob($moduleModelsDir . '/*.php', GLOB_NOSORT); |
||
| 62 | foreach ($results as $file) { |
||
| 63 | $className = pathinfo($file)['filename']; |
||
| 64 | $moduleModelClass = "\\Modules\\{$module['uniqid']}\\Models\\{$className}"; |
||
| 65 | if (class_exists($moduleModelClass) && method_exists($moduleModelClass, 'getDynamicRelations')) { |
||
| 66 | $moduleModelClass::getDynamicRelations($this); |
||
| 67 | } |
||
| 68 | } |
||
| 69 | } |
||
| 70 | } |
||
| 71 | |||
| 72 | /** |
||
| 73 | * Обработчик ошибок валидации, обычно сюда попадаем если неправильно |
||
| 74 | * сохраняются или удаляютмя модели или неправильно настроены зависимости между ними. |
||
| 75 | * Эта функция формирует список ссылок на объект который мы пытаемся удалить |
||
| 76 | * |
||
| 77 | * При описании отношений необходимо в foreignKey секцию добавлять атрибут |
||
| 78 | * message в котором указывать алиас посе слова Models, |
||
| 79 | * например Models\IvrMenuTimeout, иначе метод getRelated не сможет найти зависимые |
||
| 80 | * записи в моделях |
||
| 81 | */ |
||
| 82 | public function onValidationFails(): void |
||
| 83 | { |
||
| 84 | $errorMessages = $this->getMessages(); |
||
| 85 | if (php_sapi_name() === 'cli') { |
||
| 86 | Util::sysLogMsg(__CLASS__, implode(' ', $errorMessages)); |
||
| 87 | |||
| 88 | return; |
||
| 89 | } |
||
| 90 | foreach ($errorMessages as $errorMessage) { |
||
| 91 | if ($errorMessage->getType()==='ConstraintViolation') { |
||
| 92 | $arrMessageParts = explode('Common\\Models\\', $errorMessage->getMessage()); |
||
| 93 | if (count($arrMessageParts) === 2) { |
||
| 94 | $relatedModel = $arrMessageParts[1]; |
||
| 95 | } else { |
||
| 96 | $relatedModel = $errorMessage->getMessage(); |
||
| 97 | } |
||
| 98 | $relatedRecords = $this->getRelated($relatedModel); |
||
| 99 | $newErrorMessage = $this->t('ConstraintViolation'); |
||
| 100 | $newErrorMessage .= "<ul class='list'>"; |
||
| 101 | if ($relatedRecords === false) { |
||
| 102 | throw new Model\Exception('Error on models relationship ' . $errorMessage); |
||
| 103 | } |
||
| 104 | if ($relatedRecords instanceof Resultset) { |
||
| 105 | foreach ($relatedRecords as $item) { |
||
| 106 | $newErrorMessage .= '<li>' . $item->getRepresent(true) . '</li>'; |
||
| 107 | } |
||
| 108 | } else { |
||
| 109 | $newErrorMessage .= '<li>' . $relatedRecords->getRepresent(true) . '</li>'; |
||
| 110 | } |
||
| 111 | $newErrorMessage .= '</ul>'; |
||
| 112 | $errorMessage->setMessage($newErrorMessage); |
||
| 113 | break; |
||
| 114 | } |
||
| 115 | } |
||
| 116 | } |
||
| 117 | |||
| 118 | /** |
||
| 119 | * Функция для доступа к массиву переводов из моделей, используется для |
||
| 120 | * сообщений на понятном пользователю языке |
||
| 121 | * |
||
| 122 | * @param $message |
||
| 123 | * @param array $parameters |
||
| 124 | * |
||
| 125 | * @return mixed |
||
| 126 | */ |
||
| 127 | public function t($message, $parameters = []) |
||
| 128 | { |
||
| 129 | return $this->getDI()->getShared('translation')->t($message, $parameters); |
||
| 130 | } |
||
| 131 | |||
| 132 | /** |
||
| 133 | * Fill default values from annotations |
||
| 134 | */ |
||
| 135 | public function beforeValidationOnCreate(): void |
||
| 136 | { |
||
| 137 | $metaData = $metaData = $this->di->get('modelsMetadata'); |
||
| 138 | $defaultValues = $metaData->getDefaultValues($this); |
||
| 139 | foreach ($defaultValues as $field => $value) { |
||
| 140 | if ( ! isset($this->{$field}) || $this->{$field} === null) { |
||
| 141 | $this->{$field} = new RawValue($value); |
||
| 142 | } |
||
| 143 | } |
||
| 144 | } |
||
| 145 | |||
| 146 | /** |
||
| 147 | * Функция позволяет вывести список зависимостей с сылками, |
||
| 148 | * которые мешают удалению текущей сущности |
||
| 149 | * |
||
| 150 | * @return bool |
||
| 151 | */ |
||
| 152 | public function beforeDelete(): bool |
||
| 153 | { |
||
| 154 | return $this->checkRelationsSatisfaction($this, $this); |
||
| 155 | } |
||
| 156 | |||
| 157 | /** |
||
| 158 | * Check whether this object has unsatisfied relations or not |
||
| 159 | * |
||
| 160 | * @param $theFirstDeleteRecord |
||
| 161 | * @param $currentDeleteRecord |
||
| 162 | * |
||
| 163 | * @return bool |
||
| 164 | */ |
||
| 165 | private function checkRelationsSatisfaction($theFirstDeleteRecord, $currentDeleteRecord): bool |
||
| 166 | { |
||
| 167 | $result = true; |
||
| 168 | $relations |
||
| 169 | = $currentDeleteRecord->_modelsManager->getRelations(get_class($currentDeleteRecord)); |
||
| 170 | foreach ($relations as $relation) { |
||
| 171 | $foreignKey = $relation->getOption('foreignKey'); |
||
| 172 | if ( ! array_key_exists('action', $foreignKey)) { |
||
| 173 | continue; |
||
| 174 | } |
||
| 175 | // Проверим есть ли записи в таблице которая запрещает удаление текущих данных |
||
| 176 | $relatedModel = $relation->getReferencedModel(); |
||
| 177 | $mappedFields = $relation->getFields(); |
||
| 178 | $mappedFields = is_array($mappedFields) |
||
| 179 | ? $mappedFields : [$mappedFields]; |
||
| 180 | $referencedFields = $relation->getReferencedFields(); |
||
| 181 | $referencedFields = is_array($referencedFields) |
||
| 182 | ? $referencedFields : [$referencedFields]; |
||
| 183 | $parameters['conditions'] = ''; |
||
| 184 | $parameters['bind'] = []; |
||
| 185 | foreach ($referencedFields as $index => $referencedField) { |
||
| 186 | $parameters['conditions'] .= $index > 0 |
||
| 187 | ? ' OR ' : ''; |
||
| 188 | $parameters['conditions'] .= $referencedField |
||
| 189 | . '= :field' |
||
| 190 | . $index . ':'; |
||
| 191 | $bindField |
||
| 192 | = $mappedFields[$index]; |
||
| 193 | $parameters['bind']['field' . $index] = $currentDeleteRecord->$bindField; |
||
| 194 | } |
||
| 195 | $relatedRecords = $relatedModel::find($parameters); |
||
| 196 | switch ($foreignKey['action']) { |
||
| 197 | case Relation::ACTION_RESTRICT: // Запретим удаление и выведем информацию о том какие записи запретили удалять этот элемент |
||
| 198 | foreach ($relatedRecords as $relatedRecord) { |
||
| 199 | $message = new Message( |
||
| 200 | $theFirstDeleteRecord->t( |
||
| 201 | 'mo_BeforeDeleteFirst', |
||
| 202 | [ |
||
| 203 | 'represent' => $relatedRecord->getRepresent(true), |
||
| 204 | ] |
||
| 205 | ) |
||
| 206 | ); |
||
| 207 | $theFirstDeleteRecord->appendMessage($message); |
||
| 208 | $result = false; |
||
| 209 | } |
||
| 210 | break; |
||
| 211 | case Relation::ACTION_CASCADE: // Удалим все зависимые записи |
||
| 212 | foreach ($relatedRecords as $record) { |
||
| 213 | $result = $result && $record->checkRelationsSatisfaction($theFirstDeleteRecord, $record); |
||
| 214 | if ($result) { |
||
| 215 | $result = $record->delete(); |
||
| 216 | } |
||
| 217 | } |
||
| 218 | break; |
||
| 219 | case Relation::NO_ACTION: // Очистим ссылки на записи в таблицах зависимых |
||
| 220 | break; |
||
| 221 | default: |
||
| 222 | break; |
||
| 223 | } |
||
| 224 | } |
||
| 225 | |||
| 226 | return $result; |
||
| 227 | } |
||
| 228 | |||
| 229 | /** |
||
| 230 | * После сохранения данных любой модели |
||
| 231 | */ |
||
| 232 | public function afterSave(): void |
||
| 233 | { |
||
| 234 | $this->processSettingsChanges('afterSave'); |
||
| 235 | $this->clearCache(static::class); |
||
| 236 | } |
||
| 237 | |||
| 238 | /** |
||
| 239 | * Готовит массив действий для перезапуска модулей ядра системы |
||
| 240 | * и Asterisk |
||
| 241 | * |
||
| 242 | * @param $action string быть afterSave или afterDelete |
||
| 243 | */ |
||
| 244 | private function processSettingsChanges(string $action): void |
||
| 245 | { |
||
| 246 | if (php_sapi_name() !== 'cli') { |
||
| 247 | if ( ! $this->hasSnapshotData()) { |
||
| 248 | return; |
||
| 249 | } // nothing changed |
||
| 250 | |||
| 251 | $changedFields = $this->getUpdatedFields(); |
||
| 252 | if (empty($changedFields) && $action === 'afterSave') { |
||
| 253 | return; |
||
| 254 | } |
||
| 255 | |||
| 256 | // Add changed fields set to benstalk queue |
||
| 257 | $queue = $this->getDI()->getShared('beanstalkConnection'); |
||
| 258 | |||
| 259 | if ($this instanceof PbxSettings) { |
||
| 260 | $id = $this->key; |
||
| 261 | } else { |
||
| 262 | $id = $this->id; |
||
| 263 | } |
||
| 264 | $jobData = json_encode( |
||
| 265 | [ |
||
| 266 | 'model' => get_class($this), |
||
| 267 | 'recordId' => $id, |
||
| 268 | 'action' => $action, |
||
| 269 | 'changedFields' => $changedFields, |
||
| 270 | ] |
||
| 271 | ); |
||
| 272 | $queue->publish($jobData); |
||
| 273 | } |
||
| 274 | } |
||
| 275 | |||
| 276 | /** |
||
| 277 | * Очистка кешей при сохранении данных в базу |
||
| 278 | * |
||
| 279 | * @param $calledClass string модель, с чей кеш будем чистить в полном формате |
||
| 280 | */ |
||
| 281 | public function clearCache($calledClass): void |
||
| 282 | { |
||
| 283 | $managedCache = $this->getDI()->getManagedCache(); |
||
| 284 | $category = explode('\\', $calledClass)[3]; |
||
| 285 | $keys = $managedCache->getAdapter()->getKeys($category); |
||
| 286 | if (count($keys) > 0) { |
||
| 287 | $managedCache->deleteMultiple($keys); |
||
| 288 | } |
||
| 289 | if ($this->getDI()->has('modelsCache')) { |
||
| 290 | $this->getDI()->get('modelsCache')->delete($category); |
||
| 291 | } |
||
| 292 | } |
||
| 293 | |||
| 294 | /** |
||
| 295 | * После удаления данных любой модели |
||
| 296 | */ |
||
| 297 | public function afterDelete(): void |
||
| 301 | } |
||
| 302 | |||
| 303 | /** |
||
| 304 | * Возвращает предстваление элемента базы данных |
||
| 305 | * для сообщения об ошибках с ссылкой на элемент или для выбора в списках |
||
| 306 | * строкой |
||
| 307 | * |
||
| 308 | * @param bool $needLink - предстваление с ссылкой |
||
| 309 | * |
||
| 310 | * @return string |
||
| 311 | */ |
||
| 312 | public function getRepresent($needLink = false): string |
||
| 313 | { |
||
| 314 | if ($this->id === null) { |
||
| 315 | return $this->t('mo_NewElement'); |
||
| 316 | } |
||
| 317 | |||
| 318 | switch (static::class) { |
||
| 319 | case AsteriskManagerUsers::class: |
||
| 320 | $name = '<i class="asterisk icon"></i> ' . $this->username; |
||
| 321 | break; |
||
| 322 | case CallQueueMembers::class: |
||
| 323 | $name = $this->Extensions->getRepresent(); |
||
| 324 | break; |
||
| 325 | case CallQueues::class: |
||
| 326 | $name = '<i class="users icon"></i> ' |
||
| 327 | . $this->t('mo_CallQueueShort4Dropdown') . ': ' |
||
| 328 | . $this->name; |
||
| 329 | break; |
||
| 330 | case ConferenceRooms::class: |
||
| 331 | $name = '<i class="phone volume icon"></i> ' |
||
| 332 | . $this->t('mo_ConferenceRoomsShort4Dropdown') . ': ' |
||
| 333 | . $this->name; |
||
| 334 | break; |
||
| 335 | case CustomFiles::class: |
||
| 336 | $name = "<i class='file icon'></i> {$this->filepath}"; |
||
| 337 | break; |
||
| 338 | case DialplanApplications::class: |
||
| 339 | $name = '<i class="php icon"></i> ' |
||
| 340 | . $this->t('mo_ApplicationShort4Dropdown') . ': ' |
||
| 341 | . $this->name; |
||
| 342 | break; |
||
| 343 | case ExtensionForwardingRights::class: |
||
| 344 | $name = $this->Extensions->getRepresent(); |
||
| 345 | break; |
||
| 346 | case Extensions::class: |
||
| 347 | // Для внутреннего номера бывают разные представления |
||
| 348 | if ($this->userid > 0) { |
||
| 349 | if ($this->type === 'EXTERNAL') { |
||
| 350 | $icon = '<i class="icons"><i class="user outline icon"></i><i class="top right corner alternate mobile icon"></i></i>'; |
||
| 351 | } else { |
||
| 352 | $icon = '<i class="icons"><i class="user outline icon"></i></i>'; |
||
| 353 | } |
||
| 354 | $name = ''; |
||
| 355 | if (isset($this->Users->username)) { |
||
| 356 | $name = $this->trimName($this->Users->username); |
||
| 357 | } |
||
| 358 | |||
| 359 | $name = "{$icon} {$name} <{$this->number}>"; |
||
| 360 | } else { |
||
| 361 | switch (strtoupper($this->type)) { |
||
| 362 | case 'CONFERENCE': |
||
| 363 | $name = $this->ConferenceRooms->getRepresent(); |
||
| 364 | break; |
||
| 365 | case 'QUEUE': |
||
| 366 | $name = $this->CallQueues->getRepresent(); |
||
| 367 | break; |
||
| 368 | case 'DIALPLAN APPLICATION': |
||
| 369 | $name = $this->DialplanApplications->getRepresent(); |
||
| 370 | break; |
||
| 371 | case 'IVR MENU': |
||
| 372 | $name = $this->IvrMenu->getRepresent(); |
||
| 373 | break; |
||
| 374 | case 'MODULES': |
||
| 375 | $name = '<i class="puzzle piece icon"></i> ' |
||
| 376 | . $this->t('mo_ModuleShort4Dropdown') |
||
| 377 | . ': ' |
||
| 378 | . $this->callerid; |
||
| 379 | break; |
||
| 380 | case 'EXTERNAL': |
||
| 381 | case 'SIP': |
||
| 382 | default: |
||
| 383 | $name = "{$this->callerid} <{$this->number}>"; |
||
| 384 | } |
||
| 385 | } |
||
| 386 | break; |
||
| 387 | case ExternalPhones::class: |
||
| 388 | $name = $this->Extensions->getRepresent(); |
||
| 389 | break; |
||
| 390 | case Fail2BanRules::class: |
||
| 391 | $name = ''; |
||
| 392 | break; |
||
| 393 | case FirewallRules::class: |
||
| 394 | $name = $this->category; |
||
| 395 | break; |
||
| 396 | case Iax::class: |
||
| 397 | if ($this->disabled > 0) { |
||
| 398 | $name = "<i class='server icon'></i> {$this->description} ({$this->t( 'mo_Disabled' )})"; |
||
| 399 | } else { |
||
| 400 | $name = '<i class="server icon"></i> ' . $this->description; |
||
| 401 | } |
||
| 402 | break; |
||
| 403 | case IvrMenu::class: |
||
| 404 | $name = '<i class="sitemap icon"></i> ' |
||
| 405 | . $this->t('mo_IVRMenuShort4Dropdown') . ': ' |
||
| 406 | . $this->name; |
||
| 407 | break; |
||
| 408 | case IvrMenuActions::class: |
||
| 409 | $name = $this->IvrMenu->name; |
||
| 410 | break; |
||
| 411 | case Codecs::class: |
||
| 412 | $name = $this->name; |
||
| 413 | break; |
||
| 414 | case IaxCodecs::class: |
||
| 415 | $name = $this->codec; |
||
| 416 | break; |
||
| 417 | case IncomingRoutingTable::class: |
||
| 418 | $name = $this->t('mo_RightNumber', ['id' => $this->id]); |
||
| 419 | break; |
||
| 420 | case LanInterfaces::class: |
||
| 421 | $name = $this->name; |
||
| 422 | break; |
||
| 423 | case NetworkFilters::class: |
||
| 424 | $name = '<i class="globe icon"></i> ' . $this->description . '(' |
||
| 425 | . $this->t('fw_PermitNetwork') . ': ' . $this->permit |
||
| 426 | . ')'; |
||
| 427 | break; |
||
| 428 | case OutgoingRoutingTable::class: |
||
| 429 | $name = $this->rulename; |
||
| 430 | break; |
||
| 431 | case OutWorkTimes::class: |
||
| 432 | $name = '<i class="time icon"></i> '; |
||
| 433 | if ( ! empty($this->description)) { |
||
| 434 | $name .= $this->description; |
||
| 435 | } else { |
||
| 436 | $represent = ''; |
||
| 437 | if (is_numeric($this->date_from)) { |
||
| 438 | $represent .= date("d/m/Y", $this->date_from) . '-'; |
||
| 439 | } |
||
| 440 | if (is_numeric($this->date_to)) { |
||
| 441 | $represent .= date("d/m/Y", $this->date_to) . ' '; |
||
| 442 | } |
||
| 443 | if (isset($this->weekday_from)) { |
||
| 444 | $represent .= $this->t(date('D', strtotime("Sunday +{$this->weekday_from} days"))) . '-'; |
||
| 445 | } |
||
| 446 | if (isset($this->weekday_to)) { |
||
| 447 | $represent .= $this->t(date('D', strtotime("Sunday +{$this->weekday_to} days"))) . ' '; |
||
| 448 | } |
||
| 449 | if (isset($this->time_from) || isset($this->time_to)) { |
||
| 450 | $represent .= $this->time_from . ' - ' . $this->time_to . ' '; |
||
| 451 | } |
||
| 452 | $name .= $this->t('repOutWorkTimes', ['represent' => $represent]); |
||
| 453 | } |
||
| 454 | break; |
||
| 455 | case Providers::class: |
||
| 456 | if ($this->type === "IAX") { |
||
| 457 | $name = $this->Iax->getRepresent(); |
||
| 458 | } else { |
||
| 459 | $name = $this->Sip->getRepresent(); |
||
| 460 | } |
||
| 461 | break; |
||
| 462 | case PbxSettings::class: |
||
| 463 | $name = $this->key; |
||
| 464 | break; |
||
| 465 | case PbxExtensionModules::class: |
||
| 466 | $name = '<i class="puzzle piece icon"></i> ' |
||
| 467 | . $this->t('mo_ModuleShort4Dropdown') . ': ' |
||
| 468 | . $this->name; |
||
| 469 | break; |
||
| 470 | case Sip::class: |
||
| 471 | if ($this->Extensions) { // Это внутренний номер? |
||
| 472 | $name = $this->Extensions->getRepresent(); |
||
| 473 | } elseif ($this->Providers) { // Это провайдер |
||
| 474 | if ($this->disabled > 0) { |
||
| 475 | $name = "<i class='server icon'></i> {$this->description} ({$this->t( 'mo_Disabled' )})"; |
||
| 476 | } else { |
||
| 477 | $name = '<i class="server icon"></i> ' |
||
| 478 | . $this->description; |
||
| 479 | } |
||
| 480 | } else { // Что это? |
||
| 481 | $name = $this->description; |
||
| 482 | } |
||
| 483 | break; |
||
| 484 | case SipCodecs::class: |
||
| 485 | $name = $this->codec; |
||
| 486 | break; |
||
| 487 | case Users::class: |
||
| 488 | $name = '<i class="user outline icon"></i> ' . $this->username; |
||
| 489 | break; |
||
| 490 | case SoundFiles::class: |
||
| 491 | $name = '<i class="file audio outline icon"></i> ' |
||
| 492 | . $this->name; |
||
| 493 | break; |
||
| 494 | default: |
||
| 495 | $name = 'Unknown'; |
||
| 496 | |||
| 497 | } |
||
| 498 | |||
| 499 | if ($needLink) { |
||
| 500 | if (empty($name)) { |
||
| 501 | $name = $this->t('repLink'); |
||
| 502 | } |
||
| 503 | $link = $this->getWebInterfaceLink(); |
||
| 504 | $category = explode('\\', static::class)[3]; |
||
| 505 | $result = $this->t( |
||
| 506 | 'rep' . $category, |
||
| 507 | [ |
||
| 508 | 'represent' => "<a href='{$link}'>{$name}</a>", |
||
| 509 | ] |
||
| 510 | ); |
||
| 511 | } else { |
||
| 512 | $result = $name; |
||
| 513 | } |
||
| 514 | |||
| 515 | return $result; |
||
| 516 | } |
||
| 517 | |||
| 518 | /** |
||
| 519 | * Укорачивает длинные имена |
||
| 520 | * |
||
| 521 | * @param $s |
||
| 522 | * |
||
| 523 | * @return string |
||
| 524 | */ |
||
| 525 | private function trimName($s): string |
||
| 535 | } |
||
| 536 | |||
| 537 | /** |
||
| 538 | * Return link on database record in web interface |
||
| 539 | * |
||
| 540 | * @return string |
||
| 541 | */ |
||
| 542 | public function getWebInterfaceLink(): string |
||
| 662 | } |
||
| 663 | |||
| 664 | /** |
||
| 665 | * Возвращает массив полей, по которым следует добавить индекс в DB. |
||
| 666 | * @return array |
||
| 667 | */ |
||
| 668 | public function getIndexColumn():array { |
||
| 670 | } |
||
| 671 | } |