diff --git a/CHANGELOG.md b/CHANGELOG.md index 48ec1b645..00e1761ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ 1. [](#new) * Grav 1.6: Renamed `$grav['users']` service to `$grav['accounts']` + * Added `Flex::getObjects()` and `Flex::getMixedCollection()` methods for co-mingled collections + * Added support to use single Flex key parameter in `Flex::getObject()` method 1. [](#improved) * Renamed `Grav\Framework\File\Formatter\FormatterInterface` to `Grav\Framework\File\Interfaces\FileFormatterInterface` * Improved `File::save()` to use a temporary file if file isn't locked diff --git a/system/src/Grav/Framework/File/AbstractFile.php b/system/src/Grav/Framework/File/AbstractFile.php index 3ebb907bd..fb0473bc4 100644 --- a/system/src/Grav/Framework/File/AbstractFile.php +++ b/system/src/Grav/Framework/File/AbstractFile.php @@ -285,6 +285,10 @@ class AbstractFile implements FileInterface $filepath = $this->filepath; $dir = $this->getPath(); + if (!$this->mkdir($dir)) { + throw new \RuntimeException('Creating directory failed for ' . $filepath); + } + if ($this->handle) { $tmp = true; // As we are using non-truncating locking, make sure that the file is empty before writing. diff --git a/system/src/Grav/Framework/Flex/Flex.php b/system/src/Grav/Framework/Flex/Flex.php index bb1247a62..d50909de7 100644 --- a/system/src/Grav/Framework/Flex/Flex.php +++ b/system/src/Grav/Framework/Flex/Flex.php @@ -13,6 +13,7 @@ namespace Grav\Framework\Flex; use Grav\Framework\Collection\CollectionInterface; use Grav\Framework\Flex\Interfaces\FlexObjectInterface; +use Grav\Framework\Object\ObjectCollection; /** * Class Flex @@ -68,23 +69,40 @@ class Flex implements \Countable } /** + * @param string $type + * @return bool + */ + public function hasDirectory(string $type): bool + { + return isset($this->types[$type]); + } + + /** + * @param array|string[] $types + * @param bool $keepMissing * @return array|FlexDirectory[] */ - public function getDirectories(): array + public function getDirectories(array $types = null, bool $keepMissing = false): array { - return $this->types; + if ($types === null) { + return $this->types; + } + + // Return the directories in the given order. + $directories = []; + foreach ($types as $type) { + $directories = $this->types[$type] ?? null; + } + + return $keepMissing ? $directories : array_filter($directories); } /** * @param string|null $type * @return FlexDirectory|null */ - public function getDirectory(string $type = null): ?FlexDirectory + public function getDirectory(string $type): ?FlexDirectory { - if (!$type) { - return reset($this->types) ?: null; - } - return $this->types[$type] ?? null; } @@ -102,14 +120,117 @@ class Flex implements \Countable } /** - * @param string $type + * @param array $keys + * @param array $options In addition to the options in getObjects(), following options can be passed: + * collection_class: Class to be used to create the collection. Defaults to ObjectCollection. + * @return CollectionInterface + * @throws \RuntimeException + */ + public function getMixedCollection(array $keys, array $options = []): CollectionInterface + { + $collectionClass = $options['collection_class'] ?? ObjectCollection::class; + if (!class_exists($collectionClass)) { + throw new \RuntimeException(sprintf('Cannot create collection: Class %s does not exist', $collectionClass)); + } + + $objects = $this->getObjects($keys, $options); + + return new $collectionClass($objects); + } + + /** + * @param array $keys + * @param array $options Following optional options can be passed: + * types: List of allowed types. + * type: Allowed type if types isn't defined, otherwise acts as default_type. + * default_type: Set default type for objects given without type (only used if key_field isn't set). + * keep_missing: Set to true if you want to return missing objects as null. + * key_field: Key field which is used to match the objects. + * @return array + */ + public function getObjects(array $keys, array $options = []): array + { + $type = $options['type'] ?? null; + $defaultType = $options['default_type'] ?? $type ?? null; + $keyField = $options['key_field'] ?? 'flex_key'; + + // Prepare empty result lists for all requested Flex types. + $types = $options['types'] ?? (array)$type ?: null; + if ($types) { + $types = array_fill_keys($types, []); + } + $strict = isset($types); + + if ($keyField === 'flex_key') { + // We need to split Flex key lookups into individual directories. + $undefined = []; + $keyField = 'storage_key'; + + foreach ($keys as $key) { + // Normalize key and type using fallback to default type if it was set. + [$key, $type] = $this->resolveKeyAndType($key, $defaultType); + + if ($type === '') { + // Add keys which are not associated to any Flex type. They will be included to every Flex type. + $undefined[] = $key; + } elseif (!$strict || isset($types[$type])) { + // Collect keys by their Flex type. If allowed types are defined, only include values from those types. + $types[$type][] = $key; + } + } + } else { + // We are using a specific key field, make every key undefined. + $undefined = $keys; + } + + if (!$types) { + return []; + } + + $list = [[]]; + foreach ($types as $type => $typeKeys) { + // Also remember to look up keys from undefined Flex types. + $lookupKeys = $undefined ? array_merge($typeKeys, $undefined) : $typeKeys; + $collection = $this->getCollection($type, $lookupKeys, $keyField); + + $list[] = $collection ? $collection->toArray() : []; + } + + // Merge objects from individual types back together. + $list = array_merge(...$list); + + // Use the original key ordering. + $list = array_replace(array_fill_keys($keys, null), $list); + + // Remove missing objects if not asked to keep them. + if (empty($option['keep_missing'])) { + $list = array_filter($list); + } + + return $list; + } + + /** * @param string $key + * @param string|null $type * @param string|null $keyField * @return FlexObjectInterface|null */ - public function getObject(string $type, string $key, string $keyField = null): ?FlexObjectInterface + public function getObject(string $key, string $type = null, string $keyField = null): ?FlexObjectInterface { - $directory = $type ? $this->getDirectory($type) : null; + if (null === $type && null === $keyField) { + // Special handling for quick Flex key lookups. + $keyField = 'storage_key'; + [$type, $key] = $this->resolveKeyAndType($key, $type); + } else { + $type = $this->resolveType($type); + } + + if ($type === '' || $key === '') { + return null; + } + + $directory = $this->getDirectory($type); return $directory ? $directory->getObject($key, $keyField) : null; } @@ -117,8 +238,28 @@ class Flex implements \Countable /** * @return int */ - public function count() : int + public function count(): int { return \count($this->types); } + + protected function resolveKeyAndType(string $key, string $type = null): array + { + if (strpos($key, ':') !== false) { + [$type, $key] = explode(':', $key, 2); + + $type = $this->resolveType($type); + } + + return [$key, $type]; + } + + protected function resolveType(string $type = null): string + { + if (null !== $type && strpos($type, '.') !== false) { + return preg_replace('|\.obj$|', '', $type); + } + + return $type ?? ''; + } }