Skip to content

Commit

Permalink
fix(dataproducer): Populate context default values before resolving (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
klausi authored Jun 28, 2024
1 parent d9ad85d commit 79d09dd
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 8 deletions.
1 change: 1 addition & 0 deletions config/install/graphql.settings.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dataproducer_populate_default_values: true
10 changes: 10 additions & 0 deletions config/schema/graphql.schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,13 @@ graphql.default_persisted_query_configuration:

plugin.plugin_configuration.persisted_query.*:
type: graphql.default_persisted_query_configuration

graphql.settings:
type: config_object
label: "GraphQL Settings"
mapping:
# @todo Remove in GraphQL 5.
dataproducer_populate_default_values:
type: boolean
label: "Populate dataproducer context default values"
description: "Legacy setting: Populate dataproducer context default values before executing the resolve method. Set this to true to be future-proof. This setting is deprecated and will be removed in a future release."
12 changes: 12 additions & 0 deletions graphql.install
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,15 @@ function graphql_update_8001(): void {
*/
function graphql_update_8400() :void {
}

/**
* Preserve dataproducer default value behavior for old installations.
*
* Set dataproducer_populate_default_values to TRUE after you verified that your
* dataproducers are still working with the new default value behavior.
*/
function graphql_update_10400() :void {
\Drupal::configFactory()->getEditable('graphql.settings')
->set('dataproducer_populate_default_values', FALSE)
->save();
}
22 changes: 22 additions & 0 deletions src/Plugin/DataProducerPluginManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ class DataProducerPluginManager extends DefaultPluginManager {
*/
protected $resultCacheBackend;

/**
* Backwards compatibility flag to populate context defaults or not.
*
* @todo Remove in 5.x.
*/
protected bool $populateContextDefaults = TRUE;

/**
* DataProducerPluginManager constructor.
*
Expand Down Expand Up @@ -83,6 +90,21 @@ public function __construct(
$this->requestStack = $requestStack;
$this->contextsManager = $contextsManager;
$this->resultCacheBackend = $resultCacheBackend;

// We don't use dependency injection here to avoid a constructor signature
// change.
// @phpcs:disable
// @phpstan-ignore-next-line
$this->populateContextDefaults = \Drupal::config('graphql.settings')->get('dataproducer_populate_default_values') ?? TRUE;
// @phpcs:enable
}

/**
* {@inheritdoc}
*/
public function createInstance($plugin_id, array $configuration = []) {
$configuration['dataproducer_populate_default_values'] = $this->populateContextDefaults;
return parent::createInstance($plugin_id, $configuration);
}

/**
Expand Down
19 changes: 17 additions & 2 deletions src/Plugin/GraphQL/DataProducer/DataProducerPluginBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,27 @@ public function resolveField(FieldContext $field) {
if (!method_exists($this, 'resolve')) {
throw new \LogicException('Missing data producer resolve method.');
}

$context = $this->getContextValues();
$populateDefaultValues = $this->configuration['dataproducer_populate_default_values'] ?? TRUE;
$context = $populateDefaultValues ? $this->getContextValuesWithDefaults() : $this->getContextValues();
return call_user_func_array(
[$this, 'resolve'],
array_values(array_merge($context, [$field]))
);
}

/**
* Initializes all contexts and populates default values.
*
* We cannot use ::getContextValues() here because it does not work with
* default_value.
*/
public function getContextValuesWithDefaults(): array {
$values = [];
foreach ($this->getContextDefinitions() as $name => $definition) {
$values[$name] = $this->getContext($name)->getContextValue();
}

return $values;
}

}
98 changes: 98 additions & 0 deletions tests/src/Kernel/DataProducer/DefaultValueTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

namespace Drupal\Tests\graphql\Kernel\DataProducer;

use Drupal\Core\Session\AccountInterface;
use Drupal\graphql\GraphQL\Execution\FieldContext;
use Drupal\graphql\Plugin\GraphQL\DataProducer\Entity\EntityLoad;
use Drupal\Tests\graphql\Kernel\GraphQLTestBase;
use GraphQL\Deferred;
use PHPUnit\Framework\Assert;

/**
* Context default value test.
*
* @group graphql
*/
class DefaultValueTest extends GraphQLTestBase {

/**
* Test that the entity_load data producer has the correct default values.
*/
public function testEntityLoadDefaultValue(): void {
$manager = $this->container->get('plugin.manager.graphql.data_producer');
$plugin = $manager->createInstance('entity_load');
// Only type is required.
$plugin->setContextValue('type', 'node');
$context_values = $plugin->getContextValuesWithDefaults();
$this->assertTrue($context_values['access']);
$this->assertSame('view', $context_values['access_operation']);
}

/**
* Test that the legacy dataproducer_populate_default_values setting works.
*
* @dataProvider settingsProvider
*/
public function testLegacyDefaultValueSetting(bool $populate_setting, string $testClass): void {
$this->container->get('config.factory')->getEditable('graphql.settings')
->set('dataproducer_populate_default_values', $populate_setting)
->save();
$manager = $this->container->get('plugin.manager.graphql.data_producer');

// Manipulate the plugin definitions to use our test class for entity_load.
$definitions = $manager->getDefinitions();
$definitions['entity_load']['class'] = $testClass;
$reflection = new \ReflectionClass($manager);
$property = $reflection->getProperty('definitions');
$property->setAccessible(TRUE);
$property->setValue($manager, $definitions);

$this->executeDataProducer('entity_load', ['type' => 'node']);
}

/**
* Data provider for the testLegacyDefaultValueSetting test.
*/
public function settingsProvider(): array {
return [
[FALSE, TestLegacyEntityLoad::class],
[TRUE, TestNewEntityLoad::class],
];
}

}

/**
* Helper class to test the legacy behavior.
*/
class TestLegacyEntityLoad extends EntityLoad {

/**
* {@inheritdoc}
*/
public function resolve($type, $id, ?string $language, ?array $bundles, ?bool $access, ?AccountInterface $accessUser, ?string $accessOperation, FieldContext $context): ?Deferred {
// Old behavior: no default values applied, so we get NULL here.
Assert::assertNull($access);
Assert::assertNull($accessOperation);
return NULL;
}

}

/**
* Helper class to test the new behavior.
*/
class TestNewEntityLoad extends EntityLoad {

/**
* {@inheritdoc}
*/
public function resolve($type, $id, ?string $language, ?array $bundles, ?bool $access, ?AccountInterface $accessUser, ?string $accessOperation, FieldContext $context): ?Deferred {
// New behavior: default values are applied.
Assert::assertTrue($access);
Assert::assertSame('view', $accessOperation);
return NULL;
}

}
6 changes: 0 additions & 6 deletions tests/src/Kernel/DataProducer/EntityMultipleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,6 @@ public function testResolveEntityLoadMultiple(): void {
'type' => $this->node1->getEntityTypeId(),
'bundles' => [$this->node1->bundle(), $this->node2->bundle()],
'ids' => [$this->node1->id(), $this->node2->id(), $this->node3->id()],
// @todo We need to set these default values here to make the access
// handling work. Ideally that should not be needed.
'access' => TRUE,
'access_operation' => 'view',
]);

$nids = array_values(array_map(function (NodeInterface $item) {
Expand All @@ -104,8 +100,6 @@ public function testResolveEntityLoadWithNullId(): void {
$result = $this->executeDataProducer('entity_load_multiple', [
'type' => $this->node1->getEntityTypeId(),
'ids' => [NULL],
'access' => TRUE,
'access_operation' => 'view',
]);

$this->assertSame([], $result);
Expand Down

0 comments on commit 79d09dd

Please sign in to comment.