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

fix(dataproducer): Populate context default values before resolving #1403

Merged
merged 14 commits into from
Jun 28, 2024
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