diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a9875b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e0d81b4 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +language: php + +php: + - 7.1 + +branches: + only: + - master + - dev + +env: + matrix: + - TARGET="phpspec" + +before_script: + - COMPOSER_MEMORY_LIMIT=-1 composer install + +script: + - if [ "$TARGET" == "phpspec" ] ; then ./vendor/bin/phpspec run -n; fi + +notification: + email: false diff --git a/README.md b/README.md index e69de29..7c47bf5 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,49 @@ +# eZ Platform Query Field Type + +This Field Type will let a content manager map an executable Repository Query to a Field. + +Example use-cases: +- a `place.nearby_places` field that returns Place items less than X kilometers away + from the current content, based on its own `location` field +- a `gallery.images` field that returns Image items that are children of the current + gallery item's main location + +The idea is to move content and structure logic implemented in controllers and templates +to the repository itself. + +## Installation +Add the package's repository to `composer.json`: + +```json +{ + "repositories": [ + { + "type": "git", + "url": "https://github.com/ezsystems/ezplatform-query-fieldtype.git" + } + ] +} +``` + +Add the package to the requirements: +``` +composer require ezsystems/ezplatform-query-fieldtype:dev-master +``` + +Add the package to `app/AppKernel.php`: +```php +$bundles = [ + // ... + new EzSystems\EzPlatformQueryFieldType\Symfony\EzSystemsEzPlatformQueryFieldTypeBundle(), +] +``` + +## Usage +Add a `query` field to a content type. + +In the Field Definition settings, select a Query Type out of the ones defined in the system. Parameters are a JSON structure, with the key being the parameter's name, and the value either a scalar, or an [expression](https://symfony.com/doc/current/components/expression_language.html). + +See the [`examples`](examples/) directory for full examples. + + + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..afe32ae --- /dev/null +++ b/composer.json @@ -0,0 +1,35 @@ +{ + "name": "ezsystems/ezplatform-query-fieldtype", + "description": "An eZ Platform Field Type that defines a query.", + "type": "ezplatform-bundle", + "license": "GPL-2.0", + "authors": [ + { + "name": "Bertrand Dunogier", + "homepage": "bd@ez.no" + }, + { + "name": "eZ Systems", + "homepage": "https://github.com/ezsystems/ezplatform-query-fieldtype/contributors" + } + ], + "minimum-stability": "stable", + "require": { + "php": ">=7.1", + "ezsystems/ezplatform-graphql": "^1.0||^2.0" + }, + "autoload": { + "psr-4": { + "EzSystems\\EzPlatformQueryFieldType\\": "src" + } + }, + "require-dev": { + "phpspec/phpspec": "^5.1" + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } + +} diff --git a/doc/examples/nearby_places/NearbyPlacesQueryType.php b/doc/examples/nearby_places/NearbyPlacesQueryType.php new file mode 100644 index 0000000..ea67a69 --- /dev/null +++ b/doc/examples/nearby_places/NearbyPlacesQueryType.php @@ -0,0 +1,42 @@ + new Criterion\LogicalAnd([ + new Criterion\ContentTypeIdentifier('place'), + new Criterion\MapLocationDistance( + 'location', + Criterion\Operator::LTE, + $parameters['distance'], + $parameters['latitude'], + $parameters['longitude'] + ) + ]), + ]); + } + + public function getSupportedParameters() + { + return ['distance', 'latitude', 'longitude']; + } + + public static function getName() + { + return 'NearbyPlaces'; + } +} \ No newline at end of file diff --git a/doc/examples/nearby_places/README.md b/doc/examples/nearby_places/README.md new file mode 100644 index 0000000..e9fcc06 --- /dev/null +++ b/doc/examples/nearby_places/README.md @@ -0,0 +1,19 @@ +### Nearby places query field + +A field that lists Place items located close to the current item. + +#### Content type configuration +The following assumes a "place" content item with a "location" map location +field definition. + +##### Query type +"Nearby places" (see [NearbyPlacesQueryType](NearbyPlacesQueryType.php). + +##### Parameters +```json +{ + "distance": 3, + "latitude": "@=content.getFieldValue('location').latitude", + "longitude": "@=content.getFieldValue('location').longitude", +} +``` diff --git a/phpspec.yml b/phpspec.yml new file mode 100644 index 0000000..a6e21ea --- /dev/null +++ b/phpspec.yml @@ -0,0 +1,6 @@ +formatter.name: pretty + +suites: + default: + psr4_prefix: EzSystems\EzPlatformQueryFieldType + namespace: EzSystems\EzPlatformQueryFieldType \ No newline at end of file diff --git a/spec/API/QueryFieldServiceSpec.php b/spec/API/QueryFieldServiceSpec.php new file mode 100644 index 0000000..519b5ed --- /dev/null +++ b/spec/API/QueryFieldServiceSpec.php @@ -0,0 +1,85 @@ +searchHits = []; + $this->searchResult = new SearchResult(['searchHits' => $this->searchHits]); + + $parameters = json_encode([ + 'param1' => 'value1', + 'param2' => 'value2', + ]); + + $contentType = new Values\ContentType\ContentType([ + 'fieldDefinitions' => [ + new Values\ContentType\FieldDefinition([ + 'identifier' => self::FIELD_DEFINITION_IDENTIFIER, + 'fieldTypeIdentifier' => 'query', + 'fieldSettings' => [ + 'ReturnedType' => 'folder', + 'QueryType' => self::QUERY_TYPE_IDENTIFIER, + 'Parameters' => $parameters, + ] + ]), + ], + ]); + + $contentTypeService->loadContentType(self::CONTENT_TYPE_ID)->willReturn($contentType); + $queryTypeRegistry->getQueryType(self::QUERY_TYPE_IDENTIFIER)->willReturn($queryType); + $queryType->getQuery(Argument::any())->willReturn(new ApiQuery()); + // @todo this should fail. It does not. + $searchService->findContent(Argument::any())->willReturn($this->searchResult); + $this->beConstructedWith($searchService, $contentTypeService, $queryTypeRegistry); + } + + function it_is_initializable() + { + $this->shouldHaveType(QueryFieldService::class); + } + + function it_loads_data_from_a_query_field_for_a_given_content_item() + { + $this->loadFieldData($this->getContent(), self::FIELD_DEFINITION_IDENTIFIER)->shouldBe($this->searchHits); + } + + /** + * @return \eZ\Publish\Core\Repository\Values\Content\Content + */ + private function getContent(): Values\Content\Content + { + return new Values\Content\Content([ + 'versionInfo' => new Values\Content\VersionInfo([ + 'contentInfo' => new ContentInfo(['contentTypeId' => self::CONTENT_TYPE_ID]), + ]) + ]); + } +} diff --git a/spec/GraphQL/QueryFieldDefinitionMapperSpec.php b/spec/GraphQL/QueryFieldDefinitionMapperSpec.php new file mode 100644 index 0000000..472244e --- /dev/null +++ b/spec/GraphQL/QueryFieldDefinitionMapperSpec.php @@ -0,0 +1,93 @@ + self::RETURNED_CONTENT_TYPE_IDENTIFIER]); + + $contentTypeService + ->loadContentTypeByIdentifier(self::RETURNED_CONTENT_TYPE_IDENTIFIER) + ->willReturn($contentType); + + $nameHelper + ->domainContentName($contentType) + ->willReturn(self::GRAPHQL_TYPE); + + $this->beConstructedWith($innerMapper, $nameHelper, $contentTypeService); + } + + function it_is_initializable() + { + $this->shouldHaveType(QueryFieldDefinitionMapper::class); + $this->shouldHaveType(FieldDefinitionMapper::class); + } + + function it_returns_as_value_type_the_configured_ContentType_for_query_field_definitions(FieldDefinitionMapper $innerMapper) + { + $fieldDefinition = $this->fieldDefinition(); + $innerMapper->mapToFieldValueType($fieldDefinition)->shouldNotBeCalled(); + $this + ->mapToFieldValueType($fieldDefinition) + ->shouldBe('[' . self::GRAPHQL_TYPE . ']'); + } + + function it_delegates_value_type_to_the_inner_mapper_for_a_non_query_field_definition(FieldDefinitionMapper $innerMapper) + { + $fieldDefinition = new FieldDefinition(['fieldTypeIdentifier' => 'lambda']); + $innerMapper->mapToFieldValueType($fieldDefinition)->willReturn('SomeType'); + $this + ->mapToFieldValueType($fieldDefinition) + ->shouldBe('SomeType'); + } + + function it_delegates_the_definition_type_to_the_parent_mapper(FieldDefinitionMapper $innerMapper) + { + $fieldDefinition = $this->fieldDefinition(); + $innerMapper->mapToFieldDefinitionType($fieldDefinition)->willReturn('FieldValue'); + $this + ->mapToFieldDefinitionType($fieldDefinition) + ->shouldBe('FieldValue'); + } + + function it_delegates_the_value_resolver_to_the_parent_mapper(FieldDefinitionMapper $innerMapper) + { + $fieldDefinition = $this->fieldDefinition(); + $innerMapper->mapToFieldValueResolver($fieldDefinition)->willReturn('resolver'); + $this + ->mapToFieldValueResolver($fieldDefinition) + ->shouldBe('resolver'); + } + + /** + * @return FieldDefinition + */ + private function fieldDefinition(): FieldDefinition + { + return new FieldDefinition([ + 'identifier' => self::FIELD_IDENTIFIER, + 'fieldTypeIdentifier' => self::FIELD_TYPE_IDENTIFIER, + 'fieldSettings' => ['ReturnedType' => self::RETURNED_CONTENT_TYPE_IDENTIFIER] + ]); + } +} diff --git a/spec/GraphQL/QueryFieldResolverSpec.php b/spec/GraphQL/QueryFieldResolverSpec.php new file mode 100644 index 0000000..4011ad3 --- /dev/null +++ b/spec/GraphQL/QueryFieldResolverSpec.php @@ -0,0 +1,32 @@ +beConstructedWith($queryFieldService); + } + + function it_is_initializable() + { + $this->shouldHaveType(QueryFieldResolver::class); + } + + function it_resolves_a_query_field(QueryFieldService $queryFieldService) + { + $content = new Content(); + $field = new Field(['fieldDefIdentifier' => self::FIELD_DEFINITION_IDENTIFIER, 'value' => new \stdClass()]); + $queryFieldService->loadFieldData($content, self::FIELD_DEFINITION_IDENTIFIER)->willReturn([]); + $this->resolveQueryField($field, $content)->shouldReturn([]); + } +} diff --git a/src/API/QueryFieldService.php b/src/API/QueryFieldService.php new file mode 100644 index 0000000..55d5a32 --- /dev/null +++ b/src/API/QueryFieldService.php @@ -0,0 +1,108 @@ +searchService = $searchService; + $this->contentTypeService = $contentTypeService; + $this->queryTypeRegistry = $queryTypeRegistry; + } + + /** + * @param \eZ\Publish\API\Repository\Values\Content\Content $content + * @param string $fieldDefinitionIdentifier + * + * @return \eZ\Publish\API\Repository\Values\Content\Content[] + * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException + */ + public function loadFieldData(Content $content, string $fieldDefinitionIdentifier): array + { + $fieldDefinition = $this->getFieldDefinition($content->contentInfo, $fieldDefinitionIdentifier); + $queryType = $this->queryTypeRegistry->getQueryType($fieldDefinition->fieldSettings['QueryType']); + $parameters = $this->resolveParameters($fieldDefinition->fieldSettings['Parameters'], $content); + + return array_map( + function(SearchHit $searchHit) { + return $searchHit->valueObject; + }, + $this->searchService->findContent($queryType->getQuery($parameters))->searchHits + ); + } + + public function getFieldDefinition(ContentInfo $contentInfo, string $fieldDefinitionIdentifier): FieldDefinition + { + return $queryFieldDefinition = + $this + ->contentTypeService->loadContentType($contentInfo->contentTypeId) + ->getFieldDefinition($fieldDefinitionIdentifier); + + } + + /** + * @param string $parameters parameters in JSON format + * @param \eZ\Publish\API\Repository\Values\Content\Content $content + * + * @return array + */ + private function resolveParameters(string $parameters, Content $content): array + { + $parameters = json_decode($parameters, true); + foreach ($parameters as $key => $parameter) { + $parameters[$key] = $this->applyContentToParameter($content, $parameter); + } + + return $parameters; + } + + private function applyContentToParameter(Content $content, string $parameter) + { + if (substr($parameter, 0, 2) !== '@=') { + return $parameter; + } + + return (new ExpressionLanguage())->evaluate( + substr($parameter, 2), + [ + 'content' => $content, + 'contentInfo' => $content->contentInfo, + ] + ); + } +} \ No newline at end of file diff --git a/src/Controller/QueryFieldController.php b/src/Controller/QueryFieldController.php new file mode 100644 index 0000000..e6e7f0f --- /dev/null +++ b/src/Controller/QueryFieldController.php @@ -0,0 +1,33 @@ +queryFieldService = $queryFieldService; + } + + public function renderQueryFieldAction(ContentView $view, $queryFieldDefinitionIdentifier) + { + $view->addParameters([ + 'children_view_type' => 'line', + 'query_results' => $this->queryFieldService->loadFieldData( + $view->getContent(), + $queryFieldDefinitionIdentifier + ) + ]); + + return $view; + } +} \ No newline at end of file diff --git a/src/Controller/QueryFieldRestController.php b/src/Controller/QueryFieldRestController.php new file mode 100644 index 0000000..7d96c16 --- /dev/null +++ b/src/Controller/QueryFieldRestController.php @@ -0,0 +1,79 @@ +queryFieldService = $queryFieldService; + $this->contentService = $contentService; + $this->contentTypeService = $contentTypeService; + $this->locationService = $locationService; + } + + public function getResults($contentId, $versionNumber, $fieldDefinitionIdentifier): ContentList + { + $content = $this->contentService->loadContent($contentId, null, $versionNumber); + + return new ContentList( + array_map( + function(Content $content) { + return new RestContent( + $content->contentInfo, + $this->locationService->loadLocation($content->contentInfo->mainLocationId), + $content, + $this->getContentType($content->contentInfo), + $this->contentService->loadRelations($content->getVersionInfo()) + ); + }, + $this->queryFieldService->loadFieldData($content, $fieldDefinitionIdentifier) + ) + ); + } + + private function getContentType(ContentInfo $contentInfo): ContentType + { + static $contentTypes = []; + + if (!isset($contentTypes[$contentInfo->contentTypeId])) { + $contentTypes[$contentInfo->contentTypeId] = $this->contentTypeService->loadContentType($contentInfo->contentTypeId); + } + + return $contentTypes[$contentInfo->contentTypeId]; + } +} \ No newline at end of file diff --git a/src/FieldType/Mapper/QueryFormMapper.php b/src/FieldType/Mapper/QueryFormMapper.php new file mode 100644 index 0000000..e9bbb4e --- /dev/null +++ b/src/FieldType/Mapper/QueryFormMapper.php @@ -0,0 +1,99 @@ +contentTypeService = $contentTypeService; + $this->queryTypes = $queryTypes; + } + + public function mapFieldDefinitionForm(FormInterface $fieldDefinitionForm, FieldDefinitionData $data) + { + $fieldDefinitionForm + ->add('QueryType',Type\ChoiceType::class, + [ + 'label' => 'Query type', + 'property_path' => 'fieldSettings[QueryType]', + 'choices' => $this->queryTypes, + ] + ) + ->add('ReturnedType', Type\ChoiceType::class, + [ + 'label' => 'Returned type', + 'property_path' => 'fieldSettings[ReturnedType]', + 'choices' => $this->getContentTypes(), + ] + ) + ->add('Parameters', Type\TextareaType::class, + [ + 'label' => 'Parameters', + 'property_path' => 'fieldSettings[Parameters]' + ] + ); + } + + public function mapFieldValueForm(FormInterface $fieldForm, FieldData $data) + { + $fieldDefinition = $data->fieldDefinition; + $formConfig = $fieldForm->getConfig(); + $validatorConfiguration = $fieldDefinition->getValidatorConfiguration(); + $names = $fieldDefinition->getNames(); + $label = $fieldDefinition->getName($formConfig->getOption('mainLanguageCode')) ?: reset($names); + + $fieldForm + ->add( + $formConfig->getFormFactory()->createBuilder() + ->create( + 'value', + QueryFieldType::class, + [ + 'required' => $fieldDefinition->isRequired, + 'label' => $label, + ] + ) + ->setAutoInitialize(false) + ->getForm() + ); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setDefaults([ + 'translation_domain' => 'ezrepoforms_content_type', + ]); + } + + private function getContentTypes() + { + foreach ($this->contentTypeService->loadContentTypeGroups() as $contentTypeGroup) { + foreach ($this->contentTypeService->loadContentTypes($contentTypeGroup) as $contentType) { + yield $contentType->getName() => $contentType->identifier; + } + } + } +} diff --git a/src/FieldType/Query/SearchField.php b/src/FieldType/Query/SearchField.php new file mode 100644 index 0000000..564b3fc --- /dev/null +++ b/src/FieldType/Query/SearchField.php @@ -0,0 +1,75 @@ +value->data, + new Search\FieldType\StringField() + ), + new Search\Field( + 'fulltext', + $field->value->data, + new Search\FieldType\FullTextField() + ), + ); + } + + /** + * Get index field types for search backend. + * + * @return \eZ\Publish\SPI\Search\FieldType[] + */ + public function getIndexDefinition() + { + return array( + 'value' => new Search\FieldType\StringField(), + ); + } + + /** + * Get name of the default field to be used for matching. + * + * As field types can index multiple fields (see MapLocation field type's + * implementation of this interface), this method is used to define default + * field for matching. Default field is typically used by Field criterion. + * + * @return string + */ + public function getDefaultMatchField() + { + return 'value'; + } + + /** + * Get name of the default field to be used for sorting. + * + * As field types can index multiple fields (see MapLocation field type's + * implementation of this interface), this method is used to define default + * field for sorting. Default field is typically used by Field sort clause. + * + * @return string + */ + public function getDefaultSortField() + { + return $this->getDefaultMatchField(); + } +} diff --git a/src/FieldType/Query/Type.php b/src/FieldType/Query/Type.php new file mode 100644 index 0000000..480da52 --- /dev/null +++ b/src/FieldType/Query/Type.php @@ -0,0 +1,195 @@ + ['type' => 'string', 'default' => ''], + 'Parameters' => ['type' => 'string', 'default' => ''], + 'ReturnedType' => ['type' => 'string', 'default' => ''], + ]; + + + /** + * Validates the validatorConfiguration of a FieldDefinitionCreateStruct or FieldDefinitionUpdateStruct. + * + * @param mixed $validatorConfiguration + * + * @return \eZ\Publish\SPI\FieldType\ValidationError[] + */ + public function validateValidatorConfiguration($validatorConfiguration) + { + $validationErrors = []; + + return $validationErrors; + } + + /** + * Validates a field based on the validators in the field definition. + * + * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException + * + * @param \eZ\Publish\API\Repository\Values\ContentType\FieldDefinition $fieldDefinition The field definition of the field + * @param \eZ\Publish\Core\FieldType\TextLine\Value $fieldValue The field value for which an action is performed + * + * @return \eZ\Publish\SPI\FieldType\ValidationError[] + */ + public function validate(FieldDefinition $fieldDefinition, SPIValue $fieldValue) + { + $validationErrors = []; + + return $validationErrors; + } + + /** + * Returns the field type identifier for this field type. + * + * @return string + */ + public function getFieldTypeIdentifier() + { + return 'query'; + } + + /** + * Returns the name of the given field value. + * + * It will be used to generate content name and url alias if current field is designated + * to be used in the content name/urlAlias pattern. + * + * @param \EzSystems\EzPlatformQueryFieldType\FieldType\Query\Value $value + * + * @return string + */ + public function getName(SPIValue $value) + { + return (string)$value->text; + } + + public function getEmptyValue() + { + return new Value(); + } + + /** + * Returns if the given $value is considered empty by the field type. + * + * @param mixed $value + * + * @return bool + */ + public function isEmptyValue(SPIValue $value) + { + return false; + } + + protected function createValueFromInput($inputValue) + { + if (is_string($inputValue)) { + $inputValue = new Value($inputValue); + } + + return $inputValue; + } + + /** + * Throws an exception if value structure is not of expected format. + * + * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException If the value does not match the expected structure. + * + * @param \eZ\Publish\Core\FieldType\TextLine\Value $value + */ + protected function checkValueStructure(BaseValue $value) + { + if (!is_string($value->text)) { + throw new InvalidArgumentType( + '$value->text', + 'string', + $value->text + ); + } + } + + /** + * Returns information for FieldValue->$sortKey relevant to the field type. + * + * @param \eZ\Publish\Core\FieldType\TextLine\Value $value + * + * @return array + */ + protected function getSortInfo(BaseValue $value) + { + return $this->transformationProcessor->transformByGroup((string)$value, 'lowercase'); + } + + /** + * Converts an $hash to the Value defined by the field type. + * + * @param mixed $hash + * + * @return \eZ\Publish\Core\FieldType\TextLine\Value $value + */ + public function fromHash($hash) + { + if ($hash === null) { + return $this->getEmptyValue(); + } + + return new Value($hash); + } + + /** + * Converts a $Value to a hash. + * + * @param \eZ\Publish\Core\FieldType\TextLine\Value $value + * + * @return mixed + */ + public function toHash(SPIValue $value) + { + if ($this->isEmptyValue($value)) { + return null; + } + + return $value->text; + } + + /** + * Returns whether the field type is searchable. + * + * @return bool + */ + public function isSearchable() + { + return true; + } + + public function validateFieldSettings($fieldSettings) + { + $errors = []; + + if (isset($fieldSettings['QueryType'])) { + /** + * $errors[] = new ValidationError("Query type %query_type does not exist", null, ['%query_type%' => $fieldSettings['QueryType']]); + */ + } + + if (isset($fieldSettings['Parameters']) && $fieldSettings['Parameters']) { + if (json_decode($fieldSettings['Parameters']) === null) { + $errors[] = new ValidationError("Parameters is not a valid json structure"); + } + } + + return $errors; + } +} diff --git a/src/FieldType/Query/Value.php b/src/FieldType/Query/Value.php new file mode 100644 index 0000000..50ead20 --- /dev/null +++ b/src/FieldType/Query/Value.php @@ -0,0 +1,33 @@ +text = $text; + } + + /** + * @see \eZ\Publish\Core\FieldType\Value + */ + public function __toString() + { + return (string)$this->text; + } +} diff --git a/src/Form/Type/FieldType/QueryFieldType.php b/src/Form/Type/FieldType/QueryFieldType.php new file mode 100644 index 0000000..6ac6676 --- /dev/null +++ b/src/Form/Type/FieldType/QueryFieldType.php @@ -0,0 +1,56 @@ +fieldTypeService = $fieldTypeService; + } + + public function getName() + { + return $this->getBlockPrefix(); + } + + public function getBlockPrefix() + { + return 'ezplatform_fieldtype_query'; + } + + public function getParent() + { + return TextType::class; + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addModelTransformer(new FieldValueTransformer($this->fieldTypeService->getFieldType('query'))); + } + + public function buildView(FormView $view, FormInterface $form, array $options) + { + $attributes = []; + + // $view->vars['QueryType'] = array_merge($view->vars['QueryType'], $attributes); + // $view->vars['Test'] = array_merge($view->vars['Test'], $attributes); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([]); + } +} diff --git a/src/GraphQL/QueryFieldDefinitionMapper.php b/src/GraphQL/QueryFieldDefinitionMapper.php new file mode 100644 index 0000000..dbbcb55 --- /dev/null +++ b/src/GraphQL/QueryFieldDefinitionMapper.php @@ -0,0 +1,54 @@ +nameHelper = $nameHelper; + $this->contentTypeService = $contentTypeService; + } + + public function mapToFieldValueType(FieldDefinition $fieldDefinition): ?string + { + if (!$this->canMap($fieldDefinition)) { + return parent::mapToFieldValueType($fieldDefinition); + } + + $fieldSettings = $fieldDefinition->getFieldSettings(); + + return '[' . $this->getDomainTypeName($fieldSettings['ReturnedType']) . ']'; + } + + protected function getFieldTypeIdentifier(): string + { + return 'query'; + } + + private function getDomainTypeName($typeIdentifier) + { + return $this->nameHelper->domainContentName( + $this->contentTypeService->loadContentTypeByIdentifier($typeIdentifier) + ); + } +} \ No newline at end of file diff --git a/src/GraphQL/QueryFieldResolver.php b/src/GraphQL/QueryFieldResolver.php new file mode 100644 index 0000000..26bee14 --- /dev/null +++ b/src/GraphQL/QueryFieldResolver.php @@ -0,0 +1,23 @@ +queryFieldService = $queryFieldService; + } + + public function resolveQueryField(Field $field, Content $content) + { + return $this->queryFieldService->loadFieldData($content, $field->fieldDefIdentifier); + } +} \ No newline at end of file diff --git a/src/Persistence/Legacy/Content/FieldValue/Converter/QueryConverter.php b/src/Persistence/Legacy/Content/FieldValue/Converter/QueryConverter.php new file mode 100644 index 0000000..3cd7bd2 --- /dev/null +++ b/src/Persistence/Legacy/Content/FieldValue/Converter/QueryConverter.php @@ -0,0 +1,92 @@ +dataText = $value->data; + $storageFieldValue->sortKeyString = $value->sortKey; + } + + /** + * Converts data from $value to $fieldValue. + * + * @param \eZ\Publish\Core\Persistence\Legacy\Content\StorageFieldValue $value + * @param \eZ\Publish\SPI\Persistence\Content\FieldValue $fieldValue + */ + public function toFieldValue(StorageFieldValue $value, FieldValue $fieldValue) + { + $fieldValue->data = $value->dataText; + $fieldValue->sortKey = $value->sortKeyString; + } + + /** + * Converts field definition data in $fieldDef into $storageFieldDef. + * + * @param \eZ\Publish\SPI\Persistence\Content\Type\FieldDefinition $fieldDef + * @param \eZ\Publish\Core\Persistence\Legacy\Content\StorageFieldDefinition $storageDef + */ + public function toStorageFieldDefinition(FieldDefinition $fieldDef, StorageFieldDefinition $storageDef) + { + $storageDef->dataText1 = $fieldDef->fieldTypeConstraints->fieldSettings['QueryType']; + $storageDef->dataText2 = $fieldDef->fieldTypeConstraints->fieldSettings['ReturnedType']; + $storageDef->dataText5 = $fieldDef->fieldTypeConstraints->fieldSettings['Parameters']; + } + + /** + * Converts field definition data in $storageDef into $fieldDef. + * + * @param \eZ\Publish\Core\Persistence\Legacy\Content\StorageFieldDefinition $storageDef + * @param \eZ\Publish\SPI\Persistence\Content\Type\FieldDefinition $fieldDef + */ + public function toFieldDefinition(StorageFieldDefinition $storageDef, FieldDefinition $fieldDef) + { + $fieldDef->fieldTypeConstraints->fieldSettings = [ + 'QueryType' => $storageDef->dataText1 ?: null, + 'ReturnedType' => $storageDef->dataText2 ?: null, + 'Parameters' => $storageDef->dataText5 ?: '' + ]; + } + + /** + * Returns the name of the index column in the attribute table. + * + * Returns the name of the index column the datatype uses, which is either + * "sort_key_int" or "sort_key_string". This column is then used for + * filtering and sorting for this type. + * + * @return string + */ + public function getIndexColumn() + { + return 'sort_key_string'; + } +} diff --git a/src/Symfony/DependencyInjection/BDEzPlatformQueryFieldTypeExtension.php b/src/Symfony/DependencyInjection/BDEzPlatformQueryFieldTypeExtension.php new file mode 100644 index 0000000..3ae0f65 --- /dev/null +++ b/src/Symfony/DependencyInjection/BDEzPlatformQueryFieldTypeExtension.php @@ -0,0 +1,61 @@ +load('fieldtypes.yml'); + $loader->load('indexable_fieldtypes.yml'); + $loader->load('field_value_converters.yml'); + $loader->load('graphql.yml'); + $loader->load('services.yml'); + + $this->setContentViewConfig($container); + } + + public function prepend(ContainerBuilder $container) + { + $container->prependExtensionConfig('assetic', ['bundles' => ['BDEzPlatformQueryFieldTypeBundle']]); + + $configFile = __DIR__ . '/../Resources/config/field_templates.yml'; + $config = Yaml::parse(file_get_contents($configFile)); + $container->prependExtensionConfig('ezpublish', $config); + $container->addResource(new FileResource($configFile)); + + $configFile = __DIR__ . '/../Resources/config/field_templates_ui.yml'; + $config = Yaml::parse(file_get_contents($configFile)); + $container->prependExtensionConfig('ezpublish', $config); + $container->addResource(new FileResource($configFile)); + } + + /** + * @param ContainerBuilder $container + */ + protected function setContentViewConfig(ContainerBuilder $container): void + { + $contentViewDefaults = $container->getParameter('ezsettings.default.content_view_defaults'); + $contentViewDefaults['query_field'] = [ + 'default' => [ + 'controller' => QueryFieldController::class . ':renderQueryFieldAction', + 'template' => "BDEzPlatformQueryFieldTypeBundle::query_field_view.html.twig", + 'match' => [], + ] + ]; + $container->setParameter('ezsettings.default.content_view_defaults', $contentViewDefaults); + } +} diff --git a/src/Symfony/DependencyInjection/Compiler/ConfigurableFieldDefinitionMapperPass.php b/src/Symfony/DependencyInjection/Compiler/ConfigurableFieldDefinitionMapperPass.php new file mode 100644 index 0000000..0f173e3 --- /dev/null +++ b/src/Symfony/DependencyInjection/Compiler/ConfigurableFieldDefinitionMapperPass.php @@ -0,0 +1,37 @@ +hasParameter(self::PARAMETER)) { + return; + } + + $parameter = $container->getParameter(self::PARAMETER); + $parameter['query'] = [ + 'definition_type' => 'QueryFieldDefinition', + 'value_resolver' => 'resolver("QueryFieldValue", [field, content])', + ]; + + $container->setParameter(self::PARAMETER, $parameter); + } +} \ No newline at end of file diff --git a/src/Symfony/DependencyInjection/Compiler/QueryTypesListPass.php b/src/Symfony/DependencyInjection/Compiler/QueryTypesListPass.php new file mode 100644 index 0000000..3e1d624 --- /dev/null +++ b/src/Symfony/DependencyInjection/Compiler/QueryTypesListPass.php @@ -0,0 +1,54 @@ +nameConverter = new CamelCaseToSnakeCaseNameConverter(); + } + + public function process(ContainerBuilder $container) + { + if (!$container->has('ezpublish.query_type.registry') || !$container->has(QueryFormMapper::class)) { + return; + } + + $queryTypes = []; + foreach ($container->getDefinition('ezpublish.query_type.registry')->getMethodCalls() as $methodCall) { + if ($methodCall[0] === 'addQueryType') { + $queryTypes[] = $methodCall[1][0]; + } else if ($methodCall[0] === 'addQueryTypes') { + foreach (array_keys($methodCall[1][0]) as $queryTypeIdentifier) { + $queryTypes[$this->buildQueryTypeName($queryTypeIdentifier)] = $queryTypeIdentifier; + } + } + } + + $formMapperDefinition = $container->getDefinition(QueryFormMapper::class); + $formMapperDefinition->setArgument('$queryTypes', $queryTypes); + } + + /** + * Builds a human readable name out of a query type identifier + * + * @param $queryTypeIdentifier + * @return string + */ + private function buildQueryTypeName($queryTypeIdentifier) + { + return ucfirst( + str_replace('_', ' ', $this->nameConverter->normalize($queryTypeIdentifier)) + ); + } +} \ No newline at end of file diff --git a/src/Symfony/EzSystemsEzPlatformQueryFieldTypeBundle.php b/src/Symfony/EzSystemsEzPlatformQueryFieldTypeBundle.php new file mode 100644 index 0000000..ba12826 --- /dev/null +++ b/src/Symfony/EzSystemsEzPlatformQueryFieldTypeBundle.php @@ -0,0 +1,16 @@ +addCompilerPass(new Compiler\QueryTypesListPass()); + $container->addCompilerPass(new Compiler\ConfigurableFieldDefinitionMapperPass()); + } +} diff --git a/src/Symfony/Resources/config/field_templates.yml b/src/Symfony/Resources/config/field_templates.yml new file mode 100644 index 0000000..6c996ac --- /dev/null +++ b/src/Symfony/Resources/config/field_templates.yml @@ -0,0 +1,8 @@ +system: + default: + fielddefinition_settings_templates: + - { template: "BDEzPlatformQueryFieldTypeBundle::fielddefinition_settings.html.twig" } + fielddefinition_edit_templates: + - { template: "BDEzPlatformQueryFieldTypeBundle::field_types.html.twig" } + field_templates: + - { template: "BDEzPlatformQueryFieldTypeBundle::fieldtype_ui.html.twig" } diff --git a/src/Symfony/Resources/config/field_templates_ui.yml b/src/Symfony/Resources/config/field_templates_ui.yml new file mode 100644 index 0000000..d4b0b83 --- /dev/null +++ b/src/Symfony/Resources/config/field_templates_ui.yml @@ -0,0 +1,6 @@ +system: + admin_group: + field_templates: + - + template: "BDEzPlatformQueryFieldTypeBundle::fieldtype_ui.html.twig" + priority: 10 diff --git a/src/Symfony/Resources/config/field_value_converters.yml b/src/Symfony/Resources/config/field_value_converters.yml new file mode 100644 index 0000000..5ae41d6 --- /dev/null +++ b/src/Symfony/Resources/config/field_value_converters.yml @@ -0,0 +1,11 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + EzSystems\EzPlatformQueryFieldType\Form\Type\FieldType\QueryFieldType: ~ + + EzSystems\EzPlatformQueryFieldType\Persistence\Legacy\Content\FieldValue\Converter\QueryConverter: + tags: + - {name: ezpublish.storageEngine.legacy.converter, alias: query} diff --git a/src/Symfony/Resources/config/fieldtypes.yml b/src/Symfony/Resources/config/fieldtypes.yml new file mode 100644 index 0000000..a5d9a12 --- /dev/null +++ b/src/Symfony/Resources/config/fieldtypes.yml @@ -0,0 +1,12 @@ +services: + EzSystems\EzPlatformQueryFieldType\FieldType\Query\Type: + parent: ezpublish.fieldType + tags: + - {name: ezpublish.fieldType, alias: query} + + EzSystems\EzPlatformQueryFieldType\FieldType\Mapper\QueryFormMapper: + tags: + - { name: ez.fieldFormMapper.definition, fieldType: query } + - { name: ez.fieldFormMapper.value, fieldType: query } + arguments: + $contentTypeService: '@ezpublish.api.service.content_type' diff --git a/src/Symfony/Resources/config/graphql.yml b/src/Symfony/Resources/config/graphql.yml new file mode 100644 index 0000000..7db4154 --- /dev/null +++ b/src/Symfony/Resources/config/graphql.yml @@ -0,0 +1,14 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + EzSystems\EzPlatformQueryFieldType\GraphQL\QueryFieldResolver: + tags: + - { name: overblog_graphql.resolver, alias: "QueryFieldValue", method: "resolveQueryField" } + + EzSystems\EzPlatformQueryFieldType\GraphQL\QueryFieldDefinitionMapper: + decorates: EzSystems\EzPlatformGraphQL\Schema\Domain\Content\Mapper\FieldDefinition\FieldDefinitionMapper + arguments: + $innerMapper: '@EzSystems\EzPlatformQueryFieldType\GraphQL\QueryFieldDefinitionMapper.inner' diff --git a/src/Symfony/Resources/config/graphql/QueryFieldType.types.yml b/src/Symfony/Resources/config/graphql/QueryFieldType.types.yml new file mode 100644 index 0000000..e5657f0 --- /dev/null +++ b/src/Symfony/Resources/config/graphql/QueryFieldType.types.yml @@ -0,0 +1,25 @@ +QueryFieldDefinition: + type: object + inherits: [FieldDefinition] + config: + fields: + settings: + type: QueryFieldSettings + resolve: "@=value.getFieldSettings()" + +QueryFieldSettings: + type: object + config: + fields: + queryType: + type: String + description: "Identifier of the query type executed by the field" + resolve: "@=value['QueryType']" + parameters: + type: String + description: "JSON of query type parameters" + resolve: "@=value['Parameters']" + returnedType: + type: String + description: "Content type returned by the field" + resolve: "@=value['ReturnedType']" diff --git a/src/Symfony/Resources/config/indexable_fieldtypes.yml b/src/Symfony/Resources/config/indexable_fieldtypes.yml new file mode 100644 index 0000000..1e8205a --- /dev/null +++ b/src/Symfony/Resources/config/indexable_fieldtypes.yml @@ -0,0 +1,9 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + EzSystems\EzPlatformQueryFieldType\FieldType\Query\SearchField: + tags: + - {name: ezpublish.fieldType.indexable, alias: query} diff --git a/src/Symfony/Resources/config/routing/rest.yml b/src/Symfony/Resources/config/routing/rest.yml new file mode 100644 index 0000000..d3989fa --- /dev/null +++ b/src/Symfony/Resources/config/routing/rest.yml @@ -0,0 +1,9 @@ +ezpublish_rest_query_field_results: + path: /api/ezp/v2/content/objects/{contentId}/versions/{versionNumber}/fields/{fieldDefinitionIdentifier}/query/results + defaults: + _controller: EzSystems\EzPlatformQueryFieldType\Controller\QueryFieldRestController:getResults + methods: [GET] + requirements: + contentId: \d+ + fieldId: \d+ + versionNumber: \d+ diff --git a/src/Symfony/Resources/config/services.yml b/src/Symfony/Resources/config/services.yml new file mode 100644 index 0000000..89639a5 --- /dev/null +++ b/src/Symfony/Resources/config/services.yml @@ -0,0 +1,11 @@ +services: + _defaults: + autoconfigure: true + autowire: true + public: true + + EzSystems\EzPlatformQueryFieldType\Controller\QueryFieldController: ~ + + EzSystems\EzPlatformQueryFieldType\Controller\QueryFieldRestController: ~ + + EzSystems\EzPlatformQueryFieldType\API\QueryFieldService: ~ diff --git a/src/Symfony/Resources/translations/ezrepoforms_content_type.en.yml b/src/Symfony/Resources/translations/ezrepoforms_content_type.en.yml new file mode 100644 index 0000000..05610d3 --- /dev/null +++ b/src/Symfony/Resources/translations/ezrepoforms_content_type.en.yml @@ -0,0 +1 @@ +field_definition: { bdquery: { default_value: "Default value" }} diff --git a/src/Symfony/Resources/translations/fieldtypes.en.yml b/src/Symfony/Resources/translations/fieldtypes.en.yml new file mode 100644 index 0000000..67cb01a --- /dev/null +++ b/src/Symfony/Resources/translations/fieldtypes.en.yml @@ -0,0 +1,2 @@ +bdquery: { name: "query" } +field_definition.bdquery.default_value: "Default value" diff --git a/src/Symfony/Resources/views/content_fields.html.twig b/src/Symfony/Resources/views/content_fields.html.twig new file mode 100644 index 0000000..1897cec --- /dev/null +++ b/src/Symfony/Resources/views/content_fields.html.twig @@ -0,0 +1,8 @@ +{% extends '@EzPublishCore/content_fields.html.twig' %} + +{% block bdquery_field %} + {% spaceless %} + {% set field_value = field.value.text %} + {{ block( 'simple_inline_field' ) }} + {% endspaceless %} +{% endblock %} diff --git a/src/Symfony/Resources/views/field_types.html.twig b/src/Symfony/Resources/views/field_types.html.twig new file mode 100644 index 0000000..a7d0eeb --- /dev/null +++ b/src/Symfony/Resources/views/field_types.html.twig @@ -0,0 +1,21 @@ +{% extends '@EzSystemsRepositoryForms/ContentType/field_types.html.twig' %} + +{% block query_field_definition_edit %} +