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 double access to ArrayAccess methods #760

Closed
wants to merge 12 commits into from
13 changes: 12 additions & 1 deletion src/Executor/Executor.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Schema;
use Throwable;
use function is_array;
use function is_object;

Expand Down Expand Up @@ -173,10 +174,20 @@ public static function defaultFieldResolver($objectValue, $args, $contextValue,
$fieldName = $info->fieldName;
$property = null;

if (is_array($objectValue) || $objectValue instanceof ArrayAccess) {
if (is_array($objectValue)) {
if (isset($objectValue[$fieldName])) {
$property = $objectValue[$fieldName];
}
} elseif ($objectValue instanceof ArrayAccess) {
spawnia marked this conversation as resolved.
Show resolved Hide resolved
brunobg marked this conversation as resolved.
Show resolved Hide resolved
// handles #759: ArrayAccess::offsetExists() can call offsetGet() internally,
// which results in calling offsetGet() twice.
// This avoids the double call and handles Exceptions to have the same
// behavior as isset() in the pure array case.
try {
$property = $objectValue[$fieldName];
} catch (Throwable $e) {
// pass
}
} elseif (is_object($objectValue)) {
if (isset($objectValue->{$fieldName})) {
$property = $objectValue->{$fieldName};
Expand Down
183 changes: 183 additions & 0 deletions tests/Executor/ExecutorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace GraphQL\Tests\Executor;

use ArrayAccess;
use Exception;
use GraphQL\Deferred;
use GraphQL\Error\Error;
use GraphQL\Error\UserError;
Expand Down Expand Up @@ -1225,4 +1227,185 @@ public function testSerializesToEmptyObjectVsEmptyArray() : void
$result->toArray()
);
}

public function testDefaultResolverGrabsValuesOffOfCommonPhpDataStructures() : void
{
$Array = new ObjectType([
'name' => 'Array',
'fields' => [
'set' => Type::int(),
'unset' => Type::int(),
],
]);

$ArrayAccess = new ObjectType([
'name' => 'ArrayAccess',
'fields' => [
'set' => Type::int(),
'unsetNull' => Type::int(),
'unsetThrow' => Type::int(),
],
]);

$ObjectField = new ObjectType([
'name' => 'ObjectField',
'fields' => [
'set' => Type::int(),
'unset' => Type::int(),
'nonExistent' => Type::int(),
],
]);

$ObjectVirtual = new ObjectType([
'name' => 'ObjectVirtual',
'fields' => [
'set' => Type::int(),
'unsetNull' => Type::int(),
'unsetThrow' => Type::int(),
],
]);

$schema = new Schema([
'query' => new ObjectType([
'name' => 'Query',
'fields' => [
'array' => [
'type' => $Array,
'resolve' => static function () : array {
return ['set' => 1];
},
],
'arrayAccess' => [
'type' => $ArrayAccess,
'resolve' => static function () : ArrayAccess {
return new class implements ArrayAccess {
public function offsetExists($offset)
{
switch ($offset) {
case 'set':
return true;
default:
return false;
}
}

public function offsetGet($offset)
{
switch ($offset) {
case 'set':
return 1;
case 'unsetNull':
return null;
default:
throw new Exception('unsetThrow');
}
}

public function offsetSet($offset, $value)
{
}

public function offsetUnset($offset)
{
}
};
},
],
'objectField' => [
'type' => $ObjectField,
'resolve' => static function () : stdClass {
return new class extends stdClass {
/** @var int|null */
public $set = 1;

/** @var int|null */
public $unset;
};
},
],
'objectVirtual' => [
'type' => $ObjectVirtual,
'resolve' => static function () {
return new class {
public function __isset($name) : bool
{
switch ($name) {
case 'set':
return true;
default:
return false;
}
}

public function __get($name) : ?int
{
switch ($name) {
case 'set':
return 1;
case 'unsetNull':
return null;
default:
throw new Exception('unsetThrow');
}
}
};
},
],
],
]),
]);

$query = Parser::parse('
{
array {
set
unset
}
arrayAccess {
set
unsetNull
unsetThrow
}
objectField {
set
unset
nonExistent
}
objectVirtual {
set
unsetNull
unsetThrow
}
}
');

$result = Executor::execute($schema, $query);

self::assertEquals(
[
'data' => [
'array' => [
'set' => 1,
'unset' => null,
],
'arrayAccess' => [
'set' => 1,
'unsetNull' => null,
'unsetThrow' => null,
],
'objectField' => [
'set' => 1,
'unset' => null,
'nonExistent' => null,
],
'objectVirtual' => [
'set' => 1,
'unsetNull' => null,
'unsetThrow' => null,
],
],
],
$result->toArray()
);
}
}