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

visavi / rotor / 28556368264

02 Jul 2026 12:01AM UTC coverage: 16.555% (-0.006%) from 16.561%
28556368264

push

github

visavi
Добавлена совместимость 14.1 для сторонних модулей, добавлено уведомление об обновлениях модулей

2 of 16 new or added lines in 3 files covered. (12.5%)

991 of 5986 relevant lines covered (16.56%)

2.44 hits per line

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

0.0
/app/Console/Commands/ModuleRegistry.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace App\Console\Commands;
6

7
use Illuminate\Console\Command;
8
use Symfony\Component\Console\Command\Command as SymfonyCommand;
9

10
class ModuleRegistry extends Command
11
{
12
    /**
13
     * The name and signature of the console command.
14
     */
15
    protected $signature = 'module:registry
16
        {path? : Каталог с модулями или конкретный модуль (по умолчанию base_path(\'modules\'))}
17
        {--o|output= : Файл для записи (по умолчанию stdout)}
18
        {--name=Official Rotor Modules : Название реестра}
19
        {--base-url= : База для download_url, напр. https://example.com/modules}
20
        {--existing= : Существующий registry.json для накопления версий}';
21

22
    /**
23
     * The console command description.
24
     */
25
    protected $description = 'Build a module registry.json from local modules';
26

27
    /**
28
     * Execute the console command.
29
     */
30
    public function handle(): int
×
31
    {
32
        $path = $this->argument('path') ?: base_path('modules');
×
33

34
        if (! is_dir($path)) {
×
35
            $this->error("Каталог не найден: {$path}");
×
36

37
            return SymfonyCommand::FAILURE;
×
38
        }
39

40
        // Накопитель версий: подхватываем существующий реестр, чтобы старые версии не терялись
41
        $modules = $this->loadExisting();
×
42

43
        // Путь — либо один модуль (есть module.php), либо каталог с модулями
44
        $dirs = is_file($path . '/module.php') ? [$path] : (array) glob($path . '/*', GLOB_ONLYDIR);
×
45

46
        foreach ($dirs as $dir) {
×
47
            $manifest = $dir . '/module.php';
×
48

49
            if (! is_file($manifest)) {
×
50
                continue;
×
51
            }
52

53
            $config = include $manifest;
×
54

55
            if (! is_array($config) || empty($config['version'])) {
×
56
                continue;
×
57
            }
58

59
            $name = basename($dir);
×
60
            $modules[$name] = $this->buildModule($modules[$name] ?? null, $name, $dir, $config);
×
61
        }
62

63
        $registry = [
×
64
            'name'    => (string) $this->option('name'),
×
65
            'modules' => array_values($modules),
×
66
        ];
×
67

68
        $json = json_encode($registry, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
×
69

70
        if ($output = $this->option('output')) {
×
71
            file_put_contents($output, $json . "\n");
×
72
            $this->info('Реестр записан: ' . $output . ' (' . count($modules) . ' модулей)');
×
73
        } else {
74
            $this->line($json);
×
75
        }
76

77
        return SymfonyCommand::SUCCESS;
×
78
    }
79

80
    /**
81
     * Загружает существующий реестр в map по имени модуля
82
     *
83
     * @return array<string, array<string, mixed>>
84
     */
85
    private function loadExisting(): array
×
86
    {
87
        $file = $this->option('existing');
×
88

89
        if (! $file || ! is_file($file)) {
×
90
            return [];
×
91
        }
92

93
        $data = json_decode((string) file_get_contents($file), true);
×
94
        $modules = [];
×
95

96
        foreach ($data['modules'] ?? [] as $module) {
×
97
            if (isset($module['module'])) {
×
98
                $modules[$module['module']] = $module;
×
99
            }
100
        }
101

102
        return $modules;
×
103
    }
104

105
    /**
106
     * Собирает запись модуля, добавляя текущую версию к накопленным
107
     *
108
     * @param array<string, mixed>|null $existing
109
     * @param array<string, mixed>      $config
110
     *
111
     * @return array<string, mixed>
112
     */
113
    private function buildModule(?array $existing, string $name, string $dir, array $config): array
×
114
    {
115
        $version = (string) $config['version'];
×
116

117
        $entry = [
×
118
            'version'      => $version,
×
119
            'requires'     => (string) ($config['requires'] ?? ''),
×
120
            'download_url' => $this->downloadUrl($name, $version),
×
121
        ];
×
122

123
        if ($changelog = $this->changelog($dir, $version)) {
×
124
            $entry['changelog'] = $changelog;
×
125
        }
126

127
        // Накопленные версии минус текущая (перезапишем), сортировка по убыванию
128
        $versions = array_values(array_filter(
×
129
            $existing['versions'] ?? [],
×
130
            static fn (array $v) => ($v['version'] ?? null) !== $version,
×
131
        ));
×
132

133
        $versions[] = $entry;
×
134
        usort($versions, static fn ($a, $b) => version_compare($b['version'], $a['version']));
×
135

136
        return [
×
137
            'module'      => $name,
×
138
            'name'        => (string) ($config['name'] ?? $name),
×
139
            'description' => (string) ($config['description'] ?? ''),
×
140
            'author'      => (string) ($config['author'] ?? ''),
×
141
            'email'       => (string) ($config['email'] ?? ''),
×
142
            'homepage'    => (string) ($config['homepage'] ?? ''),
×
NEW
143
            'versions'    => $this->pruneShadowed($versions),
×
144
        ];
×
145
    }
146

147
    /**
148
     * Убирает затенённые версии: клиент берёт самую новую версию с requires
149
     * не выше своего ядра, поэтому версия достижима, только если её requires
150
     * строго ниже, чем у всех более новых. Остаётся «фронтир» — по одной
151
     * новейшей версии на каждую линию ядра
152
     *
153
     * @param array<int, array<string, mixed>> $versions отсортированы по убыванию версии
154
     *
155
     * @return array<int, array<string, mixed>>
156
     */
NEW
157
    private function pruneShadowed(array $versions): array
×
158
    {
NEW
159
        $kept = [];
×
NEW
160
        $minRequires = null;
×
161

NEW
162
        foreach ($versions as $version) {
×
NEW
163
            $requires = (string) ($version['requires'] ?? '') ?: '0';
×
164

NEW
165
            if ($minRequires !== null && version_compare($requires, $minRequires, '>=')) {
×
NEW
166
                continue;
×
167
            }
168

NEW
169
            $kept[] = $version;
×
NEW
170
            $minRequires = $requires;
×
171
        }
172

NEW
173
        return $kept;
×
174
    }
175

176
    /**
177
     * Строит download_url из --base-url (тег Модуль-версия, ассет Модуль.zip — как CI)
178
     */
179
    private function downloadUrl(string $name, string $version): string
×
180
    {
181
        $base = $this->option('base-url');
×
182

183
        if (! $base) {
×
184
            return '';
×
185
        }
186

187
        return rtrim($base, '/') . "/{$name}-{$version}/{$name}.zip";
×
188
    }
189

190
    /**
191
     * Достаёт текст секции "## <версия>" из changelog.md модуля
192
     */
193
    private function changelog(string $dir, string $version): ?string
×
194
    {
195
        foreach (['changelog.md', 'CHANGELOG.md'] as $filename) {
×
196
            $file = $dir . '/' . $filename;
×
197

198
            if (is_file($file)) {
×
199
                return $this->parseSection((string) file_get_contents($file), $version);
×
200
            }
201
        }
202

203
        return null;
×
204
    }
205

206
    /**
207
     * Вырезает текст под заголовком "## <версия>" до следующего заголовка
208
     */
209
    private function parseSection(string $content, string $version): ?string
×
210
    {
211
        $quoted = preg_quote($version, '/');
×
212
        $pattern = '/^##\s+v?' . $quoted . '\s*$(.*?)(?=^##\s|\z)/ms';
×
213

214
        if (preg_match($pattern, $content, $m)) {
×
215
            $section = trim($m[1]);
×
216

217
            return $section !== '' ? $section : null;
×
218
        }
219

220
        return null;
×
221
    }
222
}
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