Flex.php 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @package Grav\Framework\Flex
  5. *
  6. * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  7. * @license MIT License; see LICENSE file for details.
  8. */
  9. namespace Grav\Framework\Flex;
  10. use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
  11. use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
  12. use Grav\Framework\Object\ObjectCollection;
  13. /**
  14. * Class Flex
  15. * @package Grav\Framework\Flex
  16. */
  17. class Flex implements \Countable
  18. {
  19. /** @var array */
  20. protected $config;
  21. /** @var FlexDirectory[] */
  22. protected $types;
  23. /**
  24. * Flex constructor.
  25. * @param array $types List of [type => blueprint file, ...]
  26. * @param array $config
  27. */
  28. public function __construct(array $types, array $config)
  29. {
  30. $this->config = $config;
  31. $this->types = [];
  32. foreach ($types as $type => $blueprint) {
  33. $this->addDirectoryType($type, $blueprint);
  34. }
  35. }
  36. /**
  37. * @param string $type
  38. * @param string $blueprint
  39. * @param array $config
  40. * @return $this
  41. */
  42. public function addDirectoryType(string $type, string $blueprint, array $config = [])
  43. {
  44. $config = array_merge_recursive(['enabled' => true], $this->config['object'] ?? [], $config);
  45. $this->types[$type] = new FlexDirectory($type, $blueprint, $config);
  46. return $this;
  47. }
  48. /**
  49. * @param FlexDirectory $directory
  50. * @return $this
  51. */
  52. public function addDirectory(FlexDirectory $directory)
  53. {
  54. $this->types[$directory->getFlexType()] = $directory;
  55. return $this;
  56. }
  57. /**
  58. * @param string $type
  59. * @return bool
  60. */
  61. public function hasDirectory(string $type): bool
  62. {
  63. return isset($this->types[$type]);
  64. }
  65. /**
  66. * @param array|string[] $types
  67. * @param bool $keepMissing
  68. * @return array|FlexDirectory[]
  69. */
  70. public function getDirectories(array $types = null, bool $keepMissing = false): array
  71. {
  72. if ($types === null) {
  73. return $this->types;
  74. }
  75. // Return the directories in the given order.
  76. $directories = [];
  77. foreach ($types as $type) {
  78. $directories[$type] = $this->types[$type] ?? null;
  79. }
  80. return $keepMissing ? $directories : array_filter($directories);
  81. }
  82. /**
  83. * @param string $type
  84. * @return FlexDirectory|null
  85. */
  86. public function getDirectory(string $type): ?FlexDirectory
  87. {
  88. return $this->types[$type] ?? null;
  89. }
  90. /**
  91. * @param string $type
  92. * @param array|null $keys
  93. * @param string|null $keyField
  94. * @return FlexCollectionInterface|null
  95. */
  96. public function getCollection(string $type, array $keys = null, string $keyField = null): ?FlexCollectionInterface
  97. {
  98. $directory = $type ? $this->getDirectory($type) : null;
  99. return $directory ? $directory->getCollection($keys, $keyField) : null;
  100. }
  101. /**
  102. * @param array $keys
  103. * @param array $options In addition to the options in getObjects(), following options can be passed:
  104. * collection_class: Class to be used to create the collection. Defaults to ObjectCollection.
  105. * @return FlexCollectionInterface
  106. * @throws \RuntimeException
  107. */
  108. public function getMixedCollection(array $keys, array $options = []): FlexCollectionInterface
  109. {
  110. $collectionClass = $options['collection_class'] ?? ObjectCollection::class;
  111. if (!class_exists($collectionClass)) {
  112. throw new \RuntimeException(sprintf('Cannot create collection: Class %s does not exist', $collectionClass));
  113. }
  114. $objects = $this->getObjects($keys, $options);
  115. return new $collectionClass($objects);
  116. }
  117. /**
  118. * @param array $keys
  119. * @param array $options Following optional options can be passed:
  120. * types: List of allowed types.
  121. * type: Allowed type if types isn't defined, otherwise acts as default_type.
  122. * default_type: Set default type for objects given without type (only used if key_field isn't set).
  123. * keep_missing: Set to true if you want to return missing objects as null.
  124. * key_field: Key field which is used to match the objects.
  125. * @return array
  126. */
  127. public function getObjects(array $keys, array $options = []): array
  128. {
  129. $type = $options['type'] ?? null;
  130. $defaultType = $options['default_type'] ?? $type ?? null;
  131. $keyField = $options['key_field'] ?? 'flex_key';
  132. // Prepare empty result lists for all requested Flex types.
  133. $types = $options['types'] ?? (array)$type ?: null;
  134. if ($types) {
  135. $types = array_fill_keys($types, []);
  136. }
  137. $strict = isset($types);
  138. $guessed = [];
  139. if ($keyField === 'flex_key') {
  140. // We need to split Flex key lookups into individual directories.
  141. $undefined = [];
  142. $keyFieldFind = 'storage_key';
  143. foreach ($keys as $flexKey) {
  144. if (!$flexKey) {
  145. continue;
  146. }
  147. $flexKey = (string)$flexKey;
  148. // Normalize key and type using fallback to default type if it was set.
  149. [$key, $type, $guess] = $this->resolveKeyAndType($flexKey, $defaultType);
  150. if ($type === '' && $types) {
  151. // Add keys which are not associated to any Flex type. They will be included to every Flex type.
  152. foreach ($types as $type => &$array) {
  153. $array[] = $key;
  154. $guessed[$key][] = "{$type}.obj:{$key}";
  155. }
  156. unset($array);
  157. } elseif (!$strict || isset($types[$type])) {
  158. // Collect keys by their Flex type. If allowed types are defined, only include values from those types.
  159. $types[$type][] = $key;
  160. if ($guess) {
  161. $guessed[$key][] = "{$type}.obj:{$key}";
  162. }
  163. }
  164. }
  165. } else {
  166. // We are using a specific key field, make every key undefined.
  167. $undefined = $keys;
  168. $keyFieldFind = $keyField;
  169. }
  170. if (!$types) {
  171. return [];
  172. }
  173. $list = [[]];
  174. foreach ($types as $type => $typeKeys) {
  175. // Also remember to look up keys from undefined Flex types.
  176. $lookupKeys = $undefined ? array_merge($typeKeys, $undefined) : $typeKeys;
  177. $collection = $this->getCollection($type, $lookupKeys, $keyFieldFind);
  178. if ($collection && $keyFieldFind !== $keyField) {
  179. $collection = $collection->withKeyField($keyField);
  180. }
  181. $list[] = $collection ? $collection->toArray() : [];
  182. }
  183. // Merge objects from individual types back together.
  184. $list = array_merge(...$list);
  185. // Use the original key ordering.
  186. if (!$guessed) {
  187. $list = array_replace(array_fill_keys($keys, null), $list);
  188. } else {
  189. // We have mixed keys, we need to map flex keys back to storage keys.
  190. $results = [];
  191. foreach ($keys as $key) {
  192. $flexKey = $guessed[$key] ?? $key;
  193. if (\is_array($flexKey)) {
  194. $result = null;
  195. foreach ($flexKey as $tryKey) {
  196. if ($result = $list[$tryKey] ?? null) {
  197. // Use the first matching object (conflicting objects will be ignored for now).
  198. break;
  199. }
  200. }
  201. } else {
  202. $result = $list[$flexKey] ?? null;
  203. }
  204. $results[$key] = $result;
  205. }
  206. $list = $results;
  207. }
  208. // Remove missing objects if not asked to keep them.
  209. if (empty($option['keep_missing'])) {
  210. $list = array_filter($list);
  211. }
  212. return $list;
  213. }
  214. /**
  215. * @param string $key
  216. * @param string|null $type
  217. * @param string|null $keyField
  218. * @return FlexObjectInterface|null
  219. */
  220. public function getObject(string $key, string $type = null, string $keyField = null): ?FlexObjectInterface
  221. {
  222. if (null === $type && null === $keyField) {
  223. // Special handling for quick Flex key lookups.
  224. $keyField = 'storage_key';
  225. [$type, $key] = $this->resolveKeyAndType($key, $type);
  226. } else {
  227. $type = $this->resolveType($type);
  228. }
  229. if ($type === '' || $key === '') {
  230. return null;
  231. }
  232. $directory = $this->getDirectory($type);
  233. return $directory ? $directory->getObject($key, $keyField) : null;
  234. }
  235. /**
  236. * @return int
  237. */
  238. public function count(): int
  239. {
  240. return \count($this->types);
  241. }
  242. protected function resolveKeyAndType(string $flexKey, string $type = null): array
  243. {
  244. $guess = false;
  245. if (strpos($flexKey, ':') !== false) {
  246. [$type, $key] = explode(':', $flexKey, 2);
  247. $type = $this->resolveType($type);
  248. } else {
  249. $key = $flexKey;
  250. $type = (string)$type;
  251. $guess = true;
  252. }
  253. return [$key, $type, $guess];
  254. }
  255. protected function resolveType(string $type = null): string
  256. {
  257. if (null !== $type && strpos($type, '.') !== false) {
  258. return preg_replace('|\.obj$|', '', $type);
  259. }
  260. return $type ?? '';
  261. }
  262. }