AbstractFileCollection.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. <?php
  2. /**
  3. * @package Grav\Framework\Collection
  4. *
  5. * @copyright Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Framework\Collection;
  9. use Doctrine\Common\Collections\Criteria;
  10. use Doctrine\Common\Collections\Expr\ClosureExpressionVisitor;
  11. use FilesystemIterator;
  12. use Grav\Common\Grav;
  13. use RecursiveDirectoryIterator;
  14. use RocketTheme\Toolbox\ResourceLocator\RecursiveUniformResourceIterator;
  15. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  16. use RuntimeException;
  17. use SeekableIterator;
  18. use function array_slice;
  19. /**
  20. * Collection of objects stored into a filesystem.
  21. *
  22. * @package Grav\Framework\Collection
  23. * @template TKey of array-key
  24. * @template T of object
  25. * @extends AbstractLazyCollection<TKey,T>
  26. * @implements FileCollectionInterface<TKey,T>
  27. */
  28. class AbstractFileCollection extends AbstractLazyCollection implements FileCollectionInterface
  29. {
  30. /** @var string */
  31. protected $path;
  32. /** @var RecursiveDirectoryIterator|RecursiveUniformResourceIterator */
  33. protected $iterator;
  34. /** @var callable */
  35. protected $createObjectFunction;
  36. /** @var callable|null */
  37. protected $filterFunction;
  38. /** @var int */
  39. protected $flags;
  40. /** @var int */
  41. protected $nestingLimit;
  42. /**
  43. * @param string $path
  44. */
  45. protected function __construct($path)
  46. {
  47. $this->path = $path;
  48. $this->flags = self::INCLUDE_FILES | self::INCLUDE_FOLDERS;
  49. $this->nestingLimit = 0;
  50. $this->createObjectFunction = [$this, 'createObject'];
  51. $this->setIterator();
  52. }
  53. /**
  54. * @return string
  55. */
  56. public function getPath()
  57. {
  58. return $this->path;
  59. }
  60. /**
  61. * @param Criteria $criteria
  62. * @return ArrayCollection
  63. * @phpstan-return ArrayCollection<TKey,T>
  64. * @todo Implement lazy matching
  65. */
  66. public function matching(Criteria $criteria)
  67. {
  68. $expr = $criteria->getWhereExpression();
  69. $oldFilter = $this->filterFunction;
  70. if ($expr) {
  71. $visitor = new ClosureExpressionVisitor();
  72. $filter = $visitor->dispatch($expr);
  73. $this->addFilter($filter);
  74. }
  75. $filtered = $this->doInitializeByIterator($this->iterator, $this->nestingLimit);
  76. $this->filterFunction = $oldFilter;
  77. if ($orderings = $criteria->getOrderings()) {
  78. $next = null;
  79. /**
  80. * @var string $field
  81. * @var string $ordering
  82. */
  83. foreach (array_reverse($orderings) as $field => $ordering) {
  84. $next = ClosureExpressionVisitor::sortByField($field, $ordering === Criteria::DESC ? -1 : 1, $next);
  85. }
  86. /** @phpstan-ignore-next-line */
  87. if (null === $next) {
  88. throw new RuntimeException('Criteria is missing orderings');
  89. }
  90. uasort($filtered, $next);
  91. } else {
  92. ksort($filtered);
  93. }
  94. $offset = $criteria->getFirstResult();
  95. $length = $criteria->getMaxResults();
  96. if ($offset || $length) {
  97. $filtered = array_slice($filtered, (int)$offset, $length);
  98. }
  99. return new ArrayCollection($filtered);
  100. }
  101. /**
  102. * @return void
  103. */
  104. protected function setIterator()
  105. {
  106. $iteratorFlags = RecursiveDirectoryIterator::SKIP_DOTS + FilesystemIterator::UNIX_PATHS
  107. + FilesystemIterator::CURRENT_AS_SELF + FilesystemIterator::FOLLOW_SYMLINKS;
  108. if (strpos($this->path, '://')) {
  109. /** @var UniformResourceLocator $locator */
  110. $locator = Grav::instance()['locator'];
  111. $this->iterator = $locator->getRecursiveIterator($this->path, $iteratorFlags);
  112. } else {
  113. $this->iterator = new RecursiveDirectoryIterator($this->path, $iteratorFlags);
  114. }
  115. }
  116. /**
  117. * @param callable $filterFunction
  118. * @return $this
  119. */
  120. protected function addFilter(callable $filterFunction)
  121. {
  122. if ($this->filterFunction) {
  123. $oldFilterFunction = $this->filterFunction;
  124. $this->filterFunction = function ($expr) use ($oldFilterFunction, $filterFunction) {
  125. return $oldFilterFunction($expr) && $filterFunction($expr);
  126. };
  127. } else {
  128. $this->filterFunction = $filterFunction;
  129. }
  130. return $this;
  131. }
  132. /**
  133. * {@inheritDoc}
  134. */
  135. protected function doInitialize()
  136. {
  137. $filtered = $this->doInitializeByIterator($this->iterator, $this->nestingLimit);
  138. ksort($filtered);
  139. $this->collection = new ArrayCollection($filtered);
  140. }
  141. /**
  142. * @param SeekableIterator $iterator
  143. * @param int $nestingLimit
  144. * @return array
  145. * @phpstan-param SeekableIterator<int,T> $iterator
  146. */
  147. protected function doInitializeByIterator(SeekableIterator $iterator, $nestingLimit)
  148. {
  149. $children = [];
  150. $objects = [];
  151. $filter = $this->filterFunction;
  152. $objectFunction = $this->createObjectFunction;
  153. /** @var RecursiveDirectoryIterator $file */
  154. foreach ($iterator as $file) {
  155. // Skip files if they shouldn't be included.
  156. if (!($this->flags & static::INCLUDE_FILES) && $file->isFile()) {
  157. continue;
  158. }
  159. // Apply main filter.
  160. if ($filter && !$filter($file)) {
  161. continue;
  162. }
  163. // Include children if the recursive flag is set.
  164. if (($this->flags & static::RECURSIVE) && $nestingLimit > 0 && $file->hasChildren()) {
  165. $children[] = $file->getChildren();
  166. }
  167. // Skip folders if they shouldn't be included.
  168. if (!($this->flags & static::INCLUDE_FOLDERS) && $file->isDir()) {
  169. continue;
  170. }
  171. $object = $objectFunction($file);
  172. $objects[$object->key] = $object;
  173. }
  174. if ($children) {
  175. $objects += $this->doInitializeChildren($children, $nestingLimit - 1);
  176. }
  177. return $objects;
  178. }
  179. /**
  180. * @param array $children
  181. * @param int $nestingLimit
  182. * @return array
  183. */
  184. protected function doInitializeChildren(array $children, $nestingLimit)
  185. {
  186. $objects = [];
  187. foreach ($children as $iterator) {
  188. $objects += $this->doInitializeByIterator($iterator, $nestingLimit);
  189. }
  190. return $objects;
  191. }
  192. /**
  193. * @param RecursiveDirectoryIterator $file
  194. * @return object
  195. */
  196. protected function createObject($file)
  197. {
  198. return (object) [
  199. 'key' => $file->getSubPathname(),
  200. 'type' => $file->isDir() ? 'folder' : 'file:' . $file->getExtension(),
  201. 'url' => method_exists($file, 'getUrl') ? $file->getUrl() : null,
  202. 'pathname' => $file->getPathname(),
  203. 'mtime' => $file->getMTime()
  204. ];
  205. }
  206. }