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