• 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

92.57
/src/Mutation/PostObjectCreate.php
1
<?php
2

3
namespace WPGraphQL\Mutation;
4

5
use GraphQL\Error\UserError;
6
use GraphQL\Type\Definition\ResolveInfo;
7
use WPGraphQL\AppContext;
8
use WPGraphQL\Data\PostObjectMutation;
9
use WPGraphQL\Utils\Utils;
10
use WP_Post_Type;
11

12
/**
13
 * Class PostObjectCreate
14
 *
15
 * @package WPGraphQL\Mutation
16
 */
17
class PostObjectCreate {
18
        /**
19
         * Registers the PostObjectCreate mutation.
20
         *
21
         * @param \WP_Post_Type $post_type_object The post type of the mutation.
22
         *
23
         * @return void
24
         */
25
        public static function register_mutation( WP_Post_Type $post_type_object ) {
593✔
26
                $mutation_name = 'create' . ucwords( $post_type_object->graphql_single_name );
593✔
27

28
                register_graphql_mutation(
593✔
29
                        $mutation_name,
593✔
30
                        [
593✔
31
                                'inputFields'         => self::get_input_fields( $post_type_object ),
593✔
32
                                'outputFields'        => self::get_output_fields( $post_type_object ),
593✔
33
                                'mutateAndGetPayload' => self::mutate_and_get_payload( $post_type_object, $mutation_name ),
593✔
34
                        ]
593✔
35
                );
593✔
36
        }
37

38
        /**
39
         * Defines the mutation input field configuration.
40
         *
41
         * @param \WP_Post_Type $post_type_object The post type of the mutation.
42
         *
43
         * @return array<string,array<string,mixed>>
44
         */
45
        public static function get_input_fields( $post_type_object ) {
593✔
46
                $fields = [
593✔
47
                        'date'      => [
593✔
48
                                'type'        => 'String',
593✔
49
                                'description' => static function () {
593✔
50
                                        return __( 'The date of the object. Preferable to enter as year/month/day (e.g. 01/31/2017) as it will rearrange date as fit if it is not specified. Incomplete dates may have unintended results for example, "2017" as the input will use current date with timestamp 20:17 ', 'wp-graphql' );
15✔
51
                                },
593✔
52
                        ],
593✔
53
                        'menuOrder' => [
593✔
54
                                'type'        => 'Int',
593✔
55
                                'description' => static function () {
593✔
56
                                        return __( 'A field used for ordering posts. This is typically used with nav menu items or for special ordering of hierarchical content types.', 'wp-graphql' );
15✔
57
                                },
593✔
58
                        ],
593✔
59
                        'password'  => [
593✔
60
                                'type'        => 'String',
593✔
61
                                'description' => static function () {
593✔
62
                                        return __( 'The password used to protect the content of the object', 'wp-graphql' );
15✔
63
                                },
593✔
64
                        ],
593✔
65
                        'slug'      => [
593✔
66
                                'type'        => 'String',
593✔
67
                                'description' => static function () {
593✔
68
                                        return __( 'The slug of the object', 'wp-graphql' );
15✔
69
                                },
593✔
70
                        ],
593✔
71
                        'status'    => [
593✔
72
                                'type'        => 'PostStatusEnum',
593✔
73
                                'description' => static function () {
593✔
74
                                        return __( 'The status of the object', 'wp-graphql' );
15✔
75
                                },
593✔
76
                        ],
593✔
77
                ];
593✔
78

79
                if ( post_type_supports( $post_type_object->name, 'author' ) ) {
593✔
80
                        $fields['authorId'] = [
593✔
81
                                'type'        => 'ID',
593✔
82
                                'description' => static function () {
593✔
83
                                        return __( 'The userId to assign as the author of the object', 'wp-graphql' );
15✔
84
                                },
593✔
85
                        ];
593✔
86
                }
87

88
                if ( post_type_supports( $post_type_object->name, 'comments' ) ) {
593✔
89
                        $fields['commentStatus'] = [
593✔
90
                                'type'        => 'String',
593✔
91
                                'description' => static function () {
593✔
92
                                        return __( 'The comment status for the object', 'wp-graphql' );
15✔
93
                                },
593✔
94
                        ];
593✔
95
                }
96

97
                if ( post_type_supports( $post_type_object->name, 'editor' ) ) {
593✔
98
                        $fields['content'] = [
593✔
99
                                'type'        => 'String',
593✔
100
                                'description' => static function () {
593✔
101
                                        return __( 'The content of the object', 'wp-graphql' );
15✔
102
                                },
593✔
103
                        ];
593✔
104
                }
105

106
                if ( post_type_supports( $post_type_object->name, 'excerpt' ) ) {
593✔
107
                        $fields['excerpt'] = [
593✔
108
                                'type'        => 'String',
593✔
109
                                'description' => static function () {
593✔
110
                                        return __( 'The excerpt of the object', 'wp-graphql' );
15✔
111
                                },
593✔
112
                        ];
593✔
113
                }
114

115
                if ( post_type_supports( $post_type_object->name, 'title' ) ) {
593✔
116
                        $fields['title'] = [
593✔
117
                                'type'        => 'String',
593✔
118
                                'description' => static function () {
593✔
119
                                        return __( 'The title of the object', 'wp-graphql' );
15✔
120
                                },
593✔
121
                        ];
593✔
122
                }
123

124
                if ( post_type_supports( $post_type_object->name, 'trackbacks' ) ) {
593✔
125
                        $fields['pinged'] = [
593✔
126
                                'type'        => [
593✔
127
                                        'list_of' => 'String',
593✔
128
                                ],
593✔
129
                                'description' => static function () {
593✔
130
                                        return __( 'URLs that have been pinged.', 'wp-graphql' );
15✔
131
                                },
593✔
132
                        ];
593✔
133

134
                        $fields['pingStatus'] = [
593✔
135
                                'type'        => 'String',
593✔
136
                                'description' => static function () {
593✔
137
                                        return __( 'The ping status for the object', 'wp-graphql' );
15✔
138
                                },
593✔
139
                        ];
593✔
140

141
                        $fields['toPing'] = [
593✔
142
                                'type'        => [
593✔
143
                                        'list_of' => 'String',
593✔
144
                                ],
593✔
145
                                'description' => static function () {
593✔
146
                                        return __( 'URLs queued to be pinged.', 'wp-graphql' );
15✔
147
                                },
593✔
148
                        ];
593✔
149
                }
150

151
                if ( $post_type_object->hierarchical || in_array(
593✔
152
                        $post_type_object->name,
593✔
153
                        [
593✔
154
                                'attachment',
593✔
155
                                'revision',
593✔
156
                        ],
593✔
157
                        true
593✔
158
                ) ) {
593✔
159
                        $fields['parentId'] = [
592✔
160
                                'type'        => 'ID',
592✔
161
                                'description' => static function () {
592✔
162
                                        return __( 'The ID of the parent object', 'wp-graphql' );
15✔
163
                                },
592✔
164
                        ];
592✔
165
                }
166

167
                if ( 'attachment' === $post_type_object->name ) {
593✔
168
                        $fields['mimeType'] = [
×
169
                                'type'        => 'MimeTypeEnum',
×
170
                                'description' => static function () {
×
171
                                        return __( 'If the post is an attachment or a media file, this field will carry the corresponding MIME type. This field is equivalent to the value of WP_Post->post_mime_type and the post_mime_type column in the "post_objects" database table.', 'wp-graphql' );
×
172
                                },
×
173
                        ];
×
174
                }
175

176
                $allowed_taxonomies = \WPGraphQL::get_allowed_taxonomies( 'objects' );
593✔
177

178
                foreach ( $allowed_taxonomies as $tax_object ) {
593✔
179
                        // If the taxonomy is in the array of taxonomies registered to the post_type
180
                        if ( in_array( $tax_object->name, get_object_taxonomies( $post_type_object->name ), true ) ) {
593✔
181
                                $fields[ $tax_object->graphql_plural_name ] = [
593✔
182
                                        'description' => static function () use ( $post_type_object, $tax_object ) {
593✔
183
                                                return sprintf(
15✔
184
                                                        // translators: %1$s is the post type GraphQL name, %2$s is the taxonomy GraphQL name.
185
                                                        __( 'Set connections between the %1$s and %2$s', 'wp-graphql' ),
15✔
186
                                                        $post_type_object->graphql_single_name,
15✔
187
                                                        $tax_object->graphql_plural_name
15✔
188
                                                );
15✔
189
                                        },
593✔
190
                                        'type'        => ucfirst( $post_type_object->graphql_single_name ) . ucfirst( $tax_object->graphql_plural_name ) . 'Input',
593✔
191
                                ];
593✔
192
                        }
193
                }
194

195
                return $fields;
593✔
196
        }
197

198
        /**
199
         * Defines the mutation output field configuration.
200
         *
201
         * @param \WP_Post_Type $post_type_object The post type of the mutation.
202
         *
203
         * @return array<string,array<string,mixed>>
204
         */
205
        public static function get_output_fields( WP_Post_Type $post_type_object ) {
593✔
206
                return [
593✔
207
                        $post_type_object->graphql_single_name => [
593✔
208
                                'type'        => $post_type_object->graphql_single_name,
593✔
209
                                'description' => static function () {
593✔
210
                                        return __( 'The Post object mutation type.', 'wp-graphql' );
15✔
211
                                },
593✔
212
                                'resolve'     => static function ( $payload, $_args, AppContext $context ) {
593✔
213
                                        if ( empty( $payload['postObjectId'] ) || ! absint( $payload['postObjectId'] ) ) {
17✔
214
                                                return null;
×
215
                                        }
216

217
                                        return $context->get_loader( 'post' )->load_deferred( $payload['postObjectId'] );
17✔
218
                                },
593✔
219
                        ],
593✔
220
                ];
593✔
221
        }
222

223
        /**
224
         * Defines the mutation data modification closure.
225
         *
226
         * @param \WP_Post_Type $post_type_object The post type of the mutation.
227
         * @param string        $mutation_name    The mutation name.
228
         *
229
         * @return callable(array<string,mixed>$input,\WPGraphQL\AppContext $context,\GraphQL\Type\Definition\ResolveInfo $info):array<string,mixed>
230
         */
231
        public static function mutate_and_get_payload( $post_type_object, $mutation_name ) {
593✔
232
                return static function ( $input, AppContext $context, ResolveInfo $info ) use ( $post_type_object, $mutation_name ) {
593✔
233

234
                        /**
235
                         * Throw an exception if there's no input
236
                         */
237
                        if ( ( empty( $post_type_object->name ) ) || ( empty( $input ) || ! is_array( $input ) ) ) {
15✔
238
                                throw new UserError( esc_html__( 'Mutation not processed. There was no input for the mutation or the post_type_object was invalid', 'wp-graphql' ) );
×
239
                        }
240

241
                        /**
242
                         * Stop now if a user isn't allowed to create a post
243
                         */
244
                        if ( ! isset( $post_type_object->cap->create_posts ) || ! current_user_can( $post_type_object->cap->create_posts ) ) {
15✔
245
                                // translators: the $post_type_object->graphql_plural_name placeholder is the name of the object being mutated
246
                                throw new UserError( esc_html( sprintf( __( 'Sorry, you are not allowed to create %1$s', 'wp-graphql' ), $post_type_object->graphql_plural_name ) ) );
1✔
247
                        }
248

249
                        /**
250
                         * If the post being created is being assigned to another user that's not the current user, make sure
251
                         * the current user has permission to edit others posts for this post_type
252
                         */
253
                        if ( ! empty( $input['authorId'] ) ) {
14✔
254
                                // Ensure authorId is a valid databaseId.
255
                                $input['authorId'] = Utils::get_database_id_from_id( $input['authorId'] );
1✔
256

257
                                $author = ! empty( $input['authorId'] ) ? get_user_by( 'ID', $input['authorId'] ) : false;
1✔
258

259
                                if ( false === $author ) {
1✔
260
                                        throw new UserError( esc_html__( 'The provided `authorId` is not a valid user', 'wp-graphql' ) );
1✔
261
                                }
262

263
                                if ( get_current_user_id() !== $input['authorId'] && ( ! isset( $post_type_object->cap->edit_others_posts ) || ! current_user_can( $post_type_object->cap->edit_others_posts ) ) ) {
1✔
264
                                        // translators: the $post_type_object->graphql_plural_name placeholder is the name of the object being mutated
265
                                        throw new UserError( esc_html( sprintf( __( 'Sorry, you are not allowed to create %1$s as this user', 'wp-graphql' ), $post_type_object->graphql_plural_name ) ) );
×
266
                                }
267
                        }
268

269
                        /**
270
                         * @todo: When we support assigning terms and setting posts as "sticky" we need to check permissions
271
                         * @see :https://github.com/WordPress/WordPress/blob/e357195ce303017d517aff944644a7a1232926f7/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php#L504-L506
272
                         * @see : https://github.com/WordPress/WordPress/blob/e357195ce303017d517aff944644a7a1232926f7/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php#L496-L498
273
                         */
274

275
                        /**
276
                         * Insert the post object and get the ID
277
                         */
278
                        $post_args = PostObjectMutation::prepare_post_object( $input, $post_type_object, $mutation_name );
14✔
279

280
                        /**
281
                         * Filter the default post status to use when the post is initially created. Pass through a filter to
282
                         * allow other plugins to override the default (for example, Edit Flow, which provides control over
283
                         * customizing stati or various E-commerce plugins that make heavy use of custom stati)
284
                         *
285
                         * @param string       $default_status   The default status to be used when the post is initially inserted
286
                         * @param \WP_Post_Type $post_type_object The Post Type that is being inserted
287
                         * @param string       $mutation_name    The name of the mutation currently in progress
288
                         */
289
                        $default_post_status = apply_filters( 'graphql_post_object_create_default_post_status', 'draft', $post_type_object, $mutation_name );
14✔
290

291
                        /**
292
                         * We want to cache the "post_status" and set the status later. We will set the initial status
293
                         * of the inserted post as the default status for the site, allow side effects to process with the
294
                         * inserted post (set term object connections, set meta input, sideload images if necessary, etc)
295
                         * Then will follow up with setting the status as what it was declared to be later
296
                         */
297
                        $intended_post_status = ! empty( $post_args['post_status'] ) ? $post_args['post_status'] : $default_post_status;
14✔
298

299
                        /**
300
                         * If the current user cannot publish posts but their intent was to publish,
301
                         * default the status to pending.
302
                         */
303
                        if ( ( ! isset( $post_type_object->cap->publish_posts ) || ! current_user_can( $post_type_object->cap->publish_posts ) ) && ! in_array(
14✔
304
                                $intended_post_status,
14✔
305
                                [
14✔
306
                                        'draft',
14✔
307
                                        'pending',
14✔
308
                                ],
14✔
309
                                true
14✔
310
                        ) ) {
14✔
311
                                $intended_post_status = 'pending';
1✔
312
                        }
313

314
                        /**
315
                         * Set the post_status as the default for the initial insert. The intended $post_status will be set after
316
                         * side effects are complete.
317
                         */
318
                        $post_args['post_status'] = $default_post_status;
14✔
319

320
                        $clean_args = wp_slash( (array) $post_args );
14✔
321

322
                        if ( ! is_array( $clean_args ) || empty( $clean_args ) ) {
14✔
323
                                throw new UserError( esc_html__( 'The object failed to create', 'wp-graphql' ) );
×
324
                        }
325

326
                        /**
327
                         * Insert the post and retrieve the ID
328
                         */
329
                        $post_id = wp_insert_post( $clean_args, true );
14✔
330

331
                        /**
332
                         * Throw an exception if the post failed to create
333
                         */
334
                        if ( is_wp_error( $post_id ) ) {
14✔
335
                                $error_message = $post_id->get_error_message();
×
336
                                if ( ! empty( $error_message ) ) {
×
337
                                        throw new UserError( esc_html( $error_message ) );
×
338
                                }
339

340
                                throw new UserError( esc_html__( 'The object failed to create but no error was provided', 'wp-graphql' ) );
×
341
                        }
342

343
                        /**
344
                         * This updates additional data not part of the posts table (postmeta, terms, other relations, etc)
345
                         *
346
                         * The input for the postObjectMutation will be passed, along with the $new_post_id for the
347
                         * postObject that was created so that relations can be set, meta can be updated, etc.
348
                         */
349
                        PostObjectMutation::update_additional_post_object_data( $post_id, $input, $post_type_object, $mutation_name, $context, $info, $default_post_status, $intended_post_status );
14✔
350

351
                        /**
352
                         * Determine whether the intended status should be set or not.
353
                         *
354
                         * By filtering to false, the $intended_post_status will not be set at the completion of the mutation.
355
                         *
356
                         * This allows for side-effect actions to set the status later. For example, if a post
357
                         * was being created via a GraphQL Mutation, the post had additional required assets, such as images
358
                         * that needed to be sideloaded or some other semi-time-consuming side effect, those actions could
359
                         * be deferred (cron or whatever), and when those actions complete they could come back and set
360
                         * the $intended_status.
361
                         *
362
                         * @param bool      $should_set_intended_status Whether to set the intended post_status or not. Default true.
363
                         * @param \WP_Post_Type $post_type_object The Post Type Object for the post being mutated
364
                         * @param string       $mutation_name              The name of the mutation currently in progress
365
                         * @param \WPGraphQL\AppContext $context The AppContext passed down to all resolvers
366
                         * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down to all resolvers
367
                         * @param string       $intended_post_status       The intended post_status the post should have according to the mutation input
368
                         * @param string       $default_post_status        The default status posts should use if an intended status wasn't set
369
                         */
370
                        $should_set_intended_status = apply_filters( 'graphql_post_object_create_should_set_intended_post_status', true, $post_type_object, $mutation_name, $context, $info, $intended_post_status, $default_post_status );
14✔
371

372
                        /**
373
                         * If the intended post status and the default post status are not the same,
374
                         * update the post with the intended status now that side effects are complete.
375
                         */
376
                        if ( $intended_post_status !== $default_post_status && true === $should_set_intended_status ) {
14✔
377

378
                                /**
379
                                 * If the post was deleted by a side effect action before getting here,
380
                                 * don't proceed.
381
                                 */
382
                                $new_post = get_post( $post_id );
10✔
383
                                if ( empty( $new_post ) ) {
10✔
384
                                        throw new UserError( esc_html__( 'The status of the post could not be set', 'wp-graphql' ) );
×
385
                                }
386

387
                                /**
388
                                 * If the $intended_post_status is different than the current status of the post
389
                                 * proceed and update the status.
390
                                 */
391
                                if ( $intended_post_status !== $new_post->post_status ) {
10✔
392
                                        $update_args = [
10✔
393
                                                'ID'          => $post_id,
10✔
394
                                                'post_status' => $intended_post_status,
10✔
395
                                                // Prevent the post_date from being reset if the date was included in the create post $args
396
                                                // see: https://core.trac.wordpress.org/browser/tags/4.9/src/wp-includes/post.php#L3637
397
                                                'edit_date'   => ! empty( $post_args['post_date'] ) ? $post_args['post_date'] : false,
10✔
398
                                        ];
10✔
399

400
                                        wp_update_post( $update_args );
10✔
401
                                }
402
                        }
403

404
                        /**
405
                         * Return the post object
406
                         */
407
                        return [
14✔
408
                                'postObjectId' => $post_id,
14✔
409
                        ];
14✔
410
                };
593✔
411
        }
412
}
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