From 5faf8128345b5e7470dcb4c18dda7b57af0a262d Mon Sep 17 00:00:00 2001 From: Maciej Kobus Date: Wed, 27 Feb 2019 15:02:56 +0100 Subject: [PATCH] EZP-30065: As an editor I would like content tree to use real data (#854) * EZP-30065: As an editor I would like content tree to use real data * Refactorings after code review * fixup! Refactorings after code review --- .../Content/ContentTreeController.php | 94 ++++++++ .../Parser/Module/ContentTree.php | 110 +++++++++ src/bundle/EzPlatformAdminUiBundle.php | 1 + .../config/ezplatform_default_settings.yml | 7 + src/bundle/Resources/config/routing_rest.yml | 36 ++- src/bundle/Resources/config/services.yml | 1 + .../Resources/config/services/controllers.yml | 10 + .../config/services/modules/content_tree.yml | 12 + src/bundle/Resources/config/services/rest.yml | 24 ++ .../Parser/ContentTree/LoadSubtreeRequest.php | 36 +++ .../ContentTree/LoadSubtreeRequestNode.php | 54 +++++ .../ValueObjectVisitor/ContentTree/Node.php | 65 ++++++ .../ValueObjectVisitor/ContentTree/Root.php | 41 ++++ .../Value/ContentTree/LoadSubtreeRequest.php | 25 ++ .../ContentTree/LoadSubtreeRequestNode.php | 44 ++++ src/lib/REST/Value/ContentTree/Node.php | 80 +++++++ src/lib/REST/Value/ContentTree/Root.php | 25 ++ src/lib/UI/Module/ContentTree/NodeFactory.php | 221 ++++++++++++++++++ 18 files changed, 883 insertions(+), 3 deletions(-) create mode 100644 src/bundle/Controller/Content/ContentTreeController.php create mode 100644 src/bundle/DependencyInjection/Configuration/Parser/Module/ContentTree.php create mode 100644 src/bundle/Resources/config/services/modules/content_tree.yml create mode 100644 src/lib/REST/Input/Parser/ContentTree/LoadSubtreeRequest.php create mode 100644 src/lib/REST/Input/Parser/ContentTree/LoadSubtreeRequestNode.php create mode 100644 src/lib/REST/Output/ValueObjectVisitor/ContentTree/Node.php create mode 100644 src/lib/REST/Output/ValueObjectVisitor/ContentTree/Root.php create mode 100644 src/lib/REST/Value/ContentTree/LoadSubtreeRequest.php create mode 100644 src/lib/REST/Value/ContentTree/LoadSubtreeRequestNode.php create mode 100644 src/lib/REST/Value/ContentTree/Node.php create mode 100644 src/lib/REST/Value/ContentTree/Root.php create mode 100644 src/lib/UI/Module/ContentTree/NodeFactory.php diff --git a/src/bundle/Controller/Content/ContentTreeController.php b/src/bundle/Controller/Content/ContentTreeController.php new file mode 100644 index 0000000000..52787a1660 --- /dev/null +++ b/src/bundle/Controller/Content/ContentTreeController.php @@ -0,0 +1,94 @@ +locationService = $locationService; + $this->contentTreeNodeFactory = $contentTreeNodeFactory; + } + + /** + * @param int $parentLocationId + * @param int $limit + * @param int $offset + * + * @return \EzSystems\EzPlatformAdminUi\REST\Value\ContentTree\Node + * + * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException + * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException + * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException + */ + public function loadChildrenAction(int $parentLocationId, int $limit, int $offset): Node + { + $location = $this->locationService->loadLocation($parentLocationId); + + $loadSubtreeRequestNode = new LoadSubtreeRequestNode($parentLocationId, $limit, $offset); + + return $this->contentTreeNodeFactory->createNode($location, $loadSubtreeRequestNode, true); + } + + /** + * @param \Symfony\Component\HttpFoundation\Request $request + * + * @return \EzSystems\EzPlatformAdminUi\REST\Value\ContentTree\Root + * + * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException + * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException + * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException + */ + public function loadSubtreeAction(Request $request): Root + { + /** @var \EzSystems\EzPlatformAdminUi\REST\Value\ContentTree\LoadSubtreeRequest $loadSubtreeRequest */ + $loadSubtreeRequest = $this->inputDispatcher->parse( + new Message( + ['Content-Type' => $request->headers->get('Content-Type')], + $request->getContent() + ) + ); + + $locationIdList = array_column($loadSubtreeRequest->nodes, 'locationId'); + $locations = $this->locationService->loadLocationList($locationIdList); + + $elements = []; + foreach ($loadSubtreeRequest->nodes as $childLoadSubtreeRequestNode) { + $location = $locations[$childLoadSubtreeRequestNode->locationId]; + $elements[] = $this->contentTreeNodeFactory->createNode( + $location, + $childLoadSubtreeRequestNode, + true + ); + } + + return new Root($elements); + } +} diff --git a/src/bundle/DependencyInjection/Configuration/Parser/Module/ContentTree.php b/src/bundle/DependencyInjection/Configuration/Parser/Module/ContentTree.php new file mode 100644 index 0000000000..4191c3bff5 --- /dev/null +++ b/src/bundle/DependencyInjection/Configuration/Parser/Module/ContentTree.php @@ -0,0 +1,110 @@ +arrayNode('content_tree_module') + ->info('Content Tree module configuration') + ->children() + ->integerNode('load_more_limit') + ->info('Number of children to load in expand and load more operations') + ->isRequired() + ->defaultValue(30) + ->end() + ->integerNode('children_load_max_limit') + ->info('Total limit of loaded children in single node') + ->isRequired() + ->defaultValue(200) + ->end() + ->integerNode('tree_max_depth') + ->info('Max depth of expanded tree') + ->isRequired() + ->defaultValue(10) + ->end() + ->arrayNode('content_type_ignore_list') + ->info('List of content type identifiers to ignore in Content Tree') + ->arrayPrototype() + ->children() + ->scalarNode('content_type_identifier') + ->end() + ->end() + ->end() + ->end() + ->end(); + } + + /** + * {@inheritdoc} + */ + public function mapConfig(array &$scopeSettings, $currentScope, ContextualizerInterface $contextualizer): void + { + if (empty($scopeSettings['content_tree_module'])) { + return; + } + + $settings = $scopeSettings['content_tree_module']; + + if (array_key_exists('load_more_limit', $settings)) { + $contextualizer->setContextualParameter( + 'content_tree_module.load_more_limit', + $currentScope, + $settings['load_more_limit'] + ); + } + + if (array_key_exists('children_load_max_limit', $settings)) { + $contextualizer->setContextualParameter( + 'content_tree_module.children_load_max_limit', + $currentScope, + $settings['children_load_max_limit'] + ); + } + + if (array_key_exists('tree_max_depth', $settings)) { + $contextualizer->setContextualParameter( + 'content_tree_module.tree_max_depth', + $currentScope, + $settings['tree_max_depth'] + ); + } + + if (array_key_exists('content_type_ignore_list', $settings)) { + $contextualizer->setContextualParameter( + 'content_tree_module.content_type_ignore_list', + $currentScope, + $settings['content_type_ignore_list'] + ); + } + } +} diff --git a/src/bundle/EzPlatformAdminUiBundle.php b/src/bundle/EzPlatformAdminUiBundle.php index 4605ad9665..4bfd1aa45c 100644 --- a/src/bundle/EzPlatformAdminUiBundle.php +++ b/src/bundle/EzPlatformAdminUiBundle.php @@ -66,6 +66,7 @@ private function getConfigParsers(): array new Parser\LocationIds(), new Parser\Module\Subitems(), new Parser\Module\UniversalDiscoveryWidget(), + new Parser\Module\ContentTree(), new Parser\Pagination(), new Parser\Security(), new Parser\UserIdentifier(), diff --git a/src/bundle/Resources/config/ezplatform_default_settings.yml b/src/bundle/Resources/config/ezplatform_default_settings.yml index 8258a55c55..c54b19b966 100644 --- a/src/bundle/Resources/config/ezplatform_default_settings.yml +++ b/src/bundle/Resources/config/ezplatform_default_settings.yml @@ -53,3 +53,10 @@ parameters: # Additional translations e.g. ['en_US', 'nb_NO'] ezsettings.default.user_preferences.additional_translations: [] + + # Content Tree Module + ezsettings.default.content_tree_module.load_more_limit: 30 + ezsettings.default.content_tree_module.children_load_max_limit: 200 + ezsettings.default.content_tree_module.tree_max_depth: 10 + ezsettings.default.content_tree_module.content_type_ignore_list: [] + diff --git a/src/bundle/Resources/config/routing_rest.yml b/src/bundle/Resources/config/routing_rest.yml index 7783ecb1e4..a7afdd3fc1 100644 --- a/src/bundle/Resources/config/routing_rest.yml +++ b/src/bundle/Resources/config/routing_rest.yml @@ -1,7 +1,37 @@ +# +# Bulk Operation +# + ezplatform.bulk_operation: path: /bulk options: - expose: true + expose: true defaults: - _controller: 'EzPlatformAdminUiBundle:BulkOperation\BulkOperation:bulk' - methods: [POST] + _controller: 'EzPlatformAdminUiBundle:BulkOperation\BulkOperation:bulk' + methods: ['POST'] + +# +# Location Tree +# + +ezplatform.location.tree.load_children: + # @todo change name to content tree + path: /location/tree/load-subitems/{parentLocationId}/{limit}/{offset} + methods: ['GET'] + options: + expose: true + requirements: + parentLocationId: \d+ + defaults: + _controller: 'EzPlatformAdminUiBundle:Content/ContentTree:loadChildren' + limit: 10 + offset: 0 + +ezplatform.location.tree.load_subtree: + # @todo change name to content tree + path: /location/tree/load-subtree + methods: ['POST'] + options: + expose: true + defaults: + _controller: 'EzPlatformAdminUiBundle:Content/ContentTree:loadSubtree' diff --git a/src/bundle/Resources/config/services.yml b/src/bundle/Resources/config/services.yml index ef3e6096b8..dbbedfff99 100644 --- a/src/bundle/Resources/config/services.yml +++ b/src/bundle/Resources/config/services.yml @@ -8,6 +8,7 @@ imports: - { resource: services/components.yml } - { resource: services/dashboard.yml } - { resource: services/modules/subitems.yml } + - { resource: services/modules/content_tree.yml } - { resource: services/form_processors.yml } - { resource: services/validators.yml } - { resource: services/siteaccess.yml } diff --git a/src/bundle/Resources/config/services/controllers.yml b/src/bundle/Resources/config/services/controllers.yml index c9f14e7c84..2e5a446331 100644 --- a/src/bundle/Resources/config/services/controllers.yml +++ b/src/bundle/Resources/config/services/controllers.yml @@ -109,3 +109,13 @@ services: parent: EzSystems\EzPlatformAdminUiBundle\Controller\Controller arguments: $defaultPaginationLimit: '$pagination.content_draft_limit$' + + # + # REST + # + + EzSystems\EzPlatformAdminUiBundle\Controller\Content\ContentTreeController: + parent: ezpublish_rest.controller.base + tags: ['controller.service_arguments'] + autowire: true + autoconfigure: false diff --git a/src/bundle/Resources/config/services/modules/content_tree.yml b/src/bundle/Resources/config/services/modules/content_tree.yml new file mode 100644 index 0000000000..6cf488f97f --- /dev/null +++ b/src/bundle/Resources/config/services/modules/content_tree.yml @@ -0,0 +1,12 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: true + + EzSystems\EzPlatformAdminUi\UI\Module\ContentTree\NodeFactory: + arguments: + $displayLimit: '$content_tree_module.load_more_limit$' + $childrenLoadMaxLimit: '$content_tree_module.children_load_max_limit$' + $maxDepth: '$content_tree_module.tree_max_depth$' + $contentTypeIdentifierIgnoreList: '$content_tree_module.content_type_ignore_list$' diff --git a/src/bundle/Resources/config/services/rest.yml b/src/bundle/Resources/config/services/rest.yml index c1cd102d73..a91c37048c 100644 --- a/src/bundle/Resources/config/services/rest.yml +++ b/src/bundle/Resources/config/services/rest.yml @@ -13,3 +13,27 @@ services: parent: ezpublish_rest.output.value_object_visitor.base tags: - { name: ezpublish_rest.output.value_object_visitor, type: EzSystems\EzPlatformAdminUi\REST\Value\BulkOperationResponse } + + # + # Content Tree + # + + EzSystems\EzPlatformAdminUi\REST\Input\Parser\ContentTree\LoadSubtreeRequestNode: + parent: ezpublish_rest.input.parser + tags: + - { name: ezpublish_rest.input.parser, mediaType: application/vnd.ez.api.ContentTree.LoadSubtreeRequestNode } + + EzSystems\EzPlatformAdminUi\REST\Input\Parser\ContentTree\LoadSubtreeRequest: + parent: ezpublish_rest.input.parser + tags: + - { name: ezpublish_rest.input.parser, mediaType: application/vnd.ez.api.ContentTree.LoadSubtreeRequest } + - + EzSystems\EzPlatformAdminUi\REST\Output\ValueObjectVisitor\ContentTree\Node: + parent: ezpublish_rest.output.value_object_visitor.base + tags: + - { name: ezpublish_rest.output.value_object_visitor, type: EzSystems\EzPlatformAdminUi\REST\Value\ContentTree\Node } + + EzSystems\EzPlatformAdminUi\REST\Output\ValueObjectVisitor\ContentTree\Root: + parent: ezpublish_rest.output.value_object_visitor.base + tags: + - { name: ezpublish_rest.output.value_object_visitor, type: EzSystems\EzPlatformAdminUi\REST\Value\ContentTree\Root } diff --git a/src/lib/REST/Input/Parser/ContentTree/LoadSubtreeRequest.php b/src/lib/REST/Input/Parser/ContentTree/LoadSubtreeRequest.php new file mode 100644 index 0000000000..9262844706 --- /dev/null +++ b/src/lib/REST/Input/Parser/ContentTree/LoadSubtreeRequest.php @@ -0,0 +1,36 @@ +parse($node, $node['_media-type']); + } + + return new LoadSubtreeRequestValue($nodes); + } +} diff --git a/src/lib/REST/Input/Parser/ContentTree/LoadSubtreeRequestNode.php b/src/lib/REST/Input/Parser/ContentTree/LoadSubtreeRequestNode.php new file mode 100644 index 0000000000..b7ddda89f3 --- /dev/null +++ b/src/lib/REST/Input/Parser/ContentTree/LoadSubtreeRequestNode.php @@ -0,0 +1,54 @@ +parse($child, $child['_media-type']); + } + + return new LoadSubtreeRequestNodeValue( + (int) $data['locationId'], + (int) $data['limit'], + (int) $data['offset'], + $children + ); + } +} diff --git a/src/lib/REST/Output/ValueObjectVisitor/ContentTree/Node.php b/src/lib/REST/Output/ValueObjectVisitor/ContentTree/Node.php new file mode 100644 index 0000000000..6a44a583f6 --- /dev/null +++ b/src/lib/REST/Output/ValueObjectVisitor/ContentTree/Node.php @@ -0,0 +1,65 @@ +startObjectElement('ContentTreeNode'); + $visitor->setHeader('Content-Type', $generator->getMediaType('ContentTreeNode')); + $visitor->setStatus(Response::HTTP_OK); + + $generator->startValueElement('locationId', $data->locationId); + $generator->endValueElement('locationId'); + + $generator->startValueElement('contentId', $data->contentId); + $generator->endValueElement('contentId'); + + $generator->startValueElement('name', $data->name); + $generator->endValueElement('name'); + + $generator->startValueElement('contentTypeIdentifier', $data->contentTypeIdentifier); + $generator->endValueElement('contentTypeIdentifier'); + + $generator->startValueElement('isContainer', $data->isContainer); + $generator->endValueElement('isContainer'); + + $generator->startValueElement('isInvisible', $data->isInvisible); + $generator->endValueElement('isInvisible'); + + $generator->startValueElement('displayLimit', $data->displayLimit); + $generator->endValueElement('displayLimit'); + + $generator->startValueElement('totalChildrenCount', $data->totalChildrenCount); + $generator->endValueElement('totalChildrenCount'); + + $generator->startList('children'); + + foreach ($data->children as $child) { + $visitor->visitValueObject($child); + } + + $generator->endList('children'); + + $generator->endObjectElement('ContentTreeNode'); + } +} diff --git a/src/lib/REST/Output/ValueObjectVisitor/ContentTree/Root.php b/src/lib/REST/Output/ValueObjectVisitor/ContentTree/Root.php new file mode 100644 index 0000000000..1bf836e7ca --- /dev/null +++ b/src/lib/REST/Output/ValueObjectVisitor/ContentTree/Root.php @@ -0,0 +1,41 @@ +startObjectElement('ContentTreeRoot'); + $visitor->setHeader('Content-Type', $generator->getMediaType('ContentTreeRoot')); + $visitor->setStatus(Response::HTTP_OK); + + $generator->startList('ContentTreeNodeList'); + + foreach ($data->elements as $element) { + $visitor->visitValueObject($element); + } + + $generator->endList('ContentTreeNodeList'); + + $generator->endObjectElement('ContentTreeRoot'); + } +} diff --git a/src/lib/REST/Value/ContentTree/LoadSubtreeRequest.php b/src/lib/REST/Value/ContentTree/LoadSubtreeRequest.php new file mode 100644 index 0000000000..05a6426512 --- /dev/null +++ b/src/lib/REST/Value/ContentTree/LoadSubtreeRequest.php @@ -0,0 +1,25 @@ +nodes = $nodes; + } +} diff --git a/src/lib/REST/Value/ContentTree/LoadSubtreeRequestNode.php b/src/lib/REST/Value/ContentTree/LoadSubtreeRequestNode.php new file mode 100644 index 0000000000..2467f25e6e --- /dev/null +++ b/src/lib/REST/Value/ContentTree/LoadSubtreeRequestNode.php @@ -0,0 +1,44 @@ +locationId = $locationId; + $this->children = $children; + $this->limit = $limit; + $this->offset = $offset; + } +} diff --git a/src/lib/REST/Value/ContentTree/Node.php b/src/lib/REST/Value/ContentTree/Node.php new file mode 100644 index 0000000000..f3902ff13f --- /dev/null +++ b/src/lib/REST/Value/ContentTree/Node.php @@ -0,0 +1,80 @@ +depth = $depth; + $this->locationId = $locationId; + $this->contentId = $contentId; + $this->name = $name; + $this->isInvisible = $isInvisible; + $this->contentTypeIdentifier = $contentTypeIdentifier; + $this->isContainer = $isContainer; + $this->totalChildrenCount = $totalChildrenCount; + $this->displayLimit = $displayLimit; + $this->children = $children; + } +} diff --git a/src/lib/REST/Value/ContentTree/Root.php b/src/lib/REST/Value/ContentTree/Root.php new file mode 100644 index 0000000000..1188c2e29d --- /dev/null +++ b/src/lib/REST/Value/ContentTree/Root.php @@ -0,0 +1,25 @@ +elements = $elements; + } +} diff --git a/src/lib/UI/Module/ContentTree/NodeFactory.php b/src/lib/UI/Module/ContentTree/NodeFactory.php new file mode 100644 index 0000000000..fd7661663d --- /dev/null +++ b/src/lib/UI/Module/ContentTree/NodeFactory.php @@ -0,0 +1,221 @@ +searchService = $searchService; + $this->displayLimit = $displayLimit; + $this->childrenLoadMaxLimit = $childrenLoadMaxLimit; + $this->maxDepth = $maxDepth; + $this->contentTypeIdentifierIgnoreList = $contentTypeIdentifierIgnoreList; + } + + /** + * @param \eZ\Publish\API\Repository\Values\Content\Location $location + * @param \EzSystems\EzPlatformAdminUi\REST\Value\ContentTree\LoadSubtreeRequestNode $loadSubtreeRequestNode + * @param bool $loadChildren + * @param int $depth + * + * @return \EzSystems\EzPlatformAdminUi\REST\Value\ContentTree\Node + * + * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException + * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException + * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException + */ + public function createNode( + Location $location, + ?LoadSubtreeRequestNode $loadSubtreeRequestNode = null, + bool $loadChildren = false, + int $depth = 0 + ): Node { + $content = $location->getContent(); + $contentType = $content->getContentType(); + + $numberOfSubitemsToLoad = $this->resolveLoadLimit($loadSubtreeRequestNode); + + $children = []; + if ($depth < $this->maxDepth && $loadChildren) { + $searchResult = $this->findSubitems($location, $numberOfSubitemsToLoad); + $totalChildrenCount = $searchResult->totalCount; + + /** @var \eZ\Publish\API\Repository\Values\Content\Location $childLocation */ + foreach (array_column($searchResult->searchHits, 'valueObject') as $childLocation) { + $childLoadSubtreeRequestNode = null !== $loadSubtreeRequestNode + ? $this->findChild($childLocation->id, $loadSubtreeRequestNode) + : null; + + $children[] = $this->createNode( + $childLocation, + $childLoadSubtreeRequestNode, + null !== $childLoadSubtreeRequestNode, + $depth++ + ); + } + } else { + $totalChildrenCount = $this->countSubitems($location); + } + + return new Node( + $depth, + $location->id, + $location->contentId, + $content->getName(), + $contentType->identifier, + $contentType->isContainer, + $location->invisible, + $numberOfSubitemsToLoad, + $totalChildrenCount, + $children + ); + } + + /** + * @param \EzSystems\EzPlatformAdminUi\REST\Value\ContentTree\LoadSubtreeRequestNode|null $loadSubtreeRequestNode + * + * @return int + */ + private function resolveLoadLimit(?LoadSubtreeRequestNode $loadSubtreeRequestNode): int + { + $limit = $this->displayLimit; + + if (null !== $loadSubtreeRequestNode) { + $limit = $loadSubtreeRequestNode->limit; + } + + if ($limit > $this->childrenLoadMaxLimit) { + $limit = $this->childrenLoadMaxLimit; + } + + return $limit; + } + + /** + * @param \eZ\Publish\API\Repository\Values\Content\Location $parentLocation + * @param int $limit + * @param int $offset + * + * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult + * + * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException + */ + private function findSubitems(Location $parentLocation, int $limit = 10, int $offset = 0): SearchResult + { + $searchQuery = $this->getSearchQuery($parentLocation); + + $searchQuery->limit = $limit; + $searchQuery->offset = $offset; + + try { + $searchQuery->sortClauses = $parentLocation->getSortClauses(); + } catch (NotImplementedException $e) { + $searchQuery->sortClauses = []; // rely on storage engine default sorting + } + + return $this->searchService->findLocations($searchQuery); + } + + /** + * @param \eZ\Publish\API\Repository\Values\Content\Location $parentLocation + * + * @return \eZ\Publish\API\Repository\Values\Content\LocationQuery + */ + private function getSearchQuery(Location $parentLocation): LocationQuery + { + $searchQuery = new LocationQuery(); + $searchQuery->filter = new Criterion\ParentLocationId($parentLocation->id); + + if (!empty($this->contentTypeIdentifierIgnoreList)) { + $searchQuery->filter = new Criterion\LogicalAnd([ + $searchQuery->filter, + new Criterion\LogicalNot( + new Criterion\ContentTypeIdentifier($this->contentTypeIdentifierIgnoreList) + ), + ]); + } + + return $searchQuery; + } + + /** + * @param int $locationId + * @param \EzSystems\EzPlatformAdminUi\REST\Value\ContentTree\LoadSubtreeRequestNode $loadSubtreeRequestNode + * + * @return \EzSystems\EzPlatformAdminUi\REST\Value\ContentTree\LoadSubtreeRequestNode|null + */ + private function findChild(int $locationId, LoadSubtreeRequestNode $loadSubtreeRequestNode): ?LoadSubtreeRequestNode + { + foreach ($loadSubtreeRequestNode->children as $child) { + if ($child->locationId === $locationId) { + return $child; + } + } + + return null; + } + + /** + * @param \eZ\Publish\API\Repository\Values\Content\Location $parentLocation + * + * @return int + * + * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException + */ + private function countSubitems(Location $parentLocation): int + { + $searchQuery = $this->getSearchQuery($parentLocation); + + $searchQuery->limit = 0; + $searchQuery->offset = 0; + $searchQuery->performCount = true; + + return $this->searchService->findLocations($searchQuery)->totalCount; + } +}