Mirror core's DB layout at the module level: one master database.sql (full
current schema), one database_drop.sql (teardown), and forward-only
migrations/<semver>.sql deltas. ModuleMigrator gains install()/uninstall();
up() reads migrations/*.sql; down() removed.
- watch: database.sql (delete_missing folded into watch_folders),
database_drop.sql, migrations/1.0.1.sql; drop 1.0.0.up/down.sql;
getVersion() -> 1.0.1 (matches module.json).
- Move core migration 004_add_watch_delete_missing.sql into the watch module.
- installModule no longer wraps the DDL migration in a transaction: MySQL/MariaDB
implicitly commit DDL, so the wrapping rollback() only masked the real SQL error.
- syncBundledModules retries a previously-failed install (skips only admin-disabled)
so `console.php status` self-heals.
Make the bundled watch/plex modules own their database schema and lifecycle
instead of being hard-wired into the base schema, and decouple core code from
module-owned tables.
File-based module migrations:
- ModuleMigrator runs Modules/<name>/migrations/<semver>.up.sql / .down.sql,
ordered by semver; installed_version (config/modules.php) is the watermark.
- Wired into ModuleManager: install → up(null→target), update → up(from→target),
uninstall → down(installed). Install is transactional (a failing migration
rolls back and marks the module Failed).
- watch ships 1.0.0.up/down.sql (creates/drops watch_categories, watch_folders,
watch_logs, watch_refresh + seeds the default TMDb genre categories with
INSERT IGNORE). Removed those tables from bin/install/database.sql — the
module is now the single source of that DDL.
Module lifecycle:
- syncBundledModules() auto-installs never-installed bundled modules in
dependency order; called from StatusCommand after MigrationRunner::run so a
fresh install/update provisions their tables. Idempotent.
- uninstallModule() refuses to remove a module that installed dependents still
require.
- plex declares "dependencies": ["watch"], owns no tables, and on uninstall
deletes only its own rows (watch_folders type='plex', categories 3/4).
Decouple core from watch tables via events (core would otherwise fatal once the
tables are droppable):
- New StreamsDeletedEvent / BouquetDeletedEvent, dispatched from
StreamRepository::deleteStream/deleteStreams, ToolsCommand::deleteStreams and
BouquetService::deleteById in place of direct DELETE/UPDATE on watch tables.
- WatchModule subscribes via #[ListensTo] and delegates to WatchService, which
now holds the watch_refresh/watch_logs/watch_folders cleanup. Fixes a latent
bug where bouquet deletion wiped fb_bouquets (column was read but not SELECTed).
- Moved TMDbService::updateCategories() (writes watch_categories) into
WatchService; it now builds the client via the PSR-4
TmdbApiService::createClient() instead of a manual require of the legacy lib
(createClient is now self-contained).
Replace the fragile per-class setDb()/db() pattern (which threw when a
bootstrap path forgot to wire $db) with a shared trait that lazily resolves
from DatabaseFactory. Fixes the streaming UserRepository fatal and the same
latent issue in module crons. 42 classes converted.
- Exclude bundled third-party code from analysis (Modules/tmdb/lib/*,
Core/Util/MobileDetect.php) — like the vendored Composer packages, these
are not our code (php-tmdb client, Mobile-Detect). Removes 67 baselined
errors incl. the only class.notFound and phpDoc.parseError (both vendored).
- return.void (10): drop the discarded return value (return true/false/null →
return;) in void methods — NetworkUtils, LineService, StreamProcess, TMDbService.
- deadCode.unreachable (2): remove the dead `return;` after AdminHelpers::goHome()
(a never-returning call) in Plex/WatchController.
- Regenerate phpstan-baseline.neon: 440 -> 361 errors (291 -> 241 blocks).
Verified: make phpstan green, PHPUnit 298/298.
The PSR-4 migration's leading-backslash qualification leaked into string
require paths, turning 'Modules/tmdb/lib/Release.php' into
'Modules/tmdb/lib/\Release.php' → "Failed to open stream: No such file or
directory" at runtime (TmdbCronJob, TmdbCron, watch/WatchItemCommand). Remove
the stray backslash in all three. PHPStan can't catch this (constant + string
literal concatenation in require), so it surfaced only at runtime.
PHP string class names are always fully-qualified (they ignore the current
namespace and use-imports), so several short-name string references silently
broke when their classes were namespaced — failing only at runtime.
- StreamUtils::sortArguments(): the uasort() callback array('StreamUtils',
'customOrder') referenced the now-namespaced class by its old global short
name -> "class StreamUtils not found". Use array(self::class, 'customOrder').
- Router::checkPermission(): `class_exists('Authorization')` was always false
(Authorization is now XcVm\Core\Auth\Authorization), so the ['type','key']
permission branch never called check() and fell through to `return true` —
granting access unconditionally. Call Authorization::check() directly.
- TmdbController::hasMediaPermission(): `class_exists('Authorization')` always
false -> method always returned false (media permissions always denied). Drop
the dead guard; the call already imports the FQCN.
- AdminLogoutController: `class_exists('SessionManager')` always false -> the
admin session was never cleared on logout. Call SessionManager::clearContext()
directly.
- BruteforceGuard::getDB(): `class_exists('DatabaseFactory', false)` always
false -> qualify to \XcVm\Infrastructure\Database\DatabaseFactory::class.
Verified: php -l clean; StreamUtils callback is callable; make gates pass;
PHPUnit green (incl. StreamUtilsTest).
Apply 'make cs-fix' — 493 files. Mechanical, import-block only:
- sort use statements alphabetically (class/function/const grouped);
- drop imports left unused by the PSR-4 migration (e.g. classes referenced by
leading-backslash FQCN whose redundant 'use' the automated insertion had added);
- one blank line after namespace and after the import block; collapse stray
blank lines around use.
No logic changes. Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; and a
temporary PHPStan pass over src/Public/Controllers confirms no still-referenced
import was removed (0 unresolved classes). 'use' after inline HTML in view
templates is valid and aliases correctly (verified) — those imports are sorted too.
Namespace the modules' own classes into XcVm\Module\<Pascal> (Plex, Watch, Tmdb,
Ministra) — PlexController/PlexService/PlexCron/..., WatchController/WatchService/...,
TmdbController/TmdbCron/TmdbApiService/..., PortalHandler/PortalHelpers (~23 classes).
They now resolve through the Phase-2 ModuleLoader PSR-4 resolver.
- The bundled third-party tmdb/lib/* (TMDB client, Entities, roles, config) stays
GLOBAL, like the other vendored libs; namespaced Tmdb classes reference it via
\TMDB etc.
- Rewrite the *Module classes' (and any sibling) 'use ShortName;' imports → FQCN;
add use to referrers; convert leading-backslash refs. Sub-classes keep the
Core/Domain/Cli use imports added during those layers.
- Qualify built-ins (\DateTime, \Exception, \PDO, ...) and the global lib classes.
- phpstan-baseline.neon regenerated.
Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; all 4 modules load and
their sub-classes (e.g. XcVm\Module\Plex\PlexService) resolve via the module
PSR-4 autoloader; re-sweep clean.
Namespace all 55 Cli classes into XcVm\Cli (CommandInterface, CommandRegistry,
CronTrait, DaemonTrait), XcVm\Cli\Commands (28) and XcVm\Cli\CronJobs (23);
migration_logic.php stays procedural/global.
- console.php: switch command discovery from basename==classname + manual require
to FQCN resolution — each scan dir maps to its PSR-4 namespace, classes load via
Composer, implementsInterface(CommandInterface::class). Drop the manual
CommandInterface/CommandRegistry requires. This is the atomic switch the plan
required to land with the Cli namespacing.
- Commands/CronJobs get 'use XcVm\Cli\CommandInterface;' (implements) and the
trait imports 'use XcVm\Cli\CronTrait;'/'DaemonTrait;'; rewrite the modules'
'use CommandRegistry;' → FQCN; qualify built-ins/global (\ReflectionClass,
\PDO, \Exception, \RuntimeException, \XC_Autoloader, \XC_VM).
- Fixtures: 'use CommandRegistry;' → FQCN; InterfaceContractTest registerCommands
param-type → FQCN. phpstan-baseline.neon regenerated.
Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; FQCN discovery resolves
51 classes (49 implement CommandInterface); no mangled FQCNs; re-sweep clean.
Move Core/Module into XcVm\Core\Module (BaseModule, ModuleInterface,
MigratableInterface, ModuleLoader, ModuleManager, NavbarRegistry, NavbarItem,
CoreNavbarProvider) and Core/Module/Contract into XcVm\Core\Module\Contract
(the 6 provider interfaces). Completes the Core hub layer.
- Namespace 14 files (8 root + 6 Contract). Root files import the contracts via
'use XcVm\Core\Module\Contract\...'; NavbarProviderInterface imports the
root NavbarRegistry. Qualify still-global deps with leading backslash
(\ServiceContainer, \CommandRegistry, \ModuleState, \ServerEnvironment,
\EventDispatcher, \ListensTo, the Module exceptions, \ZipArchive,
\InvalidArgumentException, \RuntimeException) — Container/Events/Enum/Exception
migrate later. Migrated deps (Router, StreamPipeline, ...) keep their use.
- Rewrite the modules' 'use BaseModule/NavbarRegistry/NavbarItem;' → FQCN; add
'use XcVm\Core\Module\...;' to other referrers across src/ and tests/.
- Fix pre-existing leading-backslash refs to the FQCN.
- Tests: qualify the module fixtures' generated 'use ModuleInterface;' /
'use BaseModule;' / 'use NavbarRegistry;' and the registerRoutes type hint to
FQCN; add real top-level use imports to the fixture-builder tests; update
InterfaceContractTest registerNavbar param-type to the FQCN. Repaired
use-inserter mis-placements in the two tests that declare a namespace() method.
- Public/index.php: class_exists('ModuleLoader') → class_exists(ModuleLoader::class).
- phpstan-baseline.neon regenerated (292→292; no new/unknown-class errors).
Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; the 4 real modules
load and resolve as XcVm\Core\Module\ModuleInterface; leading-backslash
re-sweep across all migrated classes is clean.
Move Core/Http into XcVm\Core\Http (ApiClient, CurlClient, RequestManager,
Request, Response, Router) and Core/Http/Pipeline into XcVm\Core\Http\Pipeline
(StreamContext, StreamMiddlewareInterface, StreamPipeline). RequestGuard.php is
procedural (global functions) and stays global.
- Namespace the 9 classes; qualify still-global deps with leading backslash
(\ServerRepository in ApiClient; \Authorization, \ServiceContainer,
\AdminHelpers in Router) and built-in \Throwable; same-namespace siblings
unqualified.
- Add 'use XcVm\Core\Http\...;' to referencing files — RequestManager 128,
Router 19, ApiClient 15, Request 11, Response 6, CurlClient 5, plus Pipeline —
across src/ and tests/.
- Rewrite the modules' 'use Router;' → 'use XcVm\Core\Http\Router;' (plex,
watch, tmdb).
- Fix pre-existing leading-backslash global refs (\Router etc.) to the FQCN.
- Tests: fully-qualify the Router type hint in generated module fixtures
(registerRoutes(\XcVm\Core\Http\Router ...)) and update the
InterfaceContractTest param-type expectation to the FQCN. Also repaired two
fixtures where the use-inserter mis-placed a 'use' (files declare a method
literally named namespace(), which the tokenizer reports as T_NAMESPACE).
- phpstan-baseline.neon regenerated (292→292; class names in frozen messages
gained the namespace, no new/unknown-class errors).
Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; Http classes resolve
via Composer; leading-backslash re-sweep across migrated classes is clean.
Move Core/Database into XcVm\Core\Database (DatabaseHandler, Database,
MigrationRunner, QueryHelper). First of the Core hub sub-layers.
- Namespace the 4 classes; DatabaseHandler extends Database (same namespace);
built-ins/ioncube qualified (\PDO, \PDOException, \Exception, \Throwable,
\XC_VM); Database keeps its 'use XcVm\Core\Logging\FileLogger;'.
- Add 'use XcVm\Core\Database\...;' to referencing files (DatabaseHandler 51,
Database 64, QueryHelper 29, MigrationRunner 3) + 2 test files (PHPStan does not
analyse tests/, so PHPUnit is the gate there).
- Rewrite pre-existing leading-backslash global refs (\DatabaseHandler etc., e.g.
in @param docblocks of ResellerApiDispatcher/ResellerTableRenderer) to the full
FQCN \XcVm\Core\Database\... — a 'use' import does not cover a leading-\
reference. Done with a lookbehind so FQCN continuations and use-lines are intact.
- phpstan-baseline.neon regenerated (292→292; pre-existing Database/migration_logic
findings re-anchored after class names in messages gained the namespace).
Verified: php -l clean; PHPStan no errors; PHPUnit 295/295.
Move the last two Core/Config classes into XcVm\Core\Config. SettingsManager
has the largest fan-out of the whole migration (referenced by ~224 files).
- Namespace SettingsManager (self-contained singleton, no class deps) and
SettingsRepository (\FileCache:: qualified).
- Add 'use XcVm\Core\Config\SettingsManager;' to 224 referencing files and
'use ...\SettingsRepository;' to 12 — call sites (SettingsManager::get(), etc.)
unchanged. Done with a token-based inserter (after namespace/declare/<?php,
idempotent, same-namespace files skipped).
- ToolsCommand::processRecaptcha: drop dead class_exists('SettingsManager') +
method_exists guard (always autoloadable now) → call SettingsManager::clearCache()
directly. This was the only string-literal class reference.
Core/Config is now fully namespaced (ConfigReader, DomainResolver, SettingsManager,
SettingsRepository); the procedural constant files (AppConfig/Binaries/Paths)
remain global by design.
Verified: php -l clean (226 files); PHPStan no errors (baseline unchanged — it is
line-independent so the added use-lines don't disturb it); PHPUnit 295/295; all
Config FQCNs resolve via Composer; sample Public referrers lint-clean.