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('Cannot lookup dependency "'.$itemName.'" since it is not registered.');
105        }
106
107        switch ($this->store[$itemName]['lookupType']) {
108            case self::TYPE_ALIAS:
109                return $this->createAlias($itemName);
110            case self::TYPE_VALUE:
111                return $this->getValue($itemName);
112            case self::TYPE_INSTANCE:
113                return $this->createNewInstance($itemName);
114            case self::TYPE_SHARED:
115                return $this->createSharedInstance($itemName);
116            case self::TYPE_ARRAY:
117                return $this->createDependenciesFor($itemName);
118        }
119    }
120
121    /**
122     * Create an array of arguments passed to the constructor of $itemName.
123     *
124     * @param string $itemName
125     *
126     * @return array
127     */
128    public function createDependenciesFor($itemName)
129    {
130        $args = [];
131        if (isset($this->store[$itemName]['args'])) {
132            $args = $this->resolveArgs($this->store[$itemName]['args']);
133        }
134
135        return $args;
136    }
137
138    /**
139     * Register a new dependency with $itemName.
140     *
141     * This method returns the current DependencyContainer instance because it
142     * requires the use of the fluid interface to set the specific details for the
143     * dependency.
144     *
145     * @see asNewInstanceOf(), asSharedInstanceOf(), asValue()
146     *
147     * @param string $itemName
148     *
149     * @return $this
150     */
151    public function register($itemName)
152    {
153        $this->store[$itemName] = [];
154        $this->endPoint = &$this->store[$itemName];
155
156        return $this;
157    }
158
159    /**
160     * Specify the previously registered item as a literal value.
161     *
162     * {@link register()} must be called before this will work.
163     *
164     * @param mixed $value
165     *
166     * @return $this
167     */
168    public function asValue($value)
169    {
170        $endPoint = &$this->getEndPoint();
171        $endPoint['lookupType'] = self::TYPE_VALUE;
172        $endPoint['value'] = $value;
173
174        return $this;
175    }
176
177    /**
178     * Specify the previously registered item as an alias of another item.
179     *
180     * @param string $lookup
181     *
182     * @return $this
183     */
184    public function asAliasOf($lookup)
185    {
186        $endPoint = &$this->getEndPoint();
187        $endPoint['lookupType'] = self::TYPE_ALIAS;
188        $endPoint['ref'] = $lookup;
189
190        return $this;
191    }
192
193    /**
194     * Specify the previously registered item as a new instance of $className.
195     *
196     * {@link register()} must be called before this will work.
197     * Any arguments can be set with {@link withDependencies()},
198     * {@link addConstructorValue()} or {@link addConstructorLookup()}.
199     *
200     * @see withDependencies(), addConstructorValue(), addConstructorLookup()
201     *
202     * @param string $className
203     *
204     * @return $this
205     */
206    public function asNewInstanceOf($className)
207    {
208        $endPoint = &$this->getEndPoint();
209        $endPoint['lookupType'] = self::TYPE_INSTANCE;
210        $endPoint['className'] = $className;
211
212        return $this;
213    }
214
215    /**
216     * Specify the previously registered item as a shared instance of $className.
217     *
218     * {@link register()} must be called before this will work.
219     *
220     * @param string $className
221     *
222     * @return $this
223     */
224    public function asSharedInstanceOf($className)
225    {
226        $endPoint = &$this->getEndPoint();
227        $endPoint['lookupType'] = self::TYPE_SHARED;
228        $endPoint['className'] = $className;
229
230        return $this;
231    }
232
233    /**
234     * Specify the previously registered item as array of dependencies.
235     *
236     * {@link register()} must be called before this will work.
237     *
238     * @return $this
239     */
240    public function asArray()
241    {
242        $endPoint = &$this->getEndPoint();
243        $endPoint['lookupType'] = self::TYPE_ARRAY;
244
245        return $this;
246    }
247
248    /**
249     * Specify a list of injected dependencies for the previously registered item.
250     *
251     * This method takes an array of lookup names.
252     *
253     * @see addConstructorValue(), addConstructorLookup()
254     *
255     * @return $this
256     */
257    public function withDependencies(array $lookups)
258    {
259        $endPoint = &$this->getEndPoint();
260        $endPoint['args'] = [];
261        foreach ($lookups as $lookup) {
262            $this->addConstructorLookup($lookup);
263        }
264
265        return $this;
266    }
267
268    /**
269     * Specify a literal (non looked up) value for the constructor of the
270     * previously registered item.
271     *
272     * @see withDependencies(), addConstructorLookup()
273     *
274     * @param mixed $value
275     *
276     * @return $this
277     */
278    public function addConstructorValue($value)
279    {
280        $endPoint = &$this->getEndPoint();
281        if (!isset($endPoint['args'])) {
282            $endPoint['args'] = [];
283        }
284        $endPoint['args'][] = ['type' => 'value', 'item' => $value];
285
286        return $this;
287    }
288
289    /**
290     * Specify a dependency lookup for the constructor of the previously
291     * registered item.
292     *
293     * @see withDependencies(), addConstructorValue()
294     *
295     * @param string $lookup
296     *
297     * @return $this
298     */
299    public function addConstructorLookup($lookup)
300    {
301        $endPoint = &$this->getEndPoint();
302        if (!isset($this->endPoint['args'])) {
303            $endPoint['args'] = [];
304        }
305        $endPoint['args'][] = ['type' => 'lookup', 'item' => $lookup];
306
307        return $this;
308    }
309
310    /** Get the literal value with $itemName */
311    private function getValue($itemName)
312    {
313        return $this->store[$itemName]['value'];
314    }
315
316    /** Resolve an alias to another item */
317    private function createAlias($itemName)
318    {
319        return $this->lookup($this->store[$itemName]['ref']);
320    }
321
322    /** Create a fresh instance of $itemName */
323    private function createNewInstance($itemName)
324    {
325        $reflector = new ReflectionClass($this->store[$itemName]['className']);
326        if ($reflector->getConstructor()) {
327            return $reflector->newInstanceArgs(
328                $this->createDependenciesFor($itemName)
329                );
330        }
331
332        return $reflector->newInstance();
333    }
334
335    /** Create and register a shared instance of $itemName */
336    private function createSharedInstance($itemName)
337    {
338        if (!isset($this->store[$itemName]['instance'])) {
339            $this->store[$itemName]['instance'] = $this->createNewInstance($itemName);
340        }
341
342        return $this->store[$itemName]['instance'];
343    }
344
345    /** Get the current endpoint in the store */
346    private function &getEndPoint()
347    {
348        if (!isset($this->endPoint)) {
349            throw new BadMethodCallException('Component must first be registered by calling register()');
350        }
351
352        return $this->endPoint;
353    }
354
355    /** Get an argument list with dependencies resolved */
356    private function resolveArgs(array $args)
357    {
358        $resolved = [];
359        foreach ($args as $argDefinition) {
360            switch ($argDefinition['type']) {
361                case 'lookup':
362                    $resolved[] = $this->lookupRecursive($argDefinition['item']);
363                    break;
364                case 'value':
365                    $resolved[] = $argDefinition['item'];
366                    break;
367            }
368        }
369
370        return $resolved;
371    }
372
373    /** Resolve a single dependency with an collections */
374    private function lookupRecursive($item)
375    {
376        if (\is_array($item)) {
377            $collection = [];
378            foreach ($item as $k => $v) {
379                $collection[$k] = $this->lookupRecursive($v);
380            }
381
382            return $collection;
383        }
384
385        return $this->lookup($item);
386    }
387}
388