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

/vendor/nette/utils/src/Utils/ObjectMixin.php

https://gitlab.com/ramos.lauty/softlord
PHP | 571 lines | 410 code | 73 blank | 88 comment | 62 complexity | c707a860f983f9ad73a19c5aa2861f2f MD5 | raw file
  1. <?php
  2. /**
  3. * This file is part of the Nette Framework (https://nette.org)
  4. * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
  5. */
  6. namespace Nette\Utils;
  7. use Nette;
  8. use Nette\MemberAccessException;
  9. /**
  10. * Nette\Object behaviour mixin.
  11. */
  12. class ObjectMixin
  13. {
  14. use Nette\StaticClass;
  15. /** @var array [name => [type => callback]] used by extension methods */
  16. private static $extMethods = [];
  17. /********************* strictness ****************d*g**/
  18. /**
  19. * @throws MemberAccessException
  20. */
  21. public static function strictGet($class, $name)
  22. {
  23. $rc = new \ReflectionClass($class);
  24. $hint = self::getSuggestion(array_merge(
  25. array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), function ($p) { return !$p->isStatic(); }),
  26. self::parseFullDoc($rc, '~^[ \t*]*@property(?:-read)?[ \t]+(?:\S+[ \t]+)??\$(\w+)~m')
  27. ), $name);
  28. throw new MemberAccessException("Cannot read an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.'));
  29. }
  30. /**
  31. * @throws MemberAccessException
  32. */
  33. public static function strictSet($class, $name)
  34. {
  35. $rc = new \ReflectionClass($class);
  36. $hint = self::getSuggestion(array_merge(
  37. array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), function ($p) { return !$p->isStatic(); }),
  38. self::parseFullDoc($rc, '~^[ \t*]*@property(?:-write)?[ \t]+(?:\S+[ \t]+)??\$(\w+)~m')
  39. ), $name);
  40. throw new MemberAccessException("Cannot write to an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.'));
  41. }
  42. /**
  43. * @throws MemberAccessException
  44. */
  45. public static function strictCall($class, $method, $additionalMethods = [])
  46. {
  47. $hint = self::getSuggestion(array_merge(
  48. get_class_methods($class),
  49. self::parseFullDoc(new \ReflectionClass($class), '~^[ \t*]*@method[ \t]+(?:\S+[ \t]+)??(\w+)\(~m'),
  50. $additionalMethods
  51. ), $method);
  52. if (method_exists($class, $method)) { // called parent::$method()
  53. $class = 'parent';
  54. }
  55. throw new MemberAccessException("Call to undefined method $class::$method()" . ($hint ? ", did you mean $hint()?" : '.'));
  56. }
  57. /**
  58. * @throws MemberAccessException
  59. */
  60. public static function strictStaticCall($class, $method)
  61. {
  62. $hint = self::getSuggestion(
  63. array_filter((new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC), function ($m) { return $m->isStatic(); }),
  64. $method
  65. );
  66. throw new MemberAccessException("Call to undefined static method $class::$method()" . ($hint ? ", did you mean $hint()?" : '.'));
  67. }
  68. /********************* Nette\Object ****************d*g**/
  69. /**
  70. * __call() implementation.
  71. * @param object
  72. * @param string
  73. * @param array
  74. * @return mixed
  75. * @throws MemberAccessException
  76. */
  77. public static function call($_this, $name, $args)
  78. {
  79. $class = get_class($_this);
  80. $isProp = self::hasProperty($class, $name);
  81. if ($name === '') {
  82. throw new MemberAccessException("Call to class '$class' method without name.");
  83. } elseif ($isProp === 'event') { // calling event handlers
  84. if (is_array($_this->$name) || $_this->$name instanceof \Traversable) {
  85. foreach ($_this->$name as $handler) {
  86. Callback::invokeArgs($handler, $args);
  87. }
  88. } elseif ($_this->$name !== NULL) {
  89. throw new Nette\UnexpectedValueException("Property $class::$$name must be array or NULL, " . gettype($_this->$name) . ' given.');
  90. }
  91. } elseif ($isProp && $_this->$name instanceof \Closure) { // closure in property
  92. return call_user_func_array($_this->$name, $args);
  93. } elseif (($methods = & self::getMethods($class)) && isset($methods[$name]) && is_array($methods[$name])) { // magic @methods
  94. list($op, $rp, $type) = $methods[$name];
  95. if (count($args) !== ($op === 'get' ? 0 : 1)) {
  96. throw new Nette\InvalidArgumentException("$class::$name() expects " . ($op === 'get' ? 'no' : '1') . ' argument, ' . count($args) . ' given.');
  97. } elseif ($type && $args && !self::checkType($args[0], $type)) {
  98. throw new Nette\InvalidArgumentException("Argument passed to $class::$name() must be $type, " . gettype($args[0]) . ' given.');
  99. }
  100. if ($op === 'get') {
  101. return $rp->getValue($_this);
  102. } elseif ($op === 'set') {
  103. $rp->setValue($_this, $args[0]);
  104. } elseif ($op === 'add') {
  105. $val = $rp->getValue($_this);
  106. $val[] = $args[0];
  107. $rp->setValue($_this, $val);
  108. }
  109. return $_this;
  110. } elseif ($cb = self::getExtensionMethod($class, $name)) { // extension methods
  111. return Callback::invoke($cb, $_this, ...$args);
  112. } else {
  113. self::strictCall($class, $name, array_keys(self::getExtensionMethods($class)));
  114. }
  115. }
  116. /**
  117. * __callStatic() implementation.
  118. * @param string
  119. * @param string
  120. * @param array
  121. * @return void
  122. * @throws MemberAccessException
  123. */
  124. public static function callStatic($class, $method, $args)
  125. {
  126. self::strictStaticCall($class, $method);
  127. }
  128. /**
  129. * __get() implementation.
  130. * @param object
  131. * @param string property name
  132. * @return mixed property value
  133. * @throws MemberAccessException if the property is not defined.
  134. */
  135. public static function & get($_this, $name)
  136. {
  137. $class = get_class($_this);
  138. $uname = ucfirst($name);
  139. $methods = & self::getMethods($class);
  140. if ($name === '') {
  141. throw new MemberAccessException("Cannot read a class '$class' property without name.");
  142. } elseif (isset($methods[$m = 'get' . $uname]) || isset($methods[$m = 'is' . $uname])) { // property getter
  143. if ($methods[$m] === 0) {
  144. $methods[$m] = (new \ReflectionMethod($class, $m))->returnsReference();
  145. }
  146. if ($methods[$m] === TRUE) {
  147. return $_this->$m();
  148. } else {
  149. $val = $_this->$m();
  150. return $val;
  151. }
  152. } elseif (isset($methods[$name])) { // public method as closure getter
  153. if (preg_match('#^(is|get|has)([A-Z]|$)#', $name) && !(new \ReflectionMethod($class, $name))->getNumberOfRequiredParameters()) {
  154. trigger_error("Did you forget parentheses after $name" . self::getSource() . '?', E_USER_WARNING);
  155. }
  156. $val = Callback::closure($_this, $name);
  157. return $val;
  158. } elseif (isset($methods['set' . $uname])) { // property getter
  159. throw new MemberAccessException("Cannot read a write-only property $class::\$$name.");
  160. } else {
  161. self::strictGet($class, $name);
  162. }
  163. }
  164. /**
  165. * __set() implementation.
  166. * @param object
  167. * @param string property name
  168. * @param mixed property value
  169. * @return void
  170. * @throws MemberAccessException if the property is not defined or is read-only
  171. */
  172. public static function set($_this, $name, $value)
  173. {
  174. $class = get_class($_this);
  175. $uname = ucfirst($name);
  176. $methods = & self::getMethods($class);
  177. if ($name === '') {
  178. throw new MemberAccessException("Cannot write to a class '$class' property without name.");
  179. } elseif (self::hasProperty($class, $name)) { // unsetted property
  180. $_this->$name = $value;
  181. } elseif (isset($methods[$m = 'set' . $uname])) { // property setter
  182. $_this->$m($value);
  183. } elseif (isset($methods['get' . $uname]) || isset($methods['is' . $uname])) { // property setter
  184. throw new MemberAccessException("Cannot write to a read-only property $class::\$$name.");
  185. } else {
  186. self::strictSet($class, $name);
  187. }
  188. }
  189. /**
  190. * __unset() implementation.
  191. * @param object
  192. * @param string property name
  193. * @return void
  194. * @throws MemberAccessException
  195. */
  196. public static function remove($_this, $name)
  197. {
  198. $class = get_class($_this);
  199. if (!self::hasProperty($class, $name)) {
  200. throw new MemberAccessException("Cannot unset the property $class::\$$name.");
  201. }
  202. }
  203. /**
  204. * __isset() implementation.
  205. * @param object
  206. * @param string property name
  207. * @return bool
  208. */
  209. public static function has($_this, $name)
  210. {
  211. $name = ucfirst($name);
  212. $methods = & self::getMethods(get_class($_this));
  213. return $name !== '' && (isset($methods['get' . $name]) || isset($methods['is' . $name]));
  214. }
  215. /********************* magic @properties ****************d*g**/
  216. /**
  217. * Returns array of magic properties defined by annotation @property.
  218. * @return array of [name => bit mask]
  219. */
  220. public static function getMagicProperties($class)
  221. {
  222. static $cache;
  223. $props = & $cache[$class];
  224. if ($props !== NULL) {
  225. return $props;
  226. }
  227. $rc = new \ReflectionClass($class);
  228. preg_match_all(
  229. '~^ [ \t*]* @property(|-read|-write) [ \t]+ [^\s$]+ [ \t]+ \$ (\w+) ()~mx',
  230. (string) $rc->getDocComment(), $matches, PREG_SET_ORDER
  231. );
  232. $props = [];
  233. foreach ($matches as list(, $type, $name)) {
  234. $uname = ucfirst($name);
  235. $write = $type !== '-read'
  236. && $rc->hasMethod($nm = 'set' . $uname)
  237. && ($rm = $rc->getMethod($nm)) && $rm->getName() === $nm && !$rm->isPrivate() && !$rm->isStatic();
  238. $read = $type !== '-write'
  239. && ($rc->hasMethod($nm = 'get' . $uname) || $rc->hasMethod($nm = 'is' . $uname))
  240. && ($rm = $rc->getMethod($nm)) && $rm->getName() === $nm && !$rm->isPrivate() && !$rm->isStatic();
  241. if ($read || $write) {
  242. $props[$name] = $read << 0 | ($nm[0] === 'g') << 1 | $rm->returnsReference() << 2 | $write << 3;
  243. }
  244. }
  245. if ($parent = get_parent_class($class)) {
  246. $props += self::getMagicProperties($parent);
  247. }
  248. return $props;
  249. }
  250. /** @internal */
  251. public static function getMagicProperty($class, $name)
  252. {
  253. $props = self::getMagicProperties($class);
  254. return isset($props[$name]) ? $props[$name] : NULL;
  255. }
  256. /********************* magic @methods ****************d*g**/
  257. /**
  258. * Returns array of magic methods defined by annotation @method.
  259. * @return array
  260. */
  261. public static function getMagicMethods($class)
  262. {
  263. $rc = new \ReflectionClass($class);
  264. preg_match_all('~^
  265. [ \t*]* @method [ \t]+
  266. (?: [^\s(]+ [ \t]+ )?
  267. (set|get|is|add) ([A-Z]\w*)
  268. (?: ([ \t]* \() [ \t]* ([^)$\s]*) )?
  269. ()~mx', (string) $rc->getDocComment(), $matches, PREG_SET_ORDER);
  270. $methods = [];
  271. foreach ($matches as list(, $op, $prop, $bracket, $type)) {
  272. if ($bracket !== '(') {
  273. trigger_error("Bracket must be immediately after @method $op$prop() in class $class.", E_USER_WARNING);
  274. }
  275. $name = $op . $prop;
  276. $prop = strtolower($prop[0]) . substr($prop, 1) . ($op === 'add' ? 's' : '');
  277. if ($rc->hasProperty($prop) && ($rp = $rc->getProperty($prop)) && !$rp->isStatic()) {
  278. $rp->setAccessible(TRUE);
  279. if ($op === 'get' || $op === 'is') {
  280. $type = NULL;
  281. $op = 'get';
  282. } elseif (!$type && preg_match('#@var[ \t]+(\S+)' . ($op === 'add' ? '\[\]#' : '#'), (string) $rp->getDocComment(), $m)) {
  283. $type = $m[1];
  284. }
  285. if ($rc->inNamespace() && preg_match('#^[A-Z]\w+(\[|\||\z)#', (string) $type)) {
  286. $type = $rc->getNamespaceName() . '\\' . $type;
  287. }
  288. $methods[$name] = [$op, $rp, $type];
  289. }
  290. }
  291. return $methods;
  292. }
  293. /**
  294. * Finds whether a variable is of expected type and do non-data-loss conversion.
  295. * @return bool
  296. * @internal
  297. */
  298. public static function checkType(& $val, $type)
  299. {
  300. if (strpos($type, '|') !== FALSE) {
  301. $found = NULL;
  302. foreach (explode('|', $type) as $type) {
  303. $tmp = $val;
  304. if (self::checkType($tmp, $type)) {
  305. if ($val === $tmp) {
  306. return TRUE;
  307. }
  308. $found[] = $tmp;
  309. }
  310. }
  311. if ($found) {
  312. $val = $found[0];
  313. return TRUE;
  314. }
  315. return FALSE;
  316. } elseif (substr($type, -2) === '[]') {
  317. if (!is_array($val)) {
  318. return FALSE;
  319. }
  320. $type = substr($type, 0, -2);
  321. $res = [];
  322. foreach ($val as $k => $v) {
  323. if (!self::checkType($v, $type)) {
  324. return FALSE;
  325. }
  326. $res[$k] = $v;
  327. }
  328. $val = $res;
  329. return TRUE;
  330. }
  331. switch (strtolower($type)) {
  332. case NULL:
  333. case 'mixed':
  334. return TRUE;
  335. case 'bool':
  336. case 'boolean':
  337. return ($val === NULL || is_scalar($val)) && settype($val, 'bool');
  338. case 'string':
  339. return ($val === NULL || is_scalar($val) || (is_object($val) && method_exists($val, '__toString'))) && settype($val, 'string');
  340. case 'int':
  341. case 'integer':
  342. return ($val === NULL || is_bool($val) || is_numeric($val)) && ((float) (int) $val === (float) $val) && settype($val, 'int');
  343. case 'float':
  344. return ($val === NULL || is_bool($val) || is_numeric($val)) && settype($val, 'float');
  345. case 'scalar':
  346. case 'array':
  347. case 'object':
  348. case 'callable':
  349. case 'resource':
  350. case 'null':
  351. return call_user_func("is_$type", $val);
  352. default:
  353. return $val instanceof $type;
  354. }
  355. }
  356. /********************* extension methods ****************d*g**/
  357. /**
  358. * Adds a method to class.
  359. * @param string
  360. * @param string
  361. * @param mixed callable
  362. * @return void
  363. */
  364. public static function setExtensionMethod($class, $name, $callback)
  365. {
  366. $name = strtolower($name);
  367. self::$extMethods[$name][$class] = Callback::check($callback);
  368. self::$extMethods[$name][''] = NULL;
  369. }
  370. /**
  371. * Returns extension method.
  372. * @param string
  373. * @param string
  374. * @return mixed
  375. */
  376. public static function getExtensionMethod($class, $name)
  377. {
  378. $list = & self::$extMethods[strtolower($name)];
  379. $cache = & $list[''][$class];
  380. if (isset($cache)) {
  381. return $cache;
  382. }
  383. foreach ([$class] + class_parents($class) + class_implements($class) as $cl) {
  384. if (isset($list[$cl])) {
  385. return $cache = $list[$cl];
  386. }
  387. }
  388. return $cache = FALSE;
  389. }
  390. /**
  391. * Returns extension methods.
  392. * @param string
  393. * @return array
  394. */
  395. public static function getExtensionMethods($class)
  396. {
  397. $res = [];
  398. foreach (array_keys(self::$extMethods) as $name) {
  399. if ($cb = self::getExtensionMethod($class, $name)) {
  400. $res[$name] = $cb;
  401. }
  402. }
  403. return $res;
  404. }
  405. /********************* utilities ****************d*g**/
  406. /**
  407. * Finds the best suggestion (for 8-bit encoding).
  408. * @return string|NULL
  409. * @internal
  410. */
  411. public static function getSuggestion(array $possibilities, $value)
  412. {
  413. $norm = preg_replace($re = '#^(get|set|has|is|add)(?=[A-Z])#', '', $value);
  414. $best = NULL;
  415. $min = (strlen($value) / 4 + 1) * 10 + .1;
  416. foreach (array_unique($possibilities, SORT_REGULAR) as $item) {
  417. $item = $item instanceof \Reflector ? $item->getName() : $item;
  418. if ($item !== $value && (
  419. ($len = levenshtein($item, $value, 10, 11, 10)) < $min
  420. || ($len = levenshtein(preg_replace($re, '', $item), $norm, 10, 11, 10) + 20) < $min
  421. )) {
  422. $min = $len;
  423. $best = $item;
  424. }
  425. }
  426. return $best;
  427. }
  428. private static function parseFullDoc(\ReflectionClass $rc, $pattern)
  429. {
  430. do {
  431. $doc[] = $rc->getDocComment();
  432. } while ($rc = $rc->getParentClass());
  433. return preg_match_all($pattern, implode($doc), $m) ? $m[1] : [];
  434. }
  435. /**
  436. * Checks if the public non-static property exists.
  437. * @return bool|'event'
  438. * @internal
  439. */
  440. public static function hasProperty($class, $name)
  441. {
  442. static $cache;
  443. $prop = & $cache[$class][$name];
  444. if ($prop === NULL) {
  445. $prop = FALSE;
  446. try {
  447. $rp = new \ReflectionProperty($class, $name);
  448. if ($rp->isPublic() && !$rp->isStatic()) {
  449. $prop = $name >= 'onA' && $name < 'on_' ? 'event' : TRUE;
  450. }
  451. } catch (\ReflectionException $e) {
  452. }
  453. }
  454. return $prop;
  455. }
  456. /**
  457. * Returns array of public (static, non-static and magic) methods.
  458. * @return array
  459. * @internal
  460. */
  461. public static function & getMethods($class)
  462. {
  463. static $cache;
  464. if (!isset($cache[$class])) {
  465. $cache[$class] = array_fill_keys(get_class_methods($class), 0) + self::getMagicMethods($class);
  466. if ($parent = get_parent_class($class)) {
  467. $cache[$class] += self::getMethods($parent);
  468. }
  469. }
  470. return $cache[$class];
  471. }
  472. /** @internal */
  473. public static function getSource()
  474. {
  475. foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $item) {
  476. if (isset($item['file']) && dirname($item['file']) !== __DIR__) {
  477. return " in $item[file]:$item[line]";
  478. }
  479. }
  480. }
  481. }