-
Notifications
You must be signed in to change notification settings - Fork 201
Add Data Producer plugins for creating, updating and deleting entities. #1231
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
Changes from all commits
7d78890
c7f5d70
fbfc3c6
1aea77d
c7e59e5
acafe96
3253f08
80d91bb
75670c3
8a0694e
ef7eefc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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()) { | ||
$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, | ||
]; | ||
} | ||
|
||
} |
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would 'result' be a better name for this than 'was_successful'? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
]; | ||
} | ||
|
||
} |
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."); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
]; | ||
} | ||
|
||
} |
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 []; | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in https://github.com/Cryt1c/graphql/pull/2