Skip to content

Commit

Permalink
[FEATURE] Allow joining queries for filters on a target column (#893)
Browse files Browse the repository at this point in the history
Instead of assuming that joins on entity tables always happen at
the entity key level.

To do so use the `targetColumn` property in the field definition.
  • Loading branch information
e0ipso committed Apr 28, 2016
1 parent ebfbca2 commit 2f57807
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 40 deletions.
24 changes: 23 additions & 1 deletion src/Plugin/resource/DataProvider/CacheDecoratedDataProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@
use Drupal\restful\Plugin\resource\Field\ResourceFieldCollectionInterface;
use Drupal\restful\Plugin\resource\Field\ResourceFieldInterface;
use Drupal\restful\RenderCache\RenderCache;
use Drupal\restful\Util\ExplorableDecoratorInterface;

/**
* Class CacheDecoratedDataProvider.
*
* @package Drupal\restful\Plugin\resource\DataProvider
*/
class CacheDecoratedDataProvider implements CacheDecoratedDataProviderInterface {
class CacheDecoratedDataProvider implements CacheDecoratedDataProviderInterface, ExplorableDecoratorInterface {

/**
* The decorated object.
Expand Down Expand Up @@ -279,4 +280,25 @@ protected function clearRenderedCache(ArrayCollection $cache_fragments) {
$cache_object->clear();
}

/**
* Checks if the decorated object is an instance of something.
*
* @param string $class
* Class or interface to check the instance.
*
* @return bool
* TRUE if the decorated object is an instace of the $class. FALSE
* otherwise.
*/
public function isInstanceOf($class) {
if ($this instanceof $class || $this->subject instanceof $class) {
return TRUE;
}
// Check if the decorated resource is also a decorator.
if ($this->subject instanceof ExplorableDecoratorInterface) {
return $this->subject->isInstanceOf($class);
}
return FALSE;
}

}
78 changes: 48 additions & 30 deletions src/Plugin/resource/DataProvider/DataProviderEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use Drupal\restful\Plugin\resource\Field\ResourceFieldInterface;
use Drupal\restful\Plugin\resource\Resource;
use Drupal\restful\Plugin\resource\ResourceInterface;
use Drupal\restful\Util\ExplorableDecoratorInterface;
use Drupal\restful\Util\RelationalFilter;
use Drupal\restful\Util\RelationalFilterInterface;
use Drupal\entity_validator\ValidatorPluginManager;
Expand Down Expand Up @@ -603,7 +604,16 @@ protected function queryForListSort(\EntityFieldQuery $query) {
if (!$resource_field = $resource_fields->get($public_field_name)) {
return;
}
$this->alterSortQuery($public_field_name, $direction, $query);
$sort = array(
'public_field' => $public_field_name,
'direction' => $direction,
'resource_id' => $this->pluginId,
);
$sort = $this->alterSortQuery($sort, $query);
if (!empty($sort['processed'])) {
// If the sort was already processed by the alter filters, continue.
continue;
}
if (!$property_name = $resource_field->getProperty()) {
if (!$resource_field instanceof ResourceFieldEntityAlterableInterface) {
throw new BadRequestException('The current sort selection does not map to any entity property or Field API field.');
Expand All @@ -614,11 +624,11 @@ protected function queryForListSort(\EntityFieldQuery $query) {
return;
}
if (ResourceFieldEntity::propertyIsField($property_name)) {
$query->fieldOrderBy($property_name, $resource_field->getColumn(), $direction);
$query->fieldOrderBy($property_name, $resource_field->getColumn(), $sort['direction']);
}
else {
$column = $this->getColumnFromProperty($property_name);
$query->propertyOrderBy($column, $direction);
$query->propertyOrderBy($column, $sort['direction']);
}
}
}
Expand Down Expand Up @@ -656,7 +666,7 @@ protected function queryForListFilter(\EntityFieldQuery $query) {

// Give the chance for other data providers to have a special handling for
// a given field.
$this->alterFilterQuery($filter, $query);
$filter = $this->alterFilterQuery($filter, $query);
if (!empty($filter['processed'])) {
// If the filter was already processed by the alter filters, continue.
continue;
Expand Down Expand Up @@ -706,15 +716,21 @@ protected function queryForListFilter(\EntityFieldQuery $query) {
* The parsed filter information.
* @param \EntityFieldQuery $query
* The EFQ to add the filter to.
*
* @return array
* The modified $filter array.
*/
protected function alterFilterQuery(array &$filter, \EntityFieldQuery $query) {
protected function alterFilterQuery(array $filter, \EntityFieldQuery $query) {
if (!$resource_field = $this->fieldDefinitions->get($filter['public_field'])) {
return;
return $filter;
}
if (!$resource_field instanceof ResourceFieldEntityAlterableInterface) {
return;
// Check if the resource can check on decorated instances.
if (!$resource_field instanceof ExplorableDecoratorInterface || !$resource_field->isInstanceOf(ResourceFieldEntityAlterableInterface::class)) {
return $filter;
}
}
$resource_field->alterFilterEntityFieldQuery($filter, $query);
return $resource_field->alterFilterEntityFieldQuery($filter, $query);
}

/**
Expand All @@ -725,26 +741,28 @@ protected function alterFilterQuery(array &$filter, \EntityFieldQuery $query) {
* in $filter to TRUE. Otherwise normal filtering will be added on top,
* leading to unexpected results.
*
* @param string $public_field_name
* The public field name to sort by.
* @param string $direction
* The sort direction.
* @param array $sort
* The sort array containing the keys:
* - public_field: Contains the public property.
* - direction: The sorting direction, either ASC or DESC.
* - resource_id: The resource machine name.
* @param \EntityFieldQuery $query
* The EFQ to add the filter to.
*
* @return array
* The modified $sort array.
*/
protected function alterSortQuery($public_field_name, $direction, \EntityFieldQuery $query) {
if (!$resource_field = $this->fieldDefinitions->get($public_field_name)) {
return;
protected function alterSortQuery(array $sort, \EntityFieldQuery $query) {
if (!$resource_field = $this->fieldDefinitions->get($sort['public_field'])) {
return $sort;
}
if (!$resource_field instanceof ResourceFieldEntityAlterableInterface) {
return;
// Check if the resource can check on decorated instances.
if (!$resource_field instanceof ExplorableDecoratorInterface || !$resource_field->isInstanceOf(ResourceFieldEntityAlterableInterface::class)) {
return $sort;
}
}
$sort = array(
'public_field' => $public_field_name,
'direction' => $direction,
'resource_id' => $this->pluginId,
);
$resource_field->alterSortEntityFieldQuery($sort, $query);
return $resource_field->alterSortEntityFieldQuery($sort, $query);
}

/**
Expand Down Expand Up @@ -1210,7 +1228,7 @@ protected function getReferencedIds(array $values, ResourceFieldInterface $resou
protected function addNestedFilter(array $filter, \EntityFieldQuery $query) {
$relational_filters = array();
foreach ($this->getFieldsInfoFromPublicName($filter['public_field']) as $field_info) {
$relational_filters[] = new RelationalFilter($field_info['name'], $field_info['type'], $field_info['column'], $field_info['entity_type'], $field_info['bundles']);
$relational_filters[] = new RelationalFilter($field_info['name'], $field_info['type'], $field_info['column'], $field_info['entity_type'], $field_info['bundles'], $field_info['target_column']);
}
$query->addRelationship($filter + array('relational_filters' => $relational_filters));
}
Expand All @@ -1223,8 +1241,10 @@ protected function addNestedFilter(array $filter, \EntityFieldQuery $query) {
*
* @throws ServerConfigurationException
* When the required resource information is not available.
* @throws BadRequestException
* When the nested field is invalid.
*
* @return array[]
* @return array
* An array of fields with name and type.
*/
protected function getFieldsInfoFromPublicName($name) {
Expand Down Expand Up @@ -1254,6 +1274,7 @@ protected function getFieldsInfoFromPublicName($name) {
'type' => ResourceFieldEntity::propertyIsField($property) ? RelationalFilterInterface::TYPE_FIELD : RelationalFilterInterface::TYPE_PROPERTY,
'entity_type' => NULL,
'bundles' => array(),
'target_column' => NULL,
);
$item['column'] = $item['type'] == RelationalFilterInterface::TYPE_FIELD ? $resource_field->getColumn() : NULL;
$fields[] = $item;
Expand All @@ -1280,21 +1301,18 @@ protected function getFieldsInfoFromPublicName($name) {
* field is not a reference (for instance the destination field used in
* the where clause).
*/
protected function getFieldsFromPublicNameItem(ResourceFieldInterface $resource_field) {
protected function getFieldsFromPublicNameItem(ResourceFieldResourceInterface $resource_field) {
$property = $resource_field->getProperty();
$resource = $resource_field->getResource();
$item = array(
'name' => $property,
'type' => ResourceFieldEntity::propertyIsField($property) ? RelationalFilterInterface::TYPE_FIELD : RelationalFilterInterface::TYPE_PROPERTY,
'entity_type' => NULL,
'bundles' => array(),
'target_column' => $resource_field->getTargetColumn(),
);
$item['column'] = $item['type'] == RelationalFilterInterface::TYPE_FIELD ? $resource_field->getColumn() : NULL;
$instance_id = sprintf('%s:%d.%d', $resource['name'], $resource['majorVersion'], $resource['minorVersion']);
/* @var ResourceEntity $resource */
$resource = restful()
->getResourceManager()
->getPluginCopy($instance_id, Request::create('', array(), RequestInterface::METHOD_GET));
$resource = $resource_field->getResourcePlugin();

// Variables for the next iteration.
$definitions = $resource->getFieldDefinitions();
Expand Down
24 changes: 23 additions & 1 deletion src/Plugin/resource/Decorators/ResourceDecoratorBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
use Drupal\restful\Plugin\resource\DataProvider\DataProviderInterface;
use Drupal\restful\Plugin\resource\Field\ResourceFieldCollectionInterface;
use Drupal\restful\Plugin\resource\ResourceInterface;
use Drupal\restful\Util\ExplorableDecoratorInterface;

/**
* Class ResourceDecoratorBase.
*
* @package Drupal\restful\Plugin\resource\Decorators
*/
abstract class ResourceDecoratorBase extends PluginBase implements ResourceDecoratorInterface {
abstract class ResourceDecoratorBase extends PluginBase implements ResourceDecoratorInterface, ExplorableDecoratorInterface {

/**
* The decorated resource.
Expand Down Expand Up @@ -390,6 +391,27 @@ public function getPluginId() {
return $this->subject->getPluginId();
}

/**
* Checks if the decorated object is an instance of something.
*
* @param string $class
* Class or interface to check the instance.
*
* @return bool
* TRUE if the decorated object is an instace of the $class. FALSE
* otherwise.
*/
public function isInstanceOf($class) {
if ($this instanceof $class || $this->subject instanceof $class) {
return TRUE;
}
// Check if the decorated resource is also a decorator.
if ($this->subject instanceof ExplorableDecoratorInterface) {
return $this->subject->isInstanceOf($class);
}
return FALSE;
}

/**
* If any method not declared, then defer it to the decorated field.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ interface ResourceFieldEntityAlterableInterface {
* The filter array definition.
* @param \EntityFieldQuery $query
* The entity field query to modify.
*
* @return array
* The modified $filter array.
*/
public function alterFilterEntityFieldQuery(array &$filter, \EntityFieldQuery $query);
public function alterFilterEntityFieldQuery(array $filter, \EntityFieldQuery $query);

/**
* Alter the list query to add the sorting for this field.
Expand All @@ -31,7 +34,10 @@ public function alterFilterEntityFieldQuery(array &$filter, \EntityFieldQuery $q
* The sort array definition.
* @param \EntityFieldQuery $query
* The entity field query to modify.
*
* @return array
* The modified $sort array.
*/
public function alterSortEntityFieldQuery(array &$sort, \EntityFieldQuery $query);
public function alterSortEntityFieldQuery(array $sort, \EntityFieldQuery $query);

}
58 changes: 57 additions & 1 deletion src/Plugin/resource/Field/ResourceFieldResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@

namespace Drupal\restful\Plugin\resource\Field;

use Drupal\restful\Exception\ServerConfigurationException;
use Drupal\restful\Http\RequestInterface;
use Drupal\restful\Plugin\resource\DataInterpreter\DataInterpreterInterface;
use Drupal\restful\Plugin\resource\Field\PublicFieldInfo\PublicFieldInfoInterface;
use Drupal\restful\Plugin\resource\ResourceInterface;
use Drupal\restful\Util\ExplorableDecoratorInterface;

class ResourceFieldResource implements ResourceFieldResourceInterface {
class ResourceFieldResource implements ResourceFieldResourceInterface, ExplorableDecoratorInterface {

/**
* Decorated resource field.
Expand Down Expand Up @@ -44,6 +46,13 @@ class ResourceFieldResource implements ResourceFieldResourceInterface {
*/
protected $resourcePlugin;

/**
* Target Column.
*
* @var string
*/
protected $targetColumn;

/**
* Constructor.
*
Expand All @@ -58,6 +67,10 @@ public function __construct(array $field, RequestInterface $request) {
$this->setRequest($request);
}
$this->resourceMachineName = $field['resource']['name'];
// Compute the target column if empty.
if (!empty($field['targetColumn'])) {
$this->targetColumn = $field['targetColumn'];
}
}

/**
Expand Down Expand Up @@ -367,4 +380,47 @@ public function autoDiscovery() {
return ResourceFieldBase::emptyDiscoveryInfo($this->getPublicName());
}

/**
* {@inheritdoc}
*/
public function getTargetColumn() {
if (empty($this->targetColumn)) {
// Check the definition of the decorated field.
$definition = $this->decorated->getDefinition();
if (!empty($definition['targetColumn'])) {
$this->targetColumn = $definition['targetColumn'];
}
elseif ($this->isInstanceOf(ResourceFieldEntityReferenceInterface::class)) {
$entity_info = entity_get_info($this->getResourcePlugin()->getEntityType());
// Assume that the relationship is through the entity key id.
$this->targetColumn = $entity_info['entity keys']['id'];
}
else {
throw new ServerConfigurationException(sprintf('Target column could not be found for field "%s".', $this->getPublicName()));
}
}
return $this->targetColumn;
}

/**
* Checks if the decorated object is an instance of something.
*
* @param string $class
* Class or interface to check the instance.
*
* @return bool
* TRUE if the decorated object is an instace of the $class. FALSE
* otherwise.
*/
public function isInstanceOf($class) {
if ($this instanceof $class || $this->decorated instanceof $class) {
return TRUE;
}
// Check if the decorated resource is also a decorator.
if ($this->decorated instanceof ExplorableDecoratorInterface) {
return $this->decorated->isInstanceOf($class);
}
return FALSE;
}

}
8 changes: 8 additions & 0 deletions src/Plugin/resource/Field/ResourceFieldResourceInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,12 @@ public function getResourceMachineName();
*/
public function getResourcePlugin();

/**
* Gets the table column for joins.
*
* @return string
* The column to make a join for nested filters.
*/
public function getTargetColumn();

}
Loading

0 comments on commit 2f57807

Please sign in to comment.