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

wp-graphql / wp-graphql / 14605898576

22 Apr 2025 10:35PM UTC coverage: 82.671% (+0.06%) from 82.613%
14605898576

push

github

web-flow
Merge pull request #3363 from justlevine/fix/cleanup-model-resolvers

fix: cleanup Model fields for better source-of-truth and type-safety.

555 of 599 new or added lines in 27 files covered. (92.65%)

2 existing lines in 2 files now uncovered.

13916 of 16833 relevant lines covered (82.67%)

300.94 hits per line

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

90.81
/src/Registry/Utils/TermObject.php
1
<?php
2

3
namespace WPGraphQL\Registry\Utils;
4

5
use GraphQL\Type\Definition\ResolveInfo;
6
use WPGraphQL;
7
use WPGraphQL\AppContext;
8
use WPGraphQL\Data\Connection\PostObjectConnectionResolver;
9
use WPGraphQL\Data\Connection\TaxonomyConnectionResolver;
10
use WPGraphQL\Data\Connection\TermObjectConnectionResolver;
11
use WPGraphQL\Model\Term;
12
use WPGraphQL\Type\Connection\PostObjects;
13
use WPGraphQL\Type\Connection\TermObjects;
14
use WP_Taxonomy;
15

16
/**
17
 * Class TermObjectType
18
 *
19
 * @package WPGraphQL\Data
20
 * @since   1.12.0
21
 */
