PageRenderTime 56ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 0ms

/vendor/october/rain/src/Extension/ExtendableTrait.php

https://bitbucket.org/ltdwebant/laboratorium
PHP | 471 lines | 409 code | 17 blank | 45 comment | 8 complexity | decbac5ee6c13e522101453efba86c54 MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause
  1. <?php namespace October\Rain\Extension;
  2. use ReflectionClass;
  3. use ReflectionMethod;
  4. use BadMethodCallException;
  5. use Exception;
  6. /**
  7. * This extension trait is used when access to the underlying base class
  8. * is not available, such as classes that belong to the foundation
  9. * framework (Laravel). It is currently used by the Controller and
  10. * Model classes.
  11. *
  12. * @package october\extension
  13. * @author Alexey Bobkov, Samuel Georges
  14. */
  15. trait ExtendableTrait
  16. {
  17. /**
  18. * @var array Class reflection information, including behaviors.
  19. */
  20. protected $extensionData = [
  21. 'extensions' => [],
  22. 'methods' => [],
  23. 'dynamicMethods' => []
  24. ];
  25. /**
  26. * @var array Used to extend the constructor of an extendable class. Eg:
  27. *
  28. * Class::extend(function($obj) { })
  29. *
  30. */
  31. protected static $extendableCallbacks = [];
  32. /**
  33. * @var array Collection of static methods used by behaviors.
  34. */
  35. protected static $extendableStaticMethods = [];
  36. /**
  37. * @var bool Indicates if dynamic properties can be created.
  38. */
  39. protected static $extendableGuardProperties = true;
  40. /**
  41. * This method should be called as part of the constructor.
  42. */
  43. public function extendableConstruct()
  44. {
  45. /*
  46. * Apply init callbacks
  47. */
  48. $classes = array_merge([get_class($this)], class_parents($this));
  49. foreach ($classes as $class) {
  50. if (isset(self::$extendableCallbacks[$class]) && is_array(self::$extendableCallbacks[$class])) {
  51. foreach (self::$extendableCallbacks[$class] as $callback) {
  52. call_user_func($callback, $this);
  53. }
  54. }
  55. }
  56. /*
  57. * Apply extensions
  58. */
  59. if (!$this->implement) {
  60. return;
  61. }
  62. if (is_string($this->implement)) {
  63. $uses = explode(',', $this->implement);
  64. }
  65. elseif (is_array($this->implement)) {
  66. $uses = $this->implement;
  67. }
  68. else {
  69. throw new Exception(sprintf('Class %s contains an invalid $implement value', get_class($this)));
  70. }
  71. foreach ($uses as $use) {
  72. $useClass = str_replace('.', '\\', trim($use));
  73. /*
  74. * Soft implement
  75. */
  76. if (substr($useClass, 0, 1) == '@') {
  77. $useClass = substr($useClass, 1);
  78. if (!class_exists($useClass)) continue;
  79. }
  80. $this->extendClassWith($useClass);
  81. }
  82. }
  83. /**
  84. * Helper method for `::extend()` static method
  85. * @param callable $callback
  86. * @return void
  87. */
  88. public static function extendableExtendCallback($callback)
  89. {
  90. $class = get_called_class();
  91. if (
  92. !isset(self::$extendableCallbacks[$class]) ||
  93. !is_array(self::$extendableCallbacks[$class])
  94. ) {
  95. self::$extendableCallbacks[$class] = [];
  96. }
  97. self::$extendableCallbacks[$class][] = $callback;
  98. }
  99. /**
  100. * Clear the list of extended classes so they will be re-extended.
  101. * @return void
  102. */
  103. public static function clearExtendedClasses()
  104. {
  105. self::$extendableCallbacks = [];
  106. }
  107. /**
  108. * Extracts the available methods from a behavior and adds it to the
  109. * list of callable methods.
  110. * @param string $extensionName
  111. * @param object $extensionObject
  112. * @return void
  113. */
  114. protected function extensionExtractMethods($extensionName, $extensionObject)
  115. {
  116. if (!method_exists($extensionObject, 'extensionIsHiddenMethod')) {
  117. throw new Exception(sprintf(
  118. 'Extension %s should inherit October\Rain\Extension\ExtensionBase or implement October\Rain\Extension\ExtensionTrait.',
  119. $extensionName
  120. ));
  121. }
  122. $extensionMethods = get_class_methods($extensionName);
  123. foreach ($extensionMethods as $methodName) {
  124. if (
  125. $methodName == '__construct' ||
  126. $extensionObject->extensionIsHiddenMethod($methodName)
  127. ) {
  128. continue;
  129. }
  130. $this->extensionData['methods'][$methodName] = $extensionName;
  131. }
  132. }
  133. /**
  134. * Programatically adds a method to the extendable class
  135. * @param string $dynamicName
  136. * @param callable $method
  137. * @param string $extension
  138. */
  139. public function addDynamicMethod($dynamicName, $method, $extension = null)
  140. {
  141. if (
  142. is_string($method) &&
  143. $extension &&
  144. ($extensionObj = $this->getClassExtension($extension))
  145. ) {
  146. $method = [$extensionObj, $method];
  147. }
  148. $this->extensionData['dynamicMethods'][$dynamicName] = $method;
  149. }
  150. /**
  151. * Programatically adds a property to the extendable class
  152. * @param string $dynamicName
  153. * @param string $value
  154. */
  155. public function addDynamicProperty($dynamicName, $value = null)
  156. {
  157. self::$extendableGuardProperties = false;
  158. if (!property_exists($this, $dynamicName)) {
  159. $this->{$dynamicName} = $value;
  160. }
  161. self::$extendableGuardProperties = true;
  162. }
  163. /**
  164. * Dynamically extend a class with a specified behavior
  165. * @param string $extensionName
  166. * @return void
  167. */
  168. public function extendClassWith($extensionName)
  169. {
  170. if (!strlen($extensionName)) {
  171. return $this;
  172. }
  173. if (isset($this->extensionData['extensions'][$extensionName])) {
  174. throw new Exception(sprintf(
  175. 'Class %s has already been extended with %s',
  176. get_class($this),
  177. $extensionName
  178. ));
  179. }
  180. $this->extensionData['extensions'][$extensionName] = $extensionObject = new $extensionName($this);
  181. $this->extensionExtractMethods($extensionName, $extensionObject);
  182. $extensionObject->extensionApplyInitCallbacks();
  183. }
  184. /**
  185. * Check if extendable class is extended with a behavior object
  186. * @param string $name Fully qualified behavior name
  187. * @return boolean
  188. */
  189. public function isClassExtendedWith($name)
  190. {
  191. $name = str_replace('.', '\\', trim($name));
  192. return isset($this->extensionData['extensions'][$name]);
  193. }
  194. /**
  195. * Returns a behavior object from an extendable class, example:
  196. *
  197. * $this->getClassExtension('Backend.Behaviors.FormController')
  198. *
  199. * @param string $name Fully qualified behavior name
  200. * @return mixed
  201. */
  202. public function getClassExtension($name)
  203. {
  204. $name = str_replace('.', '\\', trim($name));
  205. return (isset($this->extensionData['extensions'][$name]))
  206. ? $this->extensionData['extensions'][$name]
  207. : null;
  208. }
  209. /**
  210. * Short hand for `getClassExtension()` method, except takes the short
  211. * extension name, example:
  212. *
  213. * $this->asExtension('FormController')
  214. *
  215. * @param string $shortName
  216. * @return mixed
  217. */
  218. public function asExtension($shortName)
  219. {
  220. $hints = [];
  221. foreach ($this->extensionData['extensions'] as $class => $obj) {
  222. if (
  223. preg_match('@\\\\([\w]+)$@', $class, $matches) &&
  224. $matches[1] == $shortName
  225. ) {
  226. return $obj;
  227. }
  228. }
  229. return $this->getClassExtension($shortName);
  230. }
  231. /**
  232. * Checks if a method exists, extension equivalent of method_exists()
  233. * @param string $name
  234. * @return boolean
  235. */
  236. public function methodExists($name)
  237. {
  238. return (
  239. method_exists($this, $name) ||
  240. isset($this->extensionData['methods'][$name]) ||
  241. isset($this->extensionData['dynamicMethods'][$name])
  242. );
  243. }
  244. /**
  245. * Checks if a property exists, extension equivalent of `property_exists()`
  246. * @param string $name
  247. * @return boolean
  248. */
  249. public function propertyExists($name)
  250. {
  251. if (property_exists($this, $name)) {
  252. return true;
  253. }
  254. foreach ($this->extensionData['extensions'] as $extensionObject) {
  255. if (
  256. property_exists($extensionObject, $name) &&
  257. $this->extendableIsAccessible($extensionObject, $name)
  258. ) {
  259. return true;
  260. }
  261. }
  262. return false;
  263. }
  264. /**
  265. * Checks if a property is accessible, property equivalent of `is_callabe()`
  266. * @param mixed $class
  267. * @param string $propertyName
  268. * @return boolean
  269. */
  270. protected function extendableIsAccessible($class, $propertyName)
  271. {
  272. $reflector = new ReflectionClass($class);
  273. $property = $reflector->getProperty($propertyName);
  274. return $property->isPublic();
  275. }
  276. /**
  277. * Magic method for `__get()`
  278. * @param string $name
  279. * @return string
  280. */
  281. public function extendableGet($name)
  282. {
  283. foreach ($this->extensionData['extensions'] as $extensionObject) {
  284. if (
  285. property_exists($extensionObject, $name) &&
  286. $this->extendableIsAccessible($extensionObject, $name)
  287. ) {
  288. return $extensionObject->{$name};
  289. }
  290. }
  291. $parent = get_parent_class();
  292. if ($parent !== false && method_exists($parent, '__get')) {
  293. return parent::__get($name);
  294. }
  295. }
  296. /**
  297. * Magic method for `__set()`
  298. * @param string $name
  299. * @param string $value
  300. * @return string
  301. */
  302. public function extendableSet($name, $value)
  303. {
  304. foreach ($this->extensionData['extensions'] as $extensionObject) {
  305. if (!property_exists($extensionObject, $name)) {
  306. continue;
  307. }
  308. $extensionObject->{$name} = $value;
  309. }
  310. /*
  311. * This targets trait usage in particular
  312. */
  313. $parent = get_parent_class();
  314. if ($parent !== false && method_exists($parent, '__set')) {
  315. parent::__set($name, $value);
  316. }
  317. /*
  318. * Setting an undefined property
  319. */
  320. if (!self::$extendableGuardProperties) {
  321. $this->{$name} = $value;
  322. }
  323. }
  324. /**
  325. * Magic method for `__call()`
  326. * @param string $name
  327. * @param array $params
  328. * @return mixed
  329. */
  330. public function extendableCall($name, $params = null)
  331. {
  332. if (isset($this->extensionData['methods'][$name])) {
  333. $extension = $this->extensionData['methods'][$name];
  334. $extensionObject = $this->extensionData['extensions'][$extension];
  335. if (method_exists($extension, $name) && is_callable([$extension, $name])) {
  336. return call_user_func_array([$extensionObject, $name], $params);
  337. }
  338. }
  339. if (isset($this->extensionData['dynamicMethods'][$name])) {
  340. $dynamicCallable = $this->extensionData['dynamicMethods'][$name];
  341. if (is_callable($dynamicCallable)) {
  342. return call_user_func_array($dynamicCallable, $params);
  343. }
  344. }
  345. $parent = get_parent_class();
  346. if ($parent !== false && method_exists($parent, '__call')) {
  347. return parent::__call($name, $params);
  348. }
  349. throw new BadMethodCallException(sprintf(
  350. 'Call to undefined method %s::%s()',
  351. get_class($this),
  352. $name
  353. ));
  354. }
  355. /**
  356. * Magic method for `__callStatic()`
  357. * @param string $name
  358. * @param array $params
  359. * @return mixed
  360. */
  361. public static function extendableCallStatic($name, $params = null)
  362. {
  363. $className = get_called_class();
  364. if (!array_key_exists($className, self::$extendableStaticMethods)) {
  365. self::$extendableStaticMethods[$className] = [];
  366. $class = new ReflectionClass($className);
  367. $defaultProperties = $class->getDefaultProperties();
  368. if (
  369. array_key_exists('implement', $defaultProperties) &&
  370. ($implement = $defaultProperties['implement'])
  371. ) {
  372. /*
  373. * Apply extensions
  374. */
  375. if (is_string($implement)) {
  376. $uses = explode(',', $implement);
  377. }
  378. elseif (is_array($implement)) {
  379. $uses = $implement;
  380. }
  381. else {
  382. throw new Exception(sprintf('Class %s contains an invalid $implement value', $className));
  383. }
  384. foreach ($uses as $use) {
  385. $useClassName = str_replace('.', '\\', trim($use));
  386. $useClass = new ReflectionClass($useClassName);
  387. $staticMethods = $useClass->getMethods(ReflectionMethod::IS_STATIC);
  388. foreach ($staticMethods as $method) {
  389. self::$extendableStaticMethods[$className][$method->getName()] = $useClassName;
  390. }
  391. }
  392. }
  393. }
  394. if (isset(self::$extendableStaticMethods[$className][$name])) {
  395. $extension = self::$extendableStaticMethods[$className][$name];
  396. if (method_exists($extension, $name) && is_callable([$extension, $name])) {
  397. $extension::$extendableStaticCalledClass = $className;
  398. $result = forward_static_call_array(array($extension, $name), $params);
  399. $extension::$extendableStaticCalledClass = null;
  400. return $result;
  401. }
  402. }
  403. // $parent = get_parent_class($className);
  404. // if ($parent !== false && method_exists($parent, '__callStatic')) {
  405. // return parent::__callStatic($name, $params);
  406. // }
  407. throw new BadMethodCallException(sprintf(
  408. 'Call to undefined method %s::%s()',
  409. $className,
  410. $name
  411. ));
  412. }
  413. }