1
<?php
2

3
namespace Nuwave\Lighthouse\Schema\Directives;
4

5
use Closure;
6
use GraphQL\Language\AST\DirectiveNode;
7
use GraphQL\Language\AST\EnumTypeDefinitionNode;
8
use GraphQL\Language\AST\EnumValueDefinitionNode;
9
use GraphQL\Language\AST\FieldDefinitionNode;
10
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
11
use GraphQL\Language\AST\InputValueDefinitionNode;
12
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
13
use GraphQL\Language\AST\Node;
14
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
15
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
16
use GraphQL\Language\AST\UnionTypeDefinitionNode;
17
use Illuminate\Database\Eloquent\Model;
18
use Nuwave\Lighthouse\Exceptions\DefinitionException;
19
use Nuwave\Lighthouse\Schema\AST\ASTBuilder;
20
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
21
use Nuwave\Lighthouse\Support\Contracts\Directive;
22
use Nuwave\Lighthouse\Support\Utils;
23

24
abstract class BaseDirective implements Directive
25
{
26
    /**
27
     * The AST node of the directive.
28
     *
29
     * @var \GraphQL\Language\AST\DirectiveNode
30
     */
31
    protected $directiveNode;
32

33
    /**
34
     * The node the directive is defined on.
35
     *
36
     * @see \GraphQL\Language\DirectiveLocation
37
     *
38
     * Intentionally leaving out the request definitions and the 'SCHEMA' location.
39
     *
40
     * @var ScalarTypeDefinitionNode|ObjectTypeDefinitionNode|FieldDefinitionNode|InputValueDefinitionNode|InterfaceTypeDefinitionNode|UnionTypeDefinitionNode|EnumTypeDefinitionNode|EnumValueDefinitionNode|InputObjectTypeDefinitionNode
41
     */
42
    protected $definitionNode;
43

44
    /**
45
     * Returns the name of the used directive.
46
     */
47 1
    public function name(): string
48
    {
49 1
        return $this->directiveNode->name->value;
50
    }
51

52
    /**
53
     * The hydrate function is called when retrieving a directive from the directive registry.
54
     *
55
     * @param  ScalarTypeDefinitionNode|ObjectTypeDefinitionNode|FieldDefinitionNode|InputValueDefinitionNode|InterfaceTypeDefinitionNode|UnionTypeDefinitionNode|EnumTypeDefinitionNode|EnumValueDefinitionNode|InputObjectTypeDefinitionNode  $definitionNode
56
     * @return $this
57
     */
58 1
    public function hydrate(DirectiveNode $directiveNode, Node $definitionNode): self
59
    {
60 1
        $this->directiveNode = $directiveNode;
61 1
        $this->definitionNode = $definitionNode;
62

63 1
        return $this;
64
    }
65

66
    /**
67
     * Get a Closure that is defined through an argument on the directive.
68
     */
69 1
    public function getResolverFromArgument(string $argumentName): Closure
70
    {
71 1
        [$className, $methodName] = $this->getMethodArgumentParts($argumentName);
72

73 1
        $namespacedClassName = $this->namespaceClassName($className);
74

75 1
        return Utils::constructResolver($namespacedClassName, $methodName);
76
    }
77

78
    /**
79
     * Does the current directive have an argument with the given name?
80
     */
81 1
    public function directiveHasArgument(string $name): bool
82
    {
83 1
        return ASTHelper::directiveHasArgument($this->directiveNode, $name);
84
    }
85

86
    /**
87
     * The name of the node the directive is defined upon.
88
     */
89 1
    protected function nodeName(): string
90
    {
91 1
        return $this->definitionNode->name->value;
92
    }
93

94
    /**
95
     * Get the value of an argument on the directive.
96
     *
97
     * @param  mixed|null  $default
98
     * @return mixed|null
99
     */
100 1
    protected function directiveArgValue(string $name, $default = null)
101
    {
102 1
        return ASTHelper::directiveArgValue($this->directiveNode, $name, $default);
103
    }
104

105
    /**
106
     * Get the model class from the `model` argument of the field.
107
     *
108
     * @param  string  $argumentName The default argument name "model" may be overwritten
109
     * @return class-string<\Illuminate\Database\Eloquent\Model>
110
     *
111
     * @throws \Nuwave\Lighthouse\Exceptions\DefinitionException
112
     */
113 1
    protected function getModelClass(string $argumentName = 'model'): string
114
    {
115 1
        $model = $this->directiveArgValue($argumentName);
116

117
        // Fallback to using information from the schema definition as the model name
118 1
        if (! $model) {
119 1
            if ($this->definitionNode instanceof FieldDefinitionNode) {
120 1
                $returnTypeName = ASTHelper::getUnderlyingTypeName($this->definitionNode);
121

122
                /** @var \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST */
123 1
                $documentAST = app(ASTBuilder::class)->documentAST();
124

125 1
                if (! isset($documentAST->types[$returnTypeName])) {
126 1
                    throw new DefinitionException(
127 1
                        "Type '$returnTypeName' on '{$this->nodeName()}' can not be found in the schema.'"
128
                    );
129
                }
130 1
                $type = $documentAST->types[$returnTypeName];
131

132 1
                $modelDirective = ASTHelper::directiveDefinition($type, 'model');
133 1
                if ($modelDirective !== null) {
134 1
                    $model = ASTHelper::directiveArgValue($modelDirective, 'class');
135
                } else {
136 1
                    $model = $returnTypeName;
137
                }
138 1
            } elseif ($this->definitionNode instanceof ObjectTypeDefinitionNode) {
139 1
                $model = $this->nodeName();
140
            }
141
        }
142

143 1
        if (! $model) {
144 0
            throw new DefinitionException(
145 0
                "A `model` argument must be assigned to the '{$this->name()}'directive on '{$this->nodeName()}"
146
            );
147
        }
148

149 1
        return $this->namespaceModelClass($model);
150
    }
151

152
    /**
153
     * Find a class name in a set of given namespaces.
154
     *
155
     * @param  array<string>  $namespacesToTry
156
     * @return class-string
157
     *
158
     * @throws \Nuwave\Lighthouse\Exceptions\DefinitionException
159
     */
160 1
    protected function namespaceClassName(string $classCandidate, array $namespacesToTry = [], callable $determineMatch = null): string
161
    {
162
        // Always try the explicitly set namespace first
163 1
        array_unshift(
164
            $namespacesToTry,
165 1
            ASTHelper::getNamespaceForDirective(
166 1
                $this->definitionNode,
167 1
                $this->name()
168
            )
169
        );
170

171 1
        if (! $determineMatch) {
172 1
            $determineMatch = 'class_exists';
173
        }
174

175 1
        $className = Utils::namespaceClassname(
176 1
            $classCandidate,
177 1
            $namespacesToTry,
178 1
            $determineMatch
179
        );
180

181 1
        if (! $className) {
182 1
            throw new DefinitionException(
183 1
                "No class '{$classCandidate}' was found for directive '{$this->name()}'"
184
            );
185
        }
186

187 1
        return $className;
188
    }
189

190
    /**
191
     * Split a single method argument into its parts.
192
     *
193
     * A method argument is expected to contain a class and a method name, separated by an @ symbol.
194
     * e.g. "App\My\Class@methodName"
195
     * This validates that exactly two parts are given and are not empty.
196
     *
197
     * @return array<string> Contains two entries: [string $className, string $methodName]
198
     *
199
     * @throws \Nuwave\Lighthouse\Exceptions\DefinitionException
200
     */
201 1
    protected function getMethodArgumentParts(string $argumentName): array
202
    {
203 1
        $argumentParts = explode(
204 1
            '@',
205 1
            $this->directiveArgValue($argumentName)
206
        );
207

208
        if (
209 1
            count($argumentParts) > 2
210 1
            || empty($argumentParts[0])
211
        ) {
212 0
            throw new DefinitionException(
213 0
                "Directive '{$this->name()}' must have an argument '{$argumentName}' in the form 'ClassName@methodName' or 'ClassName'"
214
            );
215
        }
216

217 1
        if (empty($argumentParts[1])) {
218 1
            $argumentParts[1] = '__invoke';
219
        }
220

221 1
        return $argumentParts;
222
    }
223

224
    /**
225
     * Try adding the default model namespace and ensure the given class is a model.
226
     *
227
     * @return class-string<\Illuminate\Database\Eloquent\Model>
228
     */
229 1
    protected function namespaceModelClass(string $modelClassCandidate): string
230
    {
231
        /**
232
         * The callback ensures this holds true.
233
         *
234
         * @var class-string<\Illuminate\Database\Eloquent\Model> $modelClass
235
         */
236 1
        $modelClass = $this->namespaceClassName(
237
            $modelClassCandidate,
238 1
            (array) config('lighthouse.namespaces.models'),
239
            static function (string $classCandidate): bool {
240 1
                return is_subclass_of($classCandidate, Model::class);
241
            }
242
        );
243

244 1
        return $modelClass;
245
    }
246
}

Read our documentation on viewing source code .

Loading