22
class TermObject {
23

24
        /**
25
         * Registers a taxonomy type to the schema as either a GraphQL object, interface, or union.
26
         *
27
         * @param \WP_Taxonomy $tax_object Taxonomy.
28
         *
29
         * @return void
30
         * @throws \Exception
31
         */
32
        public static function register_types( WP_Taxonomy $tax_object ) {
593✔
33
                $single_name = $tax_object->graphql_single_name;
593✔
34

35
                $config = [
593✔
36
                        'description' => ! empty( $tax_object->graphql_description )
593✔
37
                                ? $tax_object->graphql_description
593✔
38
                                : ( ! empty( $tax_object->description )
124✔
39
                                        ? $tax_object->description
×
40
                                        /* translators: taxonomy object singular name w/ description */
124✔
41
                                        : sprintf( __( 'The %s type', 'wp-graphql' ), $single_name )
593✔
42
                                ),
593✔
43
                        'connections' => static::get_connections( $tax_object ),
593✔
44
                        'interfaces'  => static::get_interfaces( $tax_object ),
593✔
45
                        'fields'      => static::get_fields( $tax_object ),
593✔
46
                        'model'       => Term::class,
593✔
47
                ];
593✔
48

49
                // Register as GraphQL objects.
50
                if ( 'object' === $tax_object->graphql_kind ) {
593✔
51
                        register_graphql_object_type( $single_name, $config );
593✔
52
                        return;
593✔
53
                }
54

55
                /**
56
                 * Register as GraphQL interfaces or unions.
57
                 *
58
                 * It's assumed that the types used in `resolveType` have already been registered to the schema.
59
                 */
60

61
                // Bail early if graphql_resolve_type isnt a vallable callback.
62
                if ( empty( $tax_object->graphql_resolve_type ) || ! is_callable( $tax_object->graphql_resolve_type ) ) {
3✔
63
                        graphql_debug(
1✔
64
                                sprintf(
1✔
65
                                        // translators: %1$s is the term object singular name, %2$s is the graphql kind.
66
                                        __( '%1$s is registered as a GraphQL %2$s, but has no way to resolve the type. Ensure "graphql_resolve_type" is a valid callback function', 'wp-graphql' ),
1✔
67
                                        $single_name,
1✔
68
                                        $tax_object->graphql_kind
1✔
69
                                ),
1✔
70
                                [ 'registered_taxonomy_object' => $tax_object ]
1✔
71
                        );
1✔
72

73
                        return;
1✔
74
                }
75

76
                $config['resolveType'] = $tax_object->graphql_resolve_type;
3✔
77

78
                if ( 'interface' === $tax_object->graphql_kind ) {
3✔
79
                        register_graphql_interface_type( $single_name, $config );
1✔
80

81
                        return;
1✔
82
                } elseif ( 'union' === $tax_object->graphql_kind ) {
2✔
83

84
                        // Bail early if graphql_union_types is not defined.
85
                        if ( empty( $tax_object->graphql_union_types ) || ! is_array( $tax_object->graphql_union_types ) ) {
2✔
86
                                graphql_debug(
1✔
87
                                        __( 'Registering a taxonomy with "graphql_kind" => "union" requires "graphql_union_types" to be a valid array of possible GraphQL type names.', 'wp-graphql' ),
1✔
88
                                        [ 'registered_taxonomy_object' => $tax_object ]
1✔
89
                                );
1✔
90

91
                                return;
1✔
92
                        }
93

94
                        // Set the possible types for the union.
95
                        $config['typeNames'] = $tax_object->graphql_union_types;
1✔
96

97
                        register_graphql_union_type( $single_name, $config );
1✔
98
                }
99
        }
100

101
        /**
102
         * Gets all the connections for the given post type.
103
         *
104
         * @param \WP_Taxonomy $tax_object
105
         *
106
         * @return array<string,array<string,mixed>>
107
         */
108
        protected static function get_connections( WP_Taxonomy $tax_object ) {
595✔
109
                $connections = [];
593✔
110

111
                // Taxonomy.
112
                // @todo connection move to TermNode (breaking).
113
                $connections['taxonomy'] = [
593✔
114
                        'toType'   => 'Taxonomy',
593✔
115
                        'oneToOne' => true,
593✔
116
                        'resolve'  => static function ( Term $source, $args, $context, $info ) {
593✔
117
                                if ( empty( $source->taxonomyName ) ) {
7✔
118
                                        return null;
×
119
                                }
120
                                $resolver = new TaxonomyConnectionResolver( $source, $args, $context, $info );
7✔
121
                                $resolver->set_query_arg( 'name', $source->taxonomyName );
7✔
122
                                return $resolver->one_to_one()->get_connection();
7✔
123
                        },
593✔
124
                ];
593✔
125

126
                if ( true === $tax_object->hierarchical ) {
593✔
127
                        // Children.
128
                        $connections['children'] = [
593✔
129
                                'toType'         => $tax_object->graphql_single_name,
593✔
130
                                'description'    => sprintf(
593✔
131
                                        // translators: %1$s is the term object singular name, %2$s is the term object plural name.
132
                                        __( 'Connection between the %1$s type and its children %2$s.', 'wp-graphql' ),
593✔
133
                                        $tax_object->graphql_single_name,
593✔
134
                                        $tax_object->graphql_plural_name
593✔
135
                                ),
593✔
136
                                'connectionArgs' => TermObjects::get_connection_args(),
593✔
137
                                'queryClass'     => 'WP_Term_Query',
593✔
138
                                'resolve'        => static function ( Term $term, $args, AppContext $context, $info ) {
593✔
139
                                        $resolver = new TermObjectConnectionResolver( $term, $args, $context, $info );
2✔
140
                                        $resolver->set_query_arg( 'parent', $term->databaseId );
2✔
141

142
                                        return $resolver->get_connection();
2✔
143
                                },
593✔
144
                        ];
593✔
145

146
                        // Parent.
147
                        $connections['parent'] = [
594✔
148
                                'toType'             => $tax_object->graphql_single_name,
594✔
149
                                'description'        => sprintf(
594✔
150
                                        // translators: %s is the term object singular name.
151
                                        __( 'Connection between the %1$s type and its parent %1$s.', 'wp-graphql' ),
594✔
152
                                        $tax_object->graphql_single_name
594✔
153
                                ),
594✔
154
                                'connectionTypeName' => ucfirst( $tax_object->graphql_single_name ) . 'ToParent' . ucfirst( $tax_object->graphql_single_name ) . 'Connection',
594✔
155
                                'oneToOne'           => true,
594✔
156
                                'resolve'            => static function ( Term $term, $args, AppContext $context, $info ) use ( $tax_object ) {
594✔
157
                                        if ( ! isset( $term->parentDatabaseId ) || empty( $term->parentDatabaseId ) ) {
2✔
158
                                                return null;
×
159
                                        }
160

161
                                        $resolver = new TermObjectConnectionResolver( $term, $args, $context, $info, $tax_object->name );
2✔
162
                                        $resolver->set_query_arg( 'include', $term->parentDatabaseId );
2✔
163

164
                                        return $resolver->one_to_one()->get_connection();
2✔
165
                                },
594✔
166
                        ];
594✔
167

168
                        // Ancestors.
169
                        $connections['ancestors'] = [
595✔
170
                                'toType'             => $tax_object->graphql_single_name,
595✔
171
                                'description'        => __( 'The ancestors of the node. Default ordered as lowest (closest to the child) to highest (closest to the root).', 'wp-graphql' ),
595✔
172
                                'connectionTypeName' => ucfirst( $tax_object->graphql_single_name ) . 'ToAncestors' . ucfirst( $tax_object->graphql_single_name ) . 'Connection',
595✔
173
                                'resolve'            => static function ( Term $term, $args, AppContext $context, $info ) use ( $tax_object ) {
595✔
174
                                        $ancestor_ids = isset( $term->databaseId ) ? get_ancestors( $term->databaseId, (string) $term->taxonomyName, 'taxonomy' ) : null;
2✔
175

176
                                        if ( empty( $ancestor_ids ) ) {
2✔
177
                                                return null;
×
178
                                        }
179

180
                                        $resolver = new TermObjectConnectionResolver( $term, $args, $context, $info, $tax_object->name );
2✔
181
                                        $resolver->set_query_arg( 'include', $ancestor_ids );
2✔
182
                                        $resolver->set_query_arg( 'orderby', 'include' );
2✔
183

184
                                        return $resolver->get_connection();
2✔
185
                                },
595✔
186
                        ];
595✔
187
                }
188

189
                // Used to ensure contentNodes connection doesn't get registered multiple times.
190
                $already_registered = false;
593✔
191
                $allowed_post_types = WPGraphQL::get_allowed_post_types( 'objects' );
593✔
192

193
                foreach ( $allowed_post_types as $post_type_object ) {
593✔
194
                        if ( ! in_array( $tax_object->name, get_object_taxonomies( $post_type_object->name ), true ) ) {
593✔
195
                                continue;
592✔
196
                        }
197

198
                        // ContentNodes.
199
                        if ( ! $already_registered ) {
593✔
200
                                $connections['contentNodes'] = PostObjects::get_connection_config(
593✔
201
                                        $tax_object,
593✔
202
                                        [
593✔
203
                                                'toType'  => 'ContentNode',
593✔
204
                                                'resolve' => static function ( Term $term, $args, $context, $info ) {
593✔
205
                                                        $resolver = new PostObjectConnectionResolver( $term, $args, $context, $info, 'any' );
×
206
                                                        $resolver->set_query_arg(
×
207
                                                                'tax_query',
×
208
                                                                [
×
209
                                                                        [
×
210
                                                                                'taxonomy'         => $term->taxonomyName,
×
NEW
211
                                                                                'terms'            => [ $term->databaseId ],
×
212
                                                                                'field'            => 'term_id',
×
213
                                                                                'include_children' => false,
×
214
                                                                        ],
×
215
                                                                ]
×
216
                                                        );
×
217

218
                                                        return $resolver->get_connection();
×
219
                                                },
593✔
220
                                        ]
593✔
221
                                );
593✔
222

223
                                // We won't need to register this connection again.
224
                                $already_registered = true;
593✔
225
                        }
226

227
                        // PostObjects.
228
                        $connections[ $post_type_object->graphql_plural_name ] = PostObjects::get_connection_config(
595✔
229
                                $post_type_object,
595✔
230
                                [
595✔
231
                                        'toType'     => $post_type_object->graphql_single_name,
595✔
232
                                        'queryClass' => 'WP_Query',
595✔
233
                                        'resolve'    => static function ( Term $term, $args, AppContext $context, ResolveInfo $info ) use ( $post_type_object ) {
595✔
234
                                                $resolver     = new PostObjectConnectionResolver( $term, $args, $context, $info, $post_type_object->name );
4✔
235
                                                $current_args = $resolver->get_query_args();
4✔
236
                                                $tax_query    = $current_args['tax_query'] ?? [];
4✔
237
                                                $tax_query[]  = [
4✔
238
                                                        'taxonomy'         => $term->taxonomyName,
4✔
239
                                                        'terms'            => [ $term->databaseId ],
4✔
240
                                                        'field'            => 'term_id',
4✔
241
                                                        'include_children' => false,
4✔
242
                                                ];
4✔
243
                                                $resolver->set_query_arg( 'tax_query', $tax_query );
4✔
244

245
                                                return $resolver->get_connection();
4✔
246
                                        },
595✔
247
                                ]
595✔
248
                        );
595✔
249
                }
250

251
                // Merge with connections set in register_taxonomy.
252
                if ( ! empty( $tax_object->graphql_connections ) ) {
593✔
253
                        $connections = array_merge( $connections, $tax_object->graphql_connections );
1✔
254
                }
255

256
                // Remove excluded connections.
257
                if ( ! empty( $tax_object->graphql_exclude_connections ) ) {
593✔
258
                        foreach ( $tax_object->graphql_exclude_connections as $connection_name ) {
1✔
259
                                unset( $connections[ lcfirst( $connection_name ) ] );
1✔
260
                        }
261
                }
262

263
                return $connections;
593✔
264
        }
265
        /**
266
         * Gets all the interfaces for the given Taxonomy.
267
         *
268
         * @param \WP_Taxonomy $tax_object Taxonomy.
269
         *
270
         * @return string[]
271
         */
272
        protected static function get_interfaces( WP_Taxonomy $tax_object ) {
593✔
273
                $interfaces = [ 'Node', 'TermNode', 'DatabaseIdentifier' ];
593✔
274

275
                if ( true === $tax_object->public ) {
593✔
276
                        $interfaces[] = 'UniformResourceIdentifiable';
593✔
277
                }
278

279
                if ( $tax_object->hierarchical ) {
593✔
280
                        $interfaces[] = 'HierarchicalTermNode';
593✔
281
                }
282

283
                if ( true === $tax_object->show_in_nav_menus ) {
593✔
284
                        $interfaces[] = 'MenuItemLinkable';
593✔
285
                }
286

287
                // Merge with interfaces set in register_taxonomy.
288
                if ( ! empty( $tax_object->graphql_interfaces ) ) {
593✔
289
                        $interfaces = array_merge( $interfaces, $tax_object->graphql_interfaces );
1✔
290
                }
291

292
                // Remove excluded interfaces.
293
                if ( ! empty( $tax_object->graphql_exclude_interfaces ) ) {
593✔
294
                        $interfaces = array_diff( $interfaces, $tax_object->graphql_exclude_interfaces );
1✔
295
                }
296

297
                return $interfaces;
593✔
298
        }
299

300
        /**
301
         * Registers common Taxonomy fields on schema type corresponding to provided Taxonomy object.
302
         *
303
         * @param \WP_Taxonomy $tax_object Taxonomy.
304
         *
305
         * @return array<string,array<string,mixed>>[]
306
         */
307
        protected static function get_fields( WP_Taxonomy $tax_object ) {
593✔
308
                $single_name = $tax_object->graphql_single_name;
593✔
309
                $fields      = [
593✔
310
                        $single_name . 'Id' => [
593✔
311
                                'type'              => 'Int',
593✔
312
                                'deprecationReason' => __( 'Deprecated in favor of databaseId', 'wp-graphql' ),
593✔
313
                                'description'       => __( 'The id field matches the WP_Post->ID field.', 'wp-graphql' ),
593✔
314
                                'resolve'           => static function ( Term $term ) {
593✔
315
                                        return absint( $term->databaseId );
28✔
316
                                },
593✔
317
                        ],
593✔
318
                ];
593✔
319

320
                // Merge with fields set in register_taxonomy.
321
                if ( ! empty( $tax_object->graphql_fields ) ) {
593✔
322
                        $fields = array_merge( $fields, $tax_object->graphql_fields );
2✔
323
                }
324

325
                // Remove excluded fields.
326
                if ( ! empty( $tax_object->graphql_exclude_fields ) ) {
593✔
327
                        foreach ( $tax_object->graphql_exclude_fields as $field_name ) {
1✔
328
                                unset( $fields[ $field_name ] );
1✔
329
                        }
330
                }
331

332
                return $fields;
593✔
333
        }
334
}
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

© 2026 Coveralls, Inc