PageRenderTime 43ms CodeModel.GetById 7ms RepoModel.GetById 1ms app.codeStats 0ms

/src/DocBlox/Parser/Files.php

http://github.com/mvriel/Docblox
PHP | 517 lines | 265 code | 48 blank | 204 comment | 42 complexity | 036d7018512763679e9db706cd0fe22d MD5 | raw file
Possible License(s): LGPL-3.0, BSD-3-Clause, CC-BY-SA-3.0
  1. <?php
  2. /**
  3. * DocBlox
  4. *
  5. * PHP Version 5
  6. *
  7. * @category DocBlox
  8. * @package Parser
  9. * @author Mike van Riel <mike.vanriel@naenius.com>
  10. * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com)
  11. * @license http://www.opensource.org/licenses/mit-license.php MIT
  12. * @link http://docblox-project.org
  13. */
  14. /**
  15. * Files container handling directory scanning, project root detection and ignores.
  16. *
  17. * @category DocBlox
  18. * @package Parser
  19. * @author Mike van Riel <mike.vanriel@naenius.com>
  20. * @license http://www.opensource.org/licenses/mit-license.php MIT
  21. * @link http://docblox-project.org
  22. */
  23. class DocBlox_Parser_Files extends DocBlox_Parser_Abstract
  24. {
  25. /** @var bool Whether to follow symlinks*/
  26. protected $follow_symlinks = false;
  27. /** @var bool Whether to ignore hidden files and folders */
  28. protected $ignore_hidden = false;
  29. /**
  30. * the glob patterns which directories/files to ignore during parsing and
  31. * how many files were ignored.
  32. *
  33. * Structure of this array is:
  34. *
  35. * array(
  36. * 0 => <GLOB>
  37. * 1 => <COUNT of USES>
  38. * )
  39. *
  40. * @var array[]
  41. */
  42. protected $ignore_patterns = array();
  43. /**
  44. * @var string[] Array containing a list of allowed line endings;
  45. * defaults to php, php3 and phtml.
  46. */
  47. protected $allowed_extensions = array('php', 'php3', 'phtml');
  48. /** @var string[] An array containing the file names which must be processed */
  49. protected $files = array();
  50. /** @var string Detected root folder for this project */
  51. protected $project_root = null;
  52. /**
  53. * Sets the patterns by which to detect which files to ignore.
  54. *
  55. * @param array $patterns Glob-like patterns to filter files.
  56. *
  57. * @return void
  58. */
  59. public function setIgnorePatterns(array $patterns)
  60. {
  61. $this->ignore_patterns = array();
  62. foreach ($patterns as $pattern) {
  63. $this->addIgnorePattern($pattern);
  64. }
  65. }
  66. /**
  67. * Returns the ignore patterns.
  68. *
  69. * @return array
  70. */
  71. public function getIgnorePatterns()
  72. {
  73. // extract first element; second is a count
  74. $result = array();
  75. foreach ($this->ignore_patterns as $pattern) {
  76. $result[] = $pattern[0];
  77. }
  78. return $result;
  79. }
  80. /**
  81. * Adds an ignore pattern to the collection.
  82. *
  83. * @param string $pattern Glob-like pattern to filter files with.
  84. *
  85. * @return void
  86. */
  87. public function addIgnorePattern($pattern)
  88. {
  89. $this->convertToPregCompliant($pattern);
  90. $this->ignore_patterns[] = array($pattern, 0);
  91. }
  92. /**
  93. * Sets a list of allowed extensions; if not used php, php3 and phtml
  94. * is assumed.
  95. *
  96. * @param array $extensions An array containing extensions to match for.
  97. *
  98. * @return void
  99. */
  100. public function setAllowedExtensions(array $extensions)
  101. {
  102. $this->allowed_extensions = array();
  103. foreach ($extensions as $extension) {
  104. $this->addAllowedExtension($extension);
  105. }
  106. }
  107. /**
  108. * Adds a file extension to the list of allowed extensions.
  109. *
  110. * No dot is necessary and will even prevent the extension from being
  111. * picked up.
  112. *
  113. * @param string $extension Allowed file Extension to add (i.e. php).
  114. *
  115. * @return void
  116. */
  117. public function addAllowedExtension($extension)
  118. {
  119. $this->allowed_extensions[] = $extension;
  120. }
  121. /**
  122. * Adds the content of a set of directories to the list of files to parse.
  123. *
  124. * @param array $paths The paths whose contents to add to the collection.
  125. *
  126. * @return void
  127. */
  128. public function addDirectories(array $paths)
  129. {
  130. foreach ($paths as $path) {
  131. $this->addDirectory($path);
  132. }
  133. }
  134. /**
  135. * Retrieve all files in the given directory and add them to the parsing list.
  136. *
  137. * @param string $path A path to a folder, may be relative, absolute or
  138. * even phar.
  139. *
  140. * @throws InvalidArgumentException if the given path is not a folder.
  141. *
  142. * @return void
  143. */
  144. public function addDirectory($path)
  145. {
  146. $result = substr($path, 0, 7) !== 'phar://' ? glob($path) : array($path);
  147. if ($result === false) {
  148. throw new DocBlox_Parser_Exception(
  149. '"'.$path . '" does not match an existing directory pattern'
  150. );
  151. }
  152. // if the given path is the only one AND there are no registered files.
  153. // then use this as project root instead of the calculated version.
  154. // This will make sure than when a _single_ path is given, that the
  155. // root will not inadvertently skip to a higher location because no
  156. // file were found in the given location.
  157. // i.e. if only path `src` us given and no PHP files reside there, but
  158. // they do reside in `src/php` then with this code `src` will remain
  159. // root so that ignore statements work as expected. Without this the
  160. // root would be `src/php`, which is unexpected when only a single folder
  161. // is provided.
  162. if ((count($result) == 1) && (empty($this->files))) {
  163. $this->project_root = realpath(reset($result));
  164. } else {
  165. $this->project_root = null;
  166. }
  167. foreach ($result as $result_path) {
  168. // if the given is not a directory or is hidden and must be ignored,
  169. // skip it
  170. if (!is_dir($result_path)) {
  171. continue;
  172. }
  173. // add the CATCH_GET_CHILD option to make sure that an unreadable
  174. // directory does not halt process but skip that folder
  175. $recursive_iterator = new RecursiveIteratorIterator(
  176. new RecursiveDirectoryIterator($result_path,
  177. $this->getFollowSymlinks()
  178. ? RecursiveDirectoryIterator::FOLLOW_SYMLINKS : 0
  179. ),
  180. RecursiveIteratorIterator::LEAVES_ONLY,
  181. RecursiveIteratorIterator::CATCH_GET_CHILD
  182. );
  183. /** @var SplFileInfo $file */
  184. foreach ($recursive_iterator as $file) {
  185. $is_hidden = ((substr($file->getPath(), 0, 1) == '.')
  186. || (strpos($file->getPath(), DIRECTORY_SEPARATOR.'.')
  187. !== false));
  188. // skipping dots (should any be encountered) or skipping
  189. // files starting with a dot if IgnoreHidden is true
  190. if (($file->getFilename() == '.')
  191. || ($file->getFilename() == '..')
  192. || ($this->getIgnoreHidden() && $is_hidden)
  193. ) {
  194. continue;
  195. }
  196. // Phar files return false on a call to getRealPath
  197. $this->addFile(
  198. (substr($file->getPathname(), 0, 7) != 'phar://')
  199. ? $file->getRealPath()
  200. : $file->getPathname()
  201. );
  202. }
  203. }
  204. }
  205. /**
  206. * Adds a list of individual files to the collection.
  207. *
  208. * @param array $paths File locations, may be absolute, relative or even phar.
  209. *
  210. * @return void
  211. */
  212. public function addFiles(array $paths)
  213. {
  214. if (!empty($paths)) {
  215. // if separate files are provided then the root must always be
  216. // calculated.
  217. $this->project_root = null;
  218. }
  219. foreach ($paths as $path) {
  220. $this->addFile($path);
  221. }
  222. }
  223. /**
  224. * Adds a file to the collection.
  225. *
  226. * @param string $path File location, may be absolute, relative or even phar.
  227. *
  228. * @return void
  229. */
  230. public function addFile($path)
  231. {
  232. $is_hidden = ((substr($path, 0, 1) == '.')
  233. || (strpos($path, DIRECTORY_SEPARATOR . '.') !== false));
  234. // ignore hidden files if option is set
  235. if ($this->getIgnoreHidden() && $is_hidden) {
  236. return;
  237. }
  238. // if it is not a file contained in a phar; check it out with a glob
  239. if (substr($path, 0, 7) != 'phar://') {
  240. // search file(s) with the given expressions
  241. $result = glob($path);
  242. foreach ($result as $file) {
  243. // if the path is not a file OR it's extension does not match
  244. // the given, then do not process it.
  245. if (!is_file($file) || (!empty($this->allowed_extensions)
  246. && !in_array(
  247. strtolower(pathinfo($file, PATHINFO_EXTENSION)),
  248. $this->allowed_extensions
  249. ))
  250. ) {
  251. continue;
  252. }
  253. $this->files[] = realpath($file);
  254. }
  255. } else {
  256. // only process if it is a file and it matches the allowed extensions
  257. if (is_file($path)
  258. && (empty($this->allowed_extensions)
  259. || in_array(
  260. strtolower(pathinfo($path, PATHINFO_EXTENSION)),
  261. $this->allowed_extensions
  262. ))
  263. ) {
  264. $this->files[] = $path;
  265. }
  266. }
  267. }
  268. /**
  269. * Returns a list of files that are ready to be parsed.
  270. *
  271. * Please note that the ignore pattern will be applied and all files are
  272. * converted to absolute paths.
  273. *
  274. * @return string[]
  275. */
  276. public function getFiles()
  277. {
  278. $result = array();
  279. foreach ($this->files as $filename) {
  280. // check whether this file is ignored; we do this in two steps:
  281. // 1. Determine whether this is a relative or absolute path, if the
  282. // string does not start with *, ?, / or \ then we assume that it is
  283. // a relative path
  284. // 2. check whether the given pattern matches with the filename (or
  285. // relative filename in case of a relative comparison)
  286. foreach ($this->ignore_patterns as $key => $pattern) {
  287. $glob = $pattern[0];
  288. if ((($glob[0] !== '*')
  289. && ($glob[0] !== '?')
  290. && ($glob[0] !== '/')
  291. && ($glob[0] !== '\\')
  292. && (preg_match(
  293. '/^' . $glob . '$/',
  294. $this->getRelativeFilename($filename)
  295. )))
  296. || (preg_match('/^' . $glob . '$/', $filename))
  297. ) {
  298. // increase ignore usage with 1
  299. $this->ignore_patterns[$key][1]++;
  300. $this->log(
  301. 'File "' . $filename . '" matches ignore pattern, '
  302. . 'will be skipped', DocBlox_Core_Log::INFO
  303. );
  304. continue 2;
  305. }
  306. }
  307. $result[] = $filename;
  308. }
  309. // detect if ignore patterns have been unused
  310. foreach ($this->ignore_patterns as $pattern) {
  311. if ($pattern[1] < 1) {
  312. $this->log(
  313. 'Ignore pattern "' . $pattern[0] . '" has not been used '
  314. . 'during processing'
  315. );
  316. }
  317. }
  318. return $result;
  319. }
  320. /**
  321. * Calculates the project root from the given files by determining their
  322. * highest common path.
  323. *
  324. * @return string
  325. */
  326. public function getProjectRoot()
  327. {
  328. if ($this->project_root === null) {
  329. $base = '';
  330. $file = reset($this->files);
  331. // realpath does not work on phar files
  332. $file = (substr($file, 0, 7) != 'phar://')
  333. ? realpath($file)
  334. : $file;
  335. $parts = explode(DIRECTORY_SEPARATOR, $file);
  336. foreach ($parts as $part) {
  337. $base_part = $base . $part . DIRECTORY_SEPARATOR;
  338. foreach ($this->files as $dir) {
  339. // realpath does not work on phar files
  340. $dir = (substr($dir, 0, 7) != 'phar://')
  341. ? realpath($dir)
  342. : $dir;
  343. if (substr($dir, 0, strlen($base_part)) != $base_part) {
  344. return $base;
  345. }
  346. }
  347. $base = $base_part;
  348. }
  349. $this->project_root = $base;
  350. }
  351. return $this->project_root;
  352. }
  353. /**
  354. * Converts $string into a string that can be used with preg_match.
  355. *
  356. * @param string &$string Glob-like pattern with wildcards ? and *.
  357. *
  358. * @author Greg Beaver <cellog@php.net>
  359. * @author mike van Riel <mike.vanriel@naenius.com>
  360. *
  361. * @see PhpDocumentor/phpDocumentor/Io.php
  362. *
  363. * @return void
  364. */
  365. protected function convertToPregCompliant(&$string)
  366. {
  367. $y = (DIRECTORY_SEPARATOR == '\\') ? '\\\\' : '\/';
  368. $string = str_replace('/', DIRECTORY_SEPARATOR, $string);
  369. $x = strtr(
  370. $string,
  371. array(
  372. '?' => '.',
  373. '*' => '.*',
  374. '.' => '\\.',
  375. '\\' => '\\\\',
  376. '/' => '\\/',
  377. '[' => '\\[',
  378. ']' => '\\]',
  379. '-' => '\\-'
  380. )
  381. );
  382. if ((strpos($string, DIRECTORY_SEPARATOR) !== false)
  383. && (strrpos($string, DIRECTORY_SEPARATOR) === strlen($string) - 1)
  384. ) {
  385. $x = "(?:.*$y$x?.*|$x.*)";
  386. }
  387. $string = $x;
  388. }
  389. /**
  390. * Returns the filename, relative to the root of the project directory.
  391. *
  392. * @param string $filename The filename to make relative.
  393. *
  394. * @throws InvalidArgumentException if file is not in the project root.
  395. *
  396. * @return string
  397. */
  398. protected function getRelativeFilename($filename)
  399. {
  400. // strip path from filename
  401. $result = ltrim(
  402. substr($filename, strlen($this->getProjectRoot())),
  403. DIRECTORY_SEPARATOR
  404. );
  405. if ($result === '') {
  406. throw new InvalidArgumentException(
  407. 'File is not present in the given project path: ' . $filename
  408. );
  409. }
  410. return $result;
  411. }
  412. /**
  413. * Sets whether to ignore hidden files and folders.
  414. *
  415. * @param boolean $ignore_hidden if true skips hidden files and folders.
  416. *
  417. * @return void
  418. */
  419. public function setIgnoreHidden($ignore_hidden)
  420. {
  421. $this->ignore_hidden = $ignore_hidden;
  422. }
  423. /**
  424. * Returns whether files and folders that are hidden are ignored.
  425. *
  426. * @return boolean
  427. */
  428. public function getIgnoreHidden()
  429. {
  430. return $this->ignore_hidden;
  431. }
  432. /**
  433. * Sets whether to follow symlinks.
  434. *
  435. * PHP version 5.2.11 is at least required since the
  436. * RecursiveDirectoryIterator does not support the FOLLOW_SYMLINKS
  437. * constant before that version.
  438. *
  439. * @param boolean $follow_symlinks
  440. *
  441. * @throws InvalidArgumentException
  442. *
  443. * @return void
  444. */
  445. public function setFollowSymlinks($follow_symlinks)
  446. {
  447. if ($follow_symlinks && version_compare(PHP_VERSION, '5.2.11', '<')) {
  448. throw new InvalidArgumentException(
  449. 'To follow symlinks you need at least PHP version 5.2.11'
  450. );
  451. }
  452. $this->follow_symlinks = $follow_symlinks;
  453. }
  454. /**
  455. * Returns whether to follow symlinks.
  456. *
  457. * @return boolean
  458. */
  459. public function getFollowSymlinks()
  460. {
  461. return $this->follow_symlinks;
  462. }
  463. }