Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Data Producer plugins for creating, updating and deleting entities. #1231

Closed
wants to merge 11 commits into from
115 changes: 115 additions & 0 deletions src/Plugin/GraphQL/DataProducer/Entity/CreateEntity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

namespace Drupal\graphql\Plugin\GraphQL\DataProducer\Entity;

use Drupal\Core\Access\AccessResultReasonInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
use Drupal\graphql\Plugin\GraphQL\DataProducer\EntityValidationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Creates an entity.
*
* @DataProducer(
* id = "create_entity",
* name = @Translation("Create Entity"),
* produces = @ContextDefinition("entity",
* label = @Translation("Entity")
* ),
* consumes = {
* "entity_type" = @ContextDefinition("string",
* label = @Translation("Entity Type"),
* required = TRUE
* ),
* "values" = @ContextDefinition("any",
* label = @Translation("Field values for creating the entity"),
* required = TRUE
* ),
* "entity_return_key" = @ContextDefinition("string",
* label = @Translation("Key name in the returned array where the entity
* will be placed"), required = TRUE
* ),
* "save" = @ContextDefinition("boolean",
* label = @Translation("Save entity"),
* required = FALSE,
* default_value = TRUE,
* ),
* }
* )
*/
class CreateEntity extends DataProducerPluginBase implements ContainerFactoryPluginInterface {

use EntityValidationTrait;

/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManager
*/
protected $entityTypeManager;

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$instance = new static($configuration, $plugin_id, $plugin_definition);
$instance->entityTypeManager = $container->get('entity_type.manager');
return $instance;
}

/**
* Resolve the values for this producer.
*/
public function resolve(string $entity_type, array $values, string $entity_return_key, ?bool $save, $context) {
$storage = $this->entityTypeManager->getStorage($entity_type);
$accessHandler = $this->entityTypeManager->getAccessControlHandler($entity_type);

// Infer the bundle type from the response and return an error if the entity
// type expects one, but one is not present.
$entity_type = $this->entityTypeManager->getDefinition($entity_type);
$bundle = $entity_type->getKey('bundle') && !empty($values[$entity_type->getKey('bundle')]) ? $values[$entity_type->getKey('bundle')] : NULL;
if ($entity_type->getKey('bundle') && !$bundle) {
return [
'errors' => [$this->t('Entity type being created requires a bundle, but none was present.')],
];
};

// Ensure the user has access to create this kind of entity.
$access = $accessHandler->createAccess($bundle, NULL, [], TRUE);
$context->addCacheableDependency($access);
if (!$access->isAllowed()) {
return [
'errors' => [$access instanceof AccessResultReasonInterface && $access->getReason() ? $access->getReason() : $this->t('Access was forbidden.')],
];
}

$entity = $storage->create($values);

// Core does not have a concept of create access for fields, so edit access
// is used instead. This is consistent with how other Drupal APIs handle
// field based create access.
$field_access_errors = [];
foreach ($values as $field_name => $value) {
$create_access = $entity->get($field_name)->access('edit', NULL, TRUE);
if (!$create_access->isALlowed()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!$create_access->isALlowed()) {
if (!$create_access->isAllowed()) {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$field_access_errors[] = sprintf('%s: %s', $field_name, $create_access instanceof AccessResultReasonInterface ? $create_access->getReason() : $this->t('Access was forbidden.'));
}
}
if (!empty($field_access_errors)) {
return ['errors' => $field_access_errors];
}

if ($violation_messages = $this->getViolationMessages($entity)) {
return ['errors' => $violation_messages];
}

if ($save) {
$entity->save();
}
return [
$entity_return_key => $entity,
];
}

}
46 changes: 46 additions & 0 deletions src/Plugin/GraphQL/DataProducer/Entity/DeleteEntity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Drupal\graphql\Plugin\GraphQL\DataProducer\Entity;

use Drupal\Core\Access\AccessResultReasonInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;

/**
* Deletes an entity.
*
* @DataProducer(
* id = "delete_entity",
* name = @Translation("Delete Entity"),
* produces = @ContextDefinition("entities",
* label = @Translation("Entities")
* ),
* consumes = {
* "entity" = @ContextDefinition("entity",
* label = @Translation("Entity")
* ),
* }
* )
*/
class DeleteEntity extends DataProducerPluginBase {

/**
* Resolve the values for this producer.
*/
public function resolve(ContentEntityInterface $entity, $context) {
$access = $entity->access('delete', NULL, TRUE);
$context->addCacheableDependency($access);
if (!$access->isAllowed()) {
return [
'was_successful' => FALSE,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would 'result' be a better name for this than 'was_successful'?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'errors' => [$access instanceof AccessResultReasonInterface ? $access->getReason() : 'Access was forbidden.'],
];
}

$entity->delete();
return [
'was_successful' => TRUE,
];
}

}
81 changes: 81 additions & 0 deletions src/Plugin/GraphQL/DataProducer/Entity/UpdateEntity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

namespace Drupal\graphql\Plugin\GraphQL\DataProducer\Entity;

use Drupal\Core\Access\AccessResultReasonInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
use Drupal\graphql\Plugin\GraphQL\DataProducer\EntityValidationTrait;

/**
* Updates entity values.
*
* @DataProducer(
* id = "update_entity",
* name = @Translation("Update Entity"),
* produces = @ContextDefinition("entities",
* label = @Translation("Entities")
* ),
* consumes = {
* "entity" = @ContextDefinition("entity",
* label = @Translation("Entity")
* ),
* "values" = @ContextDefinition("any",
* label = @Translation("Field values for creating the entity"),
* required = TRUE
* ),
* "entity_return_key" = @ContextDefinition("string",
* label = @Translation("Key name in the returned array where the entity will be placed"),
* required = TRUE
* ),
* }
* )
*/
class UpdateEntity extends DataProducerPluginBase {

use EntityValidationTrait;

/**
* Resolve the values for this producer.
*/
public function resolve(ContentEntityInterface $entity, array $values, string $entity_return_key, $context) {
// Ensure the user has access to perform an update.
$access = $entity->access('update', NULL, TRUE);
$context->addCacheableDependency($access);
if (!$access->isAllowed()) {
return [
'errors' => [$access instanceof AccessResultReasonInterface ? $access->getReason() : 'Access was forbidden.'],
];
}

// Filter out keys the user does not have access to update, this may include
// things such as the owner of the entity or the ID of the entity.
$update_fields = array_filter($values, function (string $field_name) use ($entity, $context) {
if (!$entity->hasField($field_name)) {
throw new \Exception("Could not update '$field_name' field, since it does not exist on the given entity.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of throwing an exception, should this keep track of errors and return ['errors'=>] like other code paths in this producer?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
$access = $entity->{$field_name}->access('edit', NULL, TRUE);
$context->addCacheableDependency($access);
return $access->isAllowed();
}, ARRAY_FILTER_USE_KEY);

// Hydrate the entity with the values.
foreach ($update_fields as $field_name => $field_value) {
$entity->set($field_name, $field_value);
}

if ($violation_messages = $this->getViolationMessages($entity)) {
return [
'errors' => $violation_messages,
];
}

// Once access has been granted, the save can be committed and the entity
// can be returned to the client.
$entity->save();
return [
$entity_return_key => $entity,
];
}

}
44 changes: 44 additions & 0 deletions src/Plugin/GraphQL/DataProducer/EntityValidationTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Drupal\graphql\Plugin\GraphQL\DataProducer;

use Drupal\Core\Entity\ContentEntityInterface;

/**
* Trait for entity validation.
*
* Ensure the entity passes validation, any violations will be reported back
* to the client. Validation will catch issues like invalid referenced entities,
* incorrect text formats, required fields etc. Additional validation of input
* should not be put here, but instead should be built into the entity
* validation system, so the same constraints are applied in the Drupal admin.
*/
trait EntityValidationTrait {

/**
* Get violation messages from an entity.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* An entity to validate.
*
* @return array
* Get a list of violations.
*/
public function getViolationMessages(ContentEntityInterface $entity): array {
$violations = $entity->validate();

// Remove violations of inaccessible fields as they cannot stem from our
// changes.
$violations->filterByFieldAccess();

if ($violations->count() > 0) {
$violation_messages = [];
foreach ($violations as $violation) {
$violation_messages[] = sprintf('%s: %s', $violation->getPropertyPath(), strip_tags($violation->getMessage()));
}
return $violation_messages;
}
return [];
}

}
Loading