Skip to content

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.

text
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, singleton

Providers ​

Each provider implements Phlix\Common\Container\ServiceProviderInterface:

php
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 ​

  1. Pick the provider whose subsystem owns the class. If none fits, create a new provider in src/Common/Container/Providers/ and append it to ContainerFactory::defaultProviders().
  2. For autowire-friendly classes, add Foo\Bar::class => autowire().
  3. For classes that need configuration values, use factory(static fn () => ...) and read from $appConfig.
  4. Update the matching unit test in tests/unit/Common/Container/Providers/ so the binding stays exercised (coverage target β‰₯ 85 % on src/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, leave PHLIX_CONTAINER_COMPILE unset.

Request lifecycle ​

public/index.php:

  1. include config/server.php and inject the DB / logger config paths.
  2. ContainerFactory::create($config) builds the container.
  3. Resolve AuthManager, LibraryManager, ItemRepository, PlaybackController from the container.
  4. Parse the request with Phlix\Server\Http\Request::fromGlobals().
  5. Authenticate the bearer token (if present) via AuthManager.
  6. Route: /api/* goes to the JSON API placeholder; everything else is handed to PageRenderer for 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 itself
  • src/Common/Container/Providers/ – all default providers
  • docs/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 container
  • detain/phlix-shared – the shared Composer package consumed since 0.11.0.

Namespace map ​

Every Phlix\* namespace, its key classes, and the role each plays:

NamespaceKey classesRole
Phlix\Auth\*JwtHandler, UserRepository, AuthManager, UserProfileManagerJWT auth (HS256, 1h access / 7d refresh), user management, profiles (≀5), parental PIN and rating filter
Phlix\Media\Library\*LibraryManager, MediaScanner, FolderWatcher, ItemRepositoryMedia library scanning, filesystem watching (mtime checksum), metadata_json persistence
Phlix\Media\Metadata\*MetadataManager, TmdbProvider, TvdbProvider, FanartProvider, LocalNfoProviderMetadata fetching with provider priority (tmdb→local for movies, tvdb→fanart→local for series), 24 h cache
Phlix\Media\Streaming\*HlsStreamer, QualitySelector, StreamManagerHLS 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, TranscodeManagerFFmpeg 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, RelayConsumerHub claim protocol and relay heartbeat (Phase C)
Phlix\Plugins\*Loader, PluginManager, PluginLoaderPlugin manifest loading, lifecycle management (install / enable / disable / uninstall)
Phlix\LiveTv\*ChannelManager, GuideManager, RecorderLive TV channels, EPG, DVR recording
Phlix\Dlna\*ContentDirectory, AvTransport, DlnaServerDLNA/DMS ContentDirectory and AVTransport services
Phlix\Common\*Container, ConnectionPool, QueryBuilder, LoggerFactoryPSR-11 DI container, MySQL Workerman\MySQL\Connection pool, structured Monolog logging
Phlix\Server\*Core (Application), Http (Router, Controllers), WebSocket, WebPortalWorkerman 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 namePayloadIntroduced
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 use EventDispatcher::getListeners($eventName) with the wildcard to receive all variants. All payloads are readonly DTOs β€” plugins must not mutate them.

Full twelve-event catalog β†’ docs/dev/event-reference.md.


Test harness ​

Running tests ​

bash
./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:

php
$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 assertion

Test conventions ​

ConventionValue
Locationtests/unit/{Module}/{Class}Test.php
NamespacePhlix\Tests\Unit\{Module}
Base classPHPUnit\Framework\TestCase
BDD-style output./vendor/bin/phpunit --testdox

Static analysis ​

bash
./vendor/bin/phpstan analyze src/ --level=9
./vendor/bin/phpcs --standard=PSR12 src/
find src -name '*.php' -exec php -l {} \;   # parse check only

Debug recipes ​

1. Enable debug logging ​

bash
PHLIX_LOG_LEVEL=debug php public/index.php
# Valid levels (least β†’ most verbose): emergency, alert, critical, error, warning, notice, info, debug

Debug output lands in .logs/phlix.log. Filter by channel:

bash
tail -f .logs/phlix.log | grep -i "debug\|error"

2. Xdebug ​

bash
php -d xdebug.mode=debug -d xdebug.clientHost=localhost public/index.php

VS Code β€” launch.json:

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 ​

bash
# 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 ​

bash
./vendor/bin/phpstan analyze src/Server/Http/Router.php --level=9 --no-progress

BSD-3-Clause