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

wp-graphql / wp-graphql / 14716683875

28 Apr 2025 07:58PM UTC coverage: 84.287% (+1.6%) from 82.648%
14716683875

push

github

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

15905 of 18870 relevant lines covered (84.29%)

257.23 hits per line

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

79.58
/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 {
619✔
23
                if ( ! isset( $this->config['interfaces'] ) || ! is_array( $this->config['interfaces'] ) || empty( $this->config['interfaces'] ) ) {
619✔
24
                        $interfaces = parent::getInterfaces();
619✔
25
                } else {
26
                        $interfaces = $this->config['interfaces'];
567✔
27
                }
28

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

38
                if ( empty( $interfaces ) || ! is_array( $interfaces ) ) {
619✔
39
                        return $interfaces;
619✔
40
                }
41

42
                $new_interfaces = [];
567✔
43

44
                foreach ( $interfaces as $interface ) {
567✔
45
                        if ( $interface instanceof InterfaceType && ( $this->config['name'] ?? null ) !== $interface->name ) {
567✔
46
                                $new_interfaces[ $interface->name ] = $interface;
×
47
                                continue;
×
48
                        }
49

50
                        // surface when interfaces are trying to be registered with invalid configuration
51
                        if ( ! is_string( $interface ) ) {
567✔
52
                                graphql_debug(
×
53
                                        sprintf(
×
54
                                                // translators: %s is the name of the GraphQL type.
55
                                                __( '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' ),
×
56
                                                $this->config['name'] ?? 'Unknown'
×
57
                                        ),
×
58
                                        [ 'invalid_interface' => $interface ]
×
59
                                );
×
60
                                continue;
×
61
                        }
62

63
                        // Prevent an interface from implementing itself
64
                        if ( ! empty( $this->config['name'] ) && strtolower( $this->config['name'] ) === strtolower( $interface ) ) {
567✔
65
                                graphql_debug(
1✔
66
                                        sprintf(
1✔
67
                                                // translators: %s is the name of the interface.
68
                                                __( 'The "%s" Interface attempted to implement itself, which is not allowed', 'wp-graphql' ),
1✔
69
                                                $interface
1✔
70
                                        )
1✔
71
                                );
1✔
72
                                continue;
1✔
73
                        }
74

75
                        $interface_type = $this->type_registry->get_type( $interface );
567✔
76
                        if ( ! $interface_type instanceof InterfaceType ) {
567✔
77
                                graphql_debug(
3✔
78
                                        sprintf(
3✔
79
                                                // translators: %1$s is the name of the interface, %2$s is the name of the type.
80
                                                __( '"%1$s" is not a valid Interface Type and cannot be implemented as an Interface on the "%2$s" Type', 'wp-graphql' ),
3✔
81
                                                $interface,
3✔
82
                                                $this->config['name'] ?? 'Unknown'
3✔
83
                                        )
3✔
84
                                );
3✔
85
                                continue;
3✔
86
                        }
87

88
                        $new_interfaces[ $interface ] = $interface_type;
567✔
89
                        $interface_interfaces         = $interface_type->getInterfaces();
567✔
90

91
                        if ( empty( $interface_interfaces ) ) {
567✔
92
                                continue;
567✔
93
                        }
94

95
                        foreach ( $interface_interfaces as $interface_interface_name => $interface_interface ) {
559✔
96
                                if ( ! $interface_interface instanceof InterfaceType ) {
559✔
97
                                        continue;
×
98
                                }
99

100
                                $new_interfaces[ $interface_interface_name ] = $interface_interface;
559✔
101
                        }
102
                }
103

104
                return array_unique( $new_interfaces );
567✔
105
        }
106

107

108
        /**
109
         * Returns the fields for a Type, applying any missing fields defined on interfaces implemented on the type
110
         *
111
         * @param array<mixed>                     $config
112
         * @param \WPGraphQL\Registry\TypeRegistry $type_registry
113
         *
114
         * @return array<mixed>
115
         * @throws \Exception
116
         */
