Ministra stops being a module — the whole Stalker portal (portal.php,
MinistraBootstrap, PortalHandler/PortalHelpers and the STB front-end) now
lives in src/Ministra/ under the XcVm\Ministra namespace, served at
/home/xc_vm/Ministra via the nginx alias.
- src/ministra/* and Modules/ministra_85a7d/{PortalHandler,PortalHelpers}
→ src/Ministra/; MinistraModule.php + module.json removed. Ministra was
the only committed module, so src/Modules/ keeps a .gitkeep.
- portal.php resolves PortalHandler as a sibling and derives MAIN_HOME from
its new location (glob crutch gone).
- nginx alias + AuthRepository $rAlias switched to /home/xc_vm/Ministra
(PascalCase); ministra entry dropped from bundled_modules.php.
- Makefile: Modules/ removed from LB_DIRS — all modules are MAIN-only, so
the ~50 MB of portal assets no longer ship to LB nodes.
- ArchitectureTest: zero committed modules is now a valid state.
- PHPStan: analyse src/Ministra, exclude the procedural portal.php entry,
repath the ministra baseline entries.
- Docs (architecture, ministra-browser-emulation, extraction plan) updated
to the new layout; the "extract to a separate repo" plan is cancelled.
Verified: php -l, make gates, make phpstan (No errors), full unit suite
(432 tests). On-server smoke: handshake + get_profile work end-to-end with
a registered MAC after deploy.
BoundaryInterface was a marker interface with no runtime consumer —
nothing read getEntryPoint()/isIsolated() and, being static-less
metadata, it enforced nothing. Its only implementor was MinistraModule.
Removes the interface, drops `implements BoundaryInterface` plus the two
orphaned methods from MinistraModule (getName/getVersion stay — they come
from BaseModule/ModuleInterface), and deletes the now-empty Core/Boundary/.
Test contract (InterfaceContractTest) loses the three BoundaryInterface
assertions; docs (en/ru architecture + modules, .github instructions) now
describe isolated subsystems like Ministra as a convention — own entry
point + bootstrap — rather than a marker interface.
Verified: php -l, make gates, make phpstan (No errors);
InterfaceContractTest + ArchitectureTest green (33 tests, 96 assertions).
Signals — the file-backed queue of deferred DB writes drained by the
cache-handler daemon — were produced two different ways: five call sites went
through RedisManager::setSignal() while getStreamingUserInfo inlined the same
file_put_contents(). Worse, the writer lived on RedisManager yet wrote a *file*,
not Redis — a misleading home that made the two paths look like different
transports (they were byte-identical).
Introduce XcVm\Infrastructure\Signal\SignalQueue as the single owner of the
on-disk contract (cache_<md5(key)> holding [key, data]):
- push() - the one producer API; all 7 call sites use it now
(UserRepository x4, auth.php x2, BruteforceGuard x1)
- pending() - the drain API; CacheHandlerCommand reads through it instead of its
own glob + json_decode
- pathFor() / PREFIX - path helpers
RedisManager::setSignal() stays as a thin @deprecated alias delegating to
SignalQueue::push() for any out-of-tree callers.
Behaviour unchanged (same files, same format, same consumer switch). Adds
SignalQueueTest (format / idempotency / drain / malformed). Validated live:
player_api/testxc still authenticates (auth=1) with SignalQueue autoloaded.
Verified: php -l, phpunit (435 tests, 973 assertions), make gates.
The two methods were ~90% duplicate, differing only in how they obtain
settings/cache/bouquets and in the divergent GeoIP + signal backends
(GeoIPService/file-signals vs GeoIP/Redis) that stay inline. Extract the
identical blocks into private helpers, called by both:
loadUserRow - credential resolution (token/cache file or DB),
resolving $rUserID by reference so the cached
re-verification keeps its exact behaviour
verifyCachedCredentials - cached access-token / username+password re-check
decodeUserFields - JSON line fields -> arrays
resolveOutputFormats - allowed output-format keys
aggregateBouquetIds - bouquet -> channel/series/vod/live/radio id lists
resolveCategoryIds - bouquet -> category ids
Also de-obfuscate the remaining `if (cond) {} else { body }` blocks in both
methods. Net -74 lines despite adding the shared helpers (~240 lines of
duplication removed). Behaviour preserved; the file-vs-Redis signal divergence
is intentionally left as-is (separate decision).
Tests: UserRepositoryTest covers the pure helpers (aggregate/category/decode/
verify), 12 tests total. Validated live: player_api/testxc returns auth=1,
Active, allowed_output_formats=[m3u8,ts,rtmp]; on-demand /live start still works.
Verified: php -l, phpunit (430 tests, 962 assertions), make gates.
- getE2Info: collapse the obfuscated `if (cond) {} else { body }` chain into
straight positive-form ifs; drop the redundant re-init of pair_line_info.
- getUserInfo: same de-obfuscation of the ISP block, and fix the same
"Undefined array key isp_asn" bug already fixed in getStreamingUserInfo — the
ISP-persist step ran even on a GeoIP miss (con_isp_name null), reading the
undefined isp_asn key and writing a null isp_desc/as_number to the line.
- Extract the persist predicate both methods shared into a pure, tested
UserRepository::ispChanged() helper (+ tests/Unit/UserRepositoryTest.php).
Verified: php -l, phpunit (423 tests, 944 assertions), make gates.
For continuous-TS delivery (/play/<token>/ts) the client's whole playback buffer
is the initial prebuffer burst — the feed loop then runs in real time, so the
buffer never grows past it. On a cold on-demand start the server began serving
the instant the playlist first appeared, when only the 1-2 fast-start (2s)
segments existed, so selectSegments could only return ~2s and the client was
stranded at ~2s of buffer regardless of client_prebuffer.
Wait until the playlist actually holds client_prebuffer seconds (bounded by
on_demand_wait_time, bailing if the stream stops) before the first read. Clients
only — restreamer chains keep their low-latency behaviour; warm streams already
satisfy the condition and don't wait. Adds SegmentReader::playlistBufferedSeconds()
(sum of #EXTINF) as the gate.
Validated live on stream 567: at cold start OLD served 1 seg / 2.0s; NEW served
5 seg / 10.0s after a 0.2s wait (fast-start pre-generates segments, so the added
startup latency is negligible).
Verified: php -l, phpunit (SegmentReaderTest 10/10), make gates.
The label195 and label562 probe blocks each clamped the probed of_duration to
10s, wrote it to the stream's _.dur file and raised $rSegmentTime -- identical
seven-line cores. Extract them into a shared persistSegmentDuration() helper
returning [clamped probe, updated segment time], with unit tests (temp
STREAMS_PATH). No control-flow change; behaviour preserved.
Verified: phpstan level 5 green, phpunit 11 monitor tests green, make gates green.
The post-start block derived player compatibility, audio/video codec names and
the resolution (snapped to the nearest standard height) inline from the
stream_info JSON. Pull it into a pure resolveStreamCodecMeta() helper with unit
tests. No control-flow change; behaviour preserved.
Verified: phpstan level 5 green, phpunit 9 monitor tests green, make gates green.
The scheduled auto-restart check was three chained `if (!cond) goto label195;`
statements testing the configured days + HH:MM against the current
weekday/hour/minute. Replace them with a pure isAutoRestartDue() predicate
(injectable for tests) and a single `goto label195`. Two more gotos gone
(47 -> 45), and the schedule logic is unit tested.
Verified: phpstan level 5 green, phpunit 6 monitor tests green, make gates green.
First step of untangling MonitorCommand::execute()'s obfuscated goto control
flow. The FPS baseline computation flattened a "30/1" -> 30.0 rational parse
across five labels (label768/780/1047/1052/1057). Pull the pure parse into
parseFrameRate() with unit tests; the label768 entry and the label1847 exit are
kept so control flow is unchanged -- four labels and their gotos are gone.
Guards a zero denominator (PHP 8 would otherwise throw on "x/0") -- an input
ffprobe does not produce, so behaviour is unchanged in practice.
PHPStan cannot see the self::parseFrameRate call through the surrounding goto
maze and reports the method unused; baselined until the label768 region is
destructured, then the entry drops out.
Verified: phpstan level 5 green, phpunit 408/408, make gates green.
An on-demand stream emits short (2s) fast-start segments before it ramps up to
seg_time. getPlaylistSegments picked intval(client_prebuffer / seg_time)
segments -- with the default 10s prebuffer / 10s nominal seg_time that is 1
segment, i.e. only ~2s of actual buffer at cold start, which players surfaced as
"caches 2 seconds". Persistent streams (segments already ~10s) were unaffected:
1 segment = 10s.
Select the newest segments whose #EXTINF durations sum to at least the requested
prebuffer seconds instead, so client_prebuffer means real seconds regardless of
segment length. Fast-start (hls_init_time 2) is kept. Falls back to the old
nominal count when the playlist has no #EXTINF lines; the -1 (all) and 0 (index)
modes are unchanged. Split into a pure selectSegments()/parseSegmentDurations()
so the selection is unit tested.
Verified: phpstan level 5 green, phpunit 405/405, make gates green.
Phase 1 of extracting the live ffmpeg command assembly out of startStream.
buildLive(array $data): string is a byte-faithful copy of the inline assembly,
fed from a prepared $data array instead of loop-local state, with the delay
playlist I/O and segment-start/sleep left in startStream (arriving via
$data['segmentStart']/['delayActive']). The one non-verbatim change is the
ac3/eac3 dts_legacy bin switch, now a local reassignment instead of mutating the
$rFFMPEG_CPU/$rFFPROBE globals (the $rFFPROBE write was already dead). Homed on
StreamProcess for now so the private output/logo/aac helpers resolve via self::;
a FFmpegCommand facade is a follow-up.
startStream computes buildLive() alongside the inline assembly and error_log()s
any divergence, but still executes the inline $rFFMPEG -- shadow mode, no flip.
Once the diff log stays empty on real traffic the call site can switch over.
Characterisation tests cover simple/custom_ffmpeg/loopback/delay/rtmp branches
and assert no unresolved {TOKEN} survives.
Verified: phpstan level 5 green, phpunit 397/397, make gates green.
The delayed-HLS branch inline-computed two things: the resume segment number
parsed from the existing delay playlist (last-or-previous line's _<n>.ts index),
and the delay sleep seconds (delay_minutes*60 reduced ~10s per already-produced
segment, floored at 0). Pull both into pure resolveDelaySegmentStart() and
resolveDelaySleepTime() helpers with unit tests. The playlist merge and the
short-playlist abort stay inline. No behaviour change.
Verified: phpstan level 5 green, phpunit 391/391, make gates green.
The ffprobe-skip branch of the source loop hand-rolled two things: a scan of the
stream arguments for skip_ffprobe == 1, and the literal assumed h264/aac mpegts
result used when the scan matched. Give both names via pure helpers and cover
them with unit tests. No behaviour change.
Verified: phpstan level 5 green, phpunit 385/385, make gates green.
The {AAC_FILTER} substitution carried the rule inline: emit -bsf:a
aac_adtstoasc only when a copied AAC audio stream is muxed into a non-FLV
container. Give that rule a name and a test via a pure aacBitstreamFilter()
helper. No behaviour change.
Add unit tests: applied for copied AAC in mpegts/empty container, skipped for
FLV, skipped when the codec is not AAC or the output is not a copy.
Verified: phpstan level 5 green, phpunit 382/382, make gates green.
The codec-metadata derivation persisted for a started live stream (player
compatibility, audio/video codec names, resolution snapped to the nearest
standard height) was an inline block reading ffprobe output. Pull it into a
pure resolveStreamCodecMeta() helper taking the ffprobe array and the
player_allow_hevc flag and returning [compatible, audio, video, resolution].
The only external input, SettingsManager::getAll()['player_allow_hevc'], is now
passed in (getAll() just returns the cached settings array, so hoisting the read
out of the guard is free). Add unit tests: non-array/missing-codecs defaults,
compatible h264/aac, resolution snapping, HEVC gating by the allow flag, and
incompatible-but-reported codecs.
Verified: phpstan level 5 green, phpunit 379/379, make gates green.
The X-XC_VM-Detect / X-XC_VM-Prebuffer header injection was duplicated three
times in startStream: each block looped the argument list to append the header
to an existing 'headers' entry and added a new one if absent. Collapse the three
copies into a single appendHeaderArgument() helper and drop the shared
$rProcessed flag they threaded through (it leaked state between the blocks).
Behaviour is unchanged: the helper uses a local applied flag, and the three call
sites produce the same argument lists as before. Add unit tests covering
append-to-existing, create-when-absent, append-to-every-headers-entry, and the
empty-list case.
Verified: phpstan level 5 green, phpunit 373/373, make gates green.
Continue breaking down StreamProcess::startStream:
- extract resolveProbeSettings() (ffprobe/analysis timing) and
rotateSourcesPastCurrent() (source-failover ordering) as private static
helpers, with unit tests locking in their behaviour;
- simplify 15 inverted empty-if/else blocks across the class into a single
positive condition (no behaviour change).
Also fix the latent undefined-variable bugs the refactor surfaced under PHPStan:
startStream assembles the ffmpeg command from state produced inside the source
failover loop (which closes before the command is built) and inside the
non-custom_ffmpeg branch. On an empty source list, or when a custom ffmpeg line
is used, several variables were read while possibly undefined -- masked at
runtime only by ternary guards and suppressed in the PHPStan baseline. Default
them at the two scopes that own them:
- loop-scoped, read after the loop: $rSource, $rRealSource, $rStreamSource,
$rProtocol, $rFFProbeOutput
- non-custom_ffmpeg-branch-scoped, read in the {MAP}/{GEN_PTS}/{READ_NATIVE}
substitution and delay sleep: $rMap, $rGenPTS, $rReadNative, $rSleepTime
and drop the 9 now-obsolete entries from phpstan-baseline.neon.
Behaviour is unchanged on the happy path (the loop/branch overwrite the
defaults) and the defaults match the existing runtime ternary-guard results.
Verified: phpstan level 5 green (whole project, entries un-suppressed),
phpunit 369/369, make gates green.
buildSubtitleImport nested the metadata loop inside the import loop and reused
the outer counter $i. The inner loop advanced $i to count(files), so the outer
import loop exited after its first iteration: only the first subtitle was
imported (-sub_charenc -i), while -map/-metadata was still emitted for every
file — mapping ffmpeg inputs that were never added. Multi-subtitle movies got a
single imported track plus dangling -map references (broken output).
Split the nested loop into two siblings — import every file, then map every
file — and cache the count. All configured subtitles are now imported and
mapped to the correct input index.
Add tests/Unit/StreamProcessSubtitleTest.php covering empty input, a single
local subtitle, remote-server fetch, and the multi-subtitle case that
reproduces the defect (import count was 1, must equal N). Verified red→green
against the pre-fix code.
Verified: php -l clean, make gates green, phpunit 336/336 green.
The top-right profile dropdown was a hardcoded HTML block in
admin/header.php, so modules could not add or remove entries and the
plex/watch "settings" links were baked into core.
Move the menu into the NavbarRegistry: CoreNavbarProvider now registers
the core items (edit profile, settings, backups, cache, modules, logout)
under a reserved 'profile' parent, and header.php renders
NavbarRegistry::getChildren('profile') adaptively. There is intentionally
no top-level 'profile' node, so the items never leak into the main
navigation. Order 100–980 is reserved for module-provided links.
Extract NavbarRegistry::collapseDividers() to drop separators orphaned by
permission filtering (leading, trailing, consecutive), and reuse the
shared _xc_nav_visible()/_xc_nav_label() helpers by defining them once
near the top of the header. Remove the hardcoded settings_plex /
settings_watch entries — those now belong to their owning modules.
Add NavbarProfileMenuTest covering collapseDividers, the core 'profile'
contract (no top-level leak, edit first / logout last, permission gates),
and the module-integration contract (reserved order slots, divider
collapse when folder_watch_settings is absent).
The panel is deeply coupled to TMDb (VOD import, player metadata, admin
search, two crons), so shipping it as an uninstallable module only added
failure modes: after the move to hash-suffixed dirs (tmdb_f4e6e) every
hardcoded `Modules/tmdb/lib/...` require broke, and 2.3.3 crons died with
"Failed opening required TmdbClient.php".
tmdb -> core:
- Vendored \TMDB client -> src/Infrastructure/Tmdb/lib/; the only loader
is TmdbApiService::requireLibrary() (now public, also loads Release.php).
- TmdbApiService -> XcVm\Infrastructure\Tmdb — composer-autoloaded in every
bootstrap context, no module boot required (player scope never booted
modules, so module-namespace classes were unreachable there).
- TmdbCron / TmdbPopularCron -> XcVm\Domain\Vod; cron jobs -> Cli/CronJobs
(picked up by the console.php scan; command names cron:tmdb and
cron:tmdb_popular are unchanged).
- TmdbController -> Public/Controllers/Admin; tmdb_search / tmdb api
actions registered in routes/admin.php (same dispatchApi fallback).
- Domain/Vod services and player_functions.php load the lib through
TMDbService::requireLibrary() instead of hardcoded module paths.
- tmdb removed from config/bundled_modules.php. ModuleLoader gains
CORE_PROVIDED_MODULES: released watch/plex archives still declare
"dependencies": ["tmdb"] — such deps are stripped during manifest
normalization and in ModuleManager::listModules().
- syncBundledModules() purges stale on-disk tmdb module dirs and their
config/modules.php state on upgraded panels, so the old copy cannot boot
alongside the core implementation and collide on command names.
Standard-set provisioning fix (root cause of the "Undefined variable $db"
errors from watch/plex settings views on 2.3.3):
- Production still ran watch_e6c86/plex_20cd9-less legacy copies migrated
from 2.3.2 with generated hash_ids; provisionStandardSet() treated any
same-name directory as "already on disk" and never fetched the pinned
1.0.2/1.0.1 releases that contain the fix. A same-name directory whose
identity does not match the pinned hash_id is now considered stale: it
is deleted and the pinned release is installed in its place.
- installModuleFromSource(): when the module is already recorded as
installed (files re-provisioned over a stale copy), run updateModule()
(incremental from->to migrations) instead of re-running the initial
install.
- ArchitectureTest/InterfaceContractTest resolve modules via module.json instead
of the directory basename (works with {name}_{hash5}).
- ModuleLoaderTest: loads from a hash-suffixed dir; a broken module and its
dependents are skipped without aborting the whole load.
- ModuleManagerMigrationsTest: legacy bare -> hashed migration (+ stale-dup drop),
and install of a module living in a hash-suffixed directory.
ModuleUpdateChecker resolves the latest available version per module by source:
git (via GitHubReleases), url (version.json), platform (best-effort), bundled
(no-op). ModuleUpdatesCronJob (cron:module_updates) records available_version
into config/modules.php; migration 008 registers it to run weekly. Network-free
unit tests cover the routing.
The XC_Autoloader fallback was already retired (no-op stub); this deletes it for
good. Resolution is now 100% Composer PSR-4 (+ ModuleLoader for modules), no
legacy scanner, no class-map cache.
- Move `define('MAIN_HOME', ...)` into bootstrap.php (autoload.php used to define
it); bootstrap.php now requires only vendor/autoload.php.
- Drop `\XC_Autoloader::clearCache()/warmCache()` from StartupCommand.
- tests/bootstrap.php: locate-guard + require switched to vendor/autoload.php.
- Entry points that required autoload.php directly — Public/index.php,
Public/admin/index.php, Public/stream/index.php, Public/progress/index.php,
ministra/portal.php and Admin/Reseller TableController — switched to
vendor/autoload.php (defining MAIN_HOME where they did not already). These were
not in the plan's checklist; found via grep during execution.
- phpstan.dist.neon: drop src/autoload.php from scanFiles.
- Makefile: drop autoload.php from LB_ROOT_FILES.
- deleted_files.txt: add autoload.php (client cleanup on update).
- AutoloadOrderTest: now asserts the XC_Autoloader class and file are gone.
- git rm src/autoload.php.
- PSR4_MIGRATION_PLAN.md: mark final-phase step 2 done.
Verified: grep XC_Autoloader:: = 0; php -l clean; PHPStan no errors; PHPUnit
303/303; make gates pass; bootstrap smoke — MAIN_HOME + XC_Bootstrap present,
XC_Autoloader gone, only the Composer autoloader registered.
enigma, episode, episodes, process_monitor and stream_view referenced migrated
classes (UserRepository, BouquetService, StreamRepository, RequestManager,
SettingsManager, ...) by short name with either no `use` or a `use` placed
*below* the first usage. PHP imports outside the top scope are positional, so the
short name resolved to a now-nonexistent global class — a runtime fatal on those
admin pages (php -l passes; not covered by PHPStan/PHPUnit). The migration's
automated `use` insertion missed them due to the interleaved HTML / short-tag
(<? , <?=) structure, and the later php-cs-fixer pass stripped some as 'unused'
because it could not see usage inside short-tag blocks.
- Consolidate every needed `use XcVm\...;` into a single top-of-file PHP block.
- Exclude Public/Views and Modules/*/views from php-cs-fixer (no_unused_imports
is unreliable on short-tag templates); their import correctness is enforced by
the new check_procedural_use gate instead.
Verified: php -l (short_open_tag=1) clean; PHPStan no errors; PHPUnit green.
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/Events into XcVm\Core\Events and its sub-trees: \Contract
(StoppableEventInterface), \Auth, \Module, \Settings, \Stream (the event
classes), plus root EventDispatcher, ListenerProvider, ListensTo, AbstractEvent.
- Namespace 14 files across 6 namespaces; cross-namespace refs imported via use
(AbstractEvent/EventDispatcher → Contract\StoppableEventInterface; Stream events
→ root AbstractEvent). Built-in \Attribute (ListensTo) and ioncube \XC_VM
(PackageInstalledEvent) qualified.
- Rewrite 'use ListensTo;' → FQCN; add use to referrers across src/ and tests/;
fix leading-backslash refs (\EventDispatcher, \ListensTo, \PackageInstalledEvent
from the Module commit) to their FQCNs.
- Tests: add real top-level use imports to ModuleLoaderBootTest (EventDispatcher)
and ListensToAttributeTest (ListensTo) — the use-inserter skipped/mis-placed them
due to a namespace() method and a heredoc fixture already containing the FQCN.
- phpstan-baseline.neon regenerated (292→292).
Completes Core/Container + Core/Events. Verified: php -l clean; PHPStan no errors;
PHPUnit 295/295; EventDispatcher resolves and AbstractEvent implements
XcVm\Core\Events\Contract\StoppableEventInterface.
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.
Rewrite ModuleLoader::registerModuleAutoloader() from a lossy short-name + glob
lookup into a true per-module PSR-4 resolver: the module's base namespace
(XcVm\Module\{Name}) maps onto its directory, and the namespace remainder
becomes the sub-path.
XcVm\Module\Watch\WatchModule → {modulePath}/WatchModule.php
XcVm\Module\Watch\Service\WatchService → {modulePath}/Service/WatchService.php
- Only classes under the module's own namespace are claimed; everything else
(global legacy classes, other modules, core) falls through untouched. This
removes the short-name glob, so two modules — or two sub-namespaces in one
module — can declare same-named classes without colliding.
- load() now derives the base namespace from the resolved FQCN and registers the
autoloader after class resolution (reordered, still before the main require).
- Marketplace slug dirs unaffected (real $modulePath is used); encrypted-file
handling preserved (require_once + zend_compile_file decrypt hook).
- resolveClassName()/load() already targeted XcVm\Module\{Pascal}\{Pascal}Module
(done in earlier work) — left as is.
console.php FQCN discovery is intentionally NOT changed here: the Cli layer is
still global (CommandInterface, *Command, *CronJob), so switching discovery to
\XcVm\Cli\... would break it. Per the plan it switches atomically with the Cli
layer namespacing (phases 3..N).
New test tests/Unit/ModuleLoaderPsr4ResolverTest.php: sub-namespace resolution,
same-short-name no-collision, foreign-namespace fall-through.
Verified: php -l clean; PHPUnit 295/295 (+3); PHPStan no errors; all 4 real
modules (plex/watch/tmdb/ministra) load and their *Module FQCNs resolve.
Adds tests/Support/TestDb.php — a Database-compatible wrapper over
sqlite::memory: injected via setDb() — and StreamConfigRepositoryTest as a
worked example (SELECT/list/keyed/DELETE-cascade/not-found). 286 -> 292 tests.
- Added new fields to module.json: environment, dependencies, has_navbar, and has_settings.
- Updated ModuleLoader to support environment filtering and topological sorting of modules based on dependencies.
- Implemented error handling for missing and cyclic dependencies during module loading.
- Enhanced documentation for module.json structure and ModuleLoader functionality.
- Introduced unit tests for ModuleLoader to validate loading behavior and dependency resolution.
- Updated existing modules' manifest files to include new fields.
Co-authored-by: Copilot <copilot@github.com>
- Added `StreamInterface` to define the contract for stream handling.
- Implemented `FileStream` class for file-based streaming.
- Implemented `TextStream` class for in-memory text streaming.
- Removed legacy `M3UParser` class and related resources.
- Added new tests for `AttributeStringToArray`, `Iso8601Transformer`, `Resolution`, `Byterange`, `Inf`, `Line`, and `Lines` classes.
- Updated bootstrap file to include new PhpM3u8 components.
- Replace legacy in-project M3U parser with vendored Gemorroj/M3uParser
- Initialize parser via bootstrap with proper namespacing
- Remove obsolete src/domain/Stream/M3UEntry.php implementation
- Update stream import/review mapping:
- use logo as fallback when tvg-logo is missing
- Add third-party compliance files for vendored parser:
- ATTRIBUTION.md
- LGPL LICENSE
- Add full M3uParser unit test suite:
- tag parsing
- parser behavior
- fixtures
- Add StreamService integration tests for M3U parsing/import flow
- Update test bootstrap:
- add igbinary_serialize / igbinary_unserialize polyfills
- explicitly load M3uParser bootstrap and custom test tag class