Skip to content

Avoid side-effects from mutating argument default value objects #1914

Description

@er1z

Hello,

if PHP is configured via FPM, then it's very hard to spot. Recently, I moved with the application to FrankenPHP (but I bet other worker-alike runtimes will reveal similar behavior). Let's start with a resolve function:

'items' => [
    'type' => Type::listOf(Type::string()),
    'args' => [
        'filter' => [
            'type'         => $filterType,
            'defaultValue' => new Filter(),
        ],
    ],
    'resolve' => function ($root, array $args): array {
        static $counter = 0;

        $filter = $args['filter'];

        if ($counter++ == 0) {
            $args['filter']->status = 'polluted';
        }

        return [
            "status={$filter->status}",
            "limit={$filter->limit}",
        ];
    },
],

This is a little bit synthetic proof of concept, but I want to set the status to polluted only for a first response. Other should return a default value that's held as a public property default value of Filter class.

class Filter
{
    public function __construct(
        public string $status = 'active',
        public int $limit = 10,
    ) {}
}

But:

$schema = new Schema(['query' => $queryType]);

$gql = '{ items }';

echo "=== Request 1: ===" . PHP_EOL;
$result = GraphQL::executeQuery($schema, $gql);
print_r($result->toArray());

echo PHP_EOL . "=== Request 2: ===" . PHP_EOL;
$result = GraphQL::executeQuery($schema, '{ items }');
print_r($result->toArray());

Results in:

=== Request 1: ===
Array
(
    [data] => Array
        (
            [items] => Array
                (
                    [0] => status=polluted
                    [1] => limit=10
                )
        )
)

=== Request 2: ===
Array
(
    [data] => Array
        (
            [items] => Array
                (
                    [0] => status=polluted
                    [1] => limit=10
                )
        )
)

I dug what's going on and it turns out if the defaultValue is an object, it's always passed as a reference:

Image

The fix is an oneliner (ReferenceExecutor):

$args = $this->fieldArgsCache[$fieldDef][$fieldNode] ??= $argsMapper(Values::getArgumentValues(
    $fieldDef,
    $fieldNode,
    $this->exeContext->variableValues,
    $this->exeContext->schema,
), $fieldDef, $fieldNode, $contextValue);

$args = unserialize(serialize($args));

return $resolveFn($rootValue, $args, $contextValue, $info);

And then everything works fine:

Image

If you are fine with a solution with unserialize/serialize, then I'll prepare a pull request with a fix.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions