Server architecture β
This page summarises the bootstrap path and the dependency-injection container used by the Phlix Media Server. It is the developer-facing companion to the top-level CLAUDE.md / AGENTS.md.
Bootstrap & container β
Since 0.10.0 the server bootstrap is centred on a PSR-11 container built by Phlix\Common\Container\ContainerFactory. The factory composes four service providers and returns a \Psr\Container\ContainerInterface instance. Both public/index.php (web portal) and Phlix\Server\Core\Application resolve their dependencies through the container instead of newing them up directly.
config/server.php
β
βΌ
ContainerFactory::create($config)
β
βββ CoreServicesProvider ββ DB connection, LoggerFactory, audit
βββ AuthServicesProvider ββ JwtHandler, UserRepository, AuthManager
βββ MediaServicesProvider ββ ItemRepository, Scanner, Watcher,
β LibraryManager, MetadataManager, HLS
βββ SessionServicesProvider ββ SessionManager, PlaybackController
β
βΌ
$container->get(AuthManager::class) // auto-wired, singletonProviders β
Each provider implements Phlix\Common\Container\ServiceProviderInterface:
public function register(ContainerBuilder $builder, array $appConfig): void;The interface is marked @internal β third-party code should depend on the PSR-11 ContainerInterface rather than the provider classes themselves. The canonical provider stack lives in ContainerFactory::defaultProviders(); tests or future plugins may append their own to the list passed to create().
Wrapping the legacy statics β
Workerman\MySQL\Connection is bound to a factory that calls ConnectionPool::init() / ConnectionPool::getConnection('mysql') on first resolve. The static pool is still in place β A.1 wraps it so the connection can be injected as a typed constructor parameter; a follow-up step replaces the static entirely.
LoggerFactory and one named binding per LogChannels constant (for example, logger.auth, logger.media) are exposed so providers can wire channelled loggers via DI\get('logger.auth') without consumers having to know about the factory.
Adding a new binding β
- Pick the provider whose subsystem owns the class. If none fits, create a new provider in
src/Common/Container/Providers/and append it toContainerFactory::defaultProviders(). - For autowire-friendly classes, add
Foo\Bar::class => autowire(). - For classes that need configuration values, use
factory(static fn () => ...)and read from$appConfig. - Update the matching unit test in
tests/unit/Common/Container/Providers/so the binding stays exercised (coverage target β₯ 85 % onsrc/Common/Container/**).
Compiled containers (production) β
Setting the env var PHLIX_CONTAINER_COMPILE=1 enables PHP-DI's compiled-container cache; the compiled definitions land in var/cache/container/ by default (override via compile_dir in config/server.php). Compilation is disabled in development so new bindings take effect without a manual cache clear.
Caveat: PHP-DI 7's compiler rejects closures that capture variables via
use. The current providers rely on closures for the DB / logger factories; Phase B replaces them with invokable classes so the cache works end-to-end. Until then, leavePHLIX_CONTAINER_COMPILEunset.
Request lifecycle β
public/index.php:
include config/server.phpand inject the DB / logger config paths.ContainerFactory::create($config)builds the container.- Resolve
AuthManager,LibraryManager,ItemRepository,PlaybackControllerfrom the container. - Parse the request with
Phlix\Server\Http\Request::fromGlobals(). - Authenticate the bearer token (if present) via
AuthManager. - Route:
/api/*goes to the JSON API placeholder; everything else is handed toPageRendererfor Smarty rendering.
Application::fromConfigPath() wraps steps 1β2 for callers that still pass a config path (long-running Workerman worker, certain tests). The legacy Application::getInstance() singleton is retained but @deprecated; resolve services from the container instead.
Dependencies β detain/phlix-shared β
Since 0.11.0 (Step B.3), phlix-server depends on the detain/phlix-shared Composer package for its framework-neutral pieces:
Phlix\Shared\Plugin\{LifecycleInterface, Manifest, ManifestType, ManifestValidationError, EventNameMap}Phlix\Shared\Events\*β the 12 PSR-14 event DTOs.Phlix\Shared\Auth\JwtClaimsβ value object capturing the Phlix JWT shape.Phlix\Shared\Hub\*β placeholder DTOs for the hub claim/heartbeat protocol (Phase C).
phlix-server keeps the host-side runtime (PSR-14 dispatcher wiring, JSON-Schema validator, plugin loader, JWT signing, HTTP/WS layer). The shared package is the contract surface that phlix-server, phlix-hub, and plugin authors all import.
Legacy FQCNs (Phlix\Plugins\Contract\LifecycleInterface, Phlix\Plugins\EventNameMap, etc.) remain available as deprecated aliases through 0.11.x via src/Plugins/AliasCompatShim.php and the 3-line interface bridge at src/Plugins/Contract/LifecycleInterface.php. They are removed in 0.12.0.
See also β
src/Common/Container/ContainerFactory.phpβ the factory itselfsrc/Common/Container/Providers/β all default providersdocs/reference/env-vars.mdβ environment variables that influence the container (PHLIX_CONTAINER_COMPILE,JWT_SECRET)plans/expansion/a.1-di-container.mdβ the plan that introduced the containerdetain/phlix-sharedβ the shared Composer package consumed since 0.11.0.
Namespace map β
Every Phlix\* namespace, its key classes, and the role each plays:
| Namespace | Key classes | Role |
|---|---|---|
Phlix\Auth\* | JwtHandler, UserRepository, AuthManager, UserProfileManager | JWT auth (HS256, 1h access / 7d refresh), user management, profiles (β€5), parental PIN and rating filter |
Phlix\Media\Library\* | LibraryManager, MediaScanner, FolderWatcher, ItemRepository | Media library scanning, filesystem watching (mtime checksum), metadata_json persistence |
Phlix\Media\Metadata\* | MetadataManager, TmdbProvider, TvdbProvider, FanartProvider, LocalNfoProvider | Metadata fetching with provider priority (tmdbβlocal for movies, tvdbβfanartβlocal for series), 24 h cache |
Phlix\Media\Streaming\* | HlsStreamer, QualitySelector, StreamManager | HLS master/variant .m3u8 packaging, quality profiles (generic / mobile-low / mobile-high / web / tv-4k), stream selection (direct-play vs transcode) |
Phlix\Media\Transcoding\* | FfmpegRunner, EncodingHelper, TranscodeManager | FFmpeg probe / transcode / thumbnail, CRF 23/28, libx264 / libx265, hardware acceleration |
Phlix\Session\* | SessionManager, PlaybackController, SyncPlay\* | Device sessions, continue-watched (marks complete at 95 %), SyncPlay NTP-style time-sync (OFFSET_SAMPLE_COUNT=5, weighted-mean offset) |
Phlix\Hub\* | HubClient, RelayConsumer | Hub claim protocol and relay heartbeat (Phase C) |
Phlix\Plugins\* | Loader, PluginManager, PluginLoader | Plugin manifest loading, lifecycle management (install / enable / disable / uninstall) |
Phlix\LiveTv\* | ChannelManager, GuideManager, Recorder | Live TV channels, EPG, DVR recording |
Phlix\Dlna\* | ContentDirectory, AvTransport, DlnaServer | DLNA/DMS ContentDirectory and AVTransport services |
Phlix\Common\* | Container, ConnectionPool, QueryBuilder, LoggerFactory | PSR-11 DI container, MySQL Workerman\MySQL\Connection pool, structured Monolog logging |
Phlix\Server\* | Core (Application), Http (Router, Controllers), WebSocket, WebPortal | Workerman HTTP / WS entry, {param} routing, middleware groups, Smarty page rendering |
PSR-14 event map β
All dispatched PSR-14 events, their payload shapes, and the step that introduced them:
| Event name | Payload | Introduced |
|---|---|---|
phlix.playback.started | {media_id, user_id, profile_id, position_ticks} | A.2 |
phlix.playback.stopped | {media_id, user_id, position_ticks, completed} | A.2 |
phlix.library.scanned | {library_id, item_count, duration} | A.2 |
phlix.user.created | {user_id, email} | A.2 |
phlix.scrobble.* | {media_id, user_id, scrobbler_type} | A.2 |
phlix.webhook.* | {event_type, payload} | A.2 |
Wildcard patterns (
phlix.scrobble.*,phlix.webhook.*) match all sub-events. Listeners useEventDispatcher::getListeners($eventName)with the wildcard to receive all variants. All payloads arereadonlyDTOs β plugins must not mutate them.
Full twelve-event catalog β docs/dev/event-reference.md.
Test harness β
Running tests β
./vendor/bin/phpunit # unit + integration suites
./vendor/bin/phpunit --testsuite Unit # unit tests only
./vendor/bin/phpunit tests/unit/Auth/JwtHandlerTest.php --testdox
./vendor/bin/phpunit --coverage-text # coverage β coverage.xml + coverage-report/Mocking the database β
All DB access goes through Workerman\MySQL\Connection. Mock it like this:
$db = $this->createMock(Workerman\MySQL\Connection::class);
$db->method('query')
->willReturn([['col' => 'val']]); // SELECT result rows
$db->expects($this->once())
->method('query')
->with($this->stringContains('INSERT'), $this->anything()); // write assertionTest conventions β
| Convention | Value |
|---|---|
| Location | tests/unit/{Module}/{Class}Test.php |
| Namespace | Phlix\Tests\Unit\{Module} |
| Base class | PHPUnit\Framework\TestCase |
| BDD-style output | ./vendor/bin/phpunit --testdox |
Static analysis β
./vendor/bin/phpstan analyze src/ --level=9
./vendor/bin/phpcs --standard=PSR12 src/
find src -name '*.php' -exec php -l {} \; # parse check onlyDebug recipes β
1. Enable debug logging β
PHLIX_LOG_LEVEL=debug php public/index.php
# Valid levels (least β most verbose): emergency, alert, critical, error, warning, notice, info, debugDebug output lands in .logs/phlix.log. Filter by channel:
tail -f .logs/phlix.log | grep -i "debug\|error"2. Xdebug β
php -d xdebug.mode=debug -d xdebug.clientHost=localhost public/index.phpVS Code β launch.json:
{
"request": "launch",
"pathMappings": {
"/home/sites/phlix": "${workspaceFolder}"
}
}PhpStorm β set DBGp Proxy port 9003 and map server paths via the deployment configuration.
3. Tail logs in real time β
# All errors
tail -f .logs/phlix.log | grep -i error
# Auth channel
tail -f .logs/auth.log
# Transcode logs
tail -f .logs/transcode/*.log
# Specific server
tail -f .logs/phlix.log | grep "192.168.1.100"4. Verify phpstan on a single file β
./vendor/bin/phpstan analyze src/Server/Http/Router.php --level=9 --no-progress