Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ServiceContainer
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 4
182
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getService
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 applyWiring
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 register
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * ServiceContainer.php
4 *
5 * This class provides a simple implementation of a Dependency Injection (DI) container.
6 * It allows services to be registered and lazily resolved. Services are registered with
7 * a name and a resolver function, which is executed to instantiate the service when
8 * needed. The container stores these services and resolves them when requested.
9 *
10 * This container does not throw exceptions if a service is not found; instead, it
11 * returns `null` if a service cannot be resolved.
12 *
13 * @category Infrastructure
14 * @package  Codex\Infrastructure
15 * @since    0.1.0
16 * @author   Doğu Abaris <abaris@null.net>
17 * @license  https://www.gnu.org/copyleft/gpl.html GPL-2.0-or-later
18 * @link     https://doc.wikimedia.org/codex/main/ Codex Documentation
19 */
20
21namespace Wikimedia\Codex\Infrastructure;
22
23use InvalidArgumentException;
24use Psr\Log\LoggerInterface;
25use Psr\Log\NullLogger;
26
27/**
28 * ServiceContainer
29 *
30 * The `ServiceContainer` class is responsible for managing Dependency Injection (DI) by registering and
31 * resolving services. It allows services to be lazily instantiated only when needed, using callable resolvers
32 * or method references. Once a service is resolved, it is stored in memory to avoid redundant instantiation.
33 *
34 * Key Responsibilities:
35 * - Register services using a name and a resolver (callable or method reference).
36 * - Resolve services on demand.
37 * - Provide methods for monitoring the service resolution process.
38 * - Handle and log failed service lookups to avoid redundant error logging.
39 *
40 * Usage:
41 * - Services are registered via the `register()` method.
42 * - Services can be retrieved via the `getService()` method.
43 * - Handle non-existent services gracefully.
44 *
45 * @category Infrastructure
46 * @package  Codex\Infrastructure
47 * @since    0.1.0
48 * @author   Doğu Abaris <abaris@null.net>
49 * @license  https://www.gnu.org/copyleft/gpl.html GPL-2.0-or-later
50 * @link     https://doc.wikimedia.org/codex/main/ Codex Documentation
51 */
52class ServiceContainer {
53
54    /**
55     * Array of registered services.
56     *
57     * This array stores the resolver functions for each registered service.
58     */
59    private array $services = [];
60
61    /**
62     * Cache for failed service lookups to avoid repeated error logging.
63     */
64    private array $failedServices = [];
65
66    /**
67     * Logger instance for logging errors and other messages.
68     */
69    private LoggerInterface $logger;
70
71    /**
72     * Constructor for the ServiceContainer.
73     *
74     * This constructor initializes the service container with an optional logger instance.
75     * If no logger is provided, a `NullLogger` will be used by default, which performs no operations.
76     *
77     * @since 0.1.0
78     * @param LoggerInterface|null $logger An optional logger instance. If null, a `NullLogger` will be used.
79     */
80    public function __construct( ?LoggerInterface $logger = null ) {
81        $this->logger = $logger ?: new NullLogger();
82    }
83
84    /**
85     * Resolve a service from the container.
86     *
87     * @since 0.1.0
88     * @param string $name The name of the service to resolve.
89     * @return mixed|null Returns the service instance, or `null` if the service is not found.
90     */
91    public function getService( string $name ) {
92        // Resolve the service if registered
93        if ( isset( $this->services[$name] ) ) {
94            $resolver = $this->services[$name];
95
96            if ( is_callable( $resolver ) ) {
97                return $resolver( $this );
98            } elseif ( is_array( $resolver ) && method_exists( $resolver[0], $resolver[1] ) ) {
99                return call_user_func( $resolver, $this );
100            }
101        }
102
103        // Handle non-existent service
104        if ( !isset( $this->failedServices[$name] ) ) {
105            $this->failedServices[$name] = true;
106            $this->logger->error( "Service '$name' is not registered in the container." );
107        }
108
109        return null;
110    }
111
112    /**
113     * Apply a wiring configuration to register multiple services.
114     *
115     * @since 0.1.0
116     * @param array $wiring The service wiring configuration.
117     * @return void
118     */
119    public function applyWiring( array $wiring ): void {
120        foreach ( $wiring as $name => $resolver ) {
121            $this->register( $name, $resolver );
122        }
123    }
124
125    /**
126     * Register a service in the container.
127     *
128     * @since 0.1.0
129     * @param string $name The unique name of the service.
130     * @param mixed $resolver The function, method reference, or object to return the service instance.
131     * @return void
132     */
133    public function register( string $name, $resolver ): void {
134        if ( !is_callable( $resolver ) && !is_array( $resolver ) ) {
135            throw new InvalidArgumentException( 'Service resolver must be a callable or an array.' );
136        }
137
138        $this->services[$name] = $resolver;
139    }
140}