Routes.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822
  1. <?php
  2. namespace Luracast\Restler;
  3. use Luracast\Restler\Data\ApiMethodInfo;
  4. use Luracast\Restler\Data\Text;
  5. use ReflectionClass;
  6. use ReflectionMethod;
  7. use ReflectionProperty;
  8. use Exception;
  9. /**
  10. * Router class that routes the urls to api methods along with parameters
  11. *
  12. * @category Framework
  13. * @package Restler
  14. * @author R.Arul Kumaran <arul@luracast.com>
  15. * @copyright 2010 Luracast
  16. * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
  17. * @link http://luracast.com/products/restler/
  18. *
  19. */
  20. class Routes
  21. {
  22. public static $prefixingParameterNames = array(
  23. 'id'
  24. );
  25. public static $fieldTypesByName = array(
  26. 'email' => 'email',
  27. 'password' => 'password',
  28. 'phone' => 'tel',
  29. 'mobile' => 'tel',
  30. 'tel' => 'tel',
  31. 'search' => 'search',
  32. 'date' => 'date',
  33. 'created_at' => 'datetime',
  34. 'modified_at' => 'datetime',
  35. 'url' => 'url',
  36. 'link' => 'url',
  37. 'href' => 'url',
  38. 'website' => 'url',
  39. 'color' => 'color',
  40. 'colour' => 'color',
  41. );
  42. protected static $routes = array();
  43. protected static $models = array();
  44. /**
  45. * Route the public and protected methods of an Api class
  46. *
  47. * @param string $className
  48. * @param string $resourcePath
  49. * @param int $version
  50. *
  51. * @throws RestException
  52. */
  53. public static function addAPIClass($className, $resourcePath = '', $version = 1)
  54. {
  55. /*
  56. * Mapping Rules
  57. * =============
  58. *
  59. * - Optional parameters should not be mapped to URL
  60. * - If a required parameter is of primitive type
  61. * - If one of the self::$prefixingParameterNames
  62. * - Map it to URL
  63. * - Else If request method is POST/PUT/PATCH
  64. * - Map it to body
  65. * - Else If request method is GET/DELETE
  66. * - Map it to body
  67. * - If a required parameter is not primitive type
  68. * - Do not include it in URL
  69. */
  70. $class = new ReflectionClass($className);
  71. $dataName = CommentParser::$embeddedDataName;
  72. try {
  73. $classMetadata = CommentParser::parse($class->getDocComment());
  74. } catch (Exception $e) {
  75. throw new RestException(500, "Error while parsing comments of `$className` class. " . $e->getMessage());
  76. }
  77. $classMetadata['scope'] = $scope = static::scope($class);
  78. $methods = $class->getMethods(ReflectionMethod::IS_PUBLIC +
  79. ReflectionMethod::IS_PROTECTED);
  80. foreach ($methods as $method) {
  81. $methodUrl = strtolower($method->getName());
  82. //method name should not begin with _
  83. if ($methodUrl[0] == '_') {
  84. continue;
  85. }
  86. $doc = $method->getDocComment();
  87. try {
  88. $metadata = CommentParser::parse($doc) + $classMetadata;
  89. } catch (Exception $e) {
  90. throw new RestException(500, "Error while parsing comments of `{$className}::{$method->getName()}` method. " . $e->getMessage());
  91. }
  92. //@access should not be private
  93. if (isset($metadata['access'])
  94. && $metadata['access'] == 'private'
  95. ) {
  96. continue;
  97. }
  98. $arguments = array();
  99. $defaults = array();
  100. $params = $method->getParameters();
  101. $position = 0;
  102. $pathParams = array();
  103. $allowAmbiguity
  104. = (isset($metadata['smart-auto-routing'])
  105. && $metadata['smart-auto-routing'] != 'true')
  106. || !Defaults::$smartAutoRouting;
  107. $metadata['resourcePath'] = trim($resourcePath, '/');
  108. if (isset($classMetadata['description'])) {
  109. $metadata['classDescription'] = $classMetadata['description'];
  110. }
  111. if (isset($classMetadata['classLongDescription'])) {
  112. $metadata['classLongDescription']
  113. = $classMetadata['longDescription'];
  114. }
  115. if (!isset($metadata['param'])) {
  116. $metadata['param'] = array();
  117. }
  118. if (isset($metadata['return']['type'])) {
  119. if ($qualified = Scope::resolve($metadata['return']['type'], $scope))
  120. list($metadata['return']['type'], $metadata['return']['children']) =
  121. static::getTypeAndModel(new ReflectionClass($qualified), $scope);
  122. } else {
  123. //assume return type is array
  124. $metadata['return']['type'] = 'array';
  125. }
  126. foreach ($params as $param) {
  127. $children = array();
  128. $type =
  129. $param->isArray() ? 'array' : $param->getClass();
  130. $arguments[$param->getName()] = $position;
  131. $defaults[$position] = $param->isDefaultValueAvailable() ?
  132. $param->getDefaultValue() : null;
  133. if (!isset($metadata['param'][$position])) {
  134. $metadata['param'][$position] = array();
  135. }
  136. $m = & $metadata ['param'] [$position];
  137. $m ['name'] = $param->getName();
  138. if (!isset($m[$dataName])) {
  139. $m[$dataName] = array();
  140. }
  141. $p = &$m[$dataName];
  142. if (empty($m['label']))
  143. $m['label'] = Text::title($m['name']);
  144. if (is_null($type) && isset($m['type'])) {
  145. $type = $m['type'];
  146. }
  147. if (isset(static::$fieldTypesByName[$m['name']]) && empty($p['type']) && $type == 'string') {
  148. $p['type'] = static::$fieldTypesByName[$m['name']];
  149. }
  150. $m ['default'] = $defaults [$position];
  151. $m ['required'] = !$param->isOptional();
  152. $contentType = Util::nestedValue($p,'type');
  153. if ($type == 'array' && $contentType && $qualified = Scope::resolve($contentType, $scope)) {
  154. list($p['type'], $children, $modelName) = static::getTypeAndModel(
  155. new ReflectionClass($qualified), $scope,
  156. $className . Text::title($methodUrl), $p
  157. );
  158. }
  159. if ($type instanceof ReflectionClass) {
  160. list($type, $children, $modelName) = static::getTypeAndModel($type, $scope,
  161. $className . Text::title($methodUrl), $p);
  162. } elseif ($type && is_string($type) && $qualified = Scope::resolve($type, $scope)) {
  163. list($type, $children, $modelName)
  164. = static::getTypeAndModel(new ReflectionClass($qualified), $scope,
  165. $className . Text::title($methodUrl), $p);
  166. }
  167. if (isset($type)) {
  168. $m['type'] = $type;
  169. }
  170. $m['children'] = $children;
  171. if (isset($modelName)) {
  172. $m['model'] = $modelName;
  173. }
  174. if ($m['name'] == Defaults::$fullRequestDataName) {
  175. $from = 'body';
  176. if (!isset($m['type'])) {
  177. $type = $m['type'] = 'array';
  178. }
  179. } elseif (isset($p['from'])) {
  180. $from = $p['from'];
  181. } else {
  182. if ((isset($type) && Util::isObjectOrArray($type))
  183. ) {
  184. $from = 'body';
  185. if (!isset($type)) {
  186. $type = $m['type'] = 'array';
  187. }
  188. } elseif ($m['required'] && in_array($m['name'], static::$prefixingParameterNames)) {
  189. $from = 'path';
  190. } else {
  191. $from = 'body';
  192. }
  193. }
  194. $p['from'] = $from;
  195. if (!isset($m['type'])) {
  196. $type = $m['type'] = static::type($defaults[$position]);
  197. }
  198. if ($allowAmbiguity || $from == 'path') {
  199. $pathParams [] = $position;
  200. }
  201. $position++;
  202. }
  203. $accessLevel = 0;
  204. if ($method->isProtected()) {
  205. $accessLevel = 3;
  206. } elseif (isset($metadata['access'])) {
  207. if ($metadata['access'] == 'protected') {
  208. $accessLevel = 2;
  209. } elseif ($metadata['access'] == 'hybrid') {
  210. $accessLevel = 1;
  211. }
  212. } elseif (isset($metadata['protected'])) {
  213. $accessLevel = 2;
  214. }
  215. /*
  216. echo " access level $accessLevel for $className::"
  217. .$method->getName().$method->isProtected().PHP_EOL;
  218. */
  219. // take note of the order
  220. $call = array(
  221. 'url' => null,
  222. 'className' => $className,
  223. 'path' => rtrim($resourcePath, '/'),
  224. 'methodName' => $method->getName(),
  225. 'arguments' => $arguments,
  226. 'defaults' => $defaults,
  227. 'metadata' => $metadata,
  228. 'accessLevel' => $accessLevel,
  229. );
  230. // if manual route
  231. if (preg_match_all(
  232. '/@url\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)'
  233. . '[ \t]*\/?(\S*)/s',
  234. $doc, $matches, PREG_SET_ORDER
  235. )
  236. ) {
  237. foreach ($matches as $match) {
  238. $httpMethod = $match[1];
  239. $url = rtrim($resourcePath . $match[2], '/');
  240. //deep copy the call, as it may change for each @url
  241. $copy = unserialize(serialize($call));
  242. foreach ($copy['metadata']['param'] as $i => $p) {
  243. $inPath =
  244. strpos($url, '{' . $p['name'] . '}') ||
  245. strpos($url, ':' . $p['name']);
  246. if ($inPath) {
  247. $copy['metadata']['param'][$i][$dataName]['from'] = 'path';
  248. } elseif ($httpMethod == 'GET' || $httpMethod == 'DELETE') {
  249. $copy['metadata']['param'][$i][$dataName]['from'] = 'query';
  250. } elseif (empty($p[$dataName]['from']) || $p[$dataName]['from'] == 'path') {
  251. $copy['metadata']['param'][$i][$dataName]['from'] = 'body';
  252. }
  253. }
  254. $url = preg_replace_callback('/{[^}]+}|:[^\/]+/',
  255. function ($matches) use ($copy) {
  256. $match = trim($matches[0], '{}:');
  257. $index = $copy['arguments'][$match];
  258. return '{' .
  259. Routes::typeChar(isset(
  260. $copy['metadata']['param'][$index]['type'])
  261. ? $copy['metadata']['param'][$index]['type']
  262. : null)
  263. . $index . '}';
  264. }, $url);
  265. static::addPath($url, $copy, $httpMethod, $version);
  266. }
  267. //if auto route enabled, do so
  268. } elseif (Defaults::$autoRoutingEnabled) {
  269. // no configuration found so use convention
  270. if (preg_match_all(
  271. '/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)/i',
  272. $methodUrl, $matches)
  273. ) {
  274. $httpMethod = strtoupper($matches[0][0]);
  275. $methodUrl = substr($methodUrl, strlen($httpMethod));
  276. } else {
  277. $httpMethod = 'GET';
  278. }
  279. if ($methodUrl == 'index') {
  280. $methodUrl = '';
  281. }
  282. $url = empty($methodUrl) ? rtrim($resourcePath, '/')
  283. : $resourcePath . $methodUrl;
  284. for ($position = 0; $position < count($params); $position++) {
  285. $from = $metadata['param'][$position][$dataName]['from'];
  286. if ($from == 'body' && ($httpMethod == 'GET' ||
  287. $httpMethod == 'DELETE')
  288. ) {
  289. $call['metadata']['param'][$position][$dataName]['from']
  290. = 'query';
  291. }
  292. }
  293. if (empty($pathParams) || $allowAmbiguity) {
  294. static::addPath($url, $call, $httpMethod, $version);
  295. }
  296. $lastPathParam = end($pathParams);
  297. foreach ($pathParams as $position) {
  298. if (!empty($url))
  299. $url .= '/';
  300. $url .= '{' .
  301. static::typeChar(isset($call['metadata']['param'][$position]['type'])
  302. ? $call['metadata']['param'][$position]['type']
  303. : null)
  304. . $position . '}';
  305. if ($allowAmbiguity || $position == $lastPathParam) {
  306. static::addPath($url, $call, $httpMethod, $version);
  307. }
  308. }
  309. }
  310. }
  311. }
  312. /**
  313. * @access private
  314. */
  315. public static function typeChar($type = null)
  316. {
  317. if (!$type) {
  318. return 's';
  319. }
  320. switch ($type[0]) {
  321. case 'i':
  322. case 'f':
  323. return 'n';
  324. }
  325. return 's';
  326. }
  327. protected static function addPath($path, array $call,
  328. $httpMethod = 'GET', $version = 1)
  329. {
  330. $call['url'] = preg_replace_callback(
  331. "/\{\S(\d+)\}/",
  332. function ($matches) use ($call) {
  333. return '{' .
  334. $call['metadata']['param'][$matches[1]]['name'] . '}';
  335. },
  336. $path
  337. );
  338. //check for wildcard routes
  339. if (substr($path, -1, 1) == '*') {
  340. $path = rtrim($path, '/*');
  341. static::$routes["v$version"]['*'][$path][$httpMethod] = $call;
  342. } else {
  343. static::$routes["v$version"][$path][$httpMethod] = $call;
  344. //create an alias with index if the method name is index
  345. if ($call['methodName'] == 'index')
  346. static::$routes["v$version"][ltrim("$path/index", '/')][$httpMethod] = $call;
  347. }
  348. }
  349. /**
  350. * Find the api method for the given url and http method
  351. *
  352. * @param string $path Requested url path
  353. * @param string $httpMethod GET|POST|PUT|PATCH|DELETE etc
  354. * @param int $version Api Version number
  355. * @param array $data Data collected from the request
  356. *
  357. * @throws RestException
  358. * @return ApiMethodInfo
  359. */
  360. public static function find($path, $httpMethod,
  361. $version = 1, array $data = array())
  362. {
  363. $p = Util::nestedValue(static::$routes, "v$version");
  364. if (!$p) {
  365. throw new RestException(
  366. 404,
  367. $version == 1 ? '' : "Version $version is not supported"
  368. );
  369. }
  370. $status = 404;
  371. $message = null;
  372. $methods = array();
  373. if (isset($p[$path][$httpMethod])) {
  374. //================== static routes ==========================
  375. return static::populate($p[$path][$httpMethod], $data);
  376. } elseif (isset($p['*'])) {
  377. //================== wildcard routes ========================
  378. uksort($p['*'], function ($a, $b) {
  379. return strlen($b) - strlen($a);
  380. });
  381. foreach ($p['*'] as $key => $value) {
  382. if (strpos($path, $key) === 0 && isset($value[$httpMethod])) {
  383. //path found, convert rest of the path to parameters
  384. $path = substr($path, strlen($key) + 1);
  385. $call = ApiMethodInfo::__set_state($value[$httpMethod]);
  386. $call->parameters = empty($path)
  387. ? array()
  388. : explode('/', $path);
  389. return $call;
  390. }
  391. }
  392. }
  393. //================== dynamic routes =============================
  394. //add newline char if trailing slash is found
  395. if (substr($path, -1) == '/')
  396. $path .= PHP_EOL;
  397. //if double slash is found fill in newline char;
  398. $path = str_replace('//', '/' . PHP_EOL . '/', $path);
  399. ksort($p);
  400. foreach ($p as $key => $value) {
  401. if (!isset($value[$httpMethod])) {
  402. continue;
  403. }
  404. $regex = str_replace(array('{', '}'),
  405. array('(?P<', '>[^/]+)'), $key);
  406. if (preg_match_all(":^$regex$:i", $path, $matches, PREG_SET_ORDER)) {
  407. $matches = $matches[0];
  408. $found = true;
  409. foreach ($matches as $k => $v) {
  410. if (is_numeric($k)) {
  411. unset($matches[$k]);
  412. continue;
  413. }
  414. $index = intval(substr($k, 1));
  415. $details = $value[$httpMethod]['metadata']['param'][$index];
  416. if ($k[0] == 's' || strpos($k, static::pathVarTypeOf($v)) === 0) {
  417. //remove the newlines
  418. $data[$details['name']] = trim($v, PHP_EOL);
  419. } else {
  420. $status = 400;
  421. $message = 'invalid value specified for `'
  422. . $details['name'] . '`';
  423. $found = false;
  424. break;
  425. }
  426. }
  427. if ($found) {
  428. return static::populate($value[$httpMethod], $data);
  429. }
  430. }
  431. }
  432. if ($status == 404) {
  433. //check if other methods are allowed
  434. if (isset($p[$path])) {
  435. $status = 405;
  436. $methods = array_keys($p[$path]);
  437. }
  438. }
  439. if ($status == 405) {
  440. header('Allow: ' . implode(', ', $methods));
  441. }
  442. throw new RestException($status, $message);
  443. }
  444. public static function findAll(array $excludedPaths = array(), array $excludedHttpMethods = array(), $version = 1)
  445. {
  446. $map = array();
  447. $all = Util::nestedValue(self::$routes, "v$version");
  448. $filter = array();
  449. if (isset($all['*'])) {
  450. $all = $all['*'] + $all;
  451. unset($all['*']);
  452. }
  453. if(is_array($all)){
  454. foreach ($all as $fullPath => $routes) {
  455. foreach ($routes as $httpMethod => $route) {
  456. if (in_array($httpMethod, $excludedHttpMethods)) {
  457. continue;
  458. }
  459. foreach ($excludedPaths as $exclude) {
  460. if (empty($exclude)) {
  461. if ($fullPath == $exclude || $fullPath == 'index')
  462. continue 2;
  463. } elseif (Text::beginsWith($fullPath, $exclude)) {
  464. continue 2;
  465. }
  466. }
  467. $hash = "$httpMethod " . $route['url'];
  468. if (!isset($filter[$hash])) {
  469. $route['httpMethod'] = $httpMethod;
  470. $map[$route['metadata']['resourcePath']][]
  471. = array('access' => static::verifyAccess($route), 'route' => $route, 'hash' => $hash);
  472. $filter[$hash] = true;
  473. }
  474. }
  475. }
  476. }
  477. return $map;
  478. }
  479. public static function verifyAccess($route)
  480. {
  481. if ($route['accessLevel'] < 2)
  482. return true;
  483. /** @var Restler $r */
  484. $r = Scope::get('Restler');
  485. $authenticated = $r->_authenticated;
  486. if (!$authenticated && $route['accessLevel'] > 1)
  487. return false;
  488. if (
  489. $authenticated &&
  490. Defaults::$accessControlFunction &&
  491. (!call_user_func(Defaults::$accessControlFunction, $route['metadata']))
  492. ) {
  493. return false;
  494. }
  495. return true;
  496. }
  497. /**
  498. * Populates the parameter values
  499. *
  500. * @param array $call
  501. * @param $data
  502. *
  503. * @return ApiMethodInfo
  504. *
  505. * @access private
  506. */
  507. protected static function populate(array $call, $data)
  508. {
  509. $call['parameters'] = $call['defaults'];
  510. $p = & $call['parameters'];
  511. $dataName = CommentParser::$embeddedDataName;
  512. foreach ($data as $key => $value) {
  513. if (isset($call['arguments'][$key])) {
  514. $p[$call['arguments'][$key]] = $value;
  515. }
  516. }
  517. if (Defaults::$smartParameterParsing) {
  518. if (
  519. ($m = Util::nestedValue($call, 'metadata', 'param', 0)) &&
  520. !array_key_exists($m['name'], $data) &&
  521. array_key_exists(Defaults::$fullRequestDataName, $data) &&
  522. !is_null($d = $data[Defaults::$fullRequestDataName]) &&
  523. isset($m['type']) &&
  524. static::typeMatch($m['type'], $d)
  525. ) {
  526. $p[0] = $d;
  527. } else {
  528. $bodyParamCount = 0;
  529. $lastBodyParamIndex = -1;
  530. $lastM = null;
  531. foreach ($call['metadata']['param'] as $k => $m) {
  532. if ($m[$dataName]['from'] == 'body') {
  533. $bodyParamCount++;
  534. $lastBodyParamIndex = $k;
  535. $lastM = $m;
  536. }
  537. }
  538. if (
  539. $bodyParamCount == 1 &&
  540. !array_key_exists($lastM['name'], $data) &&
  541. array_key_exists(Defaults::$fullRequestDataName, $data) &&
  542. !is_null($d = $data[Defaults::$fullRequestDataName])
  543. ) {
  544. $p[$lastBodyParamIndex] = $d;
  545. }
  546. }
  547. }
  548. $r = ApiMethodInfo::__set_state($call);
  549. $modifier = "_modify_{$r->methodName}_api";
  550. if (method_exists($r->className, $modifier)) {
  551. $stage = end(Scope::get('Restler')->getEvents());
  552. if (empty($stage))
  553. $stage = 'setup';
  554. $r = Scope::get($r->className)->$modifier($r, $stage) ? : $r;
  555. }
  556. return $r;
  557. }
  558. /**
  559. * @access private
  560. */
  561. protected static function pathVarTypeOf($var)
  562. {
  563. if (is_numeric($var)) {
  564. return 'n';
  565. }
  566. if ($var === 'true' || $var === 'false') {
  567. return 'b';
  568. }
  569. return 's';
  570. }
  571. protected static function typeMatch($type, $var)
  572. {
  573. switch ($type) {
  574. case 'boolean':
  575. case 'bool':
  576. return is_bool($var);
  577. case 'array':
  578. case 'object':
  579. return is_array($var);
  580. case 'string':
  581. case 'int':
  582. case 'integer':
  583. case 'float':
  584. case 'number':
  585. return is_scalar($var);
  586. }
  587. return true;
  588. }
  589. protected static function parseMagic(ReflectionClass $class, $forResponse = true)
  590. {
  591. if (!$c = CommentParser::parse($class->getDocComment())) {
  592. return false;
  593. }
  594. $p = 'property';
  595. $r = empty($c[$p]) ? array() : $c[$p];
  596. $p .= '-' . ($forResponse ? 'read' : 'write');
  597. if (!empty($c[$p])) {
  598. $r = array_merge($r, $c[$p]);
  599. }
  600. return $r;
  601. }
  602. /**
  603. * Get the type and associated model
  604. *
  605. * @param ReflectionClass $class
  606. * @param array $scope
  607. *
  608. * @throws RestException
  609. * @throws \Exception
  610. * @return array
  611. *
  612. * @access protected
  613. */
  614. protected static function getTypeAndModel(ReflectionClass $class, array $scope, $prefix='', array $rules=array())
  615. {
  616. $className = $class->getName();
  617. $dataName = CommentParser::$embeddedDataName;
  618. if (isset(static::$models[$prefix.$className])) {
  619. return static::$models[$prefix.$className];
  620. }
  621. $children = array();
  622. try {
  623. if ($magic_properties = static::parseMagic($class, empty($prefix))) {
  624. foreach ($magic_properties as $prop) {
  625. if (!isset($prop['name'])) {
  626. throw new Exception('@property comment is not properly defined in ' . $className . ' class');
  627. }
  628. if (!isset($prop[$dataName]['label'])) {
  629. $prop[$dataName]['label'] = Text::title($prop['name']);
  630. }
  631. if (isset(static::$fieldTypesByName[$prop['name']]) && $prop['type'] == 'string' && !isset($prop[$dataName]['type'])) {
  632. $prop[$dataName]['type'] = static::$fieldTypesByName[$prop['name']];
  633. }
  634. $children[$prop['name']] = $prop;
  635. }
  636. } else {
  637. $props = $class->getProperties(ReflectionProperty::IS_PUBLIC);
  638. foreach ($props as $prop) {
  639. $name = $prop->getName();
  640. $child = array('name' => $name);
  641. if ($c = $prop->getDocComment()) {
  642. $child += Util::nestedValue(CommentParser::parse($c), 'var') ?: array();
  643. } else {
  644. $o = $class->newInstance();
  645. $p = $prop->getValue($o);
  646. if (is_object($p)) {
  647. $child['type'] = get_class($p);
  648. } elseif (is_array($p)) {
  649. $child['type'] = 'array';
  650. if (count($p)) {
  651. $pc = reset($p);
  652. if (is_object($pc)) {
  653. $child['contentType'] = get_class($pc);
  654. }
  655. }
  656. }
  657. }
  658. $child += array(
  659. 'type' => isset(static::$fieldTypesByName[$child['name']])
  660. ? static::$fieldTypesByName[$child['name']]
  661. : 'string',
  662. 'label' => Text::title($child['name'])
  663. );
  664. isset($child[$dataName])
  665. ? $child[$dataName] += array('required' => true)
  666. : $child[$dataName]['required'] = true;
  667. if ($prop->class != $className && $qualified = Scope::resolve($child['type'], $scope)) {
  668. list($child['type'], $child['children'])
  669. = static::getTypeAndModel(new ReflectionClass($qualified), $scope);
  670. } elseif (
  671. ($contentType = Util::nestedValue($child, $dataName, 'type')) &&
  672. ($qualified = Scope::resolve($contentType, $scope))
  673. ) {
  674. list($child['contentType'], $child['children'])
  675. = static::getTypeAndModel(new ReflectionClass($qualified), $scope);
  676. }
  677. $children[$name] = $child;
  678. }
  679. }
  680. } catch (Exception $e) {
  681. if (Text::endsWith($e->getFile(), 'CommentParser.php')) {
  682. throw new RestException(500, "Error while parsing comments of `$className` class. " . $e->getMessage());
  683. }
  684. throw $e;
  685. }
  686. if ($properties = Util::nestedValue($rules, 'properties')) {
  687. if (is_string($properties)) {
  688. $properties = array($properties);
  689. }
  690. $c = array();
  691. foreach ($properties as $property) {
  692. if (isset($children[$property])) {
  693. $c[$property] = $children[$property];
  694. }
  695. }
  696. $children = $c;
  697. }
  698. if ($required = Util::nestedValue($rules, 'required')) {
  699. //override required on children
  700. if (is_bool($required)) {
  701. // true means all are required false means none are required
  702. $required = $required ? array_keys($children) : array();
  703. } elseif (is_string($required)) {
  704. $required = array($required);
  705. }
  706. $required = array_fill_keys($required, true);
  707. foreach ($children as $name => $child) {
  708. $children[$name][$dataName]['required'] = isset($required[$name]);
  709. }
  710. }
  711. static::$models[$prefix.$className] = array($className, $children, $prefix.$className);
  712. return static::$models[$prefix.$className];
  713. }
  714. /**
  715. * Import previously created routes from cache
  716. *
  717. * @param array $routes
  718. */
  719. public static function fromArray(array $routes)
  720. {
  721. static::$routes = $routes;
  722. }
  723. /**
  724. * Export current routes for cache
  725. *
  726. * @return array
  727. */
  728. public static function toArray()
  729. {
  730. return static::$routes;
  731. }
  732. public static function type($var)
  733. {
  734. if (is_object($var)) return get_class($var);
  735. if (is_array($var)) return 'array';
  736. if (is_bool($var)) return 'boolean';
  737. if (is_numeric($var)) return is_float($var) ? 'float' : 'int';
  738. return 'string';
  739. }
  740. public static function scope(ReflectionClass $class)
  741. {
  742. $namespace = $class->getNamespaceName();
  743. $imports = array(
  744. '*' => empty($namespace) ? '' : $namespace . '\\'
  745. );
  746. $file = file_get_contents($class->getFileName());
  747. $tokens = token_get_all($file);
  748. $namespace = '';
  749. $alias = '';
  750. $reading = false;
  751. $last = 0;
  752. foreach ($tokens as $token) {
  753. if (is_string($token)) {
  754. if ($reading && ',' == $token) {
  755. //===== STOP =====//
  756. $reading = false;
  757. if (!empty($namespace))
  758. $imports[$alias] = trim($namespace, '\\');
  759. //===== START =====//
  760. $reading = true;
  761. $namespace = '';
  762. $alias = '';
  763. } else {
  764. //===== STOP =====//
  765. $reading = false;
  766. if (!empty($namespace))
  767. $imports[$alias] = trim($namespace, '\\');
  768. }
  769. } elseif (T_USE == $token[0]) {
  770. //===== START =====//
  771. $reading = true;
  772. $namespace = '';
  773. $alias = '';
  774. } elseif ($reading) {
  775. //echo token_name($token[0]) . ' ' . $token[1] . PHP_EOL;
  776. switch ($token[0]) {
  777. case T_WHITESPACE:
  778. continue 2;
  779. case T_STRING:
  780. $alias = $token[1];
  781. if (T_AS == $last) {
  782. break;
  783. }
  784. //don't break;
  785. case T_NS_SEPARATOR:
  786. $namespace .= $token[1];
  787. break;
  788. }
  789. $last = $token[0];
  790. }
  791. }
  792. return $imports;
  793. }
  794. }