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