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

/models/api_file.php

https://github.com/andreking2/api_generator
PHP | 521 lines | 298 code | 9 blank | 214 comment | 52 complexity | c621a8b0cb693c74f3119ba4e1ff24a6 MD5 | raw file
  1. <?php
  2. /**
  3. * Api File Model
  4. *
  5. * For interacting with the Filesystem specified by ApiGenerator.filePath
  6. *
  7. * PHP 5.2+
  8. *
  9. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  10. * Copyright 2008-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. *
  12. * Licensed under The MIT License
  13. * Redistributions of files must retain the above copyright notice.
  14. *
  15. * @copyright Copyright 2008-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
  16. * @link http://cakephp.org
  17. * @package api_generator
  18. * @subpackage api_generator.models
  19. * @since ApiGenerator 0.1
  20. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  21. **/
  22. App::import('Lib', 'ApiGenerator.DocumentorFactory');
  23. class ApiFile extends Object {
  24. /**
  25. * Name
  26. *
  27. * @var string
  28. */
  29. public $name = 'ApiFile';
  30. /**
  31. * A list of folders to ignore.
  32. *
  33. * @var array
  34. **/
  35. public $excludeDirectories = array();
  36. /**
  37. * excludeMethods property
  38. *
  39. * @var array
  40. */
  41. public $excludeMethods = array();
  42. /**
  43. * excludeProperties property
  44. *
  45. * @var array
  46. */
  47. public $excludeProperties = array();
  48. /**
  49. * A list of files to ignore.
  50. *
  51. * @var array
  52. **/
  53. public $excludeFiles = array();
  54. /**
  55. * a list of extensions to scan for
  56. *
  57. * @var array
  58. **/
  59. public $allowedExtensions = array();
  60. /**
  61. * Array of class dependancies map
  62. *
  63. * @var array
  64. **/
  65. public $dependencyMap = array();
  66. /**
  67. * Mappings of funny named classes to files
  68. *
  69. * @var string
  70. **/
  71. public $classMap = array();
  72. /**
  73. * A regexp for file names. (will be made case insenstive)
  74. *
  75. * @var string
  76. **/
  77. public $fileRegExp = '[a-z_\-0-9]+';
  78. /**
  79. * Folder instance
  80. *
  81. * @var Folder
  82. **/
  83. protected $_Folder;
  84. /**
  85. * ApiConfig Model instance
  86. *
  87. * @var object
  88. **/
  89. public $ApiConfig;
  90. /**
  91. * Current Extractor instance
  92. *
  93. * @var object
  94. **/
  95. protected $_extractor;
  96. /**
  97. * storage for defined classes
  98. *
  99. * @var array
  100. **/
  101. protected $_definedClasses = array();
  102. /**
  103. * storage for defined functions
  104. *
  105. * @var array
  106. **/
  107. protected $_definedFunctions = array();
  108. /**
  109. * Constructor
  110. *
  111. * @return void
  112. **/
  113. public function __construct() {
  114. parent::__construct();
  115. $this->ApiConfig = ClassRegistry::init('ApiGenerator.ApiConfig');
  116. $this->_initConfig();
  117. $this->_Folder = new Folder(APP);
  118. }
  119. /**
  120. * Read a path and return files and folders not in the excluded Folder list
  121. *
  122. * @param string $path The absolute path you wish to read.
  123. * @return array
  124. **/
  125. public function read($path) {
  126. if (preg_match('|\.\.|', $path)) {
  127. return array(array(), array());
  128. }
  129. $this->_Folder->cd($path);
  130. $length = strlen($path);
  131. if (substr($path, -1) !== DS) {
  132. $length++;
  133. }
  134. $ignore = $this->excludeFiles;
  135. $ignore[] = '.';
  136. $contents = $this->_Folder->read(true, $ignore);
  137. $this->_filterFolders($contents[0], false);
  138. $this->_filterFiles($contents[1]);
  139. return $contents;
  140. }
  141. /**
  142. * Recursive Read a path and return files and folders not in the excluded Folder list
  143. *
  144. * @param string $path The path you wish to read.
  145. * @return array
  146. **/
  147. public function fileList($path) {
  148. $this->_Folder->cd($path);
  149. $filePattern = $this->fileRegExp . '\.' . implode('|', $this->allowedExtensions);
  150. $contents = $this->_Folder->findRecursive($filePattern);
  151. $this->_filterFolders($contents);
  152. $this->_filterFiles($contents);
  153. return $contents;
  154. }
  155. /**
  156. * _filterFiles
  157. *
  158. * Filter a file list and remove excludeDirectories
  159. *
  160. * @param array $files List of files to filter and ignore. (reference)
  161. * @return void
  162. **/
  163. protected function _filterFolders(&$fileList, $recursiveList = true) {
  164. $count = count($fileList);
  165. foreach ($this->excludeDirectories as $blackListed) {
  166. if ($recursiveList) {
  167. $blackListed = DS . $blackListed . DS;
  168. }
  169. for ($i = 0; $i < $count; $i++) {
  170. if (isset($fileList[$i]) && strpos($fileList[$i], $blackListed) !== false) {
  171. unset($fileList[$i]);
  172. }
  173. }
  174. }
  175. $fileList = array_values($fileList);
  176. }
  177. /**
  178. * remove files that don't match the allowedExtensions
  179. * or are on the excludeFiles list
  180. *
  181. * @return void
  182. **/
  183. protected function _filterFiles(&$fileList) {
  184. foreach ($this->excludeFiles as $ignored) {
  185. $fileCount = count($fileList);
  186. $fileList = array_values($fileList);
  187. for ($i = 0; $i < $fileCount; $i++) {
  188. $basename = basename($fileList[$i]);
  189. if ($ignored == $basename) {
  190. unset($fileList[$i]);
  191. }
  192. }
  193. }
  194. if (!empty($this->allowedExtensions)) {
  195. $extPattern = '/\.' . implode('|', $this->allowedExtensions) . '$/i';
  196. foreach ($fileList as $i => $file) {
  197. if (!preg_match($extPattern, $file)) {
  198. unset($fileList[$i]);
  199. }
  200. }
  201. }
  202. $fileList = array_values($fileList);
  203. }
  204. /**
  205. * Loads the documentation extractor for a given classname.
  206. *
  207. * @param string $name Name of class to load.
  208. * @access public
  209. * @return void
  210. */
  211. public function loadExtractor($type, $name) {
  212. $this->_extractor = DocumentorFactory::getReflector($type, $name);
  213. }
  214. /**
  215. * Get the documentor extractor instance
  216. *
  217. * @access public
  218. * @return object
  219. */
  220. public function getExtractor() {
  221. return $this->_extractor;
  222. }
  223. /**
  224. * Gets the parsed docs from the Extractor
  225. *
  226. * @return object Extractor with all docs processed.
  227. **/
  228. public function getDocs() {
  229. if (!$this->_extractor) {
  230. return array();
  231. }
  232. $this->_extractor->getAll();
  233. return $this->_extractor;
  234. }
  235. /**
  236. * Load A File and extract docs for all classes contained in that file
  237. *
  238. * Options:
  239. * - 'useIndex' boolean whether or not a search should be done on the ApiClass index for any missing classes
  240. * defaults to false.
  241. *
  242. * @param string $fullPath FullPath of the file you want to load.
  243. * @param array $options Options to use see above
  244. * @return array Array of all the docs from all the classes that were loaded as a result of the file being loaded.
  245. * @throws MissingClassException If a dependancy cannot be solved, an exception will be thrown.
  246. **/
  247. public function loadFile($filePath, $options = array()) {
  248. $docs = array('class' => array(), 'function' => array());
  249. if (preg_match('|\.\.|', $filePath)) {
  250. return $docs;
  251. }
  252. if (!defined('DISABLE_AUTO_DISPATCH')) {
  253. define('DISABLE_AUTO_DISPATCH', true);
  254. }
  255. if (!$this->isAllowed($filePath)) {
  256. throw new Exception(__d('api_geneartor', 'No file with that name exists.', true));
  257. }
  258. $this->_importCakeBaseClasses($filePath);
  259. $this->_resolveDependancies($filePath, $options);
  260. $this->_getDefinedObjects();
  261. $newObjects = $this->findObjectsInFile($filePath);
  262. foreach ($newObjects as $type => $objects) {
  263. foreach ($objects as $element) {
  264. $this->loadExtractor($type, $element);
  265. if ($type == 'function' && basename($this->_extractor->getFileName()) != basename($filePath)) {
  266. continue;
  267. }
  268. $docs[$type][$element] = $this->getDocs();
  269. }
  270. }
  271. return $docs;
  272. }
  273. /**
  274. * Import the core classes (Controller, View, Helper, Model)
  275. *
  276. * @return void
  277. **/
  278. public function importCoreClasses() {
  279. App::import('Core', array('Controller', 'Model', 'View', 'Helper'));
  280. }
  281. /**
  282. * gets the currently defined functions and classes
  283. * so comparisons to new files can be made
  284. *
  285. * @return void
  286. **/
  287. protected function _getDefinedObjects() {
  288. $this->_definedClasses = get_declared_classes();
  289. $funcs = get_defined_functions();
  290. $this->_definedFunctions = $funcs['user'];
  291. }
  292. /**
  293. * Fetches the class names and functions contained in the target file.
  294. * If first pass misses, a forceParse pass will be run.
  295. *
  296. * @param string $filePath Absolute file path to file you want to read.
  297. * @param boolean $forceParse Force the manual read of a file.
  298. * @return array
  299. **/
  300. public function findObjectsInFile($filePath) {
  301. $new = $tmp = array();
  302. $tmp['class'] = $this->_parseClassNamesInFile($filePath);
  303. $tmp['function'] = $this->_parseFunctionNamesInFile($filePath);
  304. $include = false;
  305. foreach ($tmp['class'] as $classInFile) {
  306. $include = false;
  307. if (!class_exists($classInFile, false)) {
  308. $include = true;
  309. }
  310. }
  311. foreach ($tmp['function'] as $funcInFile) {
  312. if (!function_exists($funcInFile)) {
  313. $include = true;
  314. }
  315. }
  316. if (!$include) {
  317. $new = $tmp;
  318. } else {
  319. ob_start();
  320. include_once $filePath;
  321. ob_clean();
  322. $new['class'] = array_diff(get_declared_classes(), $this->_definedClasses);
  323. $funcs = get_defined_functions();
  324. $new['function'] = array_diff($funcs['user'], $this->_definedFunctions);
  325. }
  326. return $new;
  327. }
  328. /**
  329. * Retrieves the classNames defined in a file.
  330. * Solves issues of reading docs from files that have already been included.
  331. *
  332. * @param string $filePath Absolute file path to file you want to parse.
  333. * @param boolean $getParents Get the parent classes instead.
  334. * @return array Array of class names that exist in the file.
  335. **/
  336. protected function _parseClassNamesInFile($fileName, $getParents = false) {
  337. $foundClasses = array();
  338. $fileContent = file_get_contents($fileName);
  339. $pattern = '/^\s*(?:abstract\s*)?(?:class|interface)\s+([^\s\{\:]+)\s*[^\{]*\{/mi';
  340. if ($getParents) {
  341. $pattern = '/^\s*(?:abstract\s*)?(?:class|interface)\s+[^\s]*\s*(?:extends\s+([^\s\{\:]*))?(?:\s*implements\s*([^\s\{]*))?[^\{]*/mi';
  342. }
  343. preg_match_all($pattern, $fileContent, $matches, PREG_SET_ORDER);
  344. foreach ($matches as $className) {
  345. if (!empty($className[1])) {
  346. $foundClasses[] = $className[1];
  347. }
  348. if (isset($className[2])) {
  349. $foundClasses = array_merge($foundClasses, explode(', ', $className[2]));
  350. }
  351. }
  352. return $foundClasses;
  353. }
  354. /**
  355. * Retrieves global function names defined in a file.
  356. * Unlike the class parser which can cheat with regex.
  357. * Functions are a bit trickier.
  358. *
  359. * @return array
  360. **/
  361. protected function _parseFunctionNamesInFile($fileName) {
  362. $foundFuncs = array();
  363. $fileContent = file_get_contents($fileName);
  364. $funcNames = implode('|', $this->_definedFunctions);
  365. preg_match_all('/^\t*function\s*(' . $funcNames . ')[\s|\(]+/mi', $fileContent, $matches, PREG_SET_ORDER);
  366. foreach ($matches as $function) {
  367. $foundFuncs[] = $function[1];
  368. }
  369. return $foundFuncs;
  370. }
  371. /**
  372. * Parses the file for any parent classes required by the file being loaded.
  373. * Attempts to load those files.
  374. *
  375. * @param string $filePath absolute filepath to look in
  376. * @param array $options Options to use.
  377. * @return void
  378. **/
  379. protected function _resolveDependancies($filePath, $options = array()) {
  380. $defaults = array('useIndex' => false);
  381. $options = array_merge($defaults, $options);
  382. $parentClasses = $this->_parseClassNamesInFile($filePath, true);
  383. $classNamesInFile = $this->_parseClassNamesInFile($filePath);
  384. $solved = false;
  385. $loadClasses = array();
  386. while ($solved === false && !empty($parentClasses)) {
  387. $neededParent = array_pop($parentClasses);
  388. $exists = (
  389. class_exists($neededParent, false) ||
  390. interface_exists($neededParent, false) ||
  391. in_array($neededParent, $classNamesInFile)
  392. );
  393. if (!$exists && $options['useIndex']) {
  394. $ApiClass = ClassRegistry::init('ApiGenerator.ApiClass');
  395. $result = $ApiClass->findByName($neededParent);
  396. if (!empty($result['ApiClass']['file_name'])) {
  397. $this->classMap[$neededParent] = $result['ApiClass']['file_name'];
  398. }
  399. }
  400. if (!$exists && isset($this->classMap[$neededParent])) {
  401. array_unshift($loadClasses, $neededParent);
  402. $newNeeds = $this->_parseClassNamesInFile($this->classMap[$neededParent], true);
  403. $parentClasses = array_unique(array_merge($parentClasses, $newNeeds));
  404. } elseif (!$exists) {
  405. throw new MissingClassException($neededParent . ' could not be found using mappings, please add it to the mappings.');
  406. }
  407. if (empty($parentClasses)) {
  408. $solved = true;
  409. }
  410. }
  411. foreach ($loadClasses as $className) {
  412. App::import('File', $className, true, array(), $this->classMap[$className]);
  413. }
  414. }
  415. /**
  416. * Attempts to solve class dependancies by importing base CakePHP classes
  417. *
  418. * @return void
  419. **/
  420. protected function _importCakeBaseClasses($filePath) {
  421. $baseClass = array();
  422. if (strpos($filePath, 'controllers') !== false) {
  423. $baseClass['Controller'] = 'App';
  424. }
  425. if (strpos($filePath, 'models') !== false) {
  426. $baseClass['Model'] = 'App';
  427. }
  428. if (strpos($filePath, 'helpers') !== false) {
  429. $baseClass['Helper'] = 'App';
  430. }
  431. if (strpos($filePath, 'view') !== false) {
  432. $baseClass['View'] = 'View';
  433. }
  434. if (strpos($filePath, 'socket') !== false) {
  435. $baseClass['Core'] = 'Socket';
  436. }
  437. if (strpos($filePath, 'schema') !== false) {
  438. $baseClass['Model'] = 'Schema';
  439. }
  440. foreach ($baseClass as $type => $class) {
  441. App::import($type, $class);
  442. }
  443. }
  444. /**
  445. * Get the Exclusions lists.
  446. *
  447. * @return array of stuff not allowed in views.
  448. **/
  449. public function getExclusions() {
  450. $return = array();
  451. $excludeProps = array('excludeMethods', 'excludeProperties');
  452. foreach ($excludeProps as $var) {
  453. $return[$var] = $this->{$var};
  454. }
  455. return $return;
  456. }
  457. /**
  458. * Checks if a file/path has been excluded by any of the exclusions.
  459. *
  460. * @param string $filePath file to check
  461. * @return boolean True if the file has been excluded
  462. */
  463. public function isAllowed($filePath) {
  464. foreach ($this->excludeDirectories as $exclude) {
  465. if (strpos($filePath, $exclude) !== false) {
  466. return false;
  467. }
  468. }
  469. $basename = basename($filePath);
  470. if (in_array($basename, $this->excludeFiles)) {
  471. return false;
  472. }
  473. list ($file, $ext) = explode('.', $basename);
  474. if (!in_array($ext, $this->allowedExtensions)) {
  475. return false;
  476. }
  477. return true;
  478. }
  479. /**
  480. * Initialize the configuration for ApiFile.
  481. *
  482. * @return void
  483. **/
  484. protected function _initConfig() {
  485. $config = $this->ApiConfig->read();
  486. if (isset($config['exclude']) && is_array($config['exclude'])) {
  487. foreach ($config['exclude'] as $type => $exclusion) {
  488. $var = 'exclude' . Inflector::camelize($type);
  489. $this->{$var} = array_map('trim', explode(',', $exclusion));
  490. }
  491. }
  492. if (isset($config['file']['extensions'])) {
  493. $this->allowedExtensions = array_map('trim', explode(',', $config['file']['extensions']));
  494. }
  495. if (isset($config['file']['regex'])) {
  496. $this->fileRegExp = $config['file']['regex'];
  497. }
  498. $varMap = array('dependencies' => 'dependencyMap', 'mappings' => 'classMap');
  499. foreach ($varMap as $key => $var) {
  500. if (isset($config[$key]) && is_array($config[$key])) {
  501. foreach ($config[$key] as $name => $value) {
  502. if ($var == 'classMap') {
  503. $this->{$var}[$name] = $value;
  504. } else {
  505. $this->{$var}[$name] = array_map('trim', explode(',', $value));
  506. }
  507. }
  508. }
  509. }
  510. }
  511. }
  512. class MissingClassException extends Exception { }