1<?php
2
3/*
4 * This file is part of SwiftMailer.
5 * (c) 2004-2009 Chris Corbyn
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11/**
12 * Dependency Injection container.
13 *
14 * @author  Chris Corbyn
15 */
16class Swift_DependencyContainer
17{
18    /** Constant for literal value types */
19    const TYPE_VALUE = 0x00001;
20
21    /** Constant for new instance types */
22    const TYPE_INSTANCE = 0x00010;
23
24    /** Constant for shared instance types */
25    const TYPE_SHARED = 0x00100;
26
27    /** Constant for aliases */
28    const TYPE_ALIAS = 0x01000;
29
30    /** Constant for arrays */
31    const TYPE_ARRAY = 0x10000;
32
33    /** Singleton instance */
34    private static $instance = null;
35
36    /** The data container */
37    private $store = [];
38
39    /** The current endpoint in the data container */
40    private $endPoint;
41
42    /**
43     * Constructor should not be used.
44     *
45     * Use {@link getInstance()} instead.
46     */
47    public function __construct()
48    {
49    }
50
51    /**
52     * Returns a singleton of the DependencyContainer.
53     *
54     * @return self
55     */
56    public static function getInstance()
57    {
58        if (!isset(self::$instance)) {
59            self::$instance = new self();
60        }
61
62        return self::$instance;
63    }
64
65    /**
66     * List the names of all items stored in the Container.
67     *
68     * @return array
69     */
70    public function listItems()
71    {
72        return array_keys($this->store);
73    }
74
75    /**
76     * Test if an item is registered in this container with the given name.
77     *
78     * @see register()
79     *
80     * @param string $itemName
81     *
82     * @return bool
83     */
84    public function has($itemName)
85    {
86        return array_key_exists($itemName, $this->store)
87            && isset($this->store[$itemName]['lookupType']);
88    }
89
90    /**
91     * Lookup the item with the given $itemName.
92     *
93     * @see register()
94     *
95     * @param string $itemName
96     *
97     * @return mixed
98     *
99     * @throws Swift_DependencyException If the dependency is not found
100     */
101    public function lookup($itemName)
102    {
103        if (!$this->has($itemName)) {
104            throw new Swift_DependencyException(
105                'Cannot lookup dependency "'.$itemName.'" since it is not registered.'
106                );
107        }
108
109        switch ($this->store[$itemName]['lookupType']) {
110            case self::TYPE_ALIAS:
111                return $this->createAlias($itemName);
112            case self::TYPE_VALUE:
113                return $this->getValue($itemName);
114            case self::TYPE_INSTANCE:
115                return $this->createNewInstance($itemName);
116            case self::TYPE_SHARED:
117                return $this->createSharedInstance($itemName);
118            case self::TYPE_ARRAY:
119                return $this->createDependenciesFor($itemName);
120        }
121    }
122
123    /**
124     * Create an array of arguments passed to the constructor of $itemName.
125     *
126     * @param string $itemName
127     *
128     * @return array
129     */
130    public function createDependenciesFor($itemName)
131    {
132        $args = [];
133        if (isset($this->store[$itemName]['args'])) {
134            $args = $this->resolveArgs($this->store[$itemName]['args']);
135        }
136
137        return $args;
138    }
139
140    /**
141     * Register a new dependency with $itemName.
142     *
143     * This method returns the current DependencyContainer instance because it
144     * requires the use of the fluid interface to set the specific details for the
145     * dependency.
146     *
147     * @see asNewInstanceOf(), asSharedInstanceOf(), asValue()
148     *
149     * @param string $itemName
150     *
151     * @return $this
152     */
153    public function register($itemName)
154    {
155        $this->store[$itemName] = [];
156        $this->endPoint = &$this->store[$itemName];
157
158        return $this;
159    }
160
161    /**
162     * Specify the previously registered item as a literal value.
163     *
164     * {@link register()} must be called before this will work.
165     *
166     * @param mixed $value
167     *
168     * @return $this
169     */
170    public function asValue($value)
171    {
172        $endPoint = &$this->getEndPoint();
173        $endPoint['lookupType'] = self::TYPE_VALUE;
174        $endPoint['value'] = $value;
175
176        return $this;
177    }
178
179    /**
180     * Specify the previously registered item as an alias of another item.
181     *
182     * @param string $lookup
183     *
184     * @return $this
185     */
186    public function asAliasOf($lookup)
187    {
188        $endPoint = &$this->getEndPoint();
189        $endPoint['lookupType'] = self::TYPE_ALIAS;
190        $endPoint['ref'] = $lookup;
191
192        return $this;
193    }
194
195    /**
196     * Specify the previously registered item as a new instance of $className.
197     *
198     * {@link register()} must be called before this will work.
199     * Any arguments can be set with {@link withDependencies()},
200     * {@link addConstructorValue()} or {@link addConstructorLookup()}.
201     *
202     * @see withDependencies(), addConstructorValue(), addConstructorLookup()
203     *
204     * @param string $className
205     *
206     * @return $this
207     */
208    public function asNewInstanceOf($className)
209    {
210        $endPoint = &$this->getEndPoint();
211        $endPoint['lookupType'] = self::TYPE_INSTANCE;
212        $endPoint['className'] = $className;
213
214        return $this;
215    }
216
217    /**
218     * Specify the previously registered item as a shared instance of $className.
219     *
220     * {@link register()} must be called before this will work.
221     *
222     * @param string $className
223     *
224     * @return $this
225     */
226    public function asSharedInstanceOf($className)
227    {
228        $endPoint = &$this->getEndPoint();
229        $endPoint['lookupType'] = self::TYPE_SHARED;
230        $endPoint['className'] = $className;
231
232        return $this;
233    }
234
235    /**
236     * Specify the previously registered item as array of dependencies.
237     *
238     * {@link register()} must be called before this will work.
239     *
240     * @return $this
241     */
242    public function asArray()
243    {
244        $endPoint = &$this->getEndPoint();
245        $endPoint['lookupType'] = self::TYPE_ARRAY;
246
247        return $this;
248    }
249
250    /**
251     * Specify a list of injected dependencies for the previously registered item.
252     *
253     * This method takes an array of lookup names.
254     *
255     * @see addConstructorValue(), addConstructorLookup()
256     *
257     * @return $this
258     */
259    public function withDependencies(array $lookups)
260    {
261        $endPoint = &$this->getEndPoint();
262        $endPoint['args'] = [];
263        foreach ($lookups as $lookup) {
264            $this->addConstructorLookup($lookup);
265        }
266
267        return $this;
268    }
269
270    /**
271     * Specify a literal (non looked up) value for the constructor of the
272     * previously registered item.
273     *
274     * @see withDependencies(), addConstructorLookup()
275     *
276     * @param mixed $value
277     *
278     * @return $this
279     */
280    public function addConstructorValue($value)
281    {
282        $endPoint = &$this->getEndPoint();
283        if (!isset($endPoint['args'])) {
284            $endPoint['args'] = [];
285        }
286        $endPoint['args'][] = ['type' => 'value', 'item' => $value];
287
288        return $this;
289    }
290
291    /**
292     * Specify a dependency lookup for the constructor of the previously
293     * registered item.
294     *
295     * @see withDependencies(), addConstructorValue()
296     *
297     * @param string $lookup
298     *
299     * @return $this
300     */
301    public function addConstructorLookup($lookup)
302    {
303        $endPoint = &$this->getEndPoint();
304        if (!isset($this->endPoint['args'])) {
305            $endPoint['args'] = [];
306        }
307        $endPoint['args'][] = ['type' => 'lookup', 'item' => $lookup];
308
309        return $this;
310    }
311
312    /** Get the literal value with $itemName */
313    private function getValue($itemName)
314    {
315        return $this->store[$itemName]['value'];
316    }
317
318    /** Resolve an alias to another item */
319    private function createAlias($itemName)
320    {
321        return $this->lookup($this->store[$itemName]['ref']);
322    }
323
324    /** Create a fresh instance of $itemName */
325    private function createNewInstance($itemName)
326    {
327        $reflector = new ReflectionClass($this->store[$itemName]['className']);
328        if ($reflector->getConstructor()) {
329            return $reflector->newInstanceArgs(
330                $this->createDependenciesFor($itemName)
331                );
332        }
333
334        return $reflector->newInstance();
335    }
336
337    /** Create and register a shared instance of $itemName */
338    private function createSharedInstance($itemName)
339    {
340        if (!isset($this->store[$itemName]['instance'])) {
341            $this->store[$itemName]['instance'] = $this->createNewInstance($itemName);
342        }
343
344        return $this->store[$itemName]['instance'];
345    }
346
347    /** Get the current endpoint in the store */
348    private function &getEndPoint()
349    {
350        if (!isset($this->endPoint)) {
351            throw new BadMethodCallException(
352                'Component must first be registered by calling register()'
353                );
354        }
355
356        return $this->endPoint;
357    }
358
359    /** Get an argument list with dependencies resolved */
360    private function resolveArgs(array $args)
361    {
362        $resolved = [];
363        foreach ($args as $argDefinition) {
364            switch ($argDefinition['type']) {
365                case 'lookup':
366                    $resolved[] = $this->lookupRecursive($argDefinition['item']);
367                    break;
368                case 'value':
369                    $resolved[] = $argDefinition['item'];
370                    break;
371            }
372        }
373
374        return $resolved;
375    }
376
377    /** Resolve a single dependency with an collections */
378    private function lookupRecursive($item)
379    {
380        if (is_array($item)) {
381            $collection = [];
382            foreach ($item as $k => $v) {
383                $collection[$k] = $this->lookupRecursive($v);
384            }
385
386            return $collection;
387        }
388
389        return $this->lookup($item);
390    }
391}
392