1
<?php
2

3
namespace Nuwave\Lighthouse\Schema;
4

5
use Exception;
6
use GraphQL\Language\AST\DirectiveNode;
7
use GraphQL\Language\AST\Node;
8
use HaydenPierce\ClassFinder\ClassFinder;
9
use Illuminate\Contracts\Events\Dispatcher as EventsDispatcher;
10
use Illuminate\Support\Collection;
11
use Illuminate\Support\Str;
12
use Nuwave\Lighthouse\Events\RegisterDirectiveNamespaces;
13
use Nuwave\Lighthouse\Exceptions\DirectiveException;
14
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
15
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
16
use Nuwave\Lighthouse\Support\Contracts\Directive;
17
use Nuwave\Lighthouse\Support\Utils;
18

19
class DirectiveLocator
20
{
21
    /**
22
     * The paths used for locating directive classes.
23
     *
24
     * Should be tried in the order they are contained in this array,
25
     * going from the most significant to least significant.
26
     *
27
     * Lazily initialized.
28
     *
29
     * @var array<int, string>
30
     */
31
    protected $directiveNamespaces;
32

33
    /**
34
     * A map from short directive names to full class names.
35
     *
36
     * E.g.
37
     * [
38
     *   'create' => 'Nuwave\Lighthouse\Schema\Directives\CreateDirective',
39
     *   'custom' => 'App\GraphQL\Directives\CustomDirective',
40
     * ]
41
     *
42
     * @var array<string, class-string<\Nuwave\Lighthouse\Support\Contracts\Directive>>
43
     */
44
    protected $resolvedClassnames = [];
45

46
    /**
47
     * @var \Illuminate\Contracts\Events\Dispatcher
48
     */
49
    protected $eventsDispatcher;
50

51 1
    public function __construct(EventsDispatcher $eventsDispatcher)
52
    {
53 1
        $this->eventsDispatcher = $eventsDispatcher;
54
    }
55

56
    /**
57
     * A list of namespaces with directives in descending priority.
58
     *
59
     * @return array<int, string>
60
     */
61 1
    public function namespaces(): array
62
    {
63 1
        if (! isset($this->directiveNamespaces)) {
64 1
            $this->directiveNamespaces =
65
                // When looking for a directive by name, the namespaces are tried in order
66 1
                (new Collection([
67
                    // User defined directives (top priority)
68 1
                    config('lighthouse.namespaces.directives'),
69

70
                    // Plugin developers defined directives
71 1
                    $this->eventsDispatcher->dispatch(new RegisterDirectiveNamespaces),
72

73
                    // Lighthouse defined directives
74 1
                    'Nuwave\\Lighthouse\\Schema\\Directives',
75
                ]))
76 1
                ->flatten()
77 1
                ->filter()
78 1
                ->all();
79
        }
80

81 1
        return $this->directiveNamespaces;
82
    }
83

84
    /**
85
     * Scan the namespaces for directive classes.
86
     *
87
     * @return array<string, class-string<\Nuwave\Lighthouse\Support\Contracts\Directive>>
88
     */
89 1
    public function classes(): array
90
    {
91 1
        $directives = [];
92

93 1
        foreach ($this->namespaces() as $directiveNamespace) {
94
            /** @var array<class-string> $classesInNamespace */
95 1
            $classesInNamespace = ClassFinder::getClassesInNamespace($directiveNamespace);
96

97 1
            foreach ($classesInNamespace as $class) {
98 1
                $reflection = new \ReflectionClass($class);
99 1
                if (! $reflection->isInstantiable()) {
100 1
                    continue;
101
                }
102

103 1
                if (! is_a($class, Directive::class, true)) {
104 1
                    continue;
105
                }
106
                /** @var class-string<\Nuwave\Lighthouse\Support\Contracts\Directive> $class */
107 1
                $name = self::directiveName($class);
108

109
                // The directive was already found, so we do not add it twice
110 1
                if (isset($directives[$name])) {
111 0
                    continue;
112
                }
113

114 1
                $directives[$name] = $class;
115
            }
116
        }
117

118 1
        return $directives;
119
    }
120

121
    /**
122
     * Return the parsed definitions for all directive classes.
123
     *
124
     * @return array<int, \GraphQL\Language\AST\DirectiveDefinitionNode>
125
     */
126 1
    public function definitions(): array
127
    {
128 1
        $definitions = [];
129

130 1
        foreach ($this->classes() as $directiveClass) {
131 1
            $definitions [] = ASTHelper::extractDirectiveDefinition($directiveClass::definition());
132
        }
133

134 1
        return $definitions;
135
    }
136

137
    /**
138
     * Create a directive by the given directive name.
139
     */
140 1
    public function create(string $directiveName): Directive
141
    {
142 1
        $directiveClass = $this->resolve($directiveName);
143

144 1
        return app($directiveClass);
145
    }
146

147
    /**
148
     * Resolve the class for a given directive name.
149
     *
150
     * @return class-string<\Nuwave\Lighthouse\Support\Contracts\Directive>
151
     *
152
     * @throws \Nuwave\Lighthouse\Exceptions\DirectiveException
153
     */
154 1
    public function resolve(string $directiveName): string
155
    {
156
        // Bail to respect the priority of namespaces, the first resolved directive is kept
157 1
        if (array_key_exists($directiveName, $this->resolvedClassnames)) {
158 1
            return $this->resolvedClassnames[$directiveName];
159
        }
160

161 1
        foreach ($this->namespaces() as $baseNamespace) {
162 1
            $directiveClass = $baseNamespace.'\\'.static::className($directiveName);
163

164 1
            if (class_exists($directiveClass)) {
165 1
                if (! is_a($directiveClass, Directive::class, true)) {
166 0
                    throw new DirectiveException("Class $directiveClass must implement the interface ".Directive::class);
167
                }
168
                /** @var class-string<\Nuwave\Lighthouse\Support\Contracts\Directive> $directiveClass */
169 1
                $this->resolvedClassnames[$directiveName] = $directiveClass;
170

171 1
                return $directiveClass;
172
            }
173
        }
174

175 1
        throw new DirectiveException("No directive found for `{$directiveName}`");
176
    }
177

178
    /**
179
     * Returns the expected class name for a directive name.
180
     */
181 1
    protected static function className(string $directiveName): string
182
    {
183 1
        return Str::studly($directiveName).'Directive';
184
    }
185

186
    /**
187
     * Returns the expected directive name for a class name.
188
     */
189 1
    public static function directiveName(string $className): string
190
    {
191 1
        $baseName = basename(str_replace('\\', '/', $className));
192

193 1
        return lcfirst(
194 1
            Str::before($baseName, 'Directive')
195
        );
196
    }
197

198
    /**
199
     * @param  class-string<\Nuwave\Lighthouse\Support\Contracts\Directive>  $directiveClass
200
     * @return $this
201
     */
202 1
    public function setResolved(string $directiveName, string $directiveClass): self
203
    {
204 1
        $this->resolvedClassnames[$directiveName] = $directiveClass;
205

206 1
        return $this;
207
    }
208

209
    /**
210
     * Get all directives that are associated with an AST node.
211
     *
212
     * @return \Illuminate\Support\Collection<\Nuwave\Lighthouse\Support\Contracts\Directive>
213
     */
214 1
    public function associated(Node $node): Collection
215
    {
216 1
        if (! property_exists($node, 'directives')) {
217 0
            throw new Exception('Expected Node class with property `directives`, got: '.get_class($node));
218
        }
219

220 1
        return (new Collection($node->directives))
221
            ->map(function (DirectiveNode $directiveNode) use ($node): Directive {
222 1
                $directive = $this->create($directiveNode->name->value);
223

224 1
                if ($directive instanceof BaseDirective) {
225
                    // @phpstan-ignore-next-line If there were directives on the given Node, it must be of an allowed type
226 1
                    $directive->hydrate($directiveNode, $node);
227
                }
228

229 1
                return $directive;
230 1
            });
231
    }
232

233
    /**
234
     * Get all directives of a certain type that are associated with an AST node.
235
     *
236
     * @return \Illuminate\Support\Collection<\Nuwave\Lighthouse\Support\Contracts\Directive> of type <$directiveClass>
237
     */
238 1
    public function associatedOfType(Node $node, string $directiveClass): Collection
239
    {
240
        return $this
241 1
            ->associated($node)
242 1
            ->filter(Utils::instanceofMatcher($directiveClass));
243
    }
244

245
    /**
246
     * Get a single directive of a type that belongs to an AST node.
247
     *
248
     * Use this for directives types that can only occur once, such as field resolvers.
249
     * This throws if more than one such directive is found.
250
     *
251
     * @param  class-string<\Nuwave\Lighthouse\Support\Contracts\Directive> $directiveClass
252
     *
253
     * @throws \Nuwave\Lighthouse\Exceptions\DirectiveException
254
     */
255 1
    public function exclusiveOfType(Node $node, string $directiveClass): ?Directive
256
    {
257 1
        $directives = $this->associatedOfType($node, $directiveClass);
258

259 1
        if ($directives->count() > 1) {
260
            $directiveNames = $directives
261
                ->map(function (Directive $directive): string {
262 1
                    $definition = ASTHelper::extractDirectiveDefinition(
263 1
                        $directive::definition()
264
                    );
265

266 1
                    return '@'.$definition->name->value;
267 1
                })
268 1
                ->implode(', ');
269

270 1
            if (! property_exists($node, 'name')) {
271 0
                throw new Exception('Expected Node class with property `name`, got: '.get_class($node));
272
            }
273

274 1
            throw new DirectiveException(
275 1
                "Node {$node->name->value} can only have one directive of type {$directiveClass} but found [{$directiveNames}]."
276
            );
277
        }
278

279 1
        return $directives->first();
280
    }
281
}

Read our documentation on viewing source code .

Loading