-
Notifications
You must be signed in to change notification settings - Fork 103
Description
Hey.
Currently, if a user wants to store custom information in the context, they either need to:
- extend the
Context
class thatgraphqlite
provides - but this breaks as soon as two separate implementations want to extend theContext
- extend the
ContextInterface
interface thatgraphqlite
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 :)