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