• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wp-graphql / wp-graphql / 15710056976

17 Jun 2025 02:27PM UTC coverage: 84.17% (-0.1%) from 84.287%
15710056976

push

github

actions-user
release: merge develop into master for v2.3.3

15925 of 18920 relevant lines covered (84.17%)

258.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.66
/src/Type/WPInterfaceTrait.php
1
<?php
2
namespace WPGraphQL\Type;
3

4
use GraphQL\Type\Definition\InterfaceType;
5
use WPGraphQL\Registry\TypeRegistry;
6

7
/**
8
 * Trait WPInterfaceTrait
9
 *
10
 * This Trait includes methods to help Interfaces and ObjectTypes ensure they implement
11
 * the proper inherited interfaces
12
 *
13
 * @package WPGraphQL\Type
14
 */
15
trait WPInterfaceTrait {
16

17
        /**
18
         * Given an array of interfaces, this gets the Interfaces the Type should implement including inherited interfaces.
19
         *
20
         * @return \GraphQL\Type\Definition\InterfaceType[]
21
         */
22
        protected function get_implemented_interfaces(): array {
625✔
23
                $interfaces = ! empty( $this->config['interfaces'] ) && is_array( $this->config['interfaces'] ) ? $this->config['interfaces'] : null;
625✔
24

25
                if ( null === $interfaces ) {
625✔
26
                        // If no interfaces are explicitly defined, fall back to the underlying class.
27
                        $interfaces = parent::getInterfaces();
625✔
28
                }
29

30
                /**
31
                 * Filters the interfaces applied to an object type
32
                 *
33
                 * @param string[]                   $interfaces     List of interfaces applied to the Object Type
34
                 * @param array<string,mixed>        $config         The config for the Object Type
35
                 * @param mixed|\WPGraphQL\Type\WPInterfaceType|\WPGraphQL\Type\WPObjectType $type The Type instance
36
                 */
37
                $interfaces = apply_filters( 'graphql_type_interfaces', $interfaces, $this->config, $this );
625✔
38

39
                // If the filter gets rid of valid interfaces, we should return an empty array.
40
                if ( empty( $interfaces ) || ! is_array( $interfaces ) ) {
625✔
41
                        return [];
625✔
42
                }
43

44
                $implemented_interfaces = [];
547✔
45
                $implementing_type_name = $this->config['name'] ?? 'Unknown';
547✔
46

47
                foreach ( $interfaces as $maybe_interface ) {
547✔
48
                        $interface = $this->maybe_resolve_interface( $maybe_interface, $implementing_type_name );
547✔
49
                        // Skip invalid interfaces.
50
                        if ( null === $interface ) {
547✔
51
                                continue;
4✔
52
                        }
53

54
                        $implemented_interfaces[ $interface->name ] = $interface;
547✔
55

56
                        // Add interfaces implemented by this interface and their ancestors
57
                        $this->resolve_inherited_interfaces( $interface, $implemented_interfaces );
547✔
58
                }
59

60
                // We use array_unique as a final safeguard against duplicate entries.
61
                // While we're already using interface names as array keys which generally prevents duplicates,
62
                // this provides an extra layer of protection for edge cases or future modifications.
63
                return array_unique( $implemented_interfaces );
547✔
64
        }
65

66
        /**
67
         * Resolves a single interface configuration entry to an InterfaceType instance.
68
         * Handles validation and debugging messages, using early returns for clarity.
69
         *
70
         * @param mixed  $type The interface entry from the config (string name or InterfaceType instance).
71
         * @param string $implementing_type_name The name of the type that is implementing this interface (for debug messages and self-implementation check).
72
         * @return \GraphQL\Type\Definition\InterfaceType|null The resolved InterfaceType or null if invalid/skipped.
73
         */
74
        private function maybe_resolve_interface( $type, string $implementing_type_name ): ?InterfaceType {
547✔
75
                $type_name = $type instanceof InterfaceType ? $type->name : $type;
547✔
76

77
                // Bail if the entry is trying to implement itself.
78
                if ( ! empty( $implementing_type_name ) && strtolower( $implementing_type_name ) === strtolower( $type_name ) ) {
547✔
79
                        graphql_debug(
1✔
80
                                sprintf(
1✔
81
                                        // translators: %s is the name of the interface.
82
                                        __( 'The "%s" Interface attempted to implement itself, which is not allowed', 'wp-graphql' ),
1✔
83
                                        $type_name
1✔
84
                                )
1✔
85
                        );
1✔
86
                        return null;
1✔
87
                }
88

89
                // Return early if it's already a valid interface type
90
                if ( $type instanceof InterfaceType ) {
547✔
91
                        return $type;
×
92
                }
93

94
                // If it's not a string, we won't be able to resolve it.
95
                if ( ! is_string( $type ) ) {
547✔
96
                        graphql_debug(
×
97
                                sprintf(
×
98
                                        // translators: %s is the name of the GraphQL type.
99
                                        __( 'Invalid Interface registered to the "%s" Type. Interfaces can only be registered with an interface name or a valid instance of an InterfaceType', 'wp-graphql' ),
×
100
                                        $implementing_type_name
×
101
                                ),
×
102
                                [ 'invalid_interface' => $type ]
×
103
                        );
×
104
                        return null;
×
105
                }
106

107
                // Attempt to resolve the string to a type.
108
                $resolved_type = $this->type_registry->get_type( $type );
547✔
109

110
                // Check if the resolved type is a valid InterfaceType.
111
                if ( ! $resolved_type instanceof InterfaceType ) {
547✔
112
                        graphql_debug(
3✔
113
                                sprintf(
3✔
114
                                        // translators: %1$s is the name of the interface, %2$s is the name of the type.
115
                                        __( '"%1$s" is not a valid Interface Type and cannot be implemented as an Interface on the "%2$s" Type', 'wp-graphql' ),
3✔
116
                                        $type,
3✔
117
                                        $implementing_type_name
3✔
118
                                )
3✔
119
                        );
3✔
120
                        return null;
3✔
121
                }
122

123
                return $resolved_type;
547✔
124
        }
125

126
        /**
127
         * Adds interfaces implemented by the given InterfaceType to the target array.
128
         * Handles recursive collection of interfaces, avoiding duplicates.
129
         *
130
         * @param \GraphQL\Type\Definition\InterfaceType                $interface_type     The interface whose implemented interfaces should be added.
131
         * @param array<string, \GraphQL\Type\Definition\InterfaceType> &$target_interfaces The array to add interfaces to (passed by reference).
132
         *
133
         * @@param-out array<string, \GraphQL\Type\Definition\InterfaceType> $target_interfaces The array to add interfaces to (passed by reference).
134
         */
135
        private function resolve_inherited_interfaces( InterfaceType $interface_type, array &$target_interfaces ): void {
547✔
136
                // Get interfaces implemented by this interface.
137
                $interfaces = $interface_type->getInterfaces();
547✔
138

139
                if ( empty( $interfaces ) ) {
547✔
140
                        return;
547✔
141
                }
142

143
                foreach ( $interfaces as $child_interface ) {
513✔
144
                        // Skip invalid interface entries
145
                        if ( ! $child_interface instanceof InterfaceType ) {
513✔
146
                                continue;
×
147
                        }
148

149
                        // Skip if the interface is already in the target array
150
                        if ( isset( $target_interfaces[ $child_interface->name ] ) ) {
513✔
151
                                continue;
489✔
152
                        }
153

154
                        // Add the interface to our collection, keyed by name
155
                        $target_interfaces[ $child_interface->name ] = $child_interface;
504✔
156

157
                        // Recursively add interfaces from the child interface
158
                        $this->resolve_inherited_interfaces( $child_interface, $target_interfaces );
504✔
159
                }
160
        }
161

162
        /**
163
         * Returns the fields for a Type, applying any missing fields defined on interfaces implemented on the type
164
         *
165
         * @param array<string,mixed>              $config
166
         * @param \WPGraphQL\Registry\TypeRegistry $type_registry
167
         *
168
         * @return array<string, array<string,mixed>>
169
         * @throws \Exception
170
         */
171
        protected function get_fields( array $config, TypeRegistry $type_registry ): array {
610✔
172

173
                if ( is_callable( $config['fields'] ) ) {
610✔
174
                        $config['fields'] = $config['fields']();
605✔
175
                }
176

177
                $fields = array_filter( $config['fields'] );
610✔
178

179
                /**
180
                 * Get the fields of interfaces and ensure they exist as fields of this type.
181
                 *
182
                 * Types are still responsible for ensuring the fields resolve properly.
183
                 */
184
                $interface_fields = $this->get_fields_from_implemented_interfaces( $type_registry );
610✔
185

186
                // Merge fields with interface fields that are not already in fields
187
                $fields = array_merge( $fields, array_diff_key( $interface_fields, $fields ) );
610✔
188

189
                foreach ( $fields as $field_name => $field ) {
610✔
190
                        $merged_field_config = $this->inherit_field_config_from_interface( $field_name, $field, $interface_fields );
610✔
191

192
                        if ( null === $merged_field_config ) {
610✔
193
                                unset( $fields[ $field_name ] );
×
194
                                continue;
×
195
                        }
196

197
                        // Update the field.
198
                        $fields[ $field_name ] = $merged_field_config;
610✔
199
                }
200

201
                $fields = $this->prepare_fields( $fields, $config['name'], $config );
610✔
202
                $fields = $type_registry->prepare_fields( $fields, $config['name'] );
610✔
203

204
                $this->fields = $fields;
610✔
205
                return $this->fields;
610✔
206
        }
207

208
        /**
209
         * Get the fields from the implemented interfaces.
210
         *
211
         * @param \WPGraphQL\Registry\TypeRegistry $registry The TypeRegistry instance.
212
         *
213
         * @return array<string,array<string,mixed>>
214
         */
215
        private function get_fields_from_implemented_interfaces( TypeRegistry $registry ): array {
610✔
216
                $interface_fields = [];
610✔
217
                $interfaces       = $this->getInterfaces();
610✔
218

219
                // Get the fields for each interface.
220
                foreach ( $interfaces as $interface_type ) {
610✔
221
                        // Get the resolved InterfaceType instance, if it's not already an instance of InterfaceType.
222
                        if ( ! $interface_type instanceof InterfaceType ) {
547✔
223
                                $interface_type = $registry->get_type( $interface_type );
×
224
                        }
225

226
                        if ( ! $interface_type instanceof InterfaceType ) {
547✔
227
                                continue;
×
228
                        }
229

230
                        $interface_config_fields = $interface_type->getFields();
547✔
231

232
                        if ( empty( $interface_config_fields ) ) {
547✔
233
                                continue;
×
234
                        }
235

236
                        foreach ( $interface_config_fields as $interface_field_name => $interface_field ) {
547✔
237
                                $interface_fields[ $interface_field_name ] = $interface_field->config;
547✔
238
                        }
239
                }
240

241
                return $interface_fields;
610✔
242
        }
243

244
        /**
245
         * Inherit missing field configs from the interface.
246
         *
247
         * @param string                            $field_name       The field name.
248
         * @param array<string,mixed>               $field            The field config.
249
         * @param array<string,array<string,mixed>> $interface_fields The fields from the interface.
250
         *
251
         * @return ?array<string,mixed> The field config with inherited values. Null if the field type cannot be determined.
252
         */
253
        private function inherit_field_config_from_interface( string $field_name, array $field, array $interface_fields ): ?array {
610✔
254

255
                $interface_field = $interface_fields[ $field_name ] ?? [];
610✔
256

257
                // Return early if neither field nor interface type is defined, as registration cannot proceed.
258
                if ( empty( $field['type'] ) && empty( $interface_field['type'] ) ) {
610✔
259
                        graphql_debug(
×
260
                                sprintf(
×
261
                                        // translators: %1$s is the field name, %2$s is the type name.
262
                                        __( 'Invalid Interface field %1$s registered to the "%2$s" Type. Fields must be registered a valid GraphQL `type`.', 'wp-graphql' ),
×
263
                                        $field_name,
×
264
                                        $this->config['name'] ?? 'Unknown'
×
265
                                )
×
266
                        );
×
267

268
                        return null;
×
269
                }
270

271
                // Inherit the field config from the interface if it's not set on the field.
272
                foreach ( $interface_field as $key => $config ) {
610✔
273
                        // Inherit the field config from the interface if it's not set on the field.
274
                        if ( empty( $field[ $key ] ) ) {
547✔
275
                                $field[ $key ] = $config;
541✔
276
                                continue;
541✔
277
                        }
278

279
                        // If the args on both the field and the interface are set, we need to merge them.
280
                        if ( 'args' === $key ) {
547✔
281
                                $field[ $key ] = $this->merge_field_args( $field_name, $field[ $key ], $interface_field[ $key ] );
406✔
282
                        }
283
                }
284

285
                return $field;
610✔
286
        }
287

288
        /**
289
         * Merge the field args from the field and the interface.
290
         *
291
         * @param string                            $field_name     The field name.
292
         * @param array<string,array<string,mixed>> $field_args     The field args.
293
         * @param array<string,array<string,mixed>> $interface_args The interface args.
294
         *
295
         * @return array<string,array<string,mixed>>
296
         */
297
        private function merge_field_args( string $field_name, array $field_args, array $interface_args ): array {
406✔
298
                // We use the interface args as the base and overwrite them with the field args.
299
                $merged_args = $interface_args;
406✔
300

301
                foreach ( $field_args as $arg_name => $config ) {
406✔
302
                        // If the arg is not defined on the interface, simply add it from the field.
303
                        if ( empty( $merged_args[ $arg_name ] ) ) {
406✔
304
                                $merged_args[ $arg_name ] = $config;
1✔
305
                                continue;
1✔
306
                        }
307

308
                        // Check if the interface arg type is different from the new field arg type.
309
                        $field_arg_type     = $this->normalize_type_name( $config['type'] );
406✔
310
                        $interface_arg_type = $this->normalize_type_name( $merged_args[ $arg_name ]['type'] );
406✔
311

312
                        // Log a message and skip the arg if types are incompatible
313
                        if ( ! empty( $field_arg_type ) && $interface_arg_type !== $field_arg_type ) {
406✔
314
                                graphql_debug(
5✔
315
                                        sprintf(
5✔
316
                                                /* translators: 1: Object type name, 2: Field name, 3: Argument name, 4: Expected argument type, 5: Actual argument type. */
317
                                                __(
5✔
318
                                                        'Interface field argument "%1$s.%2$s(%3$s:)" expected to be of type "%4$s" but got "%5$s". Please ensure the field arguments match the interface field arguments or rename the argument.',
5✔
319
                                                        'wp-graphql'
5✔
320
                                                ),
5✔
321
                                                $this->config['name'] ?? 'Unknown',
5✔
322
                                                $field_name,
5✔
323
                                                $arg_name,
5✔
324
                                                $interface_arg_type,
5✔
325
                                                $field_arg_type
5✔
326
                                        )
5✔
327
                                );
5✔
328
                                continue;
5✔
329
                        }
330

331
                        // Merge the field arg config with the interface arg config.
332
                        $merged_args[ $arg_name ] = array_merge( $merged_args[ $arg_name ], $config );
402✔
333
                }
334

335
                return $merged_args;
406✔
336
        }
337

338
        /**
339
         * Given a type it will return a string representation of the type.
340
         *
341
         * This is used for optimistic comparison of the arg types using strings.
342
         *
343
         * @param string|array<string,mixed>|callable|\GraphQL\Type\Definition\Type $type The type to normalize.
344
         */
345
        private function normalize_type_name( $type ): string {
406✔
346
                // Bail early if the type is empty.
347
                if ( empty( $type ) ) {
406✔
348
                        return '';
×
349
                }
350

351
                // If the type is a callable, we need to resolve it.
352
                if ( is_callable( $type ) ) {
406✔
353
                        $type = $type();
406✔
354
                }
355

356
                // If the type is an instance of a Type, we can get the name.
357
                if ( $type instanceof \GraphQL\Type\Definition\Type ) {
406✔
358
                        $type = $type->name ?? $type->toString();
406✔
359
                }
360

361
                // If the type is *now* a string, we can return it.
362
                if ( is_string( $type ) ) {
406✔
363
                        return $type;
406✔
364
                } elseif ( ! is_array( $type ) ) {
3✔
365
                        // If the type is not an array, we can't do anything with it.
366
                        return '';
×
367
                }
368

369
                // Arrays mean the type can be nested in modifiers.
370
                $output   = '';
3✔
371
                $modifier = array_keys( $type )[0];
3✔
372
                $type     = $type[ $modifier ];
3✔
373

374
                // Convert the type wrappers to a string, and recursively get the internals.
375
                switch ( $modifier ) {
376
                        case 'list_of':
3✔
377
                                $output = '[' . $this->normalize_type_name( $type ) . ']';
2✔
378
                                break;
2✔
379
                        case 'non_null':
2✔
380
                                $output = '!' . $this->normalize_type_name( $type );
2✔
381
                                break;
2✔
382
                }
383

384
                return $output;
3✔
385
        }
386
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc