DiContainer.php 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. <?php
  2. namespace Elgg\Di;
  3. /**
  4. * Container holding values which can be resolved upon reading and optionally stored and shared
  5. * across reads.
  6. *
  7. * <code>
  8. * $c = new \Elgg\Di\DiContainer();
  9. *
  10. * $c->setFactory('foo', 'Foo_factory'); // $c will be passed to Foo_factory()
  11. * $c->foo; // new Foo instance
  12. * $c->foo; // same instance
  13. *
  14. * $c->setFactory('bar', 'Bar_factory', false); // non-shared
  15. * $c->bar; // new Bar instance
  16. * $c->bar; // different Bar instance
  17. *
  18. * $c->setValue('a_string', 'foo_factory'); // don't call this
  19. * $c->a_string; // 'foo_factory'
  20. * </code>
  21. *
  22. * @access private
  23. *
  24. * @package Elgg.Core
  25. * @since 1.9
  26. */
  27. class DiContainer {
  28. /**
  29. * @var array each element is an array: ['callable' => mixed $factory, 'shared' => bool $isShared]
  30. */
  31. protected $factories = array();
  32. /**
  33. * @var array
  34. */
  35. protected $cache = array();
  36. const CLASS_NAME_PATTERN_53 = '/^(\\\\?[a-z_\x7f-\xff][a-z0-9_\x7f-\xff]*)+$/i';
  37. /**
  38. * Fetch a value.
  39. *
  40. * @param string $name The name of the value to fetch
  41. * @return mixed
  42. * @throws \Elgg\Di\MissingValueException
  43. */
  44. public function __get($name) {
  45. if (array_key_exists($name, $this->cache)) {
  46. return $this->cache[$name];
  47. }
  48. if (!isset($this->factories[$name])) {
  49. throw new \Elgg\Di\MissingValueException("Value or factory was not set for: $name");
  50. }
  51. $value = $this->build($this->factories[$name]['callable'], $name);
  52. // Why check existence of factory here? A: the builder function may have set the value
  53. // directly, in which case the factory will no longer exist.
  54. if (!empty($this->factories[$name]) && $this->factories[$name]['shared']) {
  55. $this->cache[$name] = $value;
  56. }
  57. return $value;
  58. }
  59. /**
  60. * Build a value
  61. *
  62. * @param mixed $factory The factory for the value
  63. * @param string $name The name of the value
  64. * @return mixed
  65. * @throws \Elgg\Di\FactoryUncallableException
  66. */
  67. protected function build($factory, $name) {
  68. if (is_callable($factory)) {
  69. return call_user_func($factory, $this);
  70. }
  71. $msg = "Factory for '$name' was uncallable";
  72. if (is_string($factory)) {
  73. $msg .= ": '$factory'";
  74. } elseif (is_array($factory)) {
  75. if (is_string($factory[0])) {
  76. $msg .= ": '{$factory[0]}::{$factory[1]}'";
  77. } else {
  78. $msg .= ": " . get_class($factory[0]) . "->{$factory[1]}";
  79. }
  80. }
  81. throw new \Elgg\Di\FactoryUncallableException($msg);
  82. }
  83. /**
  84. * Set a value to be returned without modification
  85. *
  86. * @param string $name The name of the value
  87. * @param mixed $value The value
  88. * @return \Elgg\Di\DiContainer
  89. * @throws \InvalidArgumentException
  90. */
  91. public function setValue($name, $value) {
  92. $this->remove($name);
  93. $this->cache[$name] = $value;
  94. return $this;
  95. }
  96. /**
  97. * Set a factory to generate a value when the container is read.
  98. *
  99. * @param string $name The name of the value
  100. * @param callable $callable Factory for the value
  101. * @param bool $shared Whether the same value should be returned for every request
  102. * @return \Elgg\Di\DiContainer
  103. * @throws \InvalidArgumentException
  104. */
  105. public function setFactory($name, $callable, $shared = true) {
  106. if (!is_callable($callable, true)) {
  107. throw new \InvalidArgumentException('$factory must appear callable');
  108. }
  109. $this->remove($name);
  110. $this->factories[$name] = array(
  111. 'callable' => $callable,
  112. 'shared' => $shared
  113. );
  114. return $this;
  115. }
  116. /**
  117. * Set a factory based on instantiating a class with no arguments.
  118. *
  119. * @param string $name Name of the value
  120. * @param string $class_name Class name to be instantiated
  121. * @param bool $shared Whether the same value should be returned for every request
  122. * @return \Elgg\Di\DiContainer
  123. * @throws \InvalidArgumentException
  124. */
  125. public function setClassName($name, $class_name, $shared = true) {
  126. $classname_pattern = self::CLASS_NAME_PATTERN_53;
  127. if (!is_string($class_name) || !preg_match($classname_pattern, $class_name)) {
  128. throw new \InvalidArgumentException('Class names must be valid PHP class names');
  129. }
  130. $func = function () use ($class_name) {
  131. return new $class_name();
  132. };
  133. return $this->setFactory($name, $func, $shared);
  134. }
  135. /**
  136. * Remove a value from the container
  137. *
  138. * @param string $name The name of the value
  139. * @return \Elgg\Di\DiContainer
  140. */
  141. public function remove($name) {
  142. unset($this->cache[$name]);
  143. unset($this->factories[$name]);
  144. return $this;
  145. }
  146. /**
  147. * Does the container have this value
  148. *
  149. * @param string $name The name of the value
  150. * @return bool
  151. */
  152. public function has($name) {
  153. return isset($this->factories[$name]) || array_key_exists($name, $this->cache);
  154. }
  155. }