Skip to content

Commit

Permalink
Initial prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
Bertrand Dunogier authored and bdunogier committed Dec 6, 2019
1 parent 88002e3 commit b7ca138
Show file tree
Hide file tree
Showing 41 changed files with 1,579 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/vendor/
composer.lock
22 changes: 22 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -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
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.



35 changes: 35 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -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": "[email protected]"
},
{
"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"
}
}

}
42 changes: 42 additions & 0 deletions doc/examples/nearby_places/NearbyPlacesQueryType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php
/**
* Created by PhpStorm.
* User: bdunogier
* Date: 29/09/2018
* Time: 16:46
*/

namespace AppBundle\QueryType;

use eZ\Publish\API\Repository\Values\Content\Query;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
use eZ\Publish\Core\QueryType\QueryType;

class NearbyPlacesQueryType implements QueryType
{
public function getQuery(array $parameters = [])
{
return new Query([
'filter' => 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';
}
}
19 changes: 19 additions & 0 deletions doc/examples/nearby_places/README.md
Original file line number Diff line number Diff line change
@@ -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",
}
```
6 changes: 6 additions & 0 deletions phpspec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
formatter.name: pretty

suites:
default:
psr4_prefix: EzSystems\EzPlatformQueryFieldType
namespace: EzSystems\EzPlatformQueryFieldType
85 changes: 85 additions & 0 deletions spec/API/QueryFieldServiceSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

namespace spec\EzSystems\EzPlatformQueryFieldType\API;

use EzSystems\EzPlatformQueryFieldType\API\QueryFieldService;
use EzSystems\EzPlatformQueryFieldType\FieldType\Query;
use eZ\Publish\API\Repository\ContentTypeService;
use eZ\Publish\API\Repository\SearchService;
use eZ\Publish\API\Repository\Values\Content\ContentInfo;
use eZ\Publish\API\Repository\Values\Content\Query as ApiQuery;
use eZ\Publish\API\Repository\Values\Content\Search\SearchResult;
use eZ\Publish\Core\QueryType\QueryType;
use eZ\Publish\Core\QueryType\QueryTypeRegistry;
use eZ\Publish\Core\Repository\Values;
use EzSystems\EzPlatformGraphQL\GraphQL\Value\Field;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class QueryFieldServiceSpec extends ObjectBehavior
{
const CONTENT_TYPE_ID = 1;
const QUERY_TYPE_IDENTIFIER = 'query_type_identifier';
const FIELD_DEFINITION_IDENTIFIER = 'test';

private $searchResult;
private $searchHits;

function let(
SearchService $searchService,
ContentTypeService $contentTypeService,
QueryTypeRegistry $queryTypeRegistry,
QueryType $queryType
) {
$this->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]),
])
]);
}
}
93 changes: 93 additions & 0 deletions spec/GraphQL/QueryFieldDefinitionMapperSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

namespace spec\EzSystems\EzPlatformQueryFieldType\GraphQL;

use EzSystems\EzPlatformQueryFieldType\GraphQL\QueryFieldDefinitionMapper;
use eZ\Publish\API\Repository\ContentTypeService;
use eZ\Publish\Core\QueryType\QueryTypeRegistry;
use eZ\Publish\Core\Repository\Values\ContentType\ContentType;
use eZ\Publish\Core\Repository\Values\ContentType\FieldDefinition;
use EzSystems\EzPlatformGraphQL\Schema\Domain\Content\Mapper\FieldDefinition\FieldDefinitionMapper;
use EzSystems\EzPlatformGraphQL\Schema\Domain\Content\NameHelper;
use PhpSpec\ObjectBehavior;

class QueryFieldDefinitionMapperSpec extends ObjectBehavior
{
const FIELD_IDENTIFIER = 'test';
const FIELD_TYPE_IDENTIFIER = 'query';
const RETURNED_CONTENT_TYPE_IDENTIFIER = 'folder';
const GRAPHQL_TYPE = 'FolderContent';

function let(
FieldDefinitionMapper $innerMapper,
NameHelper $nameHelper,
ContentTypeService $contentTypeService
)
{
$contentType = new ContentType(['identifier' => 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]
]);
}
}
32 changes: 32 additions & 0 deletions spec/GraphQL/QueryFieldResolverSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace spec\EzSystems\EzPlatformQueryFieldType\GraphQL;

use EzSystems\EzPlatformQueryFieldType\API\QueryFieldService;
use EzSystems\EzPlatformQueryFieldType\GraphQL\QueryFieldResolver;
use eZ\Publish\Core\Repository\Values\Content\Content;
use EzSystems\EzPlatformGraphQL\GraphQL\Value\Field;
use PhpSpec\ObjectBehavior;

class QueryFieldResolverSpec extends ObjectBehavior
{
const FIELD_DEFINITION_IDENTIFIER = 'test';

function let(QueryFieldService $queryFieldService)
{
$this->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([]);
}
}
Loading

0 comments on commit b7ca138

Please sign in to comment.