Skip to content

Architecture

mab056 edited this page Feb 15, 2026 · 3 revisions

Architecture

Architectural Principles

The project enforces strict rules:

  • No singleton
  • No static methods/properties
  • No final classes/methods
  • Dependency Injection via constructor
  • Interface-first design

Entry Point and Bootstrap

  1. ops-health-dashboard.php
  • defines plugin constants
  • verifies Composer autoloader
  • registers activation/deactivation hooks
  • on plugins_loaded calls OpsHealthDashboard\\bootstrap()
  1. config/bootstrap.php
  • creates Container
  • registers $wpdb via instance('wpdb', $wpdb)
  • registers shared bindings (share) for services, checks, admin
  • returns new Plugin($container)

DI Container

src/Core/Container.php

  • bind(abstract, closure) for non-shared instances
  • share(abstract, closure) for container-managed shared instances
  • instance(abstract, instance) for already-instantiated objects
  • make(abstract) with circular dependency detection

Circular Dependency Detection

The container uses a $resolving array to track abstracts being resolved. If make() is called for an abstract already present in $resolving, an exception is thrown:

if ( isset( $this->resolving[ $abstract ] ) ) {
    throw new \Exception( "Circular dependency detected for [{$abstract}]" );
}

$this->resolving[ $abstract ] = true;

try {
    // resolve...
} finally {
    unset( $this->resolving[ $abstract ] );
}

The finally block ensures the array is cleaned up even if an exception occurs, allowing the container to continue functioning after an error.

Storage: Sentinel Object Pattern

src/Services/Storage.php

The has() method uses a sentinel object to distinguish between "the key exists with value false" and "the key does not exist":

public function has( string $key ): bool {
    $sentinel = new \stdClass();
    $value    = get_option( $prefixed_key, $sentinel );
    return $sentinel !== $value;
}

get_option() returns the default when the key is missing. Using a stdClass as the default, the !== comparison is safe because each new stdClass() is a unique object.

Note: update_option() is called with autoload=false to prevent large data (such as check results) from being loaded into memory on every WordPress request.

Plugin Orchestration

src/Core/Plugin.php

  • init() is idempotent with an internal flag
  • registers hooks for:
    • Admin menu
    • Dashboard widget
    • HealthScreen styles enqueue
    • Scheduler cron

Main Modules

  • Core: lifecycle, container, orchestration
  • Interfaces: contracts (CheckInterface, StorageInterface, HttpClientInterface, etc.)
  • Services: application logic (scheduler, runner, storage, redaction, http client, alert manager)
  • Checks: concrete health checks
  • Channels: concrete alert channels
  • Admin: menu and wp-admin pages

Dependency Graph (high level)

Tier 0 (foundation):
  StorageInterface, RedactionInterface

Tier 1 (infrastructure):
  HttpClientInterface  ← used by all outbound channels
  CheckInterface[]     ← 5 concrete checks

Tier 2 (orchestration):
  CheckRunnerInterface ← StorageInterface, RedactionInterface, CheckInterface[]
  AlertChannelInterface[] ← StorageInterface, HttpClientInterface
  AlertManagerInterface ← StorageInterface, RedactionInterface, AlertChannelInterface[]

Tier 3 (application):
  Scheduler  ← CheckRunnerInterface, AlertManagerInterface
  Plugin     ← Menu, DashboardWidget, HealthScreen, Scheduler

Direct dependencies:

  • PluginMenu, DashboardWidget, HealthScreen, Scheduler
  • SchedulerCheckRunnerInterface, AlertManagerInterface
  • CheckRunnerStorageInterface, RedactionInterface, CheckInterface[]
  • AlertManagerStorageInterface, RedactionInterface, AlertChannelInterface[]
  • Webhook/Slack/Telegram/WhatsAppStorageInterface, HttpClientInterface
  • EmailStorageInterface (uses wp_mail(), does not need HttpClientInterface)

Clone this wiki locally