Extending argument resolving
Using a parameter middleware, you can hook into the argument resolution of field/query/mutation/factory.
As an example, GraphQLite uses parameter middlewares internally to:
- Inject the Webonyx GraphQL resolution object when you type-hint on the
ResolveInfo
object. For instance:In the query above, the/**
* @return Product[]
*/
#[Query]
public function products(ResolveInfo $info): array$info
argument is filled with the WebonyxResolveInfo
class thanks to theResolveInfoParameterHandler parameter middleware
- Inject a service from the container when you use the
@Autowire
annotation - Perform validation with the
@Validate
annotation (in Laravel package)
Parameter middlewares
Each middleware is passed number of objects describing the parameter:
- a PHP
ReflectionParameter
object representing the parameter being manipulated - a
phpDocumentor\Reflection\DocBlock
instance (useful to analyze the@param
comment if any) - a
phpDocumentor\Reflection\Type
instance (useful to analyze the type if the argument) - a
TheCodingMachine\GraphQLite\Annotations\ParameterAnnotations
instance. This is a collection of all custom annotations that apply to this specific argument (more on that later) - a
$next
handler to pass the argument resolving to the next middleware.
Parameter resolution is done in 2 passes.
On the first pass, middlewares are traversed. They must return a TheCodingMachine\GraphQLite\Parameters\ParameterInterface
(an object that does the actual resolving).
interface ParameterMiddlewareInterface
{
public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, ?Type $paramTagType, ParameterAnnotations $parameterAnnotations, ParameterHandlerInterface $next): ParameterInterface;
}
Then, resolution actually happen by executing the resolver (this is the second pass).
Annotations parsing
If you plan to use annotations while resolving arguments, your annotation should extend the ParameterAnnotationInterface
For instance, if we want GraphQLite to inject a service in an argument, we can use @Autowire(for="myService")
.
For PHP 8 attributes, we only need to put declare the annotation can target parameters: #[Attribute(Attribute::TARGET_PARAMETER)]
.
The annotation looks like this:
use Attribute;
/**
* Use this annotation to autowire a service from the container into a given parameter of a field/query/mutation.
*
* @Annotation
*/
#[Attribute(Attribute::TARGET_PARAMETER)]
class Autowire implements ParameterAnnotationInterface
{
/**
* @var string
*/
public $for;
/**
* The getTarget method must return the name of the argument
*/
public function getTarget(): string
{
return $this->for;
}
}
Writing the parameter middleware
The middleware purpose is to analyze a parameter and decide whether or not it can handle it.
Parameter middleware class
class ContainerParameterHandler implements ParameterMiddlewareInterface
{
/** @var ContainerInterface */
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, ?Type $paramTagType, ParameterAnnotations $parameterAnnotations, ParameterHandlerInterface $next): ParameterInterface
{
// The $parameterAnnotations object can be used to fetch any annotation implementing ParameterAnnotationInterface
$autowire = $parameterAnnotations->getAnnotationByType(Autowire::class);
if ($autowire === null) {
// If there are no annotation, this middleware cannot handle the parameter. Let's ask
// the next middleware in the chain (using the $next object)
return $next->mapParameter($parameter, $docBlock, $paramTagType, $parameterAnnotations);
}
// We found a @Autowire annotation, let's return a parameter resolver.
return new ContainerParameter($this->container, $parameter->getType());
}
}
The last step is to write the actual parameter resolver.
Parameter resolver class
/**
* A parameter filled from the container.
*/
class ContainerParameter implements ParameterInterface
{
/** @var ContainerInterface */
private $container;
/** @var string */
private $identifier;
public function __construct(ContainerInterface $container, string $identifier)
{
$this->container = $container;
$this->identifier = $identifier;
}
/**
* The "resolver" returns the actual value that will be fed to the function.
*/
public function resolve(?object $source, array $args, $context, ResolveInfo $info)
{
return $this->container->get($this->identifier);
}
}
Registering a parameter middleware
The last step is to register the parameter middleware we just wrote:
You can register your own parameter middlewares using the SchemaFactory::addParameterMiddleware()
method.
$schemaFactory->addParameterMiddleware(new ContainerParameterHandler($container));
If you are using the Symfony bundle, you can tag the service as "graphql.parameter_middleware".