Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
77 / 77
100.00% covered (success)
100.00%
24 / 24
CRAP
100.00% covered (success)
100.00%
1 / 1
Locator
100.00% covered (success)
100.00%
77 / 77
100.00% covered (success)
100.00%
24 / 24
48
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 setServices
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 set
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 get
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 getCallable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasParameter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getParameters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCallable
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setParameters
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 addParameter
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 removeParameters
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 isAvailable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 has
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isLoaded
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 reload
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 remove
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 __set
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __isset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __unset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetSet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetGet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetExists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetUnset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Service;
16
17use Pop\AbstractManager;
18use Pop\Utils\CallableObject;
19use Psr\Container\ContainerInterface;
20
21/**
22 * Service locator class
23 *
24 * @category   Pop
25 * @package    Pop\Service
26 * @author     Nick Sagona, III <nick@popphp.org>
27 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
28 * @license    https://www.popphp.org/license     New BSD License
29 * @version    5.0.0
30 */
31class Locator extends AbstractManager implements ContainerInterface
32{
33
34    /**
35     * Recursion depth level tracker
36     * @var int
37     */
38    private static int $depth = 0;
39
40    /**
41     * Recursion called service name tracker
42     * @var array
43     */
44    private static array $called = [];
45
46    /**
47     * Services that are loaded/instantiated
48     * @var array
49     */
50    protected array $loaded = [];
51
52    /**
53     * Constructor
54     *
55     * Instantiate the service locator object.
56     *
57     * @param  ?array $services
58     * @param  bool   $default
59     * @throws Exception
60     */
61    public function __construct(?array $services = null, bool $default = true)
62    {
63        if ($services !== null) {
64            $this->setServices($services);
65        }
66
67        if (($default) && !(Container::has('default'))) {
68            Container::set('default', $this);
69        }
70    }
71
72    /**
73     * Set service objects from an array of services
74     *
75     * @param  array $services
76     * @throws Exception
77     * @return static
78     */
79    public function setServices(array $services): static
80    {
81        foreach ($services as $name => $service) {
82            $this->set($name, $service);
83        }
84
85        return $this;
86    }
87
88    /**
89     * Set a service. It will overwrite any previous service with the same name.
90     *
91     * A service can be a CallableObject, callable string, or an array that
92     * contains a 'call' key and an optional 'params' key.
93     * Valid callable strings are:
94     *
95     *     'someFunction'
96     *     'SomeClass'
97     *     'SomeClass->foo'
98     *     'SomeClass::bar'
99     *
100     * @param  string $name
101     * @param  mixed  $service
102     * @throws Exception
103     * @return static
104     */
105    public function set(string $name, mixed $service): static
106    {
107        if (!($service instanceof CallableObject)) {
108            $call   = null;
109            $params = null;
110
111            if (!is_array($service)) {
112                $call = $service;
113            } else if (isset($service['call'])) {
114                $call   = $service['call'];
115                $params = $service['params'] ?? null;
116            }
117
118            if ($call === null) {
119                throw new Exception('Error: A callable service was not passed');
120            }
121
122            $this->items[$name] = new CallableObject($call, $params);
123        } else {
124            $this->items[$name] = $service;
125        }
126
127        return $this;
128    }
129
130    /**
131     * Get/load a service
132     *
133     * @param  string $name
134     * @throws NotFoundException|Exception
135     * @return mixed
136     */
137    public function get(string $name): mixed
138    {
139        if (!isset($this->items[$name])) {
140            throw new NotFoundException("Error: The service '" . $name . "' has not been added to the service locator");
141        }
142        if (!isset($this->loaded[$name])) {
143            if (self::$depth > 40) {
144                throw new Exception(
145                    'Error: Possible recursion loop detected when attempting to load these services: ' .
146                    implode(', ', self::$called)
147                );
148            }
149
150            // Keep track of the called services
151            self::$depth++;
152            self::$called[] = $name;
153
154            try {
155                $this->loaded[$name] = $this->items[$name]->call();
156            } finally {
157                array_pop(self::$called);
158                self::$depth--;
159            }
160        }
161
162        return $this->loaded[$name];
163    }
164
165    /**
166     * Get a service's callable string or object
167     *
168     * @param  string $name
169     * @return mixed
170     */
171    public function getCallable(string $name): mixed
172    {
173        return $this->items[$name]?->getCallable();
174    }
175
176    /**
177     * Check if  a service has parameters
178     *
179     * @param  string $name
180     * @return bool
181     */
182    public function hasParameter(string $name): bool
183    {
184        return (isset($this->items[$name]) && $this->items[$name]->hasParameters());
185    }
186
187    /**
188     * Get a service's parameters
189     *
190     * @param  string $name
191     * @return mixed
192     */
193    public function getParameters(string $name): mixed
194    {
195        return $this->items[$name]?->getParameters();
196    }
197
198    /**
199     * Set a service's callable string or object
200     *
201     * @param  string $name
202     * @param  mixed  $call
203     * @return static
204     */
205    public function setCallable(string $name, mixed $call): static
206    {
207        if (isset($this->items[$name])) {
208            $this->items[$name]->setCallable($call);
209        }
210        return $this;
211    }
212
213    /**
214     * Set a service's parameters
215     *
216     * @param  string $name
217     * @param  mixed  $params
218     * @return static
219     */
220    public function setParameters(string $name, mixed $params): static
221    {
222        if (isset($this->items[$name])) {
223            if (is_array($params)) {
224                $this->items[$name]->setParameters($params);
225            } else {
226                $this->items[$name]->setParameters([$params]);
227            }
228        }
229        return $this;
230    }
231
232    /**
233     * Add to a service's parameters
234     *
235     * @param  string $name
236     * @param  mixed  $param
237     * @param  mixed  $key
238     * @return static
239     */
240    public function addParameter(string $name, mixed $param, mixed $key = null): static
241    {
242        if (isset($this->items[$name])) {
243            if ($key !== null) {
244                $this->items[$name]->addNamedParameter((string)$key, $param);
245            } else {
246                $this->items[$name]->addParameter($param);
247            }
248        }
249
250        return $this;
251    }
252
253    /**
254     * Remove a service's parameters
255     *
256     * @param  string $name
257     * @param  mixed  $param
258     * @param  mixed  $key
259     * @return static
260     */
261    public function removeParameters(string $name, mixed $param, mixed $key = null): static
262    {
263        if ($this->hasParameter($name)) {
264            if ($key !== null) {
265                $this->items[$name]->removeParameter((string)$key);
266            } else {
267                foreach ($this->items[$name]->getParameters() as $key => $value) {
268                    if ($value == $param) {
269                        $this->items[$name]->removeParameter((string)$key);
270                        break;
271                    }
272                }
273            }
274        }
275
276        return $this;
277    }
278
279    /**
280     * Determine of a service object is available (but not loaded)
281     *
282     * @param  string $name
283     * @return bool
284     */
285    public function isAvailable(string $name): bool
286    {
287        return isset($this->items[$name]);
288    }
289
290    /**
291     * Determine if a service is available (PSR-11 ContainerInterface)
292     *
293     * @param  string $id
294     * @return bool
295     */
296    public function has(string $id): bool
297    {
298        return $this->isAvailable($id);
299    }
300
301    /**
302     * Determine of a service object is loaded
303     *
304     * @param  string $name
305     * @return bool
306     */
307    public function isLoaded(string $name): bool
308    {
309        return isset($this->loaded[$name]);
310    }
311
312    /**
313     * Re-load a service object
314     *
315     * @param  string $name
316     * @return mixed
317     */
318    public function reload(string $name): mixed
319    {
320        if (isset($this->loaded[$name])) {
321            unset($this->loaded[$name]);
322        }
323
324        return $this->get($name);
325    }
326
327    /**
328     * Remove a service
329     *
330     * @param  string $name
331     * @return static
332     */
333    public function remove(string $name): static
334    {
335        if (isset($this->items[$name])) {
336            unset($this->items[$name]);
337        }
338        if (isset($this->loaded[$name])) {
339            unset($this->loaded[$name]);
340        }
341        return $this;
342    }
343
344    /**
345     * Set a service
346     *
347     * @param  string $name
348     * @param  mixed $value
349     * @throws Exception
350     * @return void
351     */
352    public function __set(string $name, mixed $value): void
353    {
354        $this->set($name, $value);
355    }
356
357    /**
358     * Get a service
359     *
360     * @param  string $name
361     * @throws Exception
362     * @return mixed
363     */
364    public function __get(string $name): mixed
365    {
366        return $this->get($name);
367    }
368
369    /**
370     * Determine if a service is available
371     *
372     * @param  string $name
373     * @return bool
374     */
375    public function __isset(string $name): bool
376    {
377        return isset($this->items[$name]);
378    }
379
380    /**
381     * Unset a service
382     *
383     * @param  string $name
384     * @return void
385     */
386    public function __unset(string $name): void
387    {
388        $this->remove($name);
389    }
390
391    /**
392     * Set a service
393     *
394     * @param  mixed $offset
395     * @param  mixed $value
396     * @throws Exception
397     * @return void
398     */
399    public function offsetSet(mixed $offset, mixed $value): void
400    {
401        $this->set($offset, $value);
402    }
403
404    /**
405     * Get a service
406     *
407     * @param  mixed $offset
408     * @throws Exception
409     * @return mixed
410     */
411    public function offsetGet(mixed $offset): mixed
412    {
413        return $this->get($offset);
414    }
415
416    /**
417     * Determine if a service is available
418     *
419     * @param  mixed $offset
420     * @return bool
421     */
422    public function offsetExists(mixed$offset): bool
423    {
424        return isset($this->items[$offset]);
425    }
426
427    /**
428     * Unset a service
429     *
430     * @param  mixed $offset
431     * @return void
432     */
433    public function offsetUnset(mixed $offset): void
434    {
435        $this->remove($offset);
436    }
437
438}