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

visavi / rotor / 35474116988

19 Sep 2026 10:42PM UTC coverage: 64.702% (+0.06%) from 64.644%
35474116988

push

github

visavi
Опрос реестров модулей уходит в очередь

  Недоступный реестр держал страницу до десяти секунд таймаута, а каталог
  опрашивает их все разом. Теперь при настроенной очереди опрос ставится
  задачей (уникальной по реестру, без повторов), а на sync всё работает
  как прежде: кнопка обновляет реестр на месте, протухший кэш — после
  отправки ответа.

14 of 25 new or added lines in 4 files covered. (56.0%)

4654 of 7193 relevant lines covered (64.7%)

22.46 hits per line

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

59.6
/app/Http/Controllers/Admin/ModuleController.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace App\Http\Controllers\Admin;
6

7
use App\Models\Module;
8
use App\Models\ModuleRegistry;
9
use App\Providers\ModuleServiceProvider;
10
use Illuminate\Http\RedirectResponse;
11
use Illuminate\Http\Request;
12
use Illuminate\Support\Facades\Http;
13
use Illuminate\View\View;
14
use ZipArchive;
15

16
class ModuleController extends AdminController
17
{
18
    /**
19
     * Главная страница
20
     */
21
    public function index(): View
3✔
22
    {
23
        $modules = Module::query()->get();
3✔
24
        $moduleInstall = [];
3✔
25
        foreach ($modules as $module) {
3✔
26
            $moduleInstall[$module->name] = $module;
×
27
        }
28

29
        $registryModules = ModuleRegistry::getAvailableModules();
3✔
30

31
        $moduleNames = [];
3✔
32
        $modulesLoaded = glob(base_path('modules/*'), GLOB_ONLYDIR);
3✔
33
        foreach ($modulesLoaded as $module) {
3✔
34
            if (file_exists($module . '/module.php')) {
3✔
35
                $name = basename($module);
3✔
36
                $config = include $module . '/module.php';
3✔
37

38
                // Дата релиза берётся из реестра, но только если он описывает ровно ту
39
                // версию, что показана в карточке — иначе датой обновления Форума 1.1
40
                // стал бы релиз ещё не установленной 1.2. Для своих модулей и для
41
                // отставших версий остаётся дата файлов на диске: когда распаковали
42
                $shownVersion = $moduleInstall[$name]->version ?? ($config['version'] ?? null);
3✔
43
                $config['released_at'] = ($registryModules[$name]['version'] ?? null) === $shownVersion
3✔
44
                    ? ($registryModules[$name]['released_at'] ?? null)
1✔
45
                    : null;
2✔
46

47
                $config['released_at'] ??= date('Y-m-d', (int) filemtime($module . '/module.php'));
3✔
48

49
                $moduleNames[$name] = $config;
3✔
50
            }
51
        }
52

53
        $installed = array_intersect_key($moduleInstall, $moduleNames);
3✔
54
        $counts = [
3✔
55
            'all'           => count($moduleNames),
3✔
56
            'installed'     => count(array_filter($installed, fn ($m) => $m->active)),
3✔
57
            'disabled'      => count(array_filter($installed, fn ($m) => ! $m->active)),
3✔
58
            'not-installed' => count($moduleNames) - count($installed),
3✔
59
        ];
3✔
60

61
        $failedModules = ModuleServiceProvider::$failed;
3✔
62

63
        return view('admin/modules/index', compact('moduleInstall', 'moduleNames', 'counts', 'registryModules', 'failedModules'));
3✔
64
    }
65

66
    /**
67
     * Просмотр модуля
68
     */
69
    public function module(Request $request): View
3✔
70
    {
71
        $moduleName = (string) $request->input('module');
3✔
72
        $modulePath = base_path('modules/' . $moduleName);
3✔
73

74
        if (! preg_match('|^[A-Z][\w\-]+$|', $moduleName) || ! file_exists($modulePath)) {
3✔
75
            abort(200, __('admin.modules.module_not_found'));
×
76
        }
77

78
        $moduleConfig = include $modulePath . '/module.php';
3✔
79
        $module = Module::query()->where('name', $moduleName)->first();
3✔
80

81
        if (file_exists($modulePath . '/screenshots')) {
3✔
82
            $moduleConfig['screenshots'] = glob($modulePath . '/screenshots/*.{gif,png,jpg,jpeg,webp}', GLOB_BRACE);
×
83
        }
84

85
        if (file_exists($modulePath . '/database/migrations')) {
3✔
86
            $migrations = [];
×
87
            foreach (glob($modulePath . '/database/migrations/*.php') as $migration) {
×
88
                $migrations[basename($migration)] = file_get_contents($migration);
×
89
            }
90
            $moduleConfig['migrations'] = $migrations;
×
91
        }
92

93
        if (file_exists($modulePath . '/resources/assets')) {
3✔
94
            $moduleConfig['symlink'] = Module::getLinkNameByPath($modulePath);
×
95
        }
96

97
        if (file_exists($modulePath . '/config.php')) {
3✔
98
            $moduleConfig['config'] = file_get_contents($modulePath . '/config.php');
×
99
        }
100

101
        if (file_exists($modulePath . '/routes.php')) {
3✔
102
            $moduleConfig['routes'] = file_get_contents($modulePath . '/routes.php');
×
103
        }
104

105
        if (file_exists($modulePath . '/hooks.php')) {
3✔
106
            $moduleConfig['hooks'] = file_get_contents($modulePath . '/hooks.php');
×
107
        }
108

109
        if (file_exists($modulePath . '/helpers.php')) {
3✔
110
            $moduleConfig['helpers'] = file_get_contents($modulePath . '/helpers.php');
×
111
        }
112

113
        if (file_exists($modulePath . '/middleware.php')) {
3✔
114
            $moduleConfig['middleware'] = file_get_contents($modulePath . '/middleware.php');
×
115
        }
116

117
        foreach (['changelog.md', 'CHANGELOG.md'] as $changelog) {
3✔
118
            if (file_exists($modulePath . '/' . $changelog)) {
3✔
119
                // Убираем шапку "# Changelog" — на странице уже есть свой заголовок
120
                $moduleConfig['changelog'] = trim((string) preg_replace(
×
121
                    '/^#\s+changelog\s*/ui',
×
122
                    '',
×
123
                    file_get_contents($modulePath . '/' . $changelog)
×
124
                ));
×
125
                break;
×
126
            }
127
        }
128

129
        $registryInfo = ModuleRegistry::getAvailableModules()[$moduleName] ?? null;
3✔
130

131
        // Дата релиза берётся из реестра, но только если он описывает ровно ту
132
        // версию, что стоит на сайте — иначе датой установленной 1.1 стал бы
133
        // релиз ещё не поставленной 1.2. Иначе дата файла: когда распаковали
134
        $shownVersion = $module->version ?? ($moduleConfig['version'] ?? null);
3✔
135
        $moduleConfig['released_at'] = ($registryInfo['version'] ?? null) === $shownVersion
3✔
136
            ? ($registryInfo['released_at'] ?? null)
×
137
            : null;
3✔
138

139
        $moduleConfig['released_at'] ??= date('Y-m-d', (int) filemtime($modulePath . '/module.php'));
3✔
140

141
        return view('admin/modules/module', compact('module', 'moduleConfig', 'moduleName', 'registryInfo'));
3✔
142
    }
143

144
    /**
145
     * Установка модуля
146
     */
147
    public function install(Request $request): RedirectResponse
1✔
148
    {
149
        $moduleName = $request->input('module');
1✔
150
        $enable = int($request->input('enable'));
1✔
151
        $update = int($request->input('update'));
1✔
152
        $modulePath = base_path('modules/' . $moduleName);
1✔
153

154
        if (! preg_match('|^[A-Z][\w\-]+$|', $moduleName) || ! file_exists($modulePath)) {
1✔
155
            abort(200, __('admin.modules.module_not_found'));
×
156
        }
157

158
        $module = Module::query()->firstOrNew(['name' => $moduleName]);
1✔
159

160
        $moduleConfig = include $modulePath . '/module.php';
1✔
161

162
        if ($requires = $this->incompatibleWith($moduleConfig)) {
1✔
163
            return redirect('admin/modules/module?module=' . $moduleName)
×
164
                ->with('danger', __('admin.modules.requires') . ' ' . $requires . '!');
×
165
        }
166

167
        $result = $this->applyModule($module, $moduleConfig, (bool) $enable, (bool) $update);
1✔
168

169
        return redirect('admin/modules/module?module=' . $moduleName)
1✔
170
            ->with('success', $result);
1✔
171
    }
172

173
    /**
174
     * Раскладывает файлы модуля и фиксирует его состояние в БД
175
     */
176
    private function applyModule(Module $module, array $moduleConfig, bool $enable, bool $update): string
3✔
177
    {
178
        // Файлы на диск кладём только для активного модуля: свежая установка,
179
        // включение или обновление уже активного. Обновление выключенного модуля
180
        // не должно возвращать его файлы в public.
181
        if (! $module->exists || $enable || $module->active) {
3✔
182
            $module->createSymlink();
1✔
183
            $module->publish();
1✔
184
        }
185

186
        // Миграции применяем и выключенному: его таблицы при выключении остаются
187
        // (rollback только при полном удалении), а версия в БД поднимается в любом
188
        // случае — схема не должна отставать от того, что записано как версия
189
        $module->migrate();
3✔
190

191
        $result = __('admin.modules.module_success_installed');
3✔
192

193
        if ($module->exists) {
3✔
194
            if ($update) {
2✔
195
                $module->update([
2✔
196
                    'version' => $moduleConfig['version'],
2✔
197
                ]);
2✔
198
                $result = __('admin.modules.module_success_updated');
2✔
199
            }
200

201
            if ($enable) {
2✔
202
                $module->update([
×
203
                    'active' => true,
×
204
                ]);
×
205
                $result = __('admin.modules.module_success_enabled');
×
206
            }
207
        } else {
208
            $module->fill([
1✔
209
                'version' => $moduleConfig['version'],
1✔
210
            ])->save();
1✔
211
        }
212

213
        // Полная синхронизация активных модулей: порядок установки перестаёт
214
        // иметь значение (напр. перевод модуля-языка подмешается в Форум,
215
        // даже если Форум поставили позже). Сама сбрасывает кэш модулей.
216
        Module::syncAll();
3✔
217

218
        // После syncAll — пересборка увидит роуты нового модуля
219
        refreshCaches();
3✔
220

221
        return $result;
3✔
222
    }
223

224
    /**
225
     * Возвращает требуемую версию движка, если модуль с ней несовместим
226
     */
227
    private function incompatibleWith(array $moduleConfig): ?string
5✔
228
    {
229
        $requires = $moduleConfig['requires'] ?? null;
5✔
230

231
        return $requires && version_compare(ROTOR_VERSION, $requires, '<') ? $requires : null;
5✔
232
    }
233

234
    /**
235
     * Каталог модулей из реестров
236
     */
237
    public function marketplace(Request $request): View|RedirectResponse
4✔
238
    {
239
        $force = (bool) $request->input('refresh');
4✔
240

241
        // Реестров может быть несколько, и каждый недоступный стоит десять секунд
242
        // таймаута. С очередью страница открывается сразу, опрос идёт в фоне
243
        if ($force && ModuleRegistry::queueFetchAll()) {
4✔
NEW
244
            return redirect()->route('admin.modules.marketplace')
×
NEW
245
                ->with('success', __('admin.registries.registry_refresh_queued'));
×
246
        }
247

248
        $available = ModuleRegistry::getAvailableModules($force);
4✔
249

250
        $modules = Module::query()->get()->keyBy('name');
4✔
251
        $moduleNames = [];
4✔
252

253
        // Версии с диска: распакованный, но ещё не применённый релиз незачем
254
        // предлагать скачать заново — его хватает применить на странице модуля
255
        $localVersions = [];
4✔
256

257
        $modulesLoaded = glob(base_path('modules/*'), GLOB_ONLYDIR);
4✔
258
        foreach ($modulesLoaded as $module) {
4✔
259
            $name = basename($module);
4✔
260
            $moduleNames[] = $name;
4✔
261

262
            if (file_exists($module . '/module.php')) {
4✔
263
                $config = include $module . '/module.php';
4✔
264
                $localVersions[$name] = $config['version'] ?? null;
4✔
265
            }
266
        }
267

268
        $counts = ['all' => count($available), 'installed' => 0, 'disabled' => 0, 'not-installed' => 0];
4✔
269
        foreach ($available as $name => $info) {
4✔
270
            $localExists = in_array($name, $moduleNames, true);
4✔
271
            $installed = $modules->has($name) && $localExists;
4✔
272

273
            if ($installed) {
4✔
274
                $counts[$modules[$name]->active ? 'installed' : 'disabled']++;
2✔
275
            } else {
276
                $counts['not-installed']++;
2✔
277
            }
278
        }
279

280
        return view('admin/modules/marketplace', compact('available', 'modules', 'moduleNames', 'localVersions', 'counts'));
4✔
281
    }
282

283
    /**
284
     * Форма загрузки модуля
285
     */
286
    public function upload(): View
×
287
    {
288
        return view('admin/modules/upload');
×
289
    }
290

291
    /**
292
     * Установка модуля из ZIP-файла
293
     */
294
    public function uploadZip(Request $request): RedirectResponse
×
295
    {
296
        if (! $request->hasFile('zip') || ! $request->file('zip')->isValid()) {
×
297
            return redirect()->route('admin.modules.upload')
×
298
                ->with('danger', __('admin.modules.upload_invalid_file'));
×
299
        }
300

301
        try {
302
            $moduleName = $this->extractZip($request->file('zip')->getPathname());
×
303
        } catch (\Exception $e) {
×
304
            return redirect()->route('admin.modules.upload')
×
305
                ->with('danger', $e->getMessage());
×
306
        }
307

308
        // Ручная заливка уже установленного модуля — единственный случай, когда
309
        // версию применяет админ: подсказываем про «Применить обновление»
310
        $extracted = Module::query()->where('name', $moduleName)->exists()
×
311
            ? __('admin.modules.update_extracted')
×
312
            : __('admin.modules.upload_success_extracted');
×
313

314
        return redirect('/admin/modules/module?module=' . $moduleName)
×
315
            ->with('success', $extracted);
×
316
    }
317

318
    /**
319
     * Установка модуля по URL
320
     */
321
    public function download(Request $request): RedirectResponse
4✔
322
    {
323
        $url = trim($request->input('url', ''));
4✔
324

325
        if (! filter_var($url, FILTER_VALIDATE_URL) || ! in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https'], true)) {
4✔
326
            return redirect()->back()
×
327
                ->with('danger', __('admin.modules.download_invalid_url'));
×
328
        }
329

330
        $maxSize = (int) config('modules.download_max_size') * 1024 * 1024;
4✔
331

332
        $tempDir = storage_path('app/temp');
4✔
333
        if (! is_dir($tempDir)) {
4✔
334
            mkdir($tempDir, 0755, true);
×
335
        }
336
        $tempFile = $tempDir . '/rotor_module_' . uniqid() . '.zip';
4✔
337

338
        try {
339
            // Потоковая запись: лимит проверяется по мере чтения, тело не держим в памяти
340
            $response = Http::timeout(30)->withOptions(['stream' => true])->get($url);
4✔
341

342
            if (! $response->ok()) {
4✔
343
                return redirect()->back()
×
344
                    ->with('danger', __('admin.modules.download_failed'));
×
345
            }
346

347
            $stream = $response->toPsrResponse()->getBody();
4✔
348
            $handle = fopen($tempFile, 'wb');
4✔
349

350
            if ($handle === false) {
4✔
351
                return redirect()->back()
×
352
                    ->with('danger', __('admin.modules.download_failed'));
×
353
            }
354
            $written = 0;
4✔
355
            $tooLarge = false;
4✔
356

357
            while (! $stream->eof()) {
4✔
358
                $chunk = $stream->read(8192);
4✔
359
                $written += strlen($chunk);
4✔
360

361
                if ($written > $maxSize) {
4✔
362
                    $tooLarge = true;
×
363
                    break;
×
364
                }
365

366
                fwrite($handle, $chunk);
4✔
367
            }
368

369
            fclose($handle);
4✔
370

371
            if ($tooLarge) {
4✔
372
                @unlink($tempFile);
×
373

374
                return redirect()->back()
×
375
                    ->with('danger', __('admin.modules.download_too_large', ['size' => formatSize($maxSize)]));
×
376
            }
377

378
            if (file_get_contents($tempFile, false, null, 0, 4) !== "PK\x03\x04") {
4✔
379
                @unlink($tempFile);
×
380

381
                return redirect()->back()
×
382
                    ->with('danger', __('admin.modules.download_not_zip'));
×
383
            }
384

385
            try {
386
                $moduleName = $this->extractZip($tempFile);
4✔
387
            } finally {
388
                @unlink($tempFile);
4✔
389
            }
390
        } catch (\Exception $e) {
×
391
            @unlink($tempFile);
×
392

393
            return redirect()->back()
×
394
                ->with('danger', $e->getMessage());
×
395
        }
396

397
        $module = Module::query()->firstOrNew(['name' => $moduleName]);
4✔
398
        $moduleConfig = include base_path('modules/' . $moduleName . '/module.php');
4✔
399

400
        // Несовместимую версию не применяем, но файлы уже распакованы: модуль
401
        // остаётся в промежуточном состоянии, о чём и говорит сообщение
402
        if ($requires = $this->incompatibleWith($moduleConfig)) {
4✔
403
            return redirect('/admin/modules/module?module=' . $moduleName)
2✔
404
                ->with('danger', __('admin.modules.requires') . ' ' . $requires . '! ' . __('admin.modules.update_extracted'));
2✔
405
        }
406

407
        // Кнопка в каталоге называется «Установить» и «Обновить» — она это и делает.
408
        // Для обновления применить обязательно: распаковка уже заменила код
409
        // работающего модуля, без миграций сайт остался бы на новой версии со
410
        // старой схемой БД
411
        $result = $this->applyModule($module, $moduleConfig, false, true);
2✔
412

413
        return redirect('/admin/modules/module?module=' . $moduleName)
2✔
414
            ->with('success', $result);
2✔
415
    }
416

417
    /**
418
     * Распаковка ZIP-архива модуля
419
     */
420
    private function extractZip(string $zipPath): string
4✔
421
    {
422
        $zip = new ZipArchive();
4✔
423

424
        if ($zip->open($zipPath) !== true) {
4✔
425
            throw new \RuntimeException(__('admin.modules.zip_open_failed'));
×
426
        }
427

428
        $topDirs = [];
4✔
429
        for ($i = 0; $i < $zip->numFiles; $i++) {
4✔
430
            $name = $zip->getNameIndex($i);
4✔
431

432
            if (str_contains($name, '..')) {
4✔
433
                $zip->close();
×
434
                throw new \RuntimeException(__('admin.modules.zip_invalid_path'));
×
435
            }
436

437
            $parts = explode('/', $name);
4✔
438
            if ($parts[0] !== '') {
4✔
439
                $topDirs[$parts[0]] = true;
4✔
440
            }
441
        }
442

443
        if (count($topDirs) !== 1) {
4✔
444
            $zip->close();
×
445
            throw new \RuntimeException(__('admin.modules.zip_invalid_structure'));
×
446
        }
447

448
        $moduleName = array_key_first($topDirs);
4✔
449

450
        if (! preg_match('/^[A-Z][A-Za-z0-9]+$/', $moduleName)) {
4✔
451
            $zip->close();
×
452
            throw new \RuntimeException(__('admin.modules.zip_invalid_name'));
×
453
        }
454

455
        $targetPath = base_path('modules/' . $moduleName);
4✔
456

457
        // Существующий модуль уводим в резервную копию, чтобы чистая распаковка
458
        // не оставила старых файлов и можно было откатиться при сбое
459
        $backupPath = null;
4✔
460
        if (is_dir($targetPath)) {
4✔
461
            $backupPath = base_path('modules/.backup_' . $moduleName . '_' . time());
4✔
462
            if (! rename($targetPath, $backupPath)) {
4✔
463
                $zip->close();
×
464
                throw new \RuntimeException(__('admin.modules.zip_backup_failed'));
×
465
            }
466
        }
467

468
        if (! $zip->extractTo(base_path('modules/'))) {
4✔
469
            $zip->close();
×
470
            $this->restoreBackup($targetPath, $backupPath);
×
471
            throw new \RuntimeException(__('admin.modules.zip_extract_failed'));
×
472
        }
473
        $zip->close();
4✔
474

475
        $this->chmodRecursive($targetPath);
4✔
476

477
        if (! file_exists($targetPath . '/module.php')) {
4✔
478
            $this->restoreBackup($targetPath, $backupPath);
×
479
            throw new \RuntimeException(__('admin.modules.zip_no_module_file'));
×
480
        }
481

482
        if ($backupPath) {
4✔
483
            $this->deleteDirectory($backupPath);
4✔
484
        }
485

486
        // Файлы перезаписаны на диске, но opcache (revalidate_freq) ещё держит
487
        // старый module.php — без сброса кнопка обновления и новый код модуля
488
        // подхватятся только со следующим запросом после ревалидации
489
        if (function_exists('opcache_reset')) {
4✔
490
            opcache_reset();
4✔
491
        }
492

493
        return $moduleName;
4✔
494
    }
495

496
    /**
497
     * Удаление файлов модуля с диска
498
     */
499
    public function deleteFiles(Request $request): RedirectResponse
×
500
    {
501
        $moduleName = $request->input('module');
×
502
        $modulePath = base_path('modules/' . $moduleName);
×
503

504
        if (! preg_match('|^[A-Z][\w\-]+$|', $moduleName) || ! file_exists($modulePath)) {
×
505
            abort(200, __('admin.modules.module_not_found'));
×
506
        }
507

508
        if (Module::query()->where('name', $moduleName)->exists()) {
×
509
            abort(200, __('admin.modules.delete_files_not_uninstalled'));
×
510
        }
511

512
        $this->deleteDirectory($modulePath);
×
513

514
        refreshCaches();
×
515

516
        return redirect()->route('admin.modules.index')
×
517
            ->with('success', __('admin.modules.module_files_deleted'));
×
518
    }
519

520
    /**
521
     * Откат распаковки: удалить частично распакованное и вернуть резервную копию
522
     */
523
    private function restoreBackup(string $targetPath, ?string $backupPath): void
×
524
    {
525
        $this->deleteDirectory($targetPath);
×
526

527
        if ($backupPath && is_dir($backupPath)) {
×
528
            rename($backupPath, $targetPath);
×
529
        }
530
    }
531

532
    /**
533
     * Рекурсивно устанавливает права доступа (755 для директорий, 644 для файлов)
534
     */
535
    private function chmodRecursive(string $path): void
4✔
536
    {
537
        chmod($path, 0755);
4✔
538

539
        foreach (scandir($path) as $item) {
4✔
540
            if ($item === '.' || $item === '..') {
4✔
541
                continue;
4✔
542
            }
543

544
            $full = $path . '/' . $item;
4✔
545
            if (is_dir($full)) {
4✔
546
                $this->chmodRecursive($full);
×
547
            } else {
548
                chmod($full, 0644);
4✔
549
            }
550
        }
551
    }
552

553
    /**
554
     * Рекурсивно удаляет директорию, включая симлинки
555
     */
556
    private function deleteDirectory(string $path): void
4✔
557
    {
558
        if (is_link($path)) {
4✔
559
            unlink($path);
×
560

561
            return;
×
562
        }
563

564
        if (! is_dir($path)) {
4✔
565
            return;
×
566
        }
567

568
        foreach (scandir($path) as $item) {
4✔
569
            if ($item === '.' || $item === '..') {
4✔
570
                continue;
4✔
571
            }
572

573
            $full = $path . '/' . $item;
4✔
574
            is_dir($full) && ! is_link($full) ? $this->deleteDirectory($full) : unlink($full);
4✔
575
        }
576

577
        rmdir($path);
4✔
578
    }
579

580
    /**
581
     * Удаление/Выключение модуля
582
     */
583
    public function uninstall(Request $request): RedirectResponse
×
584
    {
585
        $moduleName = $request->input('module');
×
586
        $disable = int($request->input('disable'));
×
587
        $modulePath = base_path('modules/' . $moduleName);
×
588

589
        if (! preg_match('|^[A-Z][\w\-]+$|', $moduleName) || ! file_exists($modulePath)) {
×
590
            abort(200, __('admin.modules.module_not_found'));
×
591
        }
592

593
        $module = Module::query()->where('name', $moduleName)->first();
×
594
        if (! $module) {
×
595
            abort(200, __('admin.modules.module_not_found'));
×
596
        }
597

598
        $module->deleteSymlink();
×
599
        $module->unpublish();
×
600

601
        if ($disable) {
×
602
            $module->update([
×
603
                'active' => false,
×
604
            ]);
×
605
            $result = __('admin.modules.module_success_disabled');
×
606
        } else {
607
            if (config('modules.safe_mode')) {
×
608
                return redirect('admin/modules/module?module=' . $moduleName)
×
609
                    ->with('danger', __('admin.modules.safe_mode_enabled'));
×
610
            }
611

612
            $module->rollback();
×
613
            $module->delete();
×
614
            $result = __('admin.modules.module_success_deleted');
×
615
        }
616

617
        clearCache(['modules', 'settings']);
×
618

619
        // После смены статуса модуля — пересборка соберёт роуты без него
620
        refreshCaches();
×
621

622
        return redirect('admin/modules/module?module=' . $moduleName)
×
623
            ->with('success', $result);
×
624
    }
625
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc