-
Couldn't load subscription status.
- Fork 104
Description
Hey.
Currently, if a user wants to store custom information in the context, they either need to:
- extend the
Contextclass thatgraphqliteprovides - but this breaks as soon as two separate implementations want to extend theContext - extend the
ContextInterfaceinterface thatgraphqliteprovides, 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
ContextInterfacewould 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 :)