PageRenderTime 59ms CodeModel.GetById 12ms RepoModel.GetById 1ms app.codeStats 0ms

/api/vendor/peej/tonic/src/Tonic/Application.php

https://gitlab.com/x33n/respond
PHP | 414 lines | 317 code | 42 blank | 55 comment | 66 complexity | 99f07da286dbca271009796925c4f63e MD5 | raw file
  1. <?php
  2. namespace Tonic;
  3. /**
  4. * A Tonic application
  5. */
  6. class Application
  7. {
  8. /**
  9. * Application configuration options
  10. */
  11. private $options = array();
  12. private $baseUri = '';
  13. /**
  14. * Metadata of the loaded resources
  15. */
  16. private $resources = array();
  17. public function __construct($options = array())
  18. {
  19. if (isset($options['baseUri'])) {
  20. $this->baseUri = $options['baseUri'];
  21. } elseif (isset($_SERVER['DOCUMENT_URI'])) {
  22. $this->baseUri = dirname($_SERVER['DOCUMENT_URI']);
  23. }
  24. $this->options = $options;
  25. // load resource metadata passed in via options array
  26. if (isset($options['resources']) && is_array($options['resources'])) {
  27. $this->resources = $options['resources'];
  28. }
  29. $cache = isset($options['cache']) ? $options['cache'] : NULL;
  30. if ($cache && $cache->isCached()) { // if we've been given a annotation cache, use it
  31. $this->resources = $cache->load();
  32. } else { // otherwise load from loaded resource files
  33. if (isset($options['load'])) { // load given resource class files
  34. $this->loadResourceFiles($options['load']);
  35. }
  36. $this->loadResourceMetadata();
  37. if ($cache) { // save metadata into annotation cache
  38. $cache->save($this->resources);
  39. }
  40. }
  41. // set any URI-space mount points we've been given
  42. if (isset($options['mount']) && is_array($options['mount'])) {
  43. foreach ($options['mount'] as $namespaceName => $uriSpace) {
  44. $this->mount($namespaceName, $uriSpace);
  45. }
  46. }
  47. }
  48. /**
  49. * Include PHP files containing resources in the given filename globs
  50. * @paramstr[] $filenames Array of filename globs
  51. */
  52. private function loadResourceFiles($filenames)
  53. {
  54. if (!is_array($filenames)) {
  55. $filenames = array($filenames);
  56. }
  57. foreach ($filenames as $glob) {
  58. $globs = glob(str_replace('[', '[[]', $glob));
  59. if ($globs) {
  60. foreach ($globs as $filename) {
  61. require_once $filename;
  62. }
  63. }
  64. }
  65. }
  66. /**
  67. * Load the metadata for all loaded resource classes
  68. * @param str $uriSpace Optional URI-space to mount the resources into
  69. */
  70. private function loadResourceMetadata($uriSpace = NULL)
  71. {
  72. foreach (get_declared_classes() as $className) {
  73. if (
  74. !isset($this->resources[$className]) &&
  75. is_subclass_of($className, 'Tonic\Resource')
  76. ) {
  77. $this->resources[$className] = $this->readResourceAnnotations($className);
  78. if ($uriSpace) {
  79. $this->resources[$className]['uri'][0] = $uriSpace.$this->resources[$className]['uri'][0];
  80. }
  81. $this->resources[$className]['methods'] = $this->readMethodAnnotations($className);
  82. }
  83. }
  84. }
  85. /**
  86. * Add a namespace to a specific URI-space
  87. *
  88. * @param str $namespaceName
  89. * @param str $uriSpace
  90. */
  91. public function mount($namespaceName, $uriSpace)
  92. {
  93. foreach ($this->resources as $className => $metadata) {
  94. if ($metadata['namespace'][0] == $namespaceName) {
  95. if (isset($metadata['uri'])) {
  96. foreach ($metadata['uri'] as $index => $uri) {
  97. $this->resources[$className]['uri'][$index][0] = $uriSpace.$uri[0];
  98. }
  99. }
  100. }
  101. }
  102. }
  103. /**
  104. * Get the URL for the given resource class
  105. *
  106. * @param str $className
  107. * @param str[] $params
  108. * @return str
  109. */
  110. public function uri($className, $params = array())
  111. {
  112. if (is_object($className)) {
  113. $className = get_class($className);
  114. }
  115. if (isset($this->resources[$className])) {
  116. if ($params && !is_array($params)) {
  117. $params = array($params);
  118. }
  119. foreach ($this->resources[$className]['uri'] as $uri) {
  120. if (count($params) == count($uri) - 1) {
  121. $parts = explode('([^/]+)', $uri[0]);
  122. $path = '';
  123. foreach ($parts as $key => $part) {
  124. $path .= $part;
  125. if (isset($params[$key])) {
  126. $path .= $params[$key];
  127. }
  128. }
  129. return $this->baseUri.$path;
  130. }
  131. }
  132. }
  133. }
  134. /**
  135. * Given the request data and the loaded resource metadata, pick the best matching
  136. * resource to handle the request based on URI and priority.
  137. *
  138. * @param Request $request
  139. * @return Resource
  140. */
  141. public function getResource($request = NULL)
  142. {
  143. $matchedResource = NULL;
  144. if (!$request) {
  145. $request= new Request();
  146. }
  147. foreach ($this->resources as $className => $resourceMetadata) {
  148. if (isset($resourceMetadata['uri'])) {
  149. if (!is_array($resourceMetadata['uri'])) {
  150. $resourceMetadata['uri'] = array($resourceMetadata['uri']);
  151. }
  152. foreach ($resourceMetadata['uri'] as $uri) {
  153. if (!is_array($uri)) {
  154. $uri = array($uri);
  155. }
  156. $uriRegex = '|^'.$uri[0].'$|';
  157. if (!isset($resourceMetadata['priority'])) {
  158. $resourceMetadata['priority'] = 1;
  159. }
  160. if (!isset($resourceMetadata['class'])) {
  161. $resourceMetadata['class'] = $className;
  162. }
  163. if (
  164. ($matchedResource == NULL || $matchedResource[0]['priority'] < $resourceMetadata['priority'])
  165. &&
  166. preg_match($uriRegex, $request->uri, $params)
  167. ) {
  168. if (count($uri) > 1) { // has params within URI
  169. $params = array_combine($uri, $params);
  170. }
  171. array_shift($params);
  172. $matchedResource = array($resourceMetadata, $params);
  173. }
  174. }
  175. }
  176. }
  177. if ($matchedResource) {
  178. if (isset($matchedResource[0]['filename']) && is_readable($matchedResource[0]['filename'])) {
  179. require_once($matchedResource[0]['filename']);
  180. }
  181. $request->params = $matchedResource[1];
  182. return new $matchedResource[0]['class']($this, $request, $matchedResource[1]);
  183. } else {
  184. throw new NotFoundException(sprintf('Resource matching URI "%s" not found', $request->uri));
  185. }
  186. }
  187. /**
  188. * Get the already loaded resource annotation metadata
  189. * @param Tonic/Resource $resource
  190. * @return str[]
  191. */
  192. public function getResourceMetadata($resource)
  193. {
  194. if (is_object($resource)) {
  195. $className = get_class($resource);
  196. } else {
  197. $className = $resource;
  198. }
  199. return isset($this->resources[$className]) ? $this->resources[$className] : NULL;
  200. }
  201. /**
  202. * Read the annotation metadata for the given class
  203. * @return str[] Annotation metadata
  204. */
  205. private function readResourceAnnotations($className)
  206. {
  207. $metadata = array();
  208. // get data from reflector
  209. $classReflector = new \ReflectionClass($className);
  210. $metadata['class'] = '\\'.$classReflector->getName();
  211. $metadata['namespace'] = array($classReflector->getNamespaceName());
  212. $metadata['filename'] = $classReflector->getFileName();
  213. $metadata['priority'] = array(1);
  214. // get data from docComment
  215. $docComment = $this->parseDocComment($classReflector->getDocComment());
  216. if (isset($docComment['@uri'])) {
  217. foreach ($docComment['@uri'] as $uri) {
  218. $metadata['uri'][] = $this->uriTemplateToRegex($uri);
  219. }
  220. }
  221. if (isset($docComment['@namespace'])) $metadata['namespace'] = $docComment['@namespace'][0];
  222. if (isset($docComment['@priority'])) $metadata['priority'] = $docComment['@priority'][0];
  223. return $metadata;
  224. }
  225. /**
  226. * Turn a URL template into a regular expression
  227. * @param str[] $uri URL template
  228. * @return str[] Regular expression and parameter names
  229. */
  230. private function uriTemplateToRegex($uri)
  231. {
  232. preg_match_all('#((?<!\?):[^/]+|{[^0-9][^}]*}|\(.+?\))#', $uri[0], $params, PREG_PATTERN_ORDER);
  233. $return = $uri;
  234. if (isset($params[1])) {
  235. foreach ($params[1] as $index => $param) {
  236. if (substr($param, 0, 1) == ':') {
  237. $return[] = substr($param, 1);
  238. } elseif (substr($param, 0, 1) == '{' && substr($param, -1, 1) == '}') {
  239. $return[] = substr($param, 1, -1);
  240. } else {
  241. $return[] = $index;
  242. }
  243. }
  244. }
  245. $return[0] = preg_replace('#((?<!\?):[^(/]+|{[^0-9][^}]*})#', '([^/]+)', $return[0]);
  246. return $return;
  247. }
  248. private function mergeMetadata($array1, $array2) {
  249. foreach ($array2 as $method => $metadata) {
  250. foreach ($metadata as $annotation => $values) {
  251. foreach ($values as $value) {
  252. if (isset($array1[$method][$annotation])) {
  253. if (!in_array($value, $array1[$method][$annotation])) {
  254. $array1[$method][$annotation][] = $value;
  255. }
  256. } else {
  257. $array1[$method][$annotation] = array($value);
  258. }
  259. }
  260. }
  261. }
  262. return $array1;
  263. }
  264. private function readMethodAnnotations($className, $targetClass = null)
  265. {
  266. if (isset($this->resources[$className]) && isset($this->resources[$className]['methods'])) {
  267. return $this->resources[$className]['methods'];
  268. }
  269. if (!$targetClass) {
  270. $targetClass = $className;
  271. }
  272. $metadata = array();
  273. foreach (get_class_methods($className) as $methodName) {
  274. $methodReflector = new \ReflectionMethod($className, $methodName);
  275. if ($methodReflector->isPublic() && $methodReflector->getDeclaringClass()->name != 'Tonic\Resource') {
  276. $methodMetadata = array();
  277. $docComment = $this->parseDocComment($methodReflector->getDocComment());
  278. foreach ($docComment as $annotationName => $value) {
  279. $annotationMethodName = substr($annotationName, 1);
  280. if (method_exists($targetClass, $annotationMethodName)) {
  281. foreach ($value as $v) {
  282. $methodMetadata[$annotationMethodName][] = $v;
  283. }
  284. }
  285. }
  286. $metadata[$methodReflector->getName()] = $methodMetadata;
  287. }
  288. }
  289. // recurse through parent classes and merge in parent class metadata
  290. $classReflector = new \ReflectionClass($className);
  291. $parentReflector = $classReflector->getParentClass();
  292. if ($parentReflector) {
  293. $metadata = $this->mergeMetadata($this->readMethodAnnotations($parentReflector->name, $targetClass), $metadata);
  294. }
  295. $interfaces = $classReflector->getInterfaceNames();
  296. foreach ($interfaces as $interface) {
  297. $metadata = $this->mergeMetadata($this->readMethodAnnotations($interface, $targetClass), $metadata);
  298. }
  299. return $metadata;
  300. }
  301. /**
  302. * Parse annotations out of a doc comment
  303. * @param str $comment Doc comment to parse
  304. * @return str[]
  305. */
  306. private function parseDocComment($comment)
  307. {
  308. $data = array();
  309. preg_match_all('/^\s*\*[*\s]*(@.+)$/m', $comment, $items);
  310. if ($items && isset($items[1])) {
  311. foreach ($items[1] as $item) {
  312. preg_match_all('/"[^"]+"|[^\s]+/', $item, $parts);
  313. $key = array_shift($parts[0]);
  314. array_walk($parts[0], create_function('&$v', '$v = trim($v, \'"\');'));
  315. $data[$key][] = $parts[0];
  316. }
  317. }
  318. return $data;
  319. }
  320. public function __toString()
  321. {
  322. $baseUri = $this->baseUri;
  323. if (isset($this->options['load']) && is_array($this->options['load'])) {
  324. $loadPath = join(', ', $this->options['load']);
  325. } else $loadPath = '';
  326. $mount = array();
  327. if (isset($this->options['mount']) && is_array($this->options['mount'])) {
  328. foreach ($this->options['mount'] as $namespaceName => $uriSpace) {
  329. $mount[] = $namespaceName.'="'.$uriSpace.'"';
  330. }
  331. }
  332. $mount = join(', ', $mount);
  333. $cache = isset($this->options['cache']) ? $this->options['cache'] : NULL;
  334. $resources = array();
  335. foreach ($this->resources as $resource) {
  336. $uri = array();
  337. foreach ($resource['uri'] as $u) {
  338. $uri[] = $u[0];
  339. }
  340. $uri = join(', ', $uri);
  341. $r = $resource['class'].' '.$uri.' '.join(', ', $resource['priority']);
  342. foreach ($resource['methods'] as $methodName => $method) {
  343. $r .= "\n\t\t".$methodName;
  344. foreach ($method as $itemName => $items) {
  345. foreach ($items as $item) {
  346. $r .= ' '.$itemName;
  347. if ($item) {
  348. $r .= '="'.join(', ', $item).'"';
  349. }
  350. }
  351. }
  352. }
  353. $resources[] = $r;
  354. }
  355. $resources = join("\n\t", $resources);
  356. return <<<EOF
  357. =================
  358. Tonic\Application
  359. =================
  360. Base URI: $baseUri
  361. Load path: $loadPath
  362. Mount points: $mount
  363. Annotation cache: $cache
  364. Loaded resources:
  365. \t$resources
  366. EOF;
  367. }
  368. }