AbstractPluginManager.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  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. namespace Zend\ServiceManager;
  10. use Interop\Container\ContainerInterface;
  11. use Exception as BaseException;
  12. use ReflectionMethod;
  13. /**
  14. * ServiceManager implementation for managing plugins
  15. *
  16. * Automatically registers an initializer which should be used to verify that
  17. * a plugin instance is of a valid type. Additionally, allows plugins to accept
  18. * an array of options for the constructor, which can be used to configure
  19. * the plugin when retrieved. Finally, enables the allowOverride property by
  20. * default to allow registering factories, aliases, and invokables to take
  21. * the place of those provided by the implementing class.
  22. */
  23. abstract class AbstractPluginManager extends ServiceManager implements ServiceLocatorAwareInterface
  24. {
  25. /**
  26. * Allow overriding by default
  27. *
  28. * @var bool
  29. */
  30. protected $allowOverride = true;
  31. /**
  32. * Whether or not to auto-add a class as an invokable class if it exists
  33. *
  34. * @var bool
  35. */
  36. protected $autoAddInvokableClass = true;
  37. /**
  38. * Options to use when creating an instance
  39. *
  40. * @var mixed
  41. */
  42. protected $creationOptions = null;
  43. /**
  44. * The main service locator
  45. *
  46. * @var ServiceLocatorInterface
  47. */
  48. protected $serviceLocator;
  49. /**
  50. * Constructor
  51. *
  52. * Add a default initializer to ensure the plugin is valid after instance
  53. * creation.
  54. *
  55. * Additionally, the constructor provides forwards compatibility with v3 by
  56. * overloading the initial argument. v2 usage expects either null or a
  57. * ConfigInterface instance, and will ignore any other arguments. v3 expects
  58. * a ContainerInterface instance, and will use an array of configuration to
  59. * seed the current instance with services. In most cases, you can ignore the
  60. * constructor unless you are writing a specialized factory for your plugin
  61. * manager or overriding it.
  62. *
  63. * @param null|ConfigInterface|ContainerInterface $configOrContainerInstance
  64. * @param array $v3config If $configOrContainerInstance is a container, this
  65. * value will be passed to the parent constructor.
  66. * @throws Exception\InvalidArgumentException if $configOrContainerInstance
  67. * is neither null, nor a ConfigInterface, nor a ContainerInterface.
  68. */
  69. public function __construct($configOrContainerInstance = null, array $v3config = [])
  70. {
  71. if (null !== $configOrContainerInstance
  72. && ! $configOrContainerInstance instanceof ConfigInterface
  73. && ! $configOrContainerInstance instanceof ContainerInterface
  74. ) {
  75. throw new Exception\InvalidArgumentException(sprintf(
  76. '%s expects a ConfigInterface instance or ContainerInterface instance; received %s',
  77. get_class($this),
  78. (is_object($configOrContainerInstance)
  79. ? get_class($configOrContainerInstance)
  80. : gettype($configOrContainerInstance)
  81. )
  82. ));
  83. }
  84. if ($configOrContainerInstance instanceof ContainerInterface) {
  85. if (property_exists($this, 'serviceLocator')) {
  86. if (! empty($v3config)) {
  87. parent::__construct(new Config($v3config));
  88. }
  89. $this->serviceLocator = $configOrContainerInstance;
  90. }
  91. if (property_exists($this, 'creationContext')) {
  92. if (! empty($v3config)) {
  93. parent::__construct($v3config);
  94. }
  95. $this->creationContext = $configOrContainerInstance;
  96. }
  97. }
  98. if ($configOrContainerInstance instanceof ConfigInterface) {
  99. parent::__construct($configOrContainerInstance);
  100. }
  101. $this->addInitializer(function ($instance) {
  102. if ($instance instanceof ServiceLocatorAwareInterface) {
  103. $instance->setServiceLocator($this);
  104. }
  105. });
  106. }
  107. /**
  108. * Validate the plugin
  109. *
  110. * Checks that the filter loaded is either a valid callback or an instance
  111. * of FilterInterface.
  112. *
  113. * @param mixed $plugin
  114. * @return void
  115. * @throws Exception\RuntimeException if invalid
  116. */
  117. abstract public function validatePlugin($plugin);
  118. /**
  119. * Retrieve a service from the manager by name
  120. *
  121. * Allows passing an array of options to use when creating the instance.
  122. * createFromInvokable() will use these and pass them to the instance
  123. * constructor if not null and a non-empty array.
  124. *
  125. * @param string $name
  126. * @param array $options
  127. * @param bool $usePeeringServiceManagers
  128. *
  129. * @return object
  130. *
  131. * @throws Exception\ServiceNotFoundException
  132. * @throws Exception\ServiceNotCreatedException
  133. * @throws Exception\RuntimeException
  134. */
  135. public function get($name, $options = [], $usePeeringServiceManagers = true)
  136. {
  137. $isAutoInvokable = false;
  138. $cName = null;
  139. $sharedInstance = null;
  140. // Allow specifying a class name directly; registers as an invokable class
  141. if (!$this->has($name) && $this->autoAddInvokableClass && class_exists($name)) {
  142. $isAutoInvokable = true;
  143. $this->setInvokableClass($name, $name);
  144. }
  145. $this->creationOptions = $options;
  146. // If creation options were provided, we want to force creation of a
  147. // new instance.
  148. if (! empty($this->creationOptions)) {
  149. $cName = isset($this->canonicalNames[$name])
  150. ? $this->canonicalNames[$name]
  151. : $this->canonicalizeName($name);
  152. if (isset($this->instances[$cName])) {
  153. $sharedInstance = $this->instances[$cName];
  154. unset($this->instances[$cName]);
  155. }
  156. }
  157. try {
  158. $instance = parent::get($name, $usePeeringServiceManagers);
  159. } catch (Exception\ServiceNotFoundException $exception) {
  160. if ($sharedInstance) {
  161. $this->instances[$cName] = $sharedInstance;
  162. }
  163. $this->creationOptions = null;
  164. $this->tryThrowingServiceLocatorUsageException($name, $isAutoInvokable, $exception);
  165. } catch (Exception\ServiceNotCreatedException $exception) {
  166. if ($sharedInstance) {
  167. $this->instances[$cName] = $sharedInstance;
  168. }
  169. $this->creationOptions = null;
  170. $this->tryThrowingServiceLocatorUsageException($name, $isAutoInvokable, $exception);
  171. }
  172. $this->creationOptions = null;
  173. // If we had a previously shared instance, restore it.
  174. if ($sharedInstance) {
  175. $this->instances[$cName] = $sharedInstance;
  176. }
  177. try {
  178. $this->validatePlugin($instance);
  179. } catch (Exception\RuntimeException $exception) {
  180. $this->tryThrowingServiceLocatorUsageException($name, $isAutoInvokable, $exception);
  181. }
  182. // If we created a new instance using creation options, and it was
  183. // marked to share, we remove the shared instance
  184. // (options === cannot share)
  185. if ($cName
  186. && isset($this->instances[$cName])
  187. && $this->instances[$cName] === $instance
  188. ) {
  189. unset($this->instances[$cName]);
  190. }
  191. return $instance;
  192. }
  193. /**
  194. * Register a service with the locator.
  195. *
  196. * Validates that the service object via validatePlugin() prior to
  197. * attempting to register it.
  198. *
  199. * @param string $name
  200. * @param mixed $service
  201. * @param bool $shared
  202. * @return AbstractPluginManager
  203. * @throws Exception\InvalidServiceNameException
  204. */
  205. public function setService($name, $service, $shared = true)
  206. {
  207. if ($service) {
  208. $this->validatePlugin($service);
  209. }
  210. parent::setService($name, $service, $shared);
  211. return $this;
  212. }
  213. /**
  214. * Set the main service locator so factories can have access to it to pull deps
  215. *
  216. * @param ServiceLocatorInterface $serviceLocator
  217. * @return AbstractPluginManager
  218. */
  219. public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
  220. {
  221. $this->serviceLocator = $serviceLocator;
  222. return $this;
  223. }
  224. /**
  225. * Get the main plugin manager. Useful for fetching dependencies from within factories.
  226. *
  227. * @return ServiceLocatorInterface
  228. */
  229. public function getServiceLocator()
  230. {
  231. return $this->serviceLocator;
  232. }
  233. /**
  234. * Attempt to create an instance via an invokable class
  235. *
  236. * Overrides parent implementation by passing $creationOptions to the
  237. * constructor, if non-null.
  238. *
  239. * @param string $canonicalName
  240. * @param string $requestedName
  241. * @return null|\stdClass
  242. * @throws Exception\ServiceNotCreatedException If resolved class does not exist
  243. */
  244. protected function createFromInvokable($canonicalName, $requestedName)
  245. {
  246. $invokable = $this->invokableClasses[$canonicalName];
  247. if (!class_exists($invokable)) {
  248. throw new Exception\ServiceNotFoundException(sprintf(
  249. '%s: failed retrieving "%s%s" via invokable class "%s"; class does not exist',
  250. get_class($this) . '::' . __FUNCTION__,
  251. $canonicalName,
  252. ($requestedName ? '(alias: ' . $requestedName . ')' : ''),
  253. $invokable
  254. ));
  255. }
  256. if (null === $this->creationOptions
  257. || (is_array($this->creationOptions) && empty($this->creationOptions))
  258. ) {
  259. $instance = new $invokable();
  260. } else {
  261. $instance = new $invokable($this->creationOptions);
  262. }
  263. return $instance;
  264. }
  265. /**
  266. * Attempt to create an instance via a factory class
  267. *
  268. * Overrides parent implementation by passing $creationOptions to the
  269. * constructor, if non-null.
  270. *
  271. * @param string $canonicalName
  272. * @param string $requestedName
  273. * @return mixed
  274. * @throws Exception\ServiceNotCreatedException If factory is not callable
  275. */
  276. protected function createFromFactory($canonicalName, $requestedName)
  277. {
  278. $factory = $this->factories[$canonicalName];
  279. $hasCreationOptions = !(null === $this->creationOptions || (is_array($this->creationOptions) && empty($this->creationOptions)));
  280. if (is_string($factory) && class_exists($factory, true)) {
  281. if (!$hasCreationOptions) {
  282. $factory = new $factory();
  283. } else {
  284. $factory = new $factory($this->creationOptions);
  285. }
  286. $this->factories[$canonicalName] = $factory;
  287. }
  288. if ($factory instanceof FactoryInterface) {
  289. $instance = $this->createServiceViaCallback([$factory, 'createService'], $canonicalName, $requestedName);
  290. } elseif (is_callable($factory)) {
  291. $instance = $this->createServiceViaCallback($factory, $canonicalName, $requestedName);
  292. } else {
  293. throw new Exception\ServiceNotCreatedException(sprintf(
  294. 'While attempting to create %s%s an invalid factory was registered for this instance type.',
  295. $canonicalName,
  296. ($requestedName ? '(alias: ' . $requestedName . ')' : '')
  297. ));
  298. }
  299. return $instance;
  300. }
  301. /**
  302. * Create service via callback
  303. *
  304. * @param callable $callable
  305. * @param string $cName
  306. * @param string $rName
  307. * @throws Exception\ServiceNotCreatedException
  308. * @throws Exception\ServiceNotFoundException
  309. * @throws Exception\CircularDependencyFoundException
  310. * @return object
  311. */
  312. protected function createServiceViaCallback($callable, $cName, $rName)
  313. {
  314. if (is_object($callable)) {
  315. $factory = $callable;
  316. } elseif (is_array($callable)) {
  317. // reset both rewinds and returns the value of the first array element
  318. $factory = reset($callable);
  319. } else {
  320. $factory = null;
  321. }
  322. if ($factory instanceof Factory\InvokableFactory) {
  323. // InvokableFactory::setCreationOptions has a different signature than
  324. // MutableCreationOptionsInterface; allows null value.
  325. $options = is_array($this->creationOptions) && ! empty($this->creationOptions)
  326. ? $this->creationOptions
  327. : null;
  328. $factory->setCreationOptions($options);
  329. } elseif ($factory instanceof MutableCreationOptionsInterface) {
  330. // MutableCreationOptionsInterface expects an array, always; pass an
  331. // empty array for lack of creation options.
  332. $options = is_array($this->creationOptions) && ! empty($this->creationOptions)
  333. ? $this->creationOptions
  334. : [];
  335. $factory->setCreationOptions($options);
  336. } elseif (isset($factory)
  337. && method_exists($factory, 'setCreationOptions')
  338. ) {
  339. // duck-type MutableCreationOptionsInterface for forward compatibility
  340. $options = $this->creationOptions;
  341. // If we have empty creation options, we have to find out if a default
  342. // value is present and use that; otherwise, we should use an empty
  343. // array, as that's the standard type-hint.
  344. if (! is_array($options) || empty($options)) {
  345. $r = new ReflectionMethod($factory, 'setCreationOptions');
  346. $params = $r->getParameters();
  347. $optionsParam = array_shift($params);
  348. $options = $optionsParam->isDefaultValueAvailable() ? $optionsParam->getDefaultValue() : [];
  349. }
  350. $factory->setCreationOptions($options);
  351. }
  352. return parent::createServiceViaCallback($callable, $cName, $rName);
  353. }
  354. /**
  355. * @param string $serviceName
  356. * @param bool $isAutoInvokable
  357. * @param BaseException $exception
  358. *
  359. * @throws BaseException
  360. * @throws Exception\ServiceLocatorUsageException
  361. */
  362. private function tryThrowingServiceLocatorUsageException(
  363. $serviceName,
  364. $isAutoInvokable,
  365. BaseException $exception
  366. ) {
  367. if ($isAutoInvokable) {
  368. $this->unregisterService($this->canonicalizeName($serviceName));
  369. }
  370. $serviceLocator = $this->getServiceLocator();
  371. if ($serviceLocator && $serviceLocator->has($serviceName)) {
  372. throw Exception\ServiceLocatorUsageException::fromInvalidPluginManagerRequestedServiceName(
  373. $this,
  374. $serviceLocator,
  375. $serviceName,
  376. $exception
  377. );
  378. }
  379. throw $exception;
  380. }
  381. }