diff --git a/doc/10_GraphQL/README.md b/doc/10_GraphQL/README.md index 28a549b3d..9edfa4258 100644 --- a/doc/10_GraphQL/README.md +++ b/doc/10_GraphQL/README.md @@ -95,3 +95,65 @@ Open the settings and change `request.credentials` to `include`. Otherwise the `XDEBUG_SESSION` cookie header will get removed by default. ![Settings](../img/graphql/debugging.png) + +## Mockup Elements + +**!Beware!** It is a quite complex undertaking to implement this correctly, but worth it if you need a high-performance integration of product indexes. + +DataHub supports the use of Mockup Objects / Elements as used in Pimcores Ecommerce Framework Bundle IndexService. +In order to use a Mockup Class it has to implement DataHubs own interface: \Pimcore\Bundle\DataHubBundle\Model\ElementMockupInterface + +Mockup elements will be resolved differently, operators and or sub-elements are +only resolved if the requested data structure isn't already available in the mockup itself. +As such Mockup Objects have to ensure to return the same output as a non-mocked +query would. + +One particular way to do this is to implement a callback in the mockup and use +the passed in GraphQl Execution context to resolve the data, + +Example of returning Image Thumbnail Path: + +```php +class AssetMockup implements ElementMockupInterface +{ + use ElementMockupTrait; + + /** + * @return array + */ + public function getFullpath(): ?string + { + // Check if this is a graphQL callback. + $suffix = null; + if ($this->isGraphqlCallback(__FUNCTION__)) { + if ($thumbnail = $this->getGraphQLArguments()['thumbnail'] ?? null) { + $suffix = '_' . $thumbnail; + } + } + return $this->getParam('fullpath' . $suffix) ?? $this->getParam('fullpath') ?? null; + } +} +``` + +Will use the returned value, or if null is returned the original Asset is used to resolve the value. + +The mockup support can be enabled and configured with a configuration entry like this in your `config.yml` file: +```yml +#### DATAHUB MOCKUP ELEMENTS +pimcore_data_hub: + graphql: + mockup_element_support_enabled: true +``` + +## Permission Check Cache + +By default only a single permission check per request is executed. +This is different from previous versions in which the event +`pimcore.datahub.graphql.permission.preCheck` was fired multiple times during the execution. + +The new approach limits the performance impact of the permission handling - but can be disabled using following setting: +```yml +pimcore_data_hub: + graphql: + disable_permission_check_cache: true +``` \ No newline at end of file diff --git a/src/Controller/WebserviceController.php b/src/Controller/WebserviceController.php index fd4432051..5c49633ac 100644 --- a/src/Controller/WebserviceController.php +++ b/src/Controller/WebserviceController.php @@ -176,6 +176,8 @@ public function webonyxAction( if (isset($datahubConfig['graphql']) && isset($datahubConfig['graphql']['not_allowed_policy'])) { PimcoreDataHubBundle::setNotAllowedPolicy($datahubConfig['graphql']['not_allowed_policy']); } + $context['disable_permission_check_cache'] = !empty($datahubConfig['graphql']['disable_permission_check_cache']); + $context['mockup_element_support_enabled'] = !empty($datahubConfig['graphql']['mockup_element_support_enabled']); $validators = null; if ($request->get('novalidate')) { diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 51983b7c1..0636d38d5 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -39,6 +39,8 @@ public function getConfigTreeBuilder() ->scalarNode('not_allowed_policy')->info('throw exception = 1, return null = 2')->defaultValue(2)->end() ->booleanNode('output_cache_enabled')->info('enables output cache for graphql responses. It is disabled by default')->defaultValue(false)->end() ->integerNode('output_cache_lifetime')->info('output cache in seconds. Default is 30 seconds')->defaultValue(30)->end() + ->booleanNode('mockup_element_support_enabled')->info('Enable support of mockup elements as used by Ecommerce Framework Bundle IndexService. You have to use your own Mockup class(es) that implement DataHubs ElementMockupInterface')->defaultValue(false)->end() + ->booleanNode('disable_permission_check_cache')->info('Disables the permission check static cache. Use only if you have very specific cases in which the permission can change _within_ the same request!')->defaultValue(false)->end() ->booleanNode('allow_introspection')->info('enables introspection for graphql. It is enabled by default')->defaultValue(true)->end() ->booleanNode('run_subrequest_per_query')->info('If enabled a Symfony Sub-Requet is triggered for each query in a multi-query request.')->defaultValue(false)->end() ->end() diff --git a/src/GraphQL/DataObjectQueryFieldConfigGenerator/AbstractTable.php b/src/GraphQL/DataObjectQueryFieldConfigGenerator/AbstractTable.php index 597287dcc..b83c6d8f1 100644 --- a/src/GraphQL/DataObjectQueryFieldConfigGenerator/AbstractTable.php +++ b/src/GraphQL/DataObjectQueryFieldConfigGenerator/AbstractTable.php @@ -41,7 +41,7 @@ public function getGraphQlFieldConfig($attribute, Data $fieldDefinition, $class 'name' => $fieldDefinition->getName(), 'type' => $this->getFieldType($fieldDefinition, $class, $container), 'resolve' => function ($value, $args, $context = [], ResolveInfo $resolveInfo = null) use ($fieldDefinition, $attribute) { - $result = Service::resolveValue($value, $fieldDefinition, $attribute, $args); + $result = Service::resolveValue($value, $fieldDefinition, $attribute, $args, !empty($context['mockup_element_support_enabled'])); // The table has no specific definition of columns, so we cannot have a ObjectType in schema for it. // Just return the data JSON encoded diff --git a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/AssetBase.php b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/AssetBase.php index 07f85fa31..781321729 100644 --- a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/AssetBase.php +++ b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/AssetBase.php @@ -69,7 +69,7 @@ public function __construct(Service $graphQlService, $attribute, $fieldDefinitio */ public function resolve($value = null, $args = [], $context = [], ResolveInfo $resolveInfo = null) { - $asset = Service::resolveValue($value, $this->fieldDefinition, $this->attribute, $args = []); + $asset = Service::resolveValue($value, $this->fieldDefinition, $this->attribute, $args = [], !empty($context['mockup_element_support_enabled'])); if (!$asset) { return null; diff --git a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Base.php b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Base.php index ffaf51dcb..a900bbaa3 100644 --- a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Base.php +++ b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Base.php @@ -67,7 +67,7 @@ public function __construct(Service $graphQlService, $attribute, $fieldDefinitio */ public function resolve($value = null, $args = [], $context = [], ResolveInfo $resolveInfo = null) { - $result = Service::resolveValue($value, $this->fieldDefinition, $this->attribute, $args); + $result = Service::resolveValue($value, $this->fieldDefinition, $this->attribute, $args, !empty($context['mockup_element_support_enabled'])); return $result; } diff --git a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Hotspotimage.php b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Hotspotimage.php index 76338cdf9..f9fd17396 100644 --- a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Hotspotimage.php +++ b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Hotspotimage.php @@ -84,7 +84,7 @@ public function resolve($value = null, $args = [], $context = [], ResolveInfo $r return Service::resolveCachedValue($value, $resolveInfo); } /** @var $container Hotspotimage */ - $container = Service::resolveValue($value, $this->fieldDefinition, $this->attribute, $args); + $container = Service::resolveValue($value, $this->fieldDefinition, $this->attribute, $args, !empty($context['mockup_element_support_enabled'])); if ($container instanceof \Pimcore\Model\DataObject\Data\Hotspotimage) { $image = $container->getImage(); if ($image instanceof Asset) { diff --git a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Href.php b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Href.php index e04471514..2165e7333 100644 --- a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Href.php +++ b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Href.php @@ -20,6 +20,7 @@ use Pimcore\Bundle\DataHubBundle\GraphQL\ElementDescriptor; use Pimcore\Bundle\DataHubBundle\GraphQL\Service; use Pimcore\Bundle\DataHubBundle\GraphQL\Traits\ServiceTrait; +use Pimcore\Bundle\DataHubBundle\Model\ElementMockupInterface; use Pimcore\Bundle\DataHubBundle\WorkspaceHelper; use Pimcore\Model\DataObject\ClassDefinition; use Pimcore\Model\DataObject\ClassDefinition\Data; @@ -72,9 +73,9 @@ public function __construct(Service $graphQlService, $attribute, $fieldDefinitio public function resolve($value = null, $args = [], $context = [], ResolveInfo $resolveInfo = null) { if ($value instanceof BaseDescriptor) { - $relation = \Pimcore\Bundle\DataHubBundle\GraphQL\Service::resolveValue($value, $this->fieldDefinition, $this->attribute, $args); + $relation = \Pimcore\Bundle\DataHubBundle\GraphQL\Service::resolveValue($value, $this->fieldDefinition, $this->attribute, $args, !empty($context['mockup_element_support_enabled'])); - if ($relation instanceof ElementInterface) { + if ($relation instanceof ElementInterface || $relation instanceof ElementMockupInterface) { if (!WorkspaceHelper::checkPermission($relation, 'read')) { return null; } diff --git a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Image.php b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Image.php index 2a2887f4b..a87ed0478 100644 --- a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Image.php +++ b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Image.php @@ -20,6 +20,7 @@ use Pimcore\Bundle\DataHubBundle\GraphQL\ElementDescriptor; use Pimcore\Bundle\DataHubBundle\GraphQL\Service; use Pimcore\Bundle\DataHubBundle\GraphQL\Traits\ServiceTrait; +use Pimcore\Bundle\DataHubBundle\Model\ElementMockupInterface; use Pimcore\Bundle\DataHubBundle\WorkspaceHelper; use Pimcore\Model\Asset; use Pimcore\Model\DataObject\ClassDefinition; @@ -72,9 +73,9 @@ public function __construct(\Pimcore\Bundle\DataHubBundle\GraphQL\Service $graph public function resolve($value = null, $args = [], $context = [], ResolveInfo $resolveInfo = null) { if ($value instanceof BaseDescriptor) { - $relation = Service::resolveValue($value, $this->fieldDefinition, $this->attribute, $args); + $relation = Service::resolveValue($value, $this->fieldDefinition, $this->attribute, $args, !empty($context['mockup_element_support_enabled'])); - if ($relation instanceof Asset) { + if ($relation instanceof Asset || $relation instanceof ElementMockupInterface) { if (!WorkspaceHelper::checkPermission($relation, 'read')) { return null; } diff --git a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/ImageGallery.php b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/ImageGallery.php index faf63dec0..29d394d26 100644 --- a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/ImageGallery.php +++ b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/ImageGallery.php @@ -21,6 +21,7 @@ use Pimcore\Bundle\DataHubBundle\GraphQL\ElementDescriptor; use Pimcore\Bundle\DataHubBundle\GraphQL\Service as GraphQlService; use Pimcore\Bundle\DataHubBundle\GraphQL\Traits\ServiceTrait; +use Pimcore\Bundle\DataHubBundle\Model\ElementMockupInterface; use Pimcore\Bundle\DataHubBundle\WorkspaceHelper; use Pimcore\Model\Asset; use Pimcore\Model\DataObject\ClassDefinition; @@ -83,7 +84,7 @@ public function __construct( public function resolve($value = null, $args = [], $context = [], ResolveInfo $resolveInfo = null) { $result = []; - $relations = GraphQlService::resolveValue($value, $this->fieldDefinition, $this->attribute, $args); + $relations = GraphQlService::resolveValue($value, $this->fieldDefinition, $this->attribute, $args, !empty($context['mockup_element_support_enabled'])); if ($relations) { foreach ($relations as $relation) { if ($relation instanceof Hotspotimage) { @@ -92,7 +93,7 @@ public function resolve($value = null, $args = [], $context = [], ResolveInfo $r continue; } - if ($image instanceof Asset) { + if ($image instanceof Asset || $relation instanceof ElementMockupInterface) { if (!WorkspaceHelper::checkPermission($image, 'read')) { continue; } diff --git a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Multihref.php b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Multihref.php index 32aec4123..b1849774d 100644 --- a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Multihref.php +++ b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Multihref.php @@ -70,11 +70,11 @@ public function __construct(Service $graphQlService, $attribute, $fieldDefinitio public function resolve($value = null, $args = [], $context = [], ResolveInfo $resolveInfo = null) { $result = []; - $relations = \Pimcore\Bundle\DataHubBundle\GraphQL\Service::resolveValue($value, $this->fieldDefinition, $this->attribute, $args); + $relations = \Pimcore\Bundle\DataHubBundle\GraphQL\Service::resolveValue($value, $this->fieldDefinition, $this->attribute, $args, !empty($context['mockup_element_support_enabled'])); if ($relations) { /** @var AbstractElement $relation */ foreach ($relations as $relation) { - if (!WorkspaceHelper::checkPermission($relation, 'read')) { + if (!WorkspaceHelper::checkPermission($relation, 'read', $type)) { continue; } diff --git a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/MultihrefMetadata.php b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/MultihrefMetadata.php index 590a0a54e..6b26fb092 100644 --- a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/MultihrefMetadata.php +++ b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/MultihrefMetadata.php @@ -70,7 +70,7 @@ public function __construct(Service $graphQlService, $attribute, $fieldDefinitio */ public function resolve($value = null, $args = [], $context = [], ResolveInfo $resolveInfo = null) { - $relations = \Pimcore\Bundle\DataHubBundle\GraphQL\Service::resolveValue($value, $this->fieldDefinition, $this->attribute, $args); + $relations = \Pimcore\Bundle\DataHubBundle\GraphQL\Service::resolveValue($value, $this->fieldDefinition, $this->attribute, $args, !empty($context['mockup_element_support_enabled'])); if ($relations) { $result = []; /** @var ElementMetadata $relation */ diff --git a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Objects.php b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Objects.php index f59a89309..68038e0e8 100644 --- a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Objects.php +++ b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/Objects.php @@ -73,11 +73,11 @@ public function __construct(Service $graphQlService, $attribute, $fieldDefinitio public function resolve($value = null, $args = [], $context = [], ResolveInfo $resolveInfo = null) { if ($value instanceof BaseDescriptor) { - $relations = Service::resolveValue($value, $this->fieldDefinition, $this->attribute, $args); + $relations = Service::resolveValue($value, $this->fieldDefinition, $this->attribute, $args, !empty($context['mockup_element_support_enabled'])); if ($relations) { $result = []; - /** @var $relation AbstractElement */ foreach ($relations as $relation) { + /** @var $relation AbstractElement */ if (!WorkspaceHelper::checkPermission($relation, 'read')) { continue; } diff --git a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/ObjectsMetadata.php b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/ObjectsMetadata.php index 1ac5b36b2..3fd59c2ea 100644 --- a/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/ObjectsMetadata.php +++ b/src/GraphQL/DataObjectQueryFieldConfigGenerator/Helper/ObjectsMetadata.php @@ -70,7 +70,7 @@ public function __construct(Service $graphQlService, $attribute, $fieldDefinitio public function resolve($value = null, $args = [], $context = [], ResolveInfo $resolveInfo = null) { $result = []; - $relations = \Pimcore\Bundle\DataHubBundle\GraphQL\Service::resolveValue($value, $this->fieldDefinition, $this->attribute, $args); + $relations = \Pimcore\Bundle\DataHubBundle\GraphQL\Service::resolveValue($value, $this->fieldDefinition, $this->attribute, $args, !empty($context['mockup_element_support_enabled'])); if ($relations) { /** @var ObjectMetadata $relation */ foreach ($relations as $relation) { diff --git a/src/GraphQL/DataObjectType/BlockEntryType.php b/src/GraphQL/DataObjectType/BlockEntryType.php index 5dd997e51..da92f0bdd 100644 --- a/src/GraphQL/DataObjectType/BlockEntryType.php +++ b/src/GraphQL/DataObjectType/BlockEntryType.php @@ -158,7 +158,7 @@ protected function prepareField(Data $fieldDef, bool $localized = false) return $resolve($value, $args, $context, $resolveInfo); } - return $this->graphQlService::resolveValue($value, $this->fieldDefinition, $this->fieldDefinition->getName(), $args); + return $this->graphQlService::resolveValue($value, $this->fieldDefinition, $this->fieldDefinition->getName(), $args, !empty($context['mockup_element_support_enabled'])); }; return $field; diff --git a/src/GraphQL/ElementDescriptor.php b/src/GraphQL/ElementDescriptor.php index 4e95f678b..06c66a693 100644 --- a/src/GraphQL/ElementDescriptor.php +++ b/src/GraphQL/ElementDescriptor.php @@ -15,6 +15,7 @@ namespace Pimcore\Bundle\DataHubBundle\GraphQL; +use Pimcore\Bundle\DataHubBundle\Model\ElementMockupInterface; use Pimcore\Model\Asset; use Pimcore\Model\DataObject\Concrete; use Pimcore\Model\Document; @@ -25,7 +26,7 @@ class ElementDescriptor extends BaseDescriptor /** * @param ElementInterface|null $element */ - public function __construct(ElementInterface $element = null) + public function __construct(ElementInterface | ElementMockupInterface $element = null) { parent::__construct(); if ($element) { @@ -33,7 +34,10 @@ public function __construct(ElementInterface $element = null) $this->offsetSet('__elementType', \Pimcore\Model\Element\Service::getElementType($element)); $this->offsetSet('__elementSubtype', $element instanceof Concrete ? $element->getClass()->getName() : $element->getType()); - if ($element instanceof Concrete) { + if ($element instanceof ElementMockupInterface) { + $this->offsetSet('__elementType', $element->getElementType()); + $this->offsetSet('__elementSubtype', $element->getElementType() === 'object' ? $element->getClass()->getName() : $element->getType()); + } elseif ($element instanceof Concrete) { $subtype = $element->getClass()->getName(); $this->offsetSet('__elementType', 'object'); diff --git a/src/GraphQL/FieldHelper/AbstractFieldHelper.php b/src/GraphQL/FieldHelper/AbstractFieldHelper.php index a342cee49..fa313beb3 100644 --- a/src/GraphQL/FieldHelper/AbstractFieldHelper.php +++ b/src/GraphQL/FieldHelper/AbstractFieldHelper.php @@ -22,12 +22,14 @@ use GraphQL\Language\AST\NodeList; use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Type\Definition\ResolveInfo; +use Pimcore\Bundle\DataHubBundle\GraphQL\Traits\ElementLoaderTrait; use Pimcore\Bundle\DataHubBundle\GraphQL\Traits\ServiceTrait; +use Pimcore\Bundle\DataHubBundle\Model\ElementMockupInterface; use Pimcore\Model\Element\ElementInterface; abstract class AbstractFieldHelper { - use ServiceTrait; + use ServiceTrait, ElementLoaderTrait; public function __construct() { @@ -118,10 +120,10 @@ public function getArguments(FieldNode $ast) */ public function extractData(&$data, $container, $args, $context = [], ResolveInfo $resolveInfo = null) { - if ($container instanceof ElementInterface) { + if ($container instanceof ElementInterface || $container instanceof ElementMockupInterface) { // we have to at least add the ID and pass it around even if not requested because we need it internally // to resolve fields of linked elements (such as asset image and so on) - $data['id'] = $container->getId(); + $data = $this->setDataElement($data, $container); } $resolveInfoArray = (array)$resolveInfo; diff --git a/src/GraphQL/FieldHelper/AssetFieldHelper.php b/src/GraphQL/FieldHelper/AssetFieldHelper.php index bbabd2e0c..d8ed92593 100644 --- a/src/GraphQL/FieldHelper/AssetFieldHelper.php +++ b/src/GraphQL/FieldHelper/AssetFieldHelper.php @@ -17,6 +17,7 @@ use GraphQL\Language\AST\FieldNode; use GraphQL\Type\Definition\ResolveInfo; +use Pimcore\Bundle\DataHubBundle\GraphQL\Service; use Pimcore\Model\Asset; use Pimcore\Model\Asset\Image; use Pimcore\Model\Asset\Video; @@ -128,23 +129,7 @@ public function doExtractData(FieldNode $ast, &$data, $container, $args, $contex } } } else { - if (method_exists($container, $getter)) { - if ($languageArgument) { - if ($ast->alias) { - // defer it - $data[$realName] = function ($source, $args, $context, ResolveInfo $info) use ( - $container, - $getter - ) { - return $container->$getter($args['language'] ?? null); - }; - } else { - $data[$realName] = $container->$getter($languageArgument); - } - } else { - $data[$realName] = $container->$getter(); - } - } + Service::resolveContainerGetterData($container, $data, $getter, $resolveInfo, $ast, $languageArgument); } } } diff --git a/src/GraphQL/FieldHelper/DataObjectFieldHelper.php b/src/GraphQL/FieldHelper/DataObjectFieldHelper.php index e8c5efb52..07ad69531 100644 --- a/src/GraphQL/FieldHelper/DataObjectFieldHelper.php +++ b/src/GraphQL/FieldHelper/DataObjectFieldHelper.php @@ -19,18 +19,26 @@ use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; use Pimcore\Bundle\DataHubBundle\GraphQL\Exception\ClientSafeException; -use Pimcore\Bundle\EcommerceFrameworkBundle\Model\DefaultMockup; +use Pimcore\Bundle\DataHubBundle\GraphQL\Service; use Pimcore\File; use Pimcore\Logger; use Pimcore\Model\DataObject\ClassDefinition; use Pimcore\Model\DataObject\ClassDefinition\Data; use Pimcore\Model\DataObject\Concrete; -use Pimcore\Model\DataObject\Fieldcollection\Data\AbstractData; use Pimcore\Model\DataObject\Localizedfield; use Pimcore\Model\DataObject\Objectbrick\Definition; +use Pimcore\Model\Factory; class DataObjectFieldHelper extends AbstractFieldHelper { + protected Factory $modelFactory; + + public function __construct(\Pimcore\Model\Factory $modelFactory) + { + parent::__construct(); + $this->modelFactory = $modelFactory; + } + /** * @param array $nodeDef * @param ClassDefinition|\Pimcore\Model\DataObject\Fieldcollection\Definition $class @@ -70,8 +78,8 @@ public function getQueryFieldConfigFromConfig($nodeDef, $class, $container = nul 'key' => $key, 'config' => [ 'name' => $key, - 'type' => Type::int() - ] + 'type' => Type::int(), + ], ]; case 'filename': case 'fullpath': @@ -80,8 +88,8 @@ public function getQueryFieldConfigFromConfig($nodeDef, $class, $container = nul 'key' => $key, 'config' => [ 'name' => $key, - 'type' => Type::string() - ] + 'type' => Type::string(), + ], ]; case 'published': return [ @@ -89,7 +97,7 @@ public function getQueryFieldConfigFromConfig($nodeDef, $class, $container = nul 'config' => [ 'name' => $key, 'type' => Type::boolean(), - ] + ], ]; default: return null; @@ -269,7 +277,7 @@ public function getMutationFieldConfigFromConfig($nodeDef, $class) 'arg' => ['type' => Type::string()], 'processor' => function ($object, $newValue, $args) { $object->setKey($newValue); - } + }, ]; case 'published': @@ -278,7 +286,7 @@ public function getMutationFieldConfigFromConfig($nodeDef, $class) 'arg' => ['type' => Type::boolean()], 'processor' => function ($object, $newValue, $args) { $object->setPublished($newValue); - } + }, ]; default: return null; @@ -361,52 +369,12 @@ public function doExtractData(FieldNode $ast, &$data, $container, $args, $contex if ($this->skipField($container, $astName)) { return; } - // example for http://webonyx.github.io/graphql-php/error-handling/ // throw new MySafeException("fieldhelper", "TBD customized error message"); $getter = 'get' . ucfirst($astName); - - $isLocalizedField = false; - $containerDefinition = null; - - if ($container instanceof Concrete) { - $containerDefinition = $container->getClass(); - } elseif ($container instanceof AbstractData || $container instanceof \Pimcore\Model\DataObject\Objectbrick\Data\AbstractData) { - $containerDefinition = $container->getDefinition(); - } - - if ($containerDefinition) { - /** @var Data\Localizedfields|null $lfDefs */ - $lfDefs = $containerDefinition->getFieldDefinition('localizedfields'); - if ($lfDefs && $lfDefs->getFieldDefinition($astName)) { - $isLocalizedField = true; - } - } - if (method_exists($container, $getter)) { - if ($isLocalizedField) { - // defer it - $data[$astName] = function ($source, $args, $context, ResolveInfo $info) use ( - $container, - $getter - ) { - return $container->$getter($args['language'] ?? null); - }; - } else { - $data[$astName] = $container->$getter(); - } - } else { - // we could also have a Mockup objects from Elastic which not supports the "method_exists" - // in this case we just try to get the data directly - if ($container instanceof DefaultMockup) { - try { - // we don't have to take care about localization because this is already handled in elastic - $data[$astName] = $container->$getter(); - } catch (\Exception $e) { - Logger::info('Could not get data from Datahub/DataObjectFieldHelper with message: ' . $e->getMessage()); - } - } - } + $isLocalizedField = Service::isLocalizedField($container, $astName); + Service::resolveContainerGetterData($container, $data, $getter, $resolveInfo, $ast, null, $isLocalizedField); } /** diff --git a/src/GraphQL/RelationHelper.php b/src/GraphQL/RelationHelper.php index 9c724ad90..bff1fd633 100644 --- a/src/GraphQL/RelationHelper.php +++ b/src/GraphQL/RelationHelper.php @@ -16,12 +16,13 @@ namespace Pimcore\Bundle\DataHubBundle\GraphQL; use GraphQL\Type\Definition\ResolveInfo; +use Pimcore\Bundle\DataHubBundle\Model\ElementMockupInterface; use Pimcore\Model\Element\ElementInterface; class RelationHelper { /** - * @param ElementInterface $relation + * @param ElementInterface|ElementMockupInterface $relation * @param Service $graphQlService * @param array $args * @param array $context @@ -29,7 +30,7 @@ class RelationHelper * * @return ElementDescriptor */ - public static function processRelation(ElementInterface $relation, Service $graphQlService, $args, $context, ResolveInfo $resolveInfo) + public static function processRelation(ElementInterface | ElementMockupInterface $relation, Service $graphQlService, $args, $context, ResolveInfo $resolveInfo) { $data = new ElementDescriptor($relation); $graphQlService->extractData($data, $relation, $args, $context, $resolveInfo); diff --git a/src/GraphQL/Resolver/AssetType.php b/src/GraphQL/Resolver/AssetType.php index 3502d9437..8fdb6b05e 100644 --- a/src/GraphQL/Resolver/AssetType.php +++ b/src/GraphQL/Resolver/AssetType.php @@ -20,14 +20,16 @@ use Pimcore\Bundle\DataHubBundle\GraphQL\BaseDescriptor; use Pimcore\Bundle\DataHubBundle\GraphQL\ElementDescriptor; use Pimcore\Bundle\DataHubBundle\GraphQL\Service; +use Pimcore\Bundle\DataHubBundle\GraphQL\Traits\ElementLoaderTrait; use Pimcore\Bundle\DataHubBundle\GraphQL\Traits\ElementTagTrait; use Pimcore\Bundle\DataHubBundle\GraphQL\Traits\ServiceTrait; +use Pimcore\Bundle\DataHubBundle\Model\ElementMockupInterface; use Pimcore\Bundle\DataHubBundle\WorkspaceHelper; use Pimcore\Model\Asset; class AssetType { - use ServiceTrait, ElementTagTrait; + use ServiceTrait, ElementTagTrait, ElementLoaderTrait; /** * @param ElementDescriptor|null $value @@ -140,6 +142,15 @@ public function resolvePath($value = null, $args = [], $context = [], ResolveInf { if ($value instanceof BaseDescriptor) { $asset = $this->getAssetFromValue($value, $context); + // Check if the value was already resolved in a mockup object. + $returnName = $resolveInfo->fieldNodes[0]?->alias?->value ?? $resolveInfo->fieldName; + if ($asset instanceof ElementMockupInterface) { + if (isset($value[$returnName])) { + return $value[$returnName]; + } + $asset = $asset->getOriginalObject(); + } + $thumbNailConfig = $args['thumbnail'] ?? null; $thumbNailFormat = $args['format'] ?? null; $assetFieldHelper = $this->getGraphQLService()->getAssetFieldHelper(); @@ -201,6 +212,14 @@ public function resolveSrcSet($value = null, $args = [], $context = [], ResolveI $thumbNailConfig = $args['thumbnail'] ?? null; $thumbNailFormat = $args['format'] ?? null; $assetFieldHelper = $this->getGraphQLService()->getAssetFieldHelper(); + // Check if the value was already resolved in a mockup object. + $returnName = $resolveInfo->fieldNodes[0]?->alias?->value ?? $resolveInfo->fieldName; + if ($asset instanceof ElementMockupInterface) { + if (isset($value[$returnName])) { + return $value[$returnName]; + } + $asset = $asset->getOriginalObject(); + } if ($asset instanceof Asset\Image) { $mediaQueries = []; @@ -256,6 +275,14 @@ public function resolveResolutions($value = null, $args = [], $context = [], Res if (!$resolveInfo || $resolveInfo->fieldName !== 'data') { $deferredThumbnail = true; } + // Check if the value was already resolved in a mockup object. + $returnName = $resolveInfo->fieldNodes[0]?->alias?->value ?? $resolveInfo->fieldName; + if ($thumbnail instanceof ElementMockupInterface) { + if (isset($value[$returnName])) { + return $value[$returnName]; + } + $thumbnail = $thumbnail->getOriginalObject(); + } if ($thumbnail instanceof Asset\Image\Thumbnail) { $resolutions = []; @@ -291,6 +318,14 @@ public function resolveResolutions($value = null, $args = [], $context = [], Res if (!$asset) { return []; } + // Check if the value was already resolved in a mockup object. + $returnName = $resolveInfo->fieldNodes[0]?->alias?->value ?? $resolveInfo->fieldName; + if ($asset instanceof ElementMockupInterface) { + if (isset($value[$returnName])) { + return $value[$returnName]; + } + $asset = $asset->getOriginalObject(); + } $thumbnail = $assetFieldHelper->getAssetThumbnail($asset, $thumbnailName, $thumbnailFormat, $deferredThumbnail); if (isset($thumbnail)) { $thumbnailConfig = $thumbnail->getConfig(); @@ -330,6 +365,14 @@ public function resolveDimensions($value = null, $args = [], $context = [], Reso if ($value instanceof ElementDescriptor) { $thumbnailName = $args['thumbnail'] ?? null; $asset = $this->getAssetFromValue($value, $context); + // Check if the value was already resolved in a mockup object. + $returnName = $resolveInfo->fieldNodes[0]?->alias?->value ?? $resolveInfo->fieldName; + if ($asset instanceof ElementMockupInterface) { + if (isset($value[$returnName])) { + return $value[$returnName]; + } + $asset = $asset->getOriginalObject(); + } if ($asset instanceof Asset\Video) { $width = $asset->getCustomSetting('videoWidth'); @@ -410,8 +453,7 @@ protected function getAssetFromValue($value, $context) if (!$value instanceof ElementDescriptor) { return null; } - - $asset = Asset::getById($value['id']); + $asset = $this->loadDataElement($value, 'asset', !empty($context['mockup_element_support_enabled'])); if (!WorkspaceHelper::checkPermission($asset, 'read')) { return null; diff --git a/src/GraphQL/Resolver/Base.php b/src/GraphQL/Resolver/Base.php index e68216c8f..ef602dce7 100644 --- a/src/GraphQL/Resolver/Base.php +++ b/src/GraphQL/Resolver/Base.php @@ -19,13 +19,13 @@ use GraphQL\Executor\Promise\Adapter\SyncPromise; use GraphQL\Language\AST\FieldNode; use GraphQL\Type\Definition\ResolveInfo; +use Pimcore\Bundle\DataHubBundle\GraphQL\Traits\ElementLoaderTrait; use Pimcore\Bundle\DataHubBundle\GraphQL\Traits\ServiceTrait; -use Pimcore\Model\DataObject\AbstractObject; use Pimcore\Model\DataObject\ClassDefinition; class Base { - use ServiceTrait; + use ServiceTrait, ElementLoaderTrait; /** @var string */ protected $typeName; @@ -87,7 +87,7 @@ public function resolve($value = null, $args = [], $context = [], ResolveInfo $r /** @var \Pimcore\Bundle\DataHubBundle\GraphQL\Query\Operator\AbstractOperator $operatorImpl */ $operatorImpl = $this->getGraphQlService()->buildQueryOperator($this->typeName, $this->attributes); - $element = AbstractObject::getById($value['id']); + $element = $this->loadDataElement($value, 'object', !empty($context['mockup_element_support_enabled'])); $valueFromOperator = $operatorImpl->getLabeledValue($element, $resolveInfo); $value = $valueFromOperator->value ?? null; diff --git a/src/GraphQL/Resolver/DataObject.php b/src/GraphQL/Resolver/DataObject.php index d009a78f6..65074ce00 100644 --- a/src/GraphQL/Resolver/DataObject.php +++ b/src/GraphQL/Resolver/DataObject.php @@ -42,7 +42,7 @@ public function __construct(Service $graphQlService) */ public function resolveTag($value = null, $args = [], $context = [], ResolveInfo $resolveInfo = null) { - $object = \Pimcore\Model\DataObject::getById($value['id']); + $object = $this->loadDataElement($value, 'object', !empty($context['mockup_element_support_enabled'])); if ($object) { $result = $this->getTags('object', $object->getId()); @@ -68,7 +68,7 @@ public function resolveIndex($value = null, $args = [], $context = [], ResolveIn return null; } - $object = \Pimcore\Model\DataObject::getById($value['id']); + $object = $this->loadDataElement($value, 'object', !empty($context['mockup_element_support_enabled'])); if (!$object instanceof AbstractObject) { return null; @@ -91,7 +91,7 @@ public function resolveChildrenSortBy($value = null, $args = [], $context = [], return null; } - $object = \Pimcore\Model\DataObject::getById($value['id']); + $object = $this->loadDataElement($value, 'object', !empty($context['mockup_element_support_enabled'])); if (!$object instanceof \Pimcore\Model\DataObject) { return null; diff --git a/src/GraphQL/Resolver/Element.php b/src/GraphQL/Resolver/Element.php index e8335a08b..50fbc2624 100644 --- a/src/GraphQL/Resolver/Element.php +++ b/src/GraphQL/Resolver/Element.php @@ -21,6 +21,7 @@ use Pimcore\Bundle\DataHubBundle\GraphQL\Exception\ClientSafeException; use Pimcore\Bundle\DataHubBundle\GraphQL\FieldHelper\AbstractFieldHelper; use Pimcore\Bundle\DataHubBundle\GraphQL\Service; +use Pimcore\Bundle\DataHubBundle\GraphQL\Traits\ElementLoaderTrait; use Pimcore\Bundle\DataHubBundle\GraphQL\Traits\ElementTagTrait; use Pimcore\Bundle\DataHubBundle\GraphQL\Traits\ServiceTrait; use Pimcore\Bundle\DataHubBundle\WorkspaceHelper; @@ -29,12 +30,11 @@ use Pimcore\Model\DataObject\AbstractObject; use Pimcore\Model\Document; use Pimcore\Model\Element\ElementInterface; -use Pimcore\Model\Element\Service as ElementService; use Pimcore\Model\Property; class Element { - use ServiceTrait, ElementTagTrait; + use ServiceTrait, ElementTagTrait, ElementLoaderTrait; /** @var string */ protected $elementType; @@ -57,7 +57,7 @@ public function __construct(string $elementType, Service $graphQlService) */ public function resolveTag($value = null, $args = [], $context = [], ResolveInfo $resolveInfo = null) { - $element = ElementService::getElementById($this->elementType, $value['id']); + $element = $this->loadDataElement($value, $this->elementType, !empty($context['mockup_element_support_enabled'])); if ($element) { $result = $this->getTags('document', $element->getId()); @@ -82,7 +82,7 @@ public function resolveTag($value = null, $args = [], $context = [], ResolveInfo public function resolveProperties($value = null, array $args = [], array $context = [], ResolveInfo $resolveInfo = null) { $elementId = $value['id']; - $element = ElementService::getElementById($this->elementType, $elementId); + $element = $this->loadDataElement($value, $this->elementType, !empty($context['mockup_element_support_enabled'])); if (!$element) { throw new ClientSafeException('element ' . $this->elementType . ' ' . $elementId . ' not found'); @@ -116,7 +116,7 @@ public function resolveProperties($value = null, array $args = [], array $contex */ public function resolveParent($value = null, $args = [], $context = [], ResolveInfo $resolveInfo = null) { - $element = ElementService::getElementById($this->elementType, $value['id']); + $element = $this->loadDataElement($value, $this->elementType, !empty($context['mockup_element_support_enabled'])); if ($element) { $parent = $element->getParent(); if ($parent) { @@ -139,7 +139,8 @@ public function resolveParent($value = null, $args = [], $context = [], ResolveI */ public function resolveChildren($value = null, $args = [], $context = [], ResolveInfo $resolveInfo = null) { - $element = ElementService::getElementById($this->elementType, $value['id']); + $element = $this->loadDataElement($value, $this->elementType, !empty($context['mockup_element_support_enabled'])); + if ($element) { $arguments = $this->composeArguments($args); @@ -161,7 +162,7 @@ public function resolveChildren($value = null, $args = [], $context = [], Resolv */ public function resolveSiblings($value = null, $args = [], $context = [], ResolveInfo $resolveInfo = null) { - $element = ElementService::getElementById($this->elementType, $value['id']); + $element = $this->loadDataElement($value, $this->elementType, !empty($context['mockup_element_support_enabled'])); if ($element) { $arguments = $this->composeArguments($args); diff --git a/src/GraphQL/Resolver/QueryType.php b/src/GraphQL/Resolver/QueryType.php index ef9ca6e37..ef559c6b4 100644 --- a/src/GraphQL/Resolver/QueryType.php +++ b/src/GraphQL/Resolver/QueryType.php @@ -1047,7 +1047,13 @@ public function resolveFilter($value = null, $args = [], $context = [], ResolveI */ public function resolveFilterTotalCount($value = null, $args = [], $context = [], ResolveInfo $resolveInfo = null) { - return $value['totalCount'](); + // Run this deferred because it is very likely that the edges are + // resolved "later" which means at this point the totalCount resolved + // will be able to re-use the results loaded by them. This can save a + // complete call to the search index. + return new Deferred(function () use ($value) { + return $value['totalCount'](); + }); } /** diff --git a/src/GraphQL/Service.php b/src/GraphQL/Service.php index 7978370d5..7507248a1 100644 --- a/src/GraphQL/Service.php +++ b/src/GraphQL/Service.php @@ -38,18 +38,17 @@ use Pimcore\Bundle\DataHubBundle\GraphQL\FieldHelper\DocumentFieldHelper; use Pimcore\Bundle\DataHubBundle\GraphQL\Query\Operator\Factory\OperatorFactoryInterface; use Pimcore\Bundle\DataHubBundle\GraphQL\Query\Value\DefaultValue; +use Pimcore\Bundle\DataHubBundle\GraphQL\Traits\ElementLoaderTrait; +use Pimcore\Bundle\DataHubBundle\Model\ElementMockupInterface; use Pimcore\Bundle\DataHubBundle\PimcoreDataHubBundle; use Pimcore\Cache\RuntimeCache; use Pimcore\DataObject\GridColumnConfig\ConfigElementInterface; use Pimcore\Localization\LocaleServiceInterface; -use Pimcore\Model\Asset; -use Pimcore\Model\DataObject\AbstractObject; use Pimcore\Model\DataObject\ClassDefinition; use Pimcore\Model\DataObject\ClassDefinition\Data; use Pimcore\Model\DataObject\Concrete; +use Pimcore\Model\DataObject\Fieldcollection\Definition; use Pimcore\Model\DataObject\Objectbrick\Data\AbstractData; -use Pimcore\Model\DataObject\Objectbrick\Definition; -use Pimcore\Model\Document; use Pimcore\Model\Element\ElementInterface; use Pimcore\Model\Factory; use Pimcore\Translation\Translator; @@ -57,6 +56,8 @@ class Service { + use ElementLoaderTrait; + /*** * @var ContainerInterface */ @@ -829,7 +830,7 @@ public static function getValueForObject($object, $key, $brickType = null, $bric if ($fieldDefinition->isEmpty($value)) { $parent = \Pimcore\Model\DataObject\Service::hasInheritableParentObject($object); if (!empty($parent)) { - if (!($parent instanceof Concrete)) { + if (!($parent instanceof Concrete) && !($parent instanceof ElementMockupInterface)) { $parent = Concrete::getById($parent->getId()); } @@ -926,7 +927,7 @@ public static function setValue($object, $attribute, $callback) return $result; } - } elseif (method_exists($container, $setter)) { + } elseif (static::checkContainerMethodExists($container, $setter)) { $result = $callback($container, $setter); } @@ -941,11 +942,10 @@ public static function setValue($object, $attribute, $callback) * * @return mixed */ - public static function resolveValue(BaseDescriptor $descriptor, Data $fieldDefinition, $attribute, $args = []) + public static function resolveValue(BaseDescriptor $descriptor, Data $fieldDefinition, $attribute, $args = [], bool $mockupElementSupport = false) { $getter = 'get' . ucfirst($fieldDefinition->getName()); - $objectId = $descriptor['id']; - $object = Concrete::getById($objectId); + $object = self::staticLoadDataElement($descriptor, 'object', $mockupElementSupport); if (!$object) { return null; } @@ -1074,12 +1074,12 @@ public static function resolveValue(BaseDescriptor $descriptor, Data $fieldDefin return $value; } - } elseif (method_exists($container, $getter)) { + } elseif (static::checkContainerMethodExists($container, $getter)) { $isLocalizedField = self::isLocalizedField($container, $fieldDefinition->getName()); if ($isLocalizedField) { - $result = $container->$getter($args['language'] ?? null); + $result = self::callContainerGetterMethod($container, $getter, ['language' => $args['language'] ?? null]); } else { - $result = $container->$getter(); + $result = self::callContainerGetterMethod($container, $getter); } } @@ -1121,15 +1121,9 @@ public static function resolveCachedValue($value, $resolveInfo = null) * * @return bool */ - private static function isLocalizedField($container, $fieldName): bool + public static function isLocalizedField($container, $fieldName): bool { - $containerDefinition = null; - - if ($container instanceof Concrete) { - $containerDefinition = $container->getClass(); - } elseif ($container instanceof AbstractData) { - $containerDefinition = $container->getDefinition(); - } + $containerDefinition = static::getContainerClassDefinition($container); if ($containerDefinition) { /** @var Data\Localizedfields|null $lfDefs */ @@ -1176,14 +1170,17 @@ public function getDataObjectDataTypes(): array */ public function extractData($data, $target, $args = [], $context = [], ResolveInfo $resolveInfo = null) { - $fieldHelper = null; - if ($target instanceof Document) { - $fieldHelper = $this->getDocumentFieldHelper(); - } elseif ($target instanceof Asset) { - $fieldHelper = $this->getAssetFieldHelper(); - } elseif ($target instanceof AbstractObject) { - $fieldHelper = $this->getObjectFieldHelper(); + $type = null; + if ($target instanceof ElementInterface) { + $type = \Pimcore\Model\Element\Service::getElementType($target); + } elseif ($target instanceof ElementMockupInterface) { + $type = $target->getElementType(); } + $fieldHelper = match ($type) { + 'document' => $this->getDocumentFieldHelper(), + 'asset' => $this->getAssetFieldHelper(), + 'object' => $this->getObjectFieldHelper(), + }; if ($fieldHelper) { $fieldHelper->extractData($data, $target, $args, $context, $resolveInfo); @@ -1209,4 +1206,159 @@ public function querySchemaEnabled(string $type) return $enabled; } + + /** + * Checks if a container has a given method. + * + * This works with ElementMockupInterface objects too that's why it's so overly + * complex. ElementMockupInterface objects will use the class definition of the + * mocked object to determine which model class has to be checked for the + * method. + * + * @param object $container + * @param string $method + * + * @return bool + * + * @throws \ReflectionException + */ + public static function checkContainerMethodExists(object $container, string $method): bool + { + // This reflection object is used to determine if the getter can be used. + // $container isn't used directly in order to allow specialized handling + // of mock objects and other placeholders which act transparently but + // don't implement the getters themselves. + $methodCheckClass = new \ReflectionClass($container); + $skipMethodCallCheck = false; + + // Adjust meta data for data handling on type of the data container. + if ($container instanceof ElementMockupInterface) { + // If the mockup implements it use it straight away. + if (method_exists($container, $method)) { + return true; + } + // Otherwise check if the mocked class implements the method. + switch ($container->getElementType()) { + case 'object': + $containerDefinition = static::getContainerClassDefinition($container); + if ($containerDefinition) { + // Unfortunately there's no API for this so we re-implement + // what \Pimcore\Model\DataObject\AbstractObject::getById() + // does. + $baseClassName = 'Pimcore\\Model\\DataObject\\' . ucfirst($containerDefinition->getName()); + } + break; + case 'document': + $baseClassName = 'Pimcore\\Model\\Document\\' . ucfirst($container->getType()); + break; + case 'asset': + $baseClassName = 'Pimcore\\Model\\Asset\\' . ucfirst($container->getType()); + break; + } + if (isset($baseClassName)) { + // @TODO figure out a nicer way to handle this. Really naughty + // to call kernel directly - but static doesn't have DI. + /** @var self $service */ + $service = \Pimcore::getKernel()->getContainer()->get(static::class); + $className = $service->getModelFactory()->getClassNameFor($baseClassName); + $methodCheckClass = new \ReflectionClass($className); + } else { + $skipMethodCallCheck = true; + } + } + if ( + ( + $methodCheckClass->hasMethod($method) + && $methodCheckClass->getMethod($method)->isPublic() + ) + || $skipMethodCallCheck + ) { + return true; + } + + return false; + } + + /** + * Returns the ClassDefinition of a container - works with ElementMockupInterface + * objects too. + * + * @param object $container + * + * @return \Pimcore\Model\DataObject\ClassDefinition|\Pimcore\Model\DataObject\Fieldcollection\Definition|null + * + */ + public static function getContainerClassDefinition(object $container): ClassDefinition | Definition | null + { + // Adjust meta data for data handling on type of the data container. + switch (true) { + case $container instanceof Concrete: + case $container instanceof ElementMockupInterface: + return $container->getClass(); + + case $container instanceof \Pimcore\Model\DataObject\Fieldcollection\Data\AbstractData: + case $container instanceof \Pimcore\Model\DataObject\Objectbrick\Data\AbstractData: + return $container->getDefinition(); + } + + return null; + } + + /** + * Call the getter function on a container. + * + * Passes on execution context to containers with the ElementMockupInterface. + * + * @param object $container + * @param string $getter + * @param array $getterArgs + * @param \GraphQL\Type\Definition\ResolveInfo|null $resolveInfo + * @param \GraphQL\Language\AST\FieldNode|null $ast + * + * @return mixed + */ + public static function callContainerGetterMethod( + object $container, + string $getter, + array $getterArgs = [], + ?ResolveInfo $resolveInfo = null, + ?FieldNode $ast = null + ): mixed { + if ($container instanceof ElementMockupInterface) { + $container->setGraphQLContext($getter, $getterArgs, $resolveInfo, $ast); + } + try { + $return = call_user_func_array([$container, $getter], $getterArgs); + } finally { + if ($container instanceof ElementMockupInterface) { + $container->setGraphQLContext(null); + } + } + + return $return; + } + + public static function resolveContainerGetterData($container, &$data, $getter, ResolveInfo $resolveInfo, FieldNode $ast, $languageArgument = null, $defer = null) + { + if (static::checkContainerMethodExists($container, $getter)) { + $realName = $ast->name->value; + $outputName = $ast->alias?->value ?? $realName; + if ($languageArgument) { + if ($ast->alias || $defer) { + // defer it + $data[$realName] = function ($source, $args, $context, ResolveInfo $info) use ( + $container, + $getter, + $ast + ) { + return self::callContainerGetterMethod($container, $getter, [$args['language'] ?? null], $info, $ast); + }; + } else { + $data[$outputName] = $data[$realName] = self::callContainerGetterMethod($container, $getter, [$languageArgument], $resolveInfo, $ast); + } + } else { + $data[$outputName] = $data[$realName] = self::callContainerGetterMethod($container, $getter, [], $resolveInfo, $ast); + } + } + } } diff --git a/src/GraphQL/Traits/ElementLoaderTrait.php b/src/GraphQL/Traits/ElementLoaderTrait.php new file mode 100644 index 000000000..9710cfb77 --- /dev/null +++ b/src/GraphQL/Traits/ElementLoaderTrait.php @@ -0,0 +1,70 @@ +getId(); + $data[ElementInterface::class . '_type'] = $element->getType(); + $data[ElementInterface::class . '_instance'] = $element; + + return $data; + } + + /** + * + * @return ElementInterface + */ + protected function loadDataElement(&$data, $type, bool $mockupElementSupport = false) + { + return self::staticLoadDataElement($data, $type, $mockupElementSupport); + } + + /** + * @return ElementInterface + */ + protected static function staticLoadDataElement(&$data, $type, bool $mockupElementSupport = false) + { + // If mockup element support is disabled always load the object initially. + if ( + !isset($data[ElementInterface::class . '_instance']) + || (!$mockupElementSupport && $data[ElementInterface::class . '_instance'] instanceof ElementMockupInterface) + ) { + $data[ElementInterface::class . '_type'] = $type; + $data[ElementInterface::class . '_instance'] = ElementService::getElementById($type, $data['id']); + } + + return $data[ElementInterface::class . '_instance']; + } +} diff --git a/src/Model/ElementMockupInterface.php b/src/Model/ElementMockupInterface.php new file mode 100644 index 000000000..19d76c55d --- /dev/null +++ b/src/Model/ElementMockupInterface.php @@ -0,0 +1,80 @@ +getParam('elementType') ?? 'object'; + } + + /** + * @var array + */ + protected array $graphQLContext = []; + + public function setGraphQLContext( + ?string $getter, + ?array $getterArgs = null, + ?ResolveInfo $resolveInfo = null, + ?FieldNode $ast = null + ): void { + $this->graphQLContext = [ + 'getter' => $getter, + 'getterArgs' => $getterArgs, + 'resolveInfo' => $resolveInfo, + 'ast' => $ast, + ]; + } + + protected function isGraphQLCallback($function): bool + { + return ($this->graphQLContext['getter'] ?? null) === $function; + } + + /** + * @return array + */ + protected function getGraphQLArguments(): array + { + $result = []; + if ($ast = $this->graphQLContext['ast'] ?? null) { + if ($nodeList = $ast?->arguments) { + $count = $nodeList->count(); + for ($i = 0; $i < $count; $i++) { + /** @var ArgumentNode $argumentNode */ + $argumentNode = $nodeList[$i]; + $value = $argumentNode->value->kind === 'ListValue' ? $argumentNode->value->values : $argumentNode->value->value; + $result[$argumentNode->name->value] = $value; + } + } + } + + return $result; + } +} diff --git a/src/WorkspaceHelper.php b/src/WorkspaceHelper.php index 6dbf8087c..82b6f7681 100644 --- a/src/WorkspaceHelper.php +++ b/src/WorkspaceHelper.php @@ -20,6 +20,7 @@ use Pimcore\Bundle\DataHubBundle\Event\GraphQL\PermissionEvents; use Pimcore\Bundle\DataHubBundle\GraphQL\Exception\ClientSafeException; use Pimcore\Bundle\DataHubBundle\GraphQL\Exception\NotAllowedException; +use Pimcore\Bundle\DataHubBundle\Model\ElementMockupInterface; use Pimcore\Cache\RuntimeCache; use Pimcore\Db; use Pimcore\Logger; @@ -225,7 +226,7 @@ public static function deleteConfiguration(Configuration $config) * * @throws NotAllowedException */ - public static function checkPermission($element, $type, string $elementType = '') + public static function checkPermission($element, $type, string $elementType = '', bool $skipCache = false) { $context = RuntimeCache::get('datahub_context'); /** @var Configuration $configuration */ @@ -234,7 +235,26 @@ public static function checkPermission($element, $type, string $elementType = '' if ($configuration->skipPermisssionCheck()) { return true; } - + // This can be called multiple times with the same element - use static + // cache to avoid multiple execution in favor of performance. + // @TODO Is there really a reason for the configuration? + if (!$elementType) { + if ($element instanceof ElementMockupInterface) { + $elementType = $element->getElementType(); + } else { + $elementType = Service::getElementType($element); + } + } + $cid = $element->getId() . ':' . $type . ':' . $elementType ; + if (!$skipCache && empty($context['disable_permission_check_cache'])) { + if (!RuntimeCache::isRegistered(__METHOD__)) { + RuntimeCache::set(__METHOD__, []); + } + $permissionCache = RuntimeCache::get(__METHOD__); + if (isset($permissionCache[$cid])) { + return $permissionCache[$cid]; + } + } $event = new PermissionEvent($element, $type); /** @var EventDispatcher $eventDispatcher */ $eventDispatcher = \Pimcore::getContainer()->get('event_dispatcher'); @@ -244,14 +264,16 @@ public static function checkPermission($element, $type, string $elementType = '' } // we can allow nullable elements e.g. linked Assets where the asset itself was removed if (!$element) { - return true; + $permissionCache[$cid] = true; + RuntimeCache::set(__METHOD__, $permissionCache); + + return $permissionCache[$cid]; } $isAllowed = self::isAllowed($element, $configuration, $type, $elementType); + $permissionCache[$cid] = $isAllowed; + RuntimeCache::set(__METHOD__, $permissionCache); if (!$isAllowed && PimcoreDataHubBundle::getNotAllowedPolicy() === PimcoreDataHubBundle::NOT_ALLOWED_POLICY_EXCEPTION) { - if (!$elementType) { - $elementType = Service::getElementType($element); - } // Could be dealing with mock objects that can't be loaded due // to stale index so be extra cautions when using. try { @@ -280,7 +302,11 @@ public static function isAllowed($element, Configuration $configuration, string return false; } if (!$elementType) { - $elementType = Service::getElementType($element); + if ($element instanceof ElementMockupInterface) { + $elementType = $element->getElementType(); + } else { + $elementType = Service::getElementType($element); + } } // collect properties via parent - ids $parentIds = [1];