1<?php
2/**
3 * Zend Framework (http://framework.zend.com/)
4 *
5 * @link      http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license   http://framework.zend.com/license/new-bsd New BSD License
8 */
9
10namespace Zend\ServiceManager;
11
12use Exception as BaseException;
13
14/**
15 * ServiceManager implementation for managing plugins
16 *
17 * Automatically registers an initializer which should be used to verify that
18 * a plugin instance is of a valid type. Additionally, allows plugins to accept
19 * an array of options for the constructor, which can be used to configure
20 * the plugin when retrieved. Finally, enables the allowOverride property by
21 * default to allow registering factories, aliases, and invokables to take
22 * the place of those provided by the implementing class.
23 */
24abstract class AbstractPluginManager extends ServiceManager implements ServiceLocatorAwareInterface
25{
26    /**
27     * Allow overriding by default
28     *
29     * @var bool
30     */
31    protected $allowOverride = true;
32
33    /**
34     * Whether or not to auto-add a class as an invokable class if it exists
35     *
36     * @var bool
37     */
38    protected $autoAddInvokableClass = true;
39
40    /**
41     * Options to use when creating an instance
42     *
43     * @var mixed
44     */
45    protected $creationOptions = null;
46
47    /**
48     * The main service locator
49     *
50     * @var ServiceLocatorInterface
51     */
52    protected $serviceLocator;
53
54    /**
55     * Constructor
56     *
57     * Add a default initializer to ensure the plugin is valid after instance
58     * creation.
59     *
60     * @param null|ConfigInterface $configuration
61     */
62    public function __construct(ConfigInterface $configuration = null)
63    {
64        parent::__construct($configuration);
65        $self = $this;
66        $this->addInitializer(function ($instance) use ($self) {
67            if ($instance instanceof ServiceLocatorAwareInterface) {
68                $instance->setServiceLocator($self);
69            }
70        });
71    }
72
73    /**
74     * Validate the plugin
75     *
76     * Checks that the filter loaded is either a valid callback or an instance
77     * of FilterInterface.
78     *
79     * @param  mixed                      $plugin
80     * @return void
81     * @throws Exception\RuntimeException if invalid
82     */
83    abstract public function validatePlugin($plugin);
84
85    /**
86     * Retrieve a service from the manager by name
87     *
88     * Allows passing an array of options to use when creating the instance.
89     * createFromInvokable() will use these and pass them to the instance
90     * constructor if not null and a non-empty array.
91     *
92     * @param  string $name
93     * @param  array  $options
94     * @param  bool   $usePeeringServiceManagers
95     *
96     * @return object
97     *
98     * @throws Exception\ServiceNotFoundException
99     * @throws Exception\ServiceNotCreatedException
100     * @throws Exception\RuntimeException
101     */
102    public function get($name, $options = array(), $usePeeringServiceManagers = true)
103    {
104        $isAutoInvokable = false;
105
106        // Allow specifying a class name directly; registers as an invokable class
107        if (!$this->has($name) && $this->autoAddInvokableClass && class_exists($name)) {
108            $isAutoInvokable = true;
109
110            $this->setInvokableClass($name, $name);
111        }
112
113        $this->creationOptions = $options;
114
115        try {
116            $instance = parent::get($name, $usePeeringServiceManagers);
117        } catch (Exception\ServiceNotFoundException $exception) {
118            $this->tryThrowingServiceLocatorUsageException($name, $isAutoInvokable, $exception);
119        } catch (Exception\ServiceNotCreatedException $exception) {
120            $this->tryThrowingServiceLocatorUsageException($name, $isAutoInvokable, $exception);
121        }
122
123        $this->creationOptions = null;
124
125        try {
126            $this->validatePlugin($instance);
127        } catch (Exception\RuntimeException $exception) {
128            $this->tryThrowingServiceLocatorUsageException($name, $isAutoInvokable, $exception);
129        }
130
131        return $instance;
132    }
133
134    /**
135     * Register a service with the locator.
136     *
137     * Validates that the service object via validatePlugin() prior to
138     * attempting to register it.
139     *
140     * @param  string                                $name
141     * @param  mixed                                 $service
142     * @param  bool                                  $shared
143     * @return AbstractPluginManager
144     * @throws Exception\InvalidServiceNameException
145     */
146    public function setService($name, $service, $shared = true)
147    {
148        if ($service) {
149            $this->validatePlugin($service);
150        }
151        parent::setService($name, $service, $shared);
152
153        return $this;
154    }
155
156    /**
157     * Set the main service locator so factories can have access to it to pull deps
158     *
159     * @param  ServiceLocatorInterface $serviceLocator
160     * @return AbstractPluginManager
161     */
162    public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
163    {
164        $this->serviceLocator = $serviceLocator;
165
166        return $this;
167    }
168
169    /**
170     * Get the main plugin manager. Useful for fetching dependencies from within factories.
171     *
172     * @return ServiceLocatorInterface
173     */
174    public function getServiceLocator()
175    {
176        return $this->serviceLocator;
177    }
178
179    /**
180     * Attempt to create an instance via an invokable class
181     *
182     * Overrides parent implementation by passing $creationOptions to the
183     * constructor, if non-null.
184     *
185     * @param  string                               $canonicalName
186     * @param  string                               $requestedName
187     * @return null|\stdClass
188     * @throws Exception\ServiceNotCreatedException If resolved class does not exist
189     */
190    protected function createFromInvokable($canonicalName, $requestedName)
191    {
192        $invokable = $this->invokableClasses[$canonicalName];
193
194        if (!class_exists($invokable)) {
195            throw new Exception\ServiceNotFoundException(sprintf(
196                '%s: failed retrieving "%s%s" via invokable class "%s"; class does not exist',
197                get_class($this) . '::' . __FUNCTION__,
198                $canonicalName,
199                ($requestedName ? '(alias: ' . $requestedName . ')' : ''),
200                $invokable
201            ));
202        }
203
204        if (null === $this->creationOptions
205            || (is_array($this->creationOptions) && empty($this->creationOptions))
206        ) {
207            $instance = new $invokable();
208        } else {
209            $instance = new $invokable($this->creationOptions);
210        }
211
212        return $instance;
213    }
214
215    /**
216     * Attempt to create an instance via a factory class
217     *
218     * Overrides parent implementation by passing $creationOptions to the
219     * constructor, if non-null.
220     *
221     * @param  string                               $canonicalName
222     * @param  string                               $requestedName
223     * @return mixed
224     * @throws Exception\ServiceNotCreatedException If factory is not callable
225     */
226    protected function createFromFactory($canonicalName, $requestedName)
227    {
228        $factory            = $this->factories[$canonicalName];
229        $hasCreationOptions = !(null === $this->creationOptions || (is_array($this->creationOptions) && empty($this->creationOptions)));
230
231        if (is_string($factory) && class_exists($factory, true)) {
232            if (!$hasCreationOptions) {
233                $factory = new $factory();
234            } else {
235                $factory = new $factory($this->creationOptions);
236            }
237
238            $this->factories[$canonicalName] = $factory;
239        }
240
241        if ($factory instanceof FactoryInterface) {
242            $instance = $this->createServiceViaCallback(array($factory, 'createService'), $canonicalName, $requestedName);
243        } elseif (is_callable($factory)) {
244            $instance = $this->createServiceViaCallback($factory, $canonicalName, $requestedName);
245        } else {
246            throw new Exception\ServiceNotCreatedException(sprintf(
247                'While attempting to create %s%s an invalid factory was registered for this instance type.',
248                $canonicalName,
249                ($requestedName ? '(alias: ' . $requestedName . ')' : '')
250            ));
251        }
252
253        return $instance;
254    }
255
256    /**
257     * Create service via callback
258     *
259     * @param  callable                                   $callable
260     * @param  string                                     $cName
261     * @param  string                                     $rName
262     * @throws Exception\ServiceNotCreatedException
263     * @throws Exception\ServiceNotFoundException
264     * @throws Exception\CircularDependencyFoundException
265     * @return object
266     */
267    protected function createServiceViaCallback($callable, $cName, $rName)
268    {
269        if (is_object($callable)) {
270            $factory = $callable;
271        } elseif (is_array($callable)) {
272            // reset both rewinds and returns the value of the first array element
273            $factory = reset($callable);
274        }
275
276        if (isset($factory)
277            && ($factory instanceof MutableCreationOptionsInterface)
278            && is_array($this->creationOptions)
279            && !empty($this->creationOptions)
280        ) {
281            $factory->setCreationOptions($this->creationOptions);
282        }
283
284        return parent::createServiceViaCallback($callable, $cName, $rName);
285    }
286
287    /**
288     * @param string        $serviceName
289     * @param bool          $isAutoInvokable
290     * @param BaseException $exception
291     *
292     * @throws BaseException
293     * @throws Exception\ServiceLocatorUsageException
294     */
295    private function tryThrowingServiceLocatorUsageException(
296        $serviceName,
297        $isAutoInvokable,
298        BaseException $exception
299    ) {
300        if ($isAutoInvokable) {
301            $this->unregisterService($this->canonicalizeName($serviceName));
302        }
303
304        $serviceLocator = $this->getServiceLocator();
305
306        if ($serviceLocator && $serviceLocator->has($serviceName)) {
307            throw Exception\ServiceLocatorUsageException::fromInvalidPluginManagerRequestedServiceName(
308                $this,
309                $serviceLocator,
310                $serviceName,
311                $exception
312            );
313        }
314
315        throw $exception;
316    }
317}
318