Skip to content

Make context universal without class extension #746

@oprypkhantc

Description

@oprypkhantc

Hey.

Currently, if a user wants to store custom information in the context, they either need to:

  • extend the Context class that graphqlite provides - but this breaks as soon as two separate implementations want to extend the Context
  • extend the ContextInterface interface that graphqlite provides, wrap the original one and delegate all property access and method calls to the original one - but this also breaks as soon as someone attempts to type-hint it, and the implementation would be quite verbose and unstable

Instead, I propose we make Context a little more universal and a little easier to use:

class Context implements ContextInterface, ResetableContextInterface
{
	/** @var SplObjectStorage<object, mixed> */
	private SplObjectStorage $data;

	public function has(ContextToken $token): bool
	{
		return isset($this->data[$token]);
	}

	/**
	 * @template T
	 *
	 * @param ContextToken<T> $token
	 *
	 * @return T
	 */
	public function get(ContextToken $token): mixed
	{
		if ($this->has($token)) {
			return $this->data[$token];
		}

		$value = ($token->default)();

		$this->set($token, $value);

		return $value;
	}

	/**
	 * @template T
	 *
	 * @param ContextToken<T> $token
	 * @param T $value
	 */
	public function set(ContextToken $token, mixed $value): mixed
	{
		return $this->data[$token] = $value;
	}

    /** @deprecated */
	public function getPrefetchBuffer(ParameterInterface $field): PrefetchBuffer
	{
		static $token;

		if (!$token) {
			$token = new ContextToken(fn () => new PrefetchBuffer());
		}

		return $this->get($token);
	}

	public function reset(): void
	{
		$this->data = new SplObjectStorage();
	}
}

/**
 * @template-covariant T
 */
final class ContextToken
{
	/**
	 * @param Closure(): T $default
	 */
	public function __construct(
		public readonly Closure $default,
	)
	{
	}
}

which is basically an array $data, but with type-safety, autocompletion and protection from clashing. It can later be used in userland like so:

/** @return ContextToken<MyPrefetchBuffer> */
function myOwnPrefetchBufferContextToken(): ContextToken {
    // PHP 8.3+
    static $token = new ContextToken(fn () => new MyPrefetchBuffer());

    return $token;
}

// PHPStan correctly infers the type of $buffer as MyPrefetchBuffer
$buffer = $context->get(myOwnPrefetchBufferContextToken());

It could be simplified further when PHP finally allows expressions as constant values:

const PREFETCH_BUFFER_CONTEXT_TOKEN = new ContextToken(fn () => new MyPrefetchBuffer());

// PHPStan correctly infers the type of $buffer as MyPrefetchBuffer
$buffer = $context->get(PREFETCH_BUFFER_CONTEXT_TOKEN);

This isn't a new concept, and is widely used in JS/TS ecosystem. For example, a very similar use case exists in Angular: https://angular.dev/api/common/http/HttpContext#usage-notes . PHP implementation is, admittedly, clunky, but we do get some benefits in return:

  • context can be extended without extending the class
  • context can be extended from multiple sources, independently
  • ContextInterface would no longer be needed

It would be perfect to have this in webonyx/graphql, but it's a bigger change for them than it is for graphqlite, so I doubt they'd accept that idea. In case of graphqlite, we can, of course, preserve full backwards compatibility, simply deprecating the interface and getPrefetchBuffer. We could also get rid of reset() in the meantime :)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions