PageRenderTime 45ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Core/Callback.php

http://github.com/AlephTav/Aleph
PHP | 418 lines | 215 code | 24 blank | 179 comment | 63 complexity | 8716f1a1a674a47284b75f70c21fa457 MD5 | raw file
Possible License(s): LGPL-2.1, MPL-2.0-no-copyleft-exception
  1. <?php
  2. /**
  3. * Copyright (c) 2013 - 2016 Aleph Tav
  4. *
  5. * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
  6. * documentation files (the "Software"), to deal in the Software without restriction, including without limitation
  7. * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
  8. * and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
  9. *
  10. * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
  11. *
  12. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
  13. * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  14. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
  15. * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  16. *
  17. * @author Aleph Tav <4lephtav@gmail.com>
  18. * @link http://www.4leph.com
  19. * @copyright Copyright &copy; 2013 - 2016 Aleph Tav
  20. * @license http://www.opensource.org/licenses/MIT
  21. */
  22. namespace Aleph\Core;
  23. use Aleph\Core\Interfaces\ICallback;
  24. /**
  25. * This class is generalization of callable type.
  26. *
  27. * @author Aleph Tav <4lephtav@gmail.com>
  28. * @version 1.2.0
  29. * @package aleph.core
  30. */
  31. class Callback implements ICallback
  32. {
  33. /**
  34. * Error message templates.
  35. */
  36. const ERR_CALLBACK_1 = 'Callback %s is not callable.';
  37. /**
  38. * The callable object or null.
  39. *
  40. * @var object|callable|null
  41. */
  42. private $callable = null;
  43. /**
  44. * A string in the Aleph callback format.
  45. *
  46. * @var string
  47. */
  48. private $callback = '';
  49. /**
  50. * Full name (class name with namespace) of a callback class.
  51. *
  52. * @var string
  53. */
  54. private $class = '';
  55. /**
  56. * Method name of a callback class.
  57. *
  58. * @var string
  59. */
  60. private $method = '';
  61. /**
  62. * Equals TRUE if a callback method is static and FALSE if it isn't.
  63. *
  64. * @var bool
  65. */
  66. private $static = false;
  67. /**
  68. * Shows the number of callback arguments that should be passed into callback constructor.
  69. *
  70. * @var int
  71. */
  72. private $numargs = 0;
  73. /**
  74. * Can be equal 'function', 'closure' or 'class' according to callback format.
  75. *
  76. * @var string
  77. */
  78. private $type = '';
  79. /**
  80. * Constructor.
  81. *
  82. * The following formats of $callback are possible:
  83. * 'function' - invokes a global function with name 'function'.
  84. * 'class::method' - invokes a static method 'method' of a class 'class'.
  85. * 'class->method' - invokes a method 'method' of a class 'class' with its constructor without arguments.
  86. * 'class[]' - creates an object of a class 'class' without sending any arguments in its constructor.
  87. * 'class[]::method' - the same as 'class::method'.
  88. * 'class[]->method' - the same as 'class->method'.
  89. * 'class[n]' - creates an object of a class 'class' with its constructor taking n arguments.
  90. * 'class[n]::method' - the same as 'class::method'.
  91. * 'class[n]->method' - invokes a method 'method' of a class 'class' with its constructor taking n arguments.
  92. *
  93. * @param mixed $callback Any valid callable object.
  94. * @throws \InvalidArgumentException If $callback is in wrong format.
  95. */
  96. public function __construct($callback)
  97. {
  98. $this->parse($callback);
  99. }
  100. /**
  101. * Invokes a callback's method or creating a class object.
  102. * For callbacks in format 'class[n]' and 'class[n]->method' first n arguments of the method
  103. * are arguments of the constructor of a class 'class'.
  104. *
  105. * @param array $args An array of callback's arguments.
  106. * @param object $newThis The object to which the given closure function should be bound.
  107. * @return mixed
  108. */
  109. public function call(array $args = [], $newThis = null)
  110. {
  111. $args = (array)$args;
  112. if ($this->type == 'closure') {
  113. $closure = $this->callable;
  114. if (is_object($newThis)) {
  115. return $closure->call($newThis, ...$args);
  116. }
  117. return $closure(...$args);
  118. }
  119. if ($this->type == 'function') {
  120. $callable = $this->method;
  121. return $callable(...$args);
  122. }
  123. if ($this->static) {
  124. return call_user_func_array([$this->class, $this->method], $args);
  125. }
  126. $class = $this->getObject(false, $args);
  127. if ($this->method == '__construct') {
  128. return $class;
  129. }
  130. return call_user_func_array([$class, $this->method], array_splice($args, $this->numargs));
  131. }
  132. /**
  133. * The magic method allowing to invoke an object of this class as a method.
  134. * The method can take different number of arguments.
  135. *
  136. * @param array $params The callback's arguments.
  137. * @return mixed
  138. */
  139. public function __invoke(...$params)
  140. {
  141. return $this->call($params);
  142. }
  143. /**
  144. * Checks whether or not the callback can be invoked according to permissions.
  145. *
  146. * Permissions array have the following structure:
  147. * [
  148. * 'permitted' => ['regexp1', 'regexp2', ... ],
  149. * 'forbidden' => ['regexp1', 'regexp2', ...]
  150. * ]
  151. * If string representation of the callback matches at least one of 'permitted'
  152. * regular expressions and none of 'forbidden' regular expressions, the method
  153. * returns TRUE. Otherwise it returns FALSE.
  154. *
  155. * @param array $permissions Permissions to check.
  156. * @return bool
  157. */
  158. public function isPermitted(array $permissions) : bool
  159. {
  160. foreach (['permitted' => true, 'forbidden' => false] as $type => $res) {
  161. if (isset($permissions[$type])) {
  162. $flag = !$res;
  163. foreach ((array)$permissions[$type] as $regexp) {
  164. if (preg_match($regexp, $this->callback)) {
  165. $flag = $res;
  166. break;
  167. }
  168. }
  169. if (!$flag) {
  170. return false;
  171. }
  172. }
  173. }
  174. return true;
  175. }
  176. /**
  177. * Verifies that the callback exists and can be invoked.
  178. *
  179. * @param bool $autoload Whether or not to call __autoload by default.
  180. * @return bool
  181. */
  182. public function isCallable(bool $autoload = true) : bool
  183. {
  184. if ($this->type == 'closure') {
  185. return true;
  186. }
  187. if ($this->type == 'function') {
  188. return function_exists($this->method);
  189. }
  190. if ($this->callable || class_exists($this->class, $autoload)) {
  191. if (!method_exists($this->class, $this->method)) {
  192. return false;
  193. }
  194. $class = new \ReflectionClass($this->class);
  195. if (!$class->hasMethod($this->method)) {
  196. return false;
  197. }
  198. $method = $class->getMethod($this->method);
  199. return $method->isPublic() && $this->static == $method->isStatic();
  200. }
  201. return false;
  202. }
  203. /**
  204. * Returns an array of the detailed information of the callback.
  205. *
  206. * Output array has the format
  207. * [
  208. * 'type' => ... [string] ...,
  209. * 'class' => ... [string] ...,
  210. * 'method' => ... [string] ...,
  211. * 'static' => ... [boolean] ...,
  212. * 'numargs' => ... [integer] ...
  213. * ]
  214. *
  215. * @return array
  216. */
  217. public function getInfo() : array
  218. {
  219. return [
  220. 'type' => $this->type,
  221. 'class' => $this->class,
  222. 'method' => $this->method,
  223. 'static' => $this->static,
  224. 'numargs' => $this->numargs
  225. ];
  226. }
  227. /**
  228. * Returns full class name (with namespace) of the callback.
  229. *
  230. * @return string
  231. */
  232. public function getClass() : string
  233. {
  234. return $this->class;
  235. }
  236. /**
  237. * Returns method name of the callback.
  238. *
  239. * @return string
  240. */
  241. public function getMethod() : string
  242. {
  243. return $this->method;
  244. }
  245. /**
  246. * Returns callback type.
  247. * Possible values can be "closure", "function" or "class".
  248. *
  249. * @return string
  250. */
  251. public function getType() : string
  252. {
  253. return $this->type;
  254. }
  255. /**
  256. * Returns TRUE if the given callback is a static class method and FALSE otherwise.
  257. *
  258. * @return bool
  259. */
  260. public function isStatic() : bool
  261. {
  262. return $this->static;
  263. }
  264. /**
  265. * Returns parameters of a callback class method, function or closure.
  266. * Method returns FALSE if the class method doesn't exist.
  267. *
  268. * @return \ReflectionParameter[]|bool
  269. */
  270. public function getParameters()
  271. {
  272. if ($this->type != 'class') {
  273. return (new \ReflectionFunction($this->method))->getParameters();
  274. }
  275. $class = new \ReflectionClass($this->class);
  276. if (!$class->hasMethod($this->method)) {
  277. return false;
  278. }
  279. return $class->getMethod($this->method)->getParameters();
  280. }
  281. /**
  282. * Creates and returns object of callback's class.
  283. *
  284. * @param bool $createNew Determines whether the callable object should be a new instance.
  285. * @param array $args Arguments of the class constructor.
  286. * @return object|null
  287. */
  288. public function getObject(bool $createNew = false, array $args = [])
  289. {
  290. if ($this->type == 'closure') {
  291. return $createNew ? clone $this->callable : $this->callable;
  292. }
  293. if ($this->type == 'class') {
  294. if (!$createNew && $this->callable) {
  295. return $this->callable;
  296. }
  297. $class = new \ReflectionClass($this->class);
  298. if ($this->numargs == 0) {
  299. $callable = $class->newInstance();
  300. } else {
  301. $callable = $class->newInstanceArgs(array_splice($args, 0, $this->numargs));
  302. }
  303. if (!$createNew) {
  304. $this->callable = $callable;
  305. }
  306. return $callable;
  307. }
  308. return null;
  309. }
  310. /**
  311. * The magic method allowing to convert an object of this class to a callback string.
  312. *
  313. * @return string
  314. */
  315. public function __toString() : string
  316. {
  317. return $this->callback;
  318. }
  319. /**
  320. * Parses the callable object.
  321. *
  322. * @param mixed $callback Any valid callable object.
  323. * @return void
  324. * @throws \InvalidArgumentException If $callback is in wrong format.
  325. */
  326. private function parse($callback)
  327. {
  328. if ($callback instanceof ICallback) {
  329. foreach ($callback->getInfo() as $var => $value) {
  330. $this->{$var} = $value;
  331. }
  332. $this->callback = (string)$callback;
  333. $this->callable = $callback;
  334. } else if ($callback instanceof \Closure) {
  335. $this->type = 'closure';
  336. $this->class = 'Closure';
  337. $this->callback = 'Closure';
  338. $this->callable = $callback;
  339. } else if (is_object($callback)) {
  340. $constructor = (new \ReflectionClass($callback))->getConstructor();
  341. $this->type = 'class';
  342. $this->class = get_class($callback);
  343. $this->method = '__construct';
  344. $this->numargs = $constructor ? $constructor->getNumberOfParameters() : 0;
  345. $this->callback = $this->class . '[' . ($this->numargs ?: '') . ']';
  346. $this->callable = $callback;
  347. } else if (is_array($callback)) {
  348. if (!is_callable($callback)) {
  349. if (isset($callback[0]) && is_object($callback[0])) {
  350. $callback[0] = '{' . get_class($callback[0]) . '}';
  351. }
  352. throw new \InvalidArgumentException(sprintf(static::ERR_CALLBACK_1, json_encode($callback)));
  353. }
  354. $this->type = 'class';
  355. $this->static = !is_object($callback[0]);
  356. $this->class = $this->static ? $callback[0] : get_class($callback[0]);
  357. $this->method = $callback[1];
  358. if ($this->static) {
  359. $this->numargs = 0;
  360. } else {
  361. $constructor = (new \ReflectionClass($callback[0]))->getConstructor();
  362. $this->numargs = $constructor ? $constructor->getNumberOfParameters() : 0;
  363. }
  364. $this->callback = $this->class .
  365. ($this->static ? '::' : '[' . ($this->numargs ?: '') . ']->') . $this->method;
  366. $this->callable = $this->static ? null : $callback[0];
  367. } else {
  368. if ($callback == '' || is_numeric($callback)) {
  369. throw new \InvalidArgumentException(sprintf(static::ERR_CALLBACK_1, $callback));
  370. }
  371. preg_match('/^([^\[:-]*)(\[([^\]]*)\])?(::|->)?([^:-]*(?:parent::|self::|)[^:-]*)$/', $callback, $matches);
  372. if (count($matches) == 0 || $matches[1] == '') {
  373. throw new \InvalidArgumentException(sprintf(static::ERR_CALLBACK_1, $callback));
  374. }
  375. if ($matches[4] == '' && $matches[2] == '') {
  376. $this->type = 'function';
  377. $this->method = $matches[1];
  378. $this->callback = $this->method;
  379. } else {
  380. $this->type = 'class';
  381. $this->class = $matches[1];
  382. if ($this->class[0] == '\\') {
  383. $this->class = ltrim($this->class, '\\');
  384. }
  385. $this->numargs = (int)$matches[3];
  386. $this->static = ($matches[4] == '::');
  387. $this->method = $matches[5] ?: '__construct';
  388. $this->callback = $this->class .
  389. ($this->static ? '::' . $this->method : '[' . ($this->numargs ?: '') . ']' .
  390. ($this->method != '__construct' ? '->' . $this->method : ''));
  391. }
  392. }
  393. }
  394. }