PageRenderTime 48ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/2012/sample-tonic/src/Tonic/Application.php

https://bitbucket.org/alessandro-aglietti/itis-leonardo-da-vinci
PHP | 383 lines | 289 code | 40 blank | 54 comment | 61 complexity | d322e5c29db96006e8321af08f921c8c 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. foreach (glob($glob) as $filename) {
  59. require_once $filename;
  60. }
  61. }
  62. }
  63. /**
  64. * Load the metadata for all loaded resource classes
  65. * @param str $uriSpace Optional URI-space to mount the resources into
  66. */
  67. private function loadResourceMetadata($uriSpace = NULL)
  68. {
  69. foreach (get_declared_classes() as $className) {
  70. if (
  71. !isset($this->resources[$className]) &&
  72. is_subclass_of($className, 'Tonic\Resource')
  73. ) {
  74. $this->resources[$className] = $this->readResourceAnnotations($className);
  75. if ($uriSpace) {
  76. $this->resources[$className]['uri'][0] = '|^'.$uriSpace.substr($this->resources[$className]['uri'][0], 2);
  77. }
  78. $this->resources[$className]['methods'] = $this->readMethodAnnotations($className);
  79. }
  80. }
  81. }
  82. /**
  83. * Add a namespace to a specific URI-space
  84. *
  85. * @param str $namespaceName
  86. * @param str $uriSpace
  87. */
  88. public function mount($namespaceName, $uriSpace)
  89. {
  90. foreach ($this->resources as $className => $metadata) {
  91. if ($metadata['namespace'][0] == $namespaceName) {
  92. if (isset($metadata['uri'])) {
  93. foreach ($metadata['uri'] as $index => $uri) {
  94. $this->resources[$className]['uri'][$index][0] = '|^'.$uriSpace.substr($uri[0], 2);
  95. }
  96. }
  97. }
  98. }
  99. }
  100. /**
  101. * Get the URL for the given resource class
  102. *
  103. * @param str $className
  104. * @param str[] $params
  105. * @return str
  106. */
  107. public function uri($className, $params = array())
  108. {
  109. if (is_object($className)) {
  110. $className = get_class($className);
  111. }
  112. if (isset($this->resources[$className])) {
  113. if ($params && !is_array($params)) {
  114. $params = array($params);
  115. }
  116. foreach ($this->resources[$className]['uri'] as $uri) {
  117. if (count($params) == count($uri) - 1) {
  118. $parts = explode('([^/]+)', $uri[0]);
  119. $path = '';
  120. foreach ($parts as $key => $part) {
  121. $path .= $part;
  122. if (isset($params[$key])) {
  123. $path .= $params[$key];
  124. }
  125. }
  126. return $this->baseUri.substr($path, 2, -2);
  127. }
  128. }
  129. }
  130. }
  131. /**
  132. * Given the request data and the loaded resource metadata, pick the best matching
  133. * resource to handle the request based on URI and priority.
  134. *
  135. * @param Request $request
  136. * @return Resource
  137. */
  138. public function getResource($request = NULL)
  139. {
  140. $matchedResource = NULL;
  141. if (!$request) {
  142. $request= new Request();
  143. }
  144. foreach ($this->resources as $className => $resourceMetadata) {
  145. if (isset($resourceMetadata['uri'])) {
  146. if (!is_array($resourceMetadata['uri'])) {
  147. $resourceMetadata['uri'] = array($resourceMetadata['uri']);
  148. }
  149. foreach ($resourceMetadata['uri'] as $uri) {
  150. if (!is_array($uri)) {
  151. $uri = array($uri);
  152. }
  153. $uriRegex = $uri[0];
  154. if (!isset($resourceMetadata['priority'])) {
  155. $resourceMetadata['priority'] = 1;
  156. }
  157. if (!isset($resourceMetadata['class'])) {
  158. $resourceMetadata['class'] = $className;
  159. }
  160. if (
  161. ($matchedResource == NULL || $matchedResource[0]['priority'] < $resourceMetadata['priority'])
  162. &&
  163. preg_match($uriRegex, $request->uri, $params)
  164. ) {
  165. if (count($uri) > 1) { // has params within URI
  166. $params = array_combine($uri, $params);
  167. }
  168. array_shift($params);
  169. $matchedResource = array($resourceMetadata, $params);
  170. }
  171. }
  172. }
  173. }
  174. if ($matchedResource) {
  175. if (isset($matchedResource[0]['filename']) && is_readable($matchedResource[0]['filename'])) {
  176. require_once($matchedResource[0]['filename']);
  177. }
  178. return new $matchedResource[0]['class']($this, $request, $matchedResource[1]);
  179. } else {
  180. throw new NotFoundException(sprintf('Resource matching URI "%s" not found', $request->uri));
  181. }
  182. }
  183. /**
  184. * Get the already loaded resource annotation metadata
  185. * @param Tonic/Resource $resource
  186. * @return str[]
  187. */
  188. public function getResourceMetadata($resource)
  189. {
  190. if (is_object($resource)) {
  191. $className = get_class($resource);
  192. } else {
  193. $className = $resource;
  194. }
  195. return isset($this->resources[$className]) ? $this->resources[$className] : NULL;
  196. }
  197. /**
  198. * Read the annotation metadata for the given class
  199. * @return str[] Annotation metadata
  200. */
  201. private function readResourceAnnotations($className)
  202. {
  203. $metadata = array();
  204. // get data from reflector
  205. $classReflector = new \ReflectionClass($className);
  206. $metadata['class'] = '\\'.$classReflector->getName();
  207. $metadata['namespace'] = array($classReflector->getNamespaceName());
  208. $metadata['filename'] = $classReflector->getFileName();
  209. $metadata['priority'] = array(1);
  210. // get data from docComment
  211. $docComment = $this->parseDocComment($classReflector->getDocComment());
  212. if (isset($docComment['@uri'])) {
  213. foreach ($docComment['@uri'] as $uri) {
  214. $metadata['uri'][] = $this->uriTemplateToRegex($uri);
  215. }
  216. }
  217. if (isset($docComment['@namespace'])) $metadata['namespace'] = $docComment['@namespace'][0];
  218. if (isset($docComment['@priority'])) $metadata['priority'] = $docComment['@priority'][0];
  219. return $metadata;
  220. }
  221. /**
  222. * Turn a URL template into a regular expression
  223. * @param str[] $uri URL template
  224. * @return str[] Regular expression and parameter names
  225. */
  226. private function uriTemplateToRegex($uri)
  227. {
  228. preg_match_all('#((?<!\?):[^/]+|{[^0-9][^}]*}|\(.+?\))#', $uri[0], $params, PREG_PATTERN_ORDER);
  229. $return = $uri;
  230. if (isset($params[1])) {
  231. foreach ($params[1] as $index => $param) {
  232. if (substr($param, 0, 1) == ':') {
  233. $return[] = substr($param, 1);
  234. } elseif (substr($param, 0, 1) == '{' && substr($param, -1, 1) == '}') {
  235. $return[] = substr($param, 1, -1);
  236. } else {
  237. $return[] = $index;
  238. }
  239. }
  240. }
  241. $return[0] = '|^'.preg_replace('#((?<!\?):[^(/]+|{[^0-9][^}]*})#', '([^/]+)', $return[0]).'$|';
  242. return $return;
  243. }
  244. private function readMethodAnnotations($className)
  245. {
  246. if (isset($this->resources[$className]) && isset($this->resources[$className]['methods'])) {
  247. return $this->resources[$className]['methods'];
  248. }
  249. $metadata = array();
  250. foreach (get_class_methods($className) as $methodName) {
  251. $methodReflector = new \ReflectionMethod($className, $methodName);
  252. if ($methodReflector->isPublic() && $methodReflector->getDeclaringClass()->name != 'Tonic\Resource') {
  253. $methodMetadata = array();
  254. $docComment = $this->parseDocComment($methodReflector->getDocComment());
  255. foreach ($docComment as $annotationName => $value) {
  256. $methodName = substr($annotationName, 1);
  257. if (method_exists($className, $methodName)) {
  258. foreach ($value as $v) {
  259. $methodMetadata[$methodName][] = $v;
  260. }
  261. }
  262. }
  263. $metadata[$methodReflector->getName()] = $methodMetadata;
  264. }
  265. }
  266. return $metadata;
  267. }
  268. /**
  269. * Parse annotations out of a doc comment
  270. * @param str $comment Doc comment to parse
  271. * @return str[]
  272. */
  273. private function parseDocComment($comment)
  274. {
  275. $data = array();
  276. preg_match_all('/^\s*\*[*\s]*(@.+)$/m', $comment, $items);
  277. if ($items && isset($items[1])) {
  278. foreach ($items[1] as $item) {
  279. $parts = preg_split('/ +/', $item);
  280. if ($parts) {
  281. foreach ($parts as $k => $part) {
  282. $parts[$k] = trim($part);
  283. }
  284. $key = array_shift($parts);
  285. $data[$key][] = $parts;
  286. }
  287. }
  288. }
  289. return $data;
  290. }
  291. public function __toString()
  292. {
  293. $baseUri = $this->baseUri;
  294. if (isset($this->options['load']) && is_array($this->options['load'])) {
  295. $loadPath = join(', ', $this->options['load']);
  296. } else $loadPath = '';
  297. $mount = array();
  298. if (isset($this->options['mount']) && is_array($this->options['mount'])) {
  299. foreach ($this->options['mount'] as $namespaceName => $uriSpace) {
  300. $mount[] = $namespaceName.'="'.$uriSpace.'"';
  301. }
  302. }
  303. $mount = join(', ', $mount);
  304. $cache = isset($this->options['cache']) ? $this->options['cache'] : NULL;
  305. $resources = array();
  306. foreach ($this->resources as $resource) {
  307. $uri = array();
  308. foreach ($resource['uri'] as $u) {
  309. $uri[] = $u[0];
  310. }
  311. $uri = join(', ', $uri);
  312. $r = $resource['class'].' '.$uri.' '.join(', ', $resource['priority']);
  313. foreach ($resource['methods'] as $methodName => $method) {
  314. $r .= "\n\t\t".$methodName;
  315. foreach ($method as $itemName => $items) {
  316. foreach ($items as $item) {
  317. $r .= ' '.$itemName;
  318. if ($item) {
  319. $r .= '="'.join(', ', $item).'"';
  320. }
  321. }
  322. }
  323. }
  324. $resources[] = $r;
  325. }
  326. $resources = join("\n\t", $resources);
  327. return <<<EOF
  328. =================
  329. Tonic\Application
  330. =================
  331. Base URI: $baseUri
  332. Load path: $loadPath
  333. Mount points: $mount
  334. Annotation cache: $cache
  335. Loaded resources:
  336. \t$resources
  337. EOF;
  338. }
  339. }