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

visavi / rotor / 28980764164

08 Jul 2026 10:41PM UTC coverage: 32.95% (+0.2%) from 32.784%
28980764164

push

github

visavi
Улучшена установка, добавлены проверки на существование таблиц, добавлен сброс кеша

32 of 40 new or added lines in 2 files covered. (80.0%)

1864 of 5657 relevant lines covered (32.95%)

3.83 hits per line

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

38.37
/app/Http/Controllers/InstallController.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace App\Http\Controllers;
6

7
use App\Classes\Validator;
8
use App\Models\Setting;
9
use App\Models\User;
10
use App\Services\MigrationService;
11
use Illuminate\Http\JsonResponse;
12
use Illuminate\Http\RedirectResponse;
13
use Illuminate\Http\Request;
14
use Illuminate\Support\Facades\Artisan;
15
use Illuminate\Support\Facades\Auth;
16
use Illuminate\Support\Facades\DB;
17
use Illuminate\Support\Facades\Hash;
18
use Illuminate\Support\Facades\Lang;
19
use Illuminate\Support\Facades\Schema;
20
use Illuminate\Support\Str;
21
use Illuminate\View\View;
22

23
class InstallController extends Controller
24
{
25
    /**
26
     * Конструктор
27
     */
28
    public function __construct(
3✔
29
        Request $request,
30
        private readonly MigrationService $migrations
31
    ) {
32
        $lang = $request->input('lang', 'ru');
3✔
33

34
        Lang::setLocale($lang);
3✔
35

36
        view()->share('lang', $lang);
3✔
37
    }
38

39
    /**
40
     * Главная страница
41
     */
42
    public function index(): View
3✔
43
    {
44
        // Сброс всех кэшей на входе в установку: после сброса таблиц stale-кэш
45
        // (settings, modules и пр.) уводит движок в неверное состояние —
46
        // напр. держит модуль «активным», а его таблицы ещё не мигрированы.
47
        // config:clear убирает кэш конфига, чтобы config() отражал
48
        // свежеотредактированный .env, а не старые креды БД из кэша
49
        Artisan::call('config:clear');
3✔
50
        clearCache();
3✔
51

52
        // Читаем через config(), а не env(): env() возвращает null при
53
        // закэшированном конфиге, config() отдаёт значения в любом случае
54
        $driver = (string) config('database.default');
3✔
55
        $keys = [
3✔
56
            'APP_ENV'       => config('app.env'),
3✔
57
            'APP_DEBUG'     => config('app.debug'),
3✔
58
            'DB_CONNECTION' => $driver,
3✔
59
            'DB_HOST'       => config("database.connections.$driver.host"),
3✔
60
            'DB_PORT'       => config("database.connections.$driver.port"),
3✔
61
            'DB_DATABASE'   => config("database.connections.$driver.database"),
3✔
62
            'DB_USERNAME'   => config("database.connections.$driver.username"),
3✔
63
            'APP_URL'       => config('app.url'),
3✔
64
            'APP_EMAIL'     => config('app.email'),
3✔
65
            'APP_ADMIN'     => config('app.admin'),
3✔
66
        ];
3✔
67

68
        $versions = [
3✔
69
            'php'   => '8.3.0',
3✔
70
            'mysql' => '5.7.8',
3✔
71
            'maria' => '10.2.7',
3✔
72
            'pgsql' => '9.2',
3✔
73
        ];
3✔
74

75
        $storage = glob(storage_path('{*,*/*,*/*/*}'), GLOB_BRACE | GLOB_ONLYDIR);
3✔
76
        $uploads = glob(public_path('uploads/*'), GLOB_ONLYDIR);
3✔
77
        $dirs = [public_path('assets/modules'), base_path('bootstrap/cache'), base_path('modules')];
3✔
78

79
        $dirs = array_merge($storage, $uploads, $dirs);
3✔
80
        $languages = getAvailableLanguages();
3✔
81

82
        $isUpdate = $this->isUpdate();
3✔
83
        $database = $this->databaseChecks($versions);
3✔
84

85
        return view('install/index', compact('keys', 'languages', 'versions', 'dirs', 'isUpdate', 'database'));
3✔
86
    }
87

88
    /**
89
     * Проверка подключения к БД и её версии (driver-aware, с защитой от падения)
90
     */
91
    private function databaseChecks(array $versions): array
3✔
92
    {
93
        $driver = (string) config('database.default');
3✔
94
        $pdoExt = $driver === 'pgsql' ? 'pdo_pgsql' : 'pdo_mysql';
3✔
95

96
        $database = [
3✔
97
            'driver'     => $driver,
3✔
98
            'pdoExt'     => $pdoExt,
3✔
99
            'pdoLoaded'  => extension_loaded($pdoExt),
3✔
100
            'connected'  => false,
3✔
101
            'error'      => null,
3✔
102
            'client'     => '',
3✔
103
            'mysqlnd'    => true,
3✔
104
            'version'    => '',
3✔
105
            'versionOk'  => false,
3✔
106
            'minVersion' => match ($driver) {
3✔
107
                'mariadb' => $versions['maria'],
×
108
                'pgsql'   => $versions['pgsql'],
×
109
                default   => $versions['mysql'],
3✔
110
            },
3✔
111
        ];
3✔
112

113
        try {
114
            $pdo = DB::connection()->getPdo();
3✔
115
            $database['connected'] = true;
3✔
116

117
            if (in_array($driver, ['mysql', 'mariadb'], true)) {
3✔
118
                $database['client'] = (string) ($pdo->getAttribute(\PDO::ATTR_CLIENT_VERSION) ?: '');
3✔
119
                $database['mysqlnd'] = stripos($database['client'], 'mysqlnd') !== false;
3✔
120
            }
121

122
            $raw = DB::selectOne('SELECT VERSION() as version')->version ?? '';
3✔
123
            preg_match('/(\d+\.\d+(?:\.\d+)?)/', (string) $raw, $match);
3✔
124

125
            $database['version'] = (string) $raw;
3✔
126
            $database['versionOk'] = isset($match[1]) && version_compare($match[1], $database['minVersion'], '>=');
3✔
127
        } catch (\Throwable $e) {
×
128
            $database['error'] = $e->getMessage();
×
129
        }
130

131
        return $database;
3✔
132
    }
133

134
    /**
135
     * Проверка статуса и выполнение миграций
136
     */
137
    public function status(): View
×
138
    {
139
        if (! Schema::hasTable('migrations')) {
×
140
            Artisan::call('migrate:install');
×
141
        }
142

143
        $isUpdate = $this->isUpdate();
×
144
        $pendingMigrations = $this->migrations->getPendingMigrations($this->paths());
×
145

146
        return view('install/status', compact('isUpdate', 'pendingMigrations'));
×
147
    }
148

149
    /**
150
     * Выполняет одну следующую миграцию
151
     */
152
    public function migrateNext(): JsonResponse
×
153
    {
154
        ini_set('max_execution_time', 0);
×
155
        set_time_limit(0);
×
156

157
        $pending = $this->migrations->getPendingMigrations($this->paths());
×
158

159
        if (empty($pending)) {
×
160
            if (! $this->isUpdate()) {
×
161
                Artisan::call('key:generate', ['--force' => true]);
×
162
            }
163

164
            refreshCaches();
×
165

166
            return response()->json(['done' => true, 'migration' => null, 'output' => '']);
×
167
        }
168

169
        $name = $pending[0];
×
170
        $file = $this->migrations->findFile($name);
×
171

172
        if (! $file) {
×
173
            return response()->json(['error' => "Файл миграции не найден: {$name}"], 500);
×
174
        }
175

176
        $remaining = count($pending) - 1;
×
177

178
        try {
179
            $output = $this->migrations->runOne($file);
×
180
        } catch (\Throwable $e) {
×
181
            return response()->json([
×
182
                'error' => "Ошибка миграции {$name}: " . $e->getMessage(),
×
183
            ], 500);
×
184
        }
185

186
        return response()->json([
×
187
            'done'      => $remaining === 0,
×
188
            'migration' => $name,
×
189
            'output'    => $output,
×
190
            'remaining' => $remaining,
×
191
        ]);
×
192
    }
193

194
    private function paths(): array
×
195
    {
196
        $paths = [database_path('migrations')];
×
197

198
        if ($this->isUpdate()) {
×
199
            $paths[] = database_path('upgrades');
×
200
        }
201

202
        return $paths;
×
203
    }
204

205
    /**
206
     * Заполнение БД
207
     */
208
    public function seed(): View
×
209
    {
210
        if (setting('app_installed')) {
×
211
            abort(403);
×
212
        }
213

214
        Artisan::call('db:seed', ['--force' => true]);
×
215
        $output = Artisan::output();
×
216

217
        refreshCaches();
×
218

219
        return view('install/seed', compact('output'));
×
220
    }
221

222
    /**
223
     * Создание администратора
224
     */
225
    public function account(Request $request, Validator $validator): View|RedirectResponse
×
226
    {
227
        if (setting('app_installed')) {
×
228
            abort(403);
×
229
        }
230

231
        $lang = $request->input('lang', 'ru');
×
232
        $login = (string) $request->input('login');
×
233
        $password = $request->input('password');
×
234
        $password2 = $request->input('password2');
×
235
        $email = strtolower((string) $request->input('email'));
×
236

237
        if ($request->isMethod('post')) {
×
238
            $validator->regex($login, '|^[a-z0-9\-]+$|i', ['login' => __('validator.login')])
×
239
                ->regex(Str::substr($login, 0, 1), '|^[a-z0-9]+$|i', ['login' => __('users.login_begin_requirements')])
×
240
                ->email($email, ['email' => __('validator.email')])
×
241
                ->length($login, 3, 20, ['login' => __('users.login_length_requirements')])
×
242
                ->length($password, 6, 20, ['password' => __('users.password_length_requirements')])
×
243
                ->equal($password, $password2, ['password2' => __('users.passwords_different')])
×
244
                ->false(ctype_digit($login), ['login' => __('users.field_characters_requirements')])
×
245
                ->false(ctype_digit($password), ['password' => __('users.field_characters_requirements')])
×
246
                ->false(substr_count($login, '-') > 2, ['login' => __('users.login_hyphens_requirements')]);
×
247

248
            if ($validator->isValid()) {
×
249
                // Проверка логина на существование
250
                $checkLogin = User::query()->where('login', $login)->exists();
×
251
                $validator->false($checkLogin, ['login' => __('users.login_already_exists')]);
×
252

253
                // Проверка email на существование
254
                $checkMail = User::query()->where('email', $email)->exists();
×
255
                $validator->false($checkMail, ['email' => __('users.email_already_exists')]);
×
256
            }
257

258
            if ($validator->isValid()) {
×
259
                $user = User::query()->create([
×
260
                    'login'     => $login,
×
261
                    'password'  => Hash::make($password),
×
262
                    'email'     => $email,
×
263
                    'level'     => User::BOSS,
×
264
                    'gender'    => User::MALE,
×
265
                    'themes'    => 'default',
×
266
                    'point'     => 500,
×
267
                    'money'     => 100000,
×
268
                    'status'    => 'Boss',
×
269
                    'language'  => $lang,
×
270
                    'subscribe' => Str::random(32),
×
271
                ]);
×
272

273
                // ------------- Авторизация -----------//
274
                Auth::login($user, true);
×
275

276
                // -------------- Приват ---------------//
277
                $text = __('install.text_message', ['login' => $login]);
×
278
                $user->sendMessage(null, $text);
×
279

280
                return redirect('/install/finish');
×
281
            }
282

283
            setInput($request->all());
×
284
            setFlash('danger', $validator->getErrors());
×
285
        }
286

287
        return view('install/account', compact('login', 'email'));
×
288
    }
289

290
    /**
291
     * Завершение установки
292
     */
293
    public function finish(): View
×
294
    {
295
        if ($this->isUpdate()) {
×
296
            abort(403);
×
297
        }
298

299
        // Помечаем все апгрейды как выполненные — свежая схема уже содержит все изменения
300
        $batch = DB::table('migrations')->max('batch') + 1;
×
301
        foreach (glob(database_path('upgrades/*.php')) as $file) {
×
302
            DB::table('migrations')->insertOrIgnore([
×
303
                'migration' => pathinfo($file, PATHINFO_FILENAME),
×
304
                'batch'     => $batch,
×
305
            ]);
×
306
        }
307

308
        Setting::query()
×
309
            ->where('name', 'app_installed')
×
310
            ->update(['value' => 1]);
×
311

312
        clearCache('settings');
×
313

314
        return view('install/finish');
×
315
    }
316

317
    private function isUpdate(): bool
3✔
318
    {
319
        // Читаем из БД напрямую, минуя кэш настроек: после сброса таблиц кэш
320
        // может держать app_installed=1 и уводить установщик в режим обновления
321
        if (! Schema::hasTable('settings')) {
3✔
NEW
322
            return false;
×
323
        }
324

325
        return (bool) Setting::query()
3✔
326
            ->where('name', 'app_installed')
3✔
327
            ->value('value');
3✔
328
    }
329
}
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

© 2026 Coveralls, Inc