117
        protected function get_fields( array $config, TypeRegistry $type_registry ): array {
604✔
118

119
                if ( is_callable( $config['fields'] ) ) {
604✔
120
                        $config['fields'] = $config['fields']();
600✔
121
                }
122

123
                $fields = array_filter( $config['fields'] );
604✔
124

125
                /**
126
                 * Get the fields of interfaces and ensure they exist as fields of this type.
127
                 *
128
                 * Types are still responsible for ensuring the fields resolve properly.
129
                 */
130
                $interface_fields = $this->get_fields_from_implemented_interfaces( $type_registry );
604✔
131

132
                // Merge fields with interface fields that are not already in fields
133
                $fields = array_merge( $fields, array_diff_key( $interface_fields, $fields ) );
604✔
134

135
                foreach ( $fields as $field_name => $field ) {
604✔
136
                        $merged_field_config = $this->inherit_field_config_from_interface( $field_name, $field, $interface_fields );
604✔
137

138
                        if ( null === $merged_field_config ) {
604✔
139
                                unset( $fields[ $field_name ] );
×
140
                                continue;
×
141
                        }
142

143
                        // Update the field.
144
                        $fields[ $field_name ] = $merged_field_config;
604✔
145
                }
146

147
                $fields = $this->prepare_fields( $fields, $config['name'], $config );
604✔
148
                $fields = $type_registry->prepare_fields( $fields, $config['name'] );
604✔
149

150
                $this->fields = $fields;
604✔
151
                return $this->fields;
604✔
152
        }
153

154
        /**
155
         * Get the fields from the implemented interfaces.
156
         *
157
         * @param \WPGraphQL\Registry\TypeRegistry $registry The TypeRegistry instance.
158
         *
159
         * @return array<string,array<string,mixed>>
160
         */
161
        private function get_fields_from_implemented_interfaces( TypeRegistry $registry ): array {
604✔
162
                $interface_fields = [];
604✔
163

164
                $interfaces = $this->getInterfaces();
604✔
165

166
                // Get the fields for each interface.
167
                foreach ( $interfaces as $interface_type ) {
604✔
168
                        // Get the resolved InterfaceType instance, if it's not already an instance of InterfaceType.
169
                        if ( ! $interface_type instanceof InterfaceType ) {
567✔
170
                                $interface_type = $registry->get_type( $interface_type );
×
171
                        }
172

173
                        if ( ! $interface_type instanceof InterfaceType ) {
567✔
174
                                continue;
×
175
                        }
176

177
                        $interface_config_fields = $interface_type->getFields();
567✔
178

179
                        if ( empty( $interface_config_fields ) ) {
567✔
180
                                continue;
×
181
                        }
182

183
                        foreach ( $interface_config_fields as $interface_field_name => $interface_field ) {
567✔
184
                                $interface_fields[ $interface_field_name ] = $interface_field->config;
567✔
185
                        }
186
                }
187

188
                return $interface_fields;
604✔
189
        }
190

191
        /**
192
         * Inherit missing field configs from the interface.
193
         *
194
         * @param string                            $field_name The field name.
195
         * @param array<string,mixed>               $field The field config.
196
         * @param array<string,array<string,mixed>> $interface_fields The fields from the interface. This is passed by reference.
197
         *
198
         * @return ?array<string,mixed> The field config with inherited values. Null if the field type cannot be determined.
199
         */
200
        private function inherit_field_config_from_interface( string $field_name, array $field, array $interface_fields ): ?array {
604✔
201

202
                $interface_field = $interface_fields[ $field_name ] ?? [];
604✔
203

204
                // Bail early, if there is no field type or an interface to inherit it from since we won't be able to register it.
205
                if ( empty( $field['type'] ) && empty( $interface_field['type'] ) ) {
604✔
206
                        graphql_debug(
×
207
                                sprintf(
×
208
                                        // translators: %1$s is the field name, %2$s is the type name.
209
                                        __( 'Invalid Interface field %1$s registered to the "%2$s" Type. Fields must be registered a valid GraphQL `type`.', 'wp-graphql' ),
×
210
                                        $field_name,
×
211
                                        $this->config['name'] ?? 'Unknown'
×
212
                                )
×
213
                        );
×
214

215
                        return null;
×
216
                }
217

218
                // Inherit the field config from the interface if it's not set on the field.
219
                foreach ( $interface_field as $key => $config ) {
604✔
220
                        // Inherit the field config from the interface if it's not set on the field.
221
                        if ( empty( $field[ $key ] ) ) {
567✔
222
                                $field[ $key ] = $config;
566✔
223
                                continue;
566✔
224
                        }
225

226
                        // If the args on both the field and the interface are set, we need to merge them.
227
                        if ( 'args' === $key ) {
567✔
228
                                $field[ $key ] = $this->merge_field_args( $field_name, $field[ $key ], $interface_field[ $key ] );
402✔
229
                        }
230
                }
231

232
                return $field;
604✔
233
        }
234

235
        /**
236
         * Merge the field args from the field and the interface.
237
         *
238
         * @param string                            $field_name The field name.
239
         * @param array<string,array<string,mixed>> $field_args The field args.
240
         * @param array<string,array<string,mixed>> $interface_args The interface args.
241
         *
242
         * @return array<string,array<string,mixed>> The merged field args.
243
         */
244
        private function merge_field_args( string $field_name, array $field_args, array $interface_args ): array {
402✔
245
                // We use the interface args as the base and overwrite them with the field args.
246
                $merged_args = $interface_args;
402✔
247

248
                foreach ( $field_args as $arg_name => $config ) {
402✔
249
                        // If the arg is not defined on the interface, we can use the field arg config.
250
                        if ( empty( $merged_args[ $arg_name ] ) ) {
402✔
251
                                $merged_args[ $arg_name ] = $config;
1✔
252
                                continue;
1✔
253
                        }
254

255
                        // Check if the interface arg type is different from the new field arg type.
256
                        $field_arg_type     = $this->normalize_type_name( $config['type'] );
402✔
257
                        $interface_arg_type = $this->normalize_type_name( $merged_args[ $arg_name ]['type'] );
402✔
258

259
                        if ( ! empty( $field_arg_type ) && $interface_arg_type !== $field_arg_type ) {
402✔
260
                                graphql_debug(
1✔
261
                                        sprintf(
1✔
262
                                                /* translators: 1: Object type name, 2: Field name, 3: Argument name, 4: Expected argument type, 5: Actual argument type. */
263
                                                __(
1✔
264
                                                        '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.',
1✔
265
                                                        'wp-graphql'
1✔
266
                                                ),
1✔
267
                                                $this->config['name'] ?? 'Unknown',
1✔
268
                                                $field_name,
1✔
269
                                                $arg_name,
1✔
270
                                                $interface_arg_type,
1✔
271
                                                $field_arg_type
1✔
272
                                        )
1✔
273
                                );
1✔
274
                                continue;
1✔
275
                        }
276

277
                        // Merge the field arg config with the interface arg config.
278
                        $merged_args[ $arg_name ] = array_merge( $merged_args[ $arg_name ], $config );
401✔
279
                }
280

281
                return $merged_args;
402✔
282
        }
283

284
        /**
285
         * Given a type it will return a string representation of the type.
286
         *
287
         * This is used for optimistic comparison of the arg types.
288
         *
289
         * @param string|array<string,mixed>|callable|\GraphQL\Type\Definition\Type $type The type to normalize.
290
         */
291
        private function normalize_type_name( $type ): string {
402✔
292
                // Bail early if the type is empty.
293
                if ( empty( $type ) ) {
402✔
294
                        return '';
×
295
                }
296

297
                // If the type is a callable, we need to resolve it.
298
                if ( is_callable( $type ) ) {
402✔
299
                        $type = $type();
402✔
300
                }
301

302
                // If the type is an instance of a Type, we can get the name.
303
                if ( $type instanceof \GraphQL\Type\Definition\Type ) {
402✔
304
                        $type = $type->name ?? $type->toString();
402✔
305
                }
306

307
                // If the type is *now* a string, we can return it.
308
                if ( is_string( $type ) ) {
402✔
309
                        return $type;
402✔
310
                } elseif ( ! is_array( $type ) ) {
1✔
311
                        // If the type is not an array, we can't do anything with it.
312
                        return '';
×
313
                }
314

315
                // Arrays mean the type can be nested in modifiers.
316
                $output   = '';
1✔
317
                $modifier = array_keys( $type )[0];
1✔
318
                $type     = $type[ $modifier ];
1✔
319

320
                // Convert the type wrappers to a string, and recursively get the internals.
321
                switch ( $modifier ) {
322
                        case 'list_of':
1✔
323
                                $output = '[' . $this->normalize_type_name( $type ) . ']';
1✔
324
                                break;
1✔
325
                        case 'non_null':
×
326
                                $output = '!' . $this->normalize_type_name( $type );
×
327
                                break;
×
328
                }
329

330
                return $output;
1✔
331
        }
332
}
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