• 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

90.83
/src/Model/Model.php
1
<?php
2

3
namespace WPGraphQL\Model;
4

5
use Exception;
6

7
/**
8
 * Class Model - Abstract class for modeling data for all core types
9
 *
10
 * @property bool $isPrivate
11
 * @property bool $isPublic
12
 * @property bool $isRestricted
13
 *
14
 * @package WPGraphQL\Model
15
 */
16
abstract class Model {
17

18
        /**
19
         * Stores the name of the type the child class extending this one represents
20
         *
21
         * @var string
22
         */
23
        protected $model_name;
24

25
        /**
26
         * Stores the raw data passed to the child class when it's instantiated before it's transformed
27
         *
28
         * @var mixed[]|object|mixed
29
         */
30
        protected $data;
31

32
        /**
33
         * Stores the capability name for what to check on the user if the data should be considered
34
         * "Restricted"
35
         *
36
         * @var string
37
         */
38
        protected $restricted_cap;
39

40
        /**
41
         * Stores the array of allowed fields to show if the data is restricted
42
         *
43
         * @var string[]
44
         */
45
        protected $allowed_restricted_fields;
46

47
        /**
48
         * Stores the DB ID of the user that owns this piece of data, or null if there is no owner
49
         *
50
         * @var int|null
51
         */
52
        protected $owner;
53

54
        /**
55
         * Stores the WP_User object for the current user in the session
56
         *
57
         * @var \WP_User $current_user
58
         */
59
        protected $current_user;
60

61
        /**
62
         * Stores the visibility value for the current piece of data
63
         *
64
         * @var string
65
         */
66
        protected $visibility;
67

68
        /**
69
         * The fields for the modeled object. This will be populated in the child class
70
         *
71
         * @var array<string,mixed>
72
         */
73
        public $fields;
74

75
        /**
76
         * Model constructor.
77
         *
78
         * @param string   $restricted_cap            The capability to check against to determine if
79
         *                                            the data should be restricted or not
80
         * @param string[] $allowed_restricted_fields The allowed fields if the data is in fact restricted
81
         * @param int|null $owner                     Database ID of the user that owns this piece of
82
         *                                            data to compare with the current user ID
83
         *
84
         * @return void
85
         * @throws \Exception Throws Exception.
86
         */
87
        protected function __construct( $restricted_cap = '', $allowed_restricted_fields = [], $owner = null ) {
518✔
88
                if ( empty( $this->data ) ) {
518✔
89
                        // translators: %s is the name of the model.
90
                        throw new Exception( esc_html( sprintf( __( 'An empty data set was used to initialize the modeling of this %s object', 'wp-graphql' ), $this->get_model_name() ) ) );
×
91
                }
92

93
                $this->restricted_cap            = $restricted_cap;
518✔
94
                $this->allowed_restricted_fields = $allowed_restricted_fields;
518✔
95
                $this->owner                     = $owner;
518✔
96
                $this->current_user              = wp_get_current_user();
518✔
97

98
                if ( 'private' === $this->get_visibility() ) {
518✔
99
                        return;
38✔
100
                }
101

102
                $this->init();
509✔
103
                $this->prepare_fields();
509✔
104
        }
105

106
        /**
107
         * Magic method to re-map the isset check on the child class looking for properties when
108
         * resolving the fields
109
         *
110
         * @param string $key The name of the field you are trying to retrieve
111
         *
112
         * @return bool
113
         */
114
        public function __isset( $key ) {
506✔
115
                return isset( $this->fields[ $key ] );
506✔
116
        }
117

118
        /**
119
         * Magic method to re-map setting new properties to the class inside of the $fields prop rather
120
         * than on the class in unique properties
121
         *
122
         * @param string                    $key   Name of the key to set the data to
123
         * @param callable|int|string|mixed $value The value to set to the key
124
         *
125
         * @return void
126
         */
127
        public function __set( $key, $value ) {
507✔
128
                $this->fields[ $key ] = $value;
507✔
129
        }
130

131
        /**
132
         * Magic method to re-map where external calls go to look for properties on the child objects.
133
         * This is crucial to let objects modeled through this class work with the default field
134
         * resolver.
135
         *
136
         * @param string $key Name of the property that is trying to be accessed
137
         *
138
         * @return mixed|null
139
         */
140
        public function __get( $key ) {
507✔
141
                if ( isset( $this->fields[ $key ] ) ) {
507✔
142
                        /**
143
                         * If the property has already been processed and cached to the model
144
                         * return the processed value.
145
                         *
146
                         * Otherwise, if it's a callable, process it and cache the value.
147
                         */
148
                        if ( is_scalar( $this->fields[ $key ] ) || ( is_object( $this->fields[ $key ] ) && ! is_callable( $this->fields[ $key ] ) ) || is_array( $this->fields[ $key ] ) ) {
507✔
149
                                return $this->fields[ $key ];
489✔
150
                        } elseif ( is_callable( $this->fields[ $key ] ) ) {
507✔
151
                                $data       = call_user_func( $this->fields[ $key ] );
507✔
152
                                $this->$key = $data;
507✔
153

154
                                return $data;
507✔
155
                        } else {
156
                                return $this->fields[ $key ];
×
157
                        }
158
                } else {
159
                        return null;
3✔
160
                }
161
        }
162

163
        /**
164
         * Setup the global data for the model to have proper context when resolving.
165
         *
166
         * @return void
167
         */
168
        public function setup() {
136✔
169
        }
136✔
170

171
        /**
172
         * Generic model tear down after the fields are setup. This can be used
173
         * to reset state to where it was before the model was setup.
174
         *
175
         * @return void
176
         */
177
        public function tear_down() {
393✔
178
        }
393✔
179

180
        /**
181
         * Returns the name of the model, built from the child className
182
         *
183
         * @return string
184
         */
185
        protected function get_model_name() {
518✔
186
                $name = static::class;
518✔
187

188
                if ( empty( $this->model_name ) ) {
518✔
189
                        if ( false !== strpos( static::class, '\\' ) ) {
518✔
190
                                $starting_character = strrchr( static::class, '\\' );
518✔
191
                                if ( ! empty( $starting_character ) ) {
518✔
192
                                        $name = substr( $starting_character, 1 );
518✔
193
                                }
194
                        }
195
                        $this->model_name = $name . 'Object';
518✔
196
                }
197

198
                return ! empty( $this->model_name ) ? $this->model_name : $name;
518✔
199
        }
200

201
        /**
202
         * Return the visibility state for the current piece of data
203
         *
204
         * @return string|null
205
         */
206
        public function get_visibility() {
518✔
207
                if ( null === $this->visibility ) {
518✔
208

209
                        /**
210
                         * Filter for the capability to check against for restricted data
211
                         *
212
                         * @param string      $restricted_cap The capability to check against
213
                         * @param string      $model_name     Name of the model the filter is currently being executed in
214
                         * @param mixed       $data           The un-modeled incoming data
215
                         * @param string|null $visibility     The visibility that has currently been set for the data at this point
216
                         * @param int|null    $owner          The user ID for the owner of this piece of data
217
                         * @param \WP_User $current_user The current user for the session
218
                         *
219
                         * @return string
220
                         */
221
                        $protected_cap = apply_filters( 'graphql_restricted_data_cap', $this->restricted_cap, $this->get_model_name(), $this->data, $this->visibility, $this->owner, $this->current_user );
518✔
222

223
                        /**
224
                         * Filter to short circuit default is_private check for the model. This is expensive in some cases so
225
                         * this filter lets you prevent this from running by returning a true or false value.
226
                         *
227
                         * @param ?bool       $is_private   Whether the model data is private. Defaults to null.
228
                         * @param string      $model_name   Name of the model the filter is currently being executed in
229
                         * @param mixed       $data         The un-modeled incoming data
230
                         * @param string|null $visibility   The visibility that has currently been set for the data at this point
231
                         * @param int|null    $owner        The user ID for the owner of this piece of data
232
                         * @param \WP_User $current_user The current user for the session
233
                         *
234
                         * @return bool|null
235
                         */
236
                        $pre_is_private = apply_filters( 'graphql_pre_model_data_is_private', null, $this->get_model_name(), $this->data, $this->visibility, $this->owner, $this->current_user );
518✔
237

238
                        // If 3rd party code has not filtered this, use the Models default logic to determine
239
                        // whether the model should be considered private
240
                        if ( null !== $pre_is_private ) {
518✔
241
                                $is_private = $pre_is_private;
×
242
                        } else {
243
                                $is_private = $this->is_private();
518✔
244
                        }
245

246
                        /**
247
                         * Filter to determine if the data should be considered private or not
248
                         *
249
                         * @param bool     $is_private   Whether the model is private
250
                         * @param string      $model_name   Name of the model the filter is currently being executed in
251
                         * @param mixed       $data         The un-modeled incoming data
252
                         * @param string|null $visibility   The visibility that has currently been set for the data at this point
253
                         * @param int|null    $owner        The user ID for the owner of this piece of data
254
                         * @param \WP_User $current_user The current user for the session
255
                         *
256
                         * @return bool
257
                         */
258
                        $is_private = apply_filters( 'graphql_data_is_private', (bool) $is_private, $this->get_model_name(), $this->data, $this->visibility, $this->owner, $this->current_user );
518✔
259

260
                        if ( true === $is_private ) {
518✔
261
                                $this->visibility = 'private';
38✔
262
                        } elseif ( null !== $this->owner && true === $this->owner_matches_current_user() ) {
509✔
263
                                $this->visibility = 'public';
124✔
264
                        } elseif ( empty( $protected_cap ) || current_user_can( $protected_cap ) ) {
450✔
265
                                $this->visibility = 'public';
430✔
266
                        } else {
267
                                $this->visibility = 'restricted';
176✔
268
                        }
269
                }
270

271
                /**
272
                 * Filter the visibility name to be returned
273
                 *
274
                 * @param string|null $visibility   The visibility that has currently been set for the data at this point
275
                 * @param string      $model_name   Name of the model the filter is currently being executed in
276
                 * @param mixed       $data         The un-modeled incoming data
277
                 * @param int|null    $owner        The user ID for the owner of this piece of data
278
                 * @param \WP_User $current_user The current user for the session
279
                 *
280
                 * @return string
281
                 */
282
                return apply_filters( 'graphql_object_visibility', $this->visibility, $this->get_model_name(), $this->data, $this->owner, $this->current_user );
518✔
283
        }
284

285
        /**
286
         * Method to return the private state of the object. Can be overwritten in classes extending
287
         * this one.
288
         *
289
         * @return bool
290
         */
291
        protected function is_private() {
122✔
292
                return false;
122✔
293
        }
294

295
        /**
296
         * Whether or not the owner of the data matches the current user
297
         *
298
         * @return bool
299
         */
300
        protected function owner_matches_current_user() {
401✔
301
                if ( empty( $this->current_user->ID ) || empty( $this->owner ) ) {
401✔
302
                        return false;
265✔
303
                }
304

305
                return absint( $this->owner ) === absint( $this->current_user->ID );
162✔
306
        }
307

308
        /**
309
         * Restricts fields for the data to only return the allowed fields if the data is restricted
310
         *
311
         * @return void
312
         */
313
        protected function restrict_fields() {
176✔
314
                $this->fields = array_intersect_key(
176✔
315
                        $this->fields,
176✔
316
                        array_flip(
176✔
317
                        /**
318
                         * Filter for the allowed restricted fields
319
                         *
320
                         * @param string[]    $allowed_restricted_fields The fields to allow when the data is designated as restricted to the current user
321
                         * @param string      $model_name                Name of the model the filter is currently being executed in
322
                         * @param mixed       $data                      The un-modeled incoming data
323
                         * @param string|null $visibility                The visibility that has currently been set for the data at this point
324
                         * @param int|null    $owner                     The user ID for the owner of this piece of data
325
                         * @param \WP_User $current_user The current user for the session
326
                         */
327
                                apply_filters( 'graphql_allowed_fields_on_restricted_type', $this->allowed_restricted_fields, $this->get_model_name(), $this->data, $this->visibility, $this->owner, $this->current_user )
176✔
328
                        )
176✔
329
                );
176✔
330
        }
331

332
        /**
333
         * Wraps all fields with another callback layer so we can inject hooks & filters into them
334
         *
335
         * @return void
336
         */
337
        protected function wrap_fields() {
509✔
338
                if ( ! is_array( $this->fields ) || empty( $this->fields ) ) {
509✔
339
                        return;
×
340
                }
341

342
                $clean_array = [];
509✔
343
                foreach ( $this->fields as $key => $data ) {
509✔
344
                        $clean_array[ $key ] = function () use ( $key, $data ) {
509✔
345
                                if ( is_array( $data ) ) {
507✔
346
                                        $callback = ( ! empty( $data['callback'] ) ) ? $data['callback'] : null;
17✔
347

348
                                        /**
349
                                         * Capability to check required for the field
350
                                         *
351
                                         * @param string   $capability   The capability to check against to return the field
352
                                         * @param string   $key          The name of the field on the type
353
                                         * @param string   $model_name   Name of the model the filter is currently being executed in
354
                                         * @param mixed    $data         The un-modeled incoming data
355
                                         * @param string   $visibility   The visibility setting for this piece of data
356
                                         * @param int|null $owner        The user ID for the owner of this piece of data
357
                                         * @param \WP_User $current_user The current user for the session
358
                                         *
359
                                         * @return string
360
                                         */
361
                                        $cap_check = ( ! empty( $data['capability'] ) ) ? apply_filters( 'graphql_model_field_capability', $data['capability'], $key, $this->get_model_name(), $this->data, $this->visibility, $this->owner, $this->current_user ) : '';
17✔
362
                                        if ( ! empty( $cap_check ) ) {
17✔
363
                                                if ( ! current_user_can( $data['capability'] ) ) {
17✔
364
                                                        $callback = null;
1✔
365
                                                }
366
                                        }
367
                                } else {
368
                                        $callback = $data;
507✔
369
                                }
370

371
                                /**
372
                                 * Filter to short circuit the callback for any field on a type. Returning anything
373
                                 * other than null will stop the callback for the field from executing, and will
374
                                 * return your data or execute your callback instead.
375
                                 *
376
                                 * @param ?string  $result       The data returned from the callback. Null by default.
377
                                 * @param string   $key          The name of the field on the type
378
                                 * @param string   $model_name   Name of the model the filter is currently being executed in
379
                                 * @param mixed    $data         The un-modeled incoming data
380
                                 * @param string   $visibility   The visibility setting for this piece of data
381
                                 * @param int|null $owner        The user ID for the owner of this piece of data
382
                                 * @param \WP_User $current_user The current user for the session
383
                                 *
384
                                 * @return callable|int|string|mixed[]|mixed|null
385
                                 */
386
                                $pre = apply_filters( 'graphql_pre_return_field_from_model', null, $key, $this->get_model_name(), $this->data, $this->visibility, $this->owner, $this->current_user );
507✔
387

388
                                if ( ! is_null( $pre ) ) {
507✔
389
                                        $result = $pre;
×
390
                                } else {
391
                                        if ( is_callable( $callback ) ) {
507✔
392
                                                $this->setup();
507✔
393
                                                $field = call_user_func( $callback );
507✔
394
                                                $this->tear_down();
507✔
395
                                        } else {
396
                                                $field = $callback;
1✔
397
                                        }
398

399
                                        /**
400
                                         * Filter the data returned by the default callback for the field
401
                                         *
402
                                         * @param string   $field        The data returned from the callback
403
                                         * @param string   $key          The name of the field on the type
404
                                         * @param string   $model_name   Name of the model the filter is currently being executed in
405
                                         * @param mixed    $data         The un-modeled incoming data
406
                                         * @param string   $visibility   The visibility setting for this piece of data
407
                                         * @param int|null $owner        The user ID for the owner of this piece of data
408
                                         * @param \WP_User $current_user The current user for the session
409
                                         *
410
                                         * @return mixed
411
                                         */
412
                                        $result = apply_filters( 'graphql_return_field_from_model', $field, $key, $this->get_model_name(), $this->data, $this->visibility, $this->owner, $this->current_user );
507✔
413
                                }
414

415
                                /**
416
                                 * Hook that fires after the data is returned for the field
417
                                 *
418
                                 * @param string   $result       The returned data for the field
419
                                 * @param string   $key          The name of the field on the type
420
                                 * @param string   $model_name   Name of the model the filter is currently being executed in
421
                                 * @param mixed    $data         The un-modeled incoming data
422
                                 * @param string   $visibility   The visibility setting for this piece of data
423
                                 * @param int|null $owner        The user ID for the owner of this piece of data
424
                                 * @param \WP_User $current_user The current user for the session
425
                                 */
426
                                do_action( 'graphql_after_return_field_from_model', $result, $key, $this->get_model_name(), $this->data, $this->visibility, $this->owner, $this->current_user );
507✔
427

428
                                return $result;
507✔
429
                        };
509✔
430
                }
431

432
                $this->fields = $clean_array;
509✔
433
        }
434

435
        /**
436
         * Adds the model visibility fields to the data
437
         */
438
        private function add_model_visibility(): void {
509✔
439

440
                /**
441
                 * @todo: potentially abstract this out into a more central spot
442
                 */
443
                $this->fields['isPublic']     = function () {
509✔
444
                        return 'public' === $this->get_visibility();
×
445
                };
509✔
446
                $this->fields['isRestricted'] = function () {
509✔
447
                        return 'restricted' === $this->get_visibility();
1✔
448
                };
509✔
449
                $this->fields['isPrivate']    = function () {
509✔
450
                        return 'private' === $this->get_visibility();
6✔
451
                };
509✔
452
        }
453

454
        /**
455
         * Returns instance of the data fully modeled
456
         *
457
         * @return void
458
         */
459
        protected function prepare_fields() {
509✔
460
                if ( 'restricted' === $this->get_visibility() ) {
509✔
461
                        $this->restrict_fields();
176✔
462
                }
463

464
                /**
465
                 * Add support for the deprecated "graphql_return_modeled_data" filter.
466
                 *
467
                 * @param array<string,mixed>    $fields       The array of fields for the model
468
                 * @param string                 $model_name   Name of the model the filter is currently being executed in
469
                 * @param string                 $visibility   The visibility setting for this piece of data
470
                 * @param ?int                   $owner        The user ID for the owner of this piece of data
471
                 * @param \WP_User               $current_user The current user for the session
472
                 *
473
                 * @deprecated 1.7.0 use "graphql_model_prepare_fields" filter instead, which passes additional context to the filter
474
                 */
475
                $this->fields = apply_filters_deprecated( 'graphql_return_modeled_data', [ $this->fields, $this->get_model_name(), $this->visibility, $this->owner, $this->current_user ], '1.7.0', 'graphql_model_prepare_fields' );
509✔
476

477
                /**
478
                 * Filter the array of fields for the Model before the object is hydrated with it
479
                 *
480
                 * @param array<string,mixed>    $fields       The array of fields for the model
481
                 * @param string                 $model_name   Name of the model the filter is currently being executed in
482
                 * @param mixed                  $data         The un-modeled incoming data
483
                 * @param string                 $visibility   The visibility setting for this piece of data
484
                 * @param ?int                   $owner        The user ID for the owner of this piece of data
485
                 * @param \WP_User               $current_user The current user for the session
486
                 */
487
                $this->fields = apply_filters( 'graphql_model_prepare_fields', $this->fields, $this->get_model_name(), $this->data, $this->visibility, $this->owner, $this->current_user );
509✔
488
                $this->wrap_fields();
509✔
489
                $this->add_model_visibility();
509✔
490
        }
491

492
        /**
493
         * Given a string, and optional context, this decodes html entities if html_entity_decode is
494
         * enabled.
495
         *
496
         * @param string $str        The string to decode
497
         * @param string $field_name The name of the field being encoded
498
         * @param bool   $enabled    Whether decoding is enabled by default for the string passed in
499
         *
500
         * @return string
501
         */
502
        public function html_entity_decode( $str, $field_name, $enabled = false ) {
212✔
503

504
                /**
505
                 * Determine whether html_entity_decode should be applied to the string
506
                 *
507
                 * @param bool                   $enabled    Whether decoding is enabled by default for the string passed in
508
                 * @param string                 $str        The string to decode
509
                 * @param string                 $field_name The name of the field being encoded
510
                 * @param \WPGraphQL\Model\Model $model      The Model the field is being decoded on
511
                 */
512
                $decoding_enabled = apply_filters( 'graphql_html_entity_decoding_enabled', $enabled, $str, $field_name, $this );
212✔
513

514
                if ( false === $decoding_enabled ) {
212✔
515
                        return $str;
96✔
516
                }
517

518
                return html_entity_decode( $str, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8' );
175✔
519
        }
520

521
        /**
522
         * Filter the fields returned for the object
523
         *
524
         * @param string|mixed[]|null $fields The field or fields to build in the modeled object. You can
525
         *                                  pass null to build all of the fields, a string to only
526
         *                                  build an object with one field, or an array of field keys
527
         *                                  to build an object with those keys and their respective values.
528
         *
529
         * @return void
530
         */
531
        public function filter( $fields ) {
×
532
                if ( is_string( $fields ) ) {
×
533
                        $fields = [ $fields ];
×
534
                }
535

536
                if ( is_array( $fields ) ) {
×
537
                        $this->fields = array_intersect_key( $this->fields, array_flip( $fields ) );
×
538
                }
539
        }
540

541
        /**
542
         * Initialized the object.
543
         *
544
         * @return void
545
         */
546
        abstract protected function init();
547
}
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