Completed
Pull Request — master (#7)
by James
02:09
created

Model::set_attribute()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6.0131

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 13
cts 14
cp 0.9286
rs 8.439
c 0
b 0
f 0
cc 6
eloc 14
nc 6
nop 2
crap 6.0131
1
<?php
2
namespace Intraxia\Jaxion\Axolotl;
3
4
use Intraxia\Jaxion\Contract\Axolotl\Serializes;
5
use Intraxia\Jaxion\Contract\Axolotl\UsesWordPressPost;
6
use Intraxia\Jaxion\Contract\Axolotl\UsesWordPressTerm;
7
use LogicException;
8
use WP_Post;
9
use WP_Term;
10
11
/**
12
 * Class Model
13
 *
14
 * Shared model methods and properties, allowing models
15
 * to transparently map some attributes to an underlying WP_Post
16
 * object and others to postmeta or a custom table.
17
 *
18
 * @package    Intraxia\Jaxion
19
 * @subpackage Axolotl
20
 * @since      0.1.0
21
 */
22
abstract class Model implements Serializes {
23
	/**
24
	 * Table attribute key.
25
	 */
26
	const TABLE_KEY = '@@table';
27
28
	/**
29
	 * Object attribute key.
30
	 */
31
	const OBJECT_KEY = '@@object';
32
33
	/**
34
	 * Memoized values for class methods.
35
	 *
36
	 * @var array
37
	 */
38
	private static $memo = array();
39
40
	/**
41
	 * Model attributes.
42
	 *
43
	 * @var array
44
	 */
45
	private $attributes = array(
46
		self::TABLE_KEY  => array(),
47
		self::OBJECT_KEY => null,
48
	);
49
50
	/**
51
	 * Model's original attributes.
52
	 *
53
	 * @var array
54
	 */
55
	private $original = array(
56
		self::TABLE_KEY  => array(),
57
		self::OBJECT_KEY => null,
58
	);
59
60
	/**
61
	 * Properties which are allowed to be set on the model.
62
	 *
63
	 * If this array is empty, any attributes can be set on the model.
64
	 *
65
	 * @var string[]
66
	 */
67
	protected $fillable = array();
68
69
	/**
70
	 * Properties which cannot be automatically filled on the model.
71
	 *
72
	 * If the model is unguarded, these properties can be filled.
73
	 *
74
	 * @var array
75
	 */
76
	protected $guarded = array();
77
78
	/**
79
	 * Properties which should not be serialized.
80
	 *
81
	 * @var array
82
	 */
83
	protected $hidden = array();
84
85
	/**
86
	 * Properties which should be serialized.
87
	 *
88
	 * @var array
89
	 */
90
	protected $visible = array();
91
92
	/**
93
	 * Whether the model's properties are guarded.
94
	 *
95
	 * When false, allows guarded properties to be filled.
96
	 *
97
	 * @var bool
98
	 */
99
	protected $is_guarded = true;
100
101
	/**
102
	 * Constructs a new model with provided attributes.
103
	 *
104
	 * If self::OBJECT_KEY is passed as one of the attributes, the underlying post
105
	 * will be overwritten.
106
	 *
107
	 * @param array <string, mixed> $attributes
108
	 */
109 69
	public function __construct( array $attributes = array() ) {
110 69
		$this->maybe_boot();
111 69
		$this->sync_original();
112
113 69
		if ( $this->uses_wp_object() ) {
114 66
			$this->create_wp_object();
115 66
		}
116
117 69
		$this->unguard();
118 69
		$this->refresh( $attributes );
119 69
		$this->reguard();
120 69
	}
121
122
	/**
123
	 * Refreshes the model's current attributes with the provided array.
124
	 *
125
	 * The model's attributes will match what was provided in the array,
126
	 * and any attributes not passed
127
	 *
128
	 * @param array $attributes
129
	 *
130
	 * @return $this
131
	 */
132 69
	public function refresh( array $attributes ) {
133 69
		$this->clear();
134
135 69
		return $this->merge( $attributes );
136
	}
137
138
	/**
139
	 * Merges the provided attributes with the provided array.
140
	 *
141
	 * @param array $attributes
142
	 *
143
	 * @return $this
144
	 */
145 69
	public function merge( array $attributes ) {
146 69
		foreach ( $attributes as $name => $value ) {
147 39
			$this->set_attribute( $name, $value );
148 69
		}
149
150 69
		return $this;
151
	}
152
153
	/**
154
	 * Get the model's table attributes.
155
	 *
156
	 * Returns the array of for the model that will either need to be
157
	 * saved in postmeta or a separate table.
158
	 *
159
	 * @return array
160
	 */
161 12
	public function get_table_attributes() {
162 12
		return $this->attributes[ self::TABLE_KEY ];
163
	}
164
165
	/**
166
	 * Get the model's original attributes.
167
	 *
168
	 * @return array
169
	 */
170 6
	public function get_original_table_attributes() {
171 6
		return $this->original[ self::TABLE_KEY ];
172
	}
173
174
	/**
175
	 * Retrieve an array of the attributes on the model
176
	 * that have changed compared to the model's
177
	 * original data.
178
	 *
179
	 * @return array
180
	 */
181 3
	public function get_changed_table_attributes() {
182 3
		$changed = array();
183
184 3
		foreach ( $this->get_table_attributes() as $key => $value ) {
185
			if ( $value !==
186 3
			     $this->get_original_attribute( $key )
187 3
			) {
188 3
				$changed[ $key ] = $value;
189 3
			}
190 3
		}
191
192 3
		return $changed;
193
	}
194
195
	/**
196
	 * Get the model's underlying post.
197
	 *
198
	 * Returns the underlying WP_Post object for the model, representing
199
	 * the data that will be save in the wp_posts table.
200
	 *
201
	 * @return false|WP_Post|WP_Term
202
	 */
203 18
	public function get_underlying_wp_object() {
204 18
		if ( isset( $this->attributes[ self::OBJECT_KEY ] ) ) {
205 15
			return $this->attributes[ self::OBJECT_KEY ];
206
		}
207
208 3
		return false;
209
	}
210
211
	/**
212
	 * Get the model's original underlying post.
213
	 *
214
	 * @return WP_Post
215
	 */
216 6
	public function get_original_underlying_wp_object() {
217 6
		return $this->original[ self::OBJECT_KEY ];
218
	}
219
220
	/**
221
	 * Get the model attributes on the WordPress object
222
	 * that have changed compared to the model's
223
	 * original attributes.
224
	 *
225
	 * @return array
226
	 */
227 3
	public function get_changed_wp_object_attributes() {
228 3
		$changed = array();
229
230 3
		foreach ( $this->get_wp_object_keys() as $key ) {
231 3
			if ( $this->get_attribute( $key ) !==
232 3
			     $this->get_original_attribute( $key )
233 3
			) {
234 3
				$changed[ $key ] = $this->get_attribute( $key );
235 3
			}
236 3
		}
237
238 3
		return $changed;
239
	}
240
241
	/**
242
	 * Magic __set method.
243
	 *
244
	 * Passes the name and value to set_attribute, which is where the magic happens.
245
	 *
246
	 * @param string $name
247
	 * @param mixed  $value
248
	 */
249 6
	public function __set( $name, $value ) {
250 6
		$this->set_attribute( $name, $value );
251 6
	}
252
253
	/**
254
	 * Sets the model attributes.
255
	 *
256
	 * Checks whether the model attribute can be set, check if it
257
	 * maps to the WP_Post property, otherwise, assigns it to the
258
	 * table attribute array.
259
	 *
260
	 * @param string $name
261
	 * @param mixed  $value
262
	 *
263
	 * @return $this
264
	 *
265
	 * @throws GuardedPropertyException
266
	 */
267 69
	public function set_attribute( $name, $value ) {
268 69
		if ( self::OBJECT_KEY === $name ) {
269 27
			if ( ! $value ) {
270
				throw new \Exception();
271
			}
272
273 27
			return $this->override_wp_object( $value );
274
		}
275
276 69
		if ( self::TABLE_KEY === $name ) {
277 6
			return $this->override_table( $value );
278
		}
279
280 69
		if ( ! $this->is_fillable( $name ) ) {
281 9
			throw new GuardedPropertyException;
282
		}
283
284 69
		if ( $method = $this->has_map_method( $name ) ) {
285 66
			$this->attributes[ self::OBJECT_KEY ]->{$this->{$method}()} = $value;
286 66
		} else {
287 69
			$this->attributes[ self::TABLE_KEY ][ $name ] = $value;
288
		}
289
290 69
		return $this;
291
	}
292
293
	/**
294
	 * Retrieves all the attribute keys for the model.
295
	 *
296
	 * @return array
297
	 */
298 18
	public function get_attribute_keys() {
299 18
		if ( isset( self::$memo[ get_called_class() ][ __METHOD__ ] ) ) {
300 18
			return self::$memo[ get_called_class() ][ __METHOD__ ];
301
		}
302
303 9
		return self::$memo[ get_called_class() ][ __METHOD__ ]
304 9
			= array_merge(
305 9
				$this->fillable,
306 9
				$this->guarded,
307 9
				$this->get_compute_methods()
308 9
			);
309
	}
310
311
	/**
312
	 * Retrieves the attribute keys that aren't mapped to a post.
313
	 *
314
	 * @return array
315
	 */
316 69 View Code Duplication
	public function get_table_keys() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
317 69
		if ( isset( self::$memo[ get_called_class() ][ __METHOD__ ] ) ) {
318 66
			return self::$memo[ get_called_class() ][ __METHOD__ ];
319
		}
320
321 9
		$keys = array();
322
323 9
		foreach ( $this->get_attribute_keys() as $key ) {
324 9
			if ( ! $this->has_map_method( $key ) &&
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->has_map_method($key) of type false|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
325 9
			     ! $this->has_compute_method( $key )
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->has_compute_method($key) of type false|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
326 9
			) {
327 9
				$keys[] = $key;
328 9
			}
329 9
		}
330
331 9
		return self::$memo[ get_called_class() ][ __METHOD__ ] = $keys;
332
	}
333
334
	/**
335
	 * Retrieves the attribute keys that are mapped to a post.
336
	 *
337
	 * @return array
338
	 */
339 69 View Code Duplication
	public function get_wp_object_keys() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
340 69
		if ( isset( self::$memo[ get_called_class() ][ __METHOD__ ] ) ) {
341 66
			return self::$memo[ get_called_class() ][ __METHOD__ ];
342
		}
343
344 9
		$keys = array();
345
346 9
		foreach ( $this->get_attribute_keys() as $key ) {
347 9
			if ( $this->has_map_method( $key ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->has_map_method($key) of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
348 6
				$keys[] = $key;
349 6
			}
350 9
		}
351
352 9
		return self::$memo[ get_called_class() ][ __METHOD__ ] = $keys;
353
	}
354
355
	/**
356
	 * Returns the model's keys that are computed at call time.
357
	 *
358
	 * @return array
359
	 */
360 3 View Code Duplication
	public function get_computed_keys() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
361 3
		if ( isset( self::$memo[ get_called_class() ][ __METHOD__ ] ) ) {
362 3
			return self::$memo[ get_called_class() ][ __METHOD__ ];
363
		}
364
365 3
		$keys = array();
366
367 3
		foreach ( $this->get_attribute_keys() as $key ) {
368 3
			if ( $this->has_compute_method( $key ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->has_compute_method($key) of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
369 3
				$keys[] = $key;
370 3
			}
371 3
		}
372
373 3
		return self::$memo[ get_called_class() ][ __METHOD__ ] = $keys;
374
	}
375
376
	/**
377
	 * Serializes the model's public data into an array.
378
	 *
379
	 * @return array
380
	 */
381 12
	public function serialize() {
382 12
		$attributes = array();
383
384 12
		if ( $this->visible ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->visible of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
385
			// If visible attributes are set, we'll only reveal those.
386 6
			foreach ( $this->visible as $key ) {
387 6
				$attributes[ $key ] = $this->get_attribute( $key );
388 6
			}
389 12
		} elseif ( $this->hidden ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->hidden of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
390
			// If hidden attributes are set, we'll grab everything and hide those.
391 3
			foreach ( $this->get_attribute_keys() as $key ) {
392 3
				if ( ! in_array( $key, $this->hidden ) ) {
393 3
					$attributes[ $key ] = $this->get_attribute( $key );
394 3
				}
395 3
			}
396 3
		} else {
397
			// If nothing is hidden/visible, we'll grab and reveal everything.
398 3
			foreach ( $this->get_attribute_keys() as $key ) {
399 3
				$attributes[ $key ] = $this->get_attribute( $key );
400 3
			}
401
		}
402
403
		return array_map( function ( $attribute ) {
404 12
			if ( $attribute instanceof Serializes ) {
405 3
				return $attribute->serialize();
406
			}
407
408 12
			return $attribute;
409 12
		}, $attributes );
410
	}
411
412
	/**
413
	 * Syncs the current attributes to the model's original.
414
	 *
415
	 * @return $this
416
	 */
417 69
	public function sync_original() {
418 69
		$this->original = $this->attributes;
419
420 69
		if ( $this->attributes[ self::OBJECT_KEY ] ) {
421 9
			$this->original[ self::OBJECT_KEY ] = clone $this->attributes[ self::OBJECT_KEY ];
422 9
		}
423
424 69
		foreach ( $this->original[ self::TABLE_KEY ] as $key => $item ) {
425 9
			if ( is_object( $item ) ) {
426 9
				$this->original[ $key ] = clone $item;
427 9
			}
428 69
		}
429
430 69
		return $this;
431
	}
432
433
	/**
434
	 * Checks if a given attribute is mass-fillable.
435
	 *
436
	 * Returns true if the attribute can be filled, false if it can't.
437
	 *
438
	 * @param string $name
439
	 *
440
	 * @return bool
441
	 */
442 69
	private function is_fillable( $name ) {
443
		// If this model isn't guarded, everything is fillable.
444 69
		if ( ! $this->is_guarded ) {
445 69
			return true;
446
		}
447
448
		// If it's in the fillable array, then it's fillable.
449 18
		if ( in_array( $name, $this->fillable ) ) {
450 12
			return true;
451
		}
452
453
		// If it's explicitly guarded, then it's not fillable.
454 9
		if ( in_array( $name, $this->guarded ) ) {
455 9
			return false;
456
		}
457
458
		// If fillable hasn't been defined, then everything else fillable.
459
		return ! $this->fillable;
460
	}
461
462
	/**
463
	 * Overrides the current WordPress object with a provided one.
464
	 *
465
	 * Resets the post's default values and stores it in the attributes.
466
	 *
467
	 * @param WP_Post|WP_Term $value
468
	 *
469
	 * @return $this
470
	 */
471 27
	private function override_wp_object( $value ) {
472 27
		$this->attributes[ self::OBJECT_KEY ] = $this->set_wp_object_constants( $value );
473
474 27
		return $this;
475
	}
476
477
	/**
478
	 * Overrides the current table attributes array with a provided one.
479
	 *
480
	 * @param array $value
481
	 *
482
	 * @return $this
483
	 */
484 6
	private function override_table( array $value ) {
485 6
		$this->attributes[ self::TABLE_KEY ] = $value;
486
487 6
		return $this;
488
	}
489
490
	/**
491
	 * Create and set with a new blank post.
492
	 *
493
	 * Creates a new WP_Post object, assigns it the default attributes,
494
	 * and stores it in the attributes.
495
	 *
496
	 * @throws LogicException
497
	 */
498 66
	private function create_wp_object() {
499 66
		switch ( true ) {
500 66
			case $this instanceof UsesWordPressPost:
501 66
				$object = new WP_Post( (object) array() );
502 66
				break;
503
			case $this instanceof UsesWordPressTerm:
504
				$object = new WP_Term( (object) array() );
505
				break;
506
			default:
507
				throw new LogicException;
508
				break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
509
		}
510
511 66
		$this->attributes[ self::OBJECT_KEY ] = $this->set_wp_object_constants( $object );
512 66
	}
513
514
	/**
515
	 * Enforces values on the post that can't change.
516
	 *
517
	 * Primarily, this is used to make sure the post_type always maps
518
	 * to the model's "$type" property, but this can all be overridden
519
	 * by the developer to enforce other values in the model.
520
	 *
521
	 * @param object $object
522
	 *
523
	 * @return object
524
	 */
525 66
	protected function set_wp_object_constants( $object ) {
526 66
		if ( $this instanceof UsesWordPressPost ) {
527 66
			$object->post_type = static::get_post_type();
0 ignored issues
show
Bug introduced by
The method get_post_type() does not seem to exist on object<Intraxia\Jaxion\Axolotl\Model>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
528 66
		}
529
530 66
		if ( $this instanceof UsesWordPressTerm ) {
531
			$object->taxonomy = static::get_taxonomy();
0 ignored issues
show
Bug introduced by
The method get_taxonomy() does not seem to exist on object<Intraxia\Jaxion\Axolotl\Model>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
532
		}
533
534 66
		return $object;
535
	}
536
537
	/**
538
	 * Magic __get method.
539
	 *
540
	 * Passes the name and value to get_attribute, which is where the magic happens.
541
	 *
542
	 * @param string $name
543
	 *
544
	 * @return mixed
545
	 */
546 18
	public function __get( $name ) {
547 18
		return $this->get_attribute( $name );
548
	}
549
550
	/**
551
	 * Retrieves the model attribute.
552
	 *
553
	 * @param string $name
554
	 *
555
	 * @return mixed
556
	 *
557
	 * @throws PropertyDoesNotExistException If property isn't found.
558
	 */
559 45
	public function get_attribute( $name ) {
560 45
		if ( $method = $this->has_map_method( $name ) ) {
561 33
			$value = $this->attributes[ self::OBJECT_KEY ]->{$this->{$method}()};
562 45
		} elseif ( $method = $this->has_compute_method( $name ) ) {
563 15
			$value = $this->{$method}();
564 15
		} else {
565 24
			if ( ! isset( $this->attributes[ self::TABLE_KEY ][ $name ] ) ) {
566 3
				throw new PropertyDoesNotExistException( $name );
567
			}
568
569 21
			$value = $this->attributes[ self::TABLE_KEY ][ $name ];
570
		}
571
572 42
		return $value;
573
	}
574
575
	/**
576
	 * Retrieve the model's original attribute value.
577
	 *
578
	 * @param string $name
579
	 *
580
	 * @return mixed
581
	 *
582
	 * @throws PropertyDoesNotExistException If property isn't found.
583
	 */
584 6
	public function get_original_attribute( $name ) {
585 6
		$original_attributes = $this->original;
586
587 6
		if ( ! is_object( $original_attributes[ static::OBJECT_KEY ] ) ) {
588
			unset( $original_attributes[ static::OBJECT_KEY ] );
589
		}
590
591 6
		$original = new static( $original_attributes );
592
593
		try {
594 6
			return $original->get_attribute( $name );
595
		} catch ( \Exception $exception ) {
596
			return null;
597
		}
598
	}
599
600
	/**
601
	 * Fetches the Model's primary ID, depending on the model
602
	 * implementation.
603
	 *
604
	 * @return int
605
	 *
606
	 * @throws LogicException
607
	 */
608
	public function get_primary_id() {
609
		if ( $this instanceof UsesWordPressPost ) {
610
			return $this->get_underlying_wp_object()->ID;
611
		}
612
613
		if ( $this instanceof UsesWordPressTerm ) {
614
			return $this->get_underlying_wp_object()->term_id;
615
		}
616
617
		// Model w/o wp_object not yet supported.
618
		throw new LogicException;
619
	}
620
621
	/**
622
	 * Checks whether the attribute has a map method.
623
	 *
624
	 * This is used to determine whether the attribute maps to a
625
	 * property on the underlying WP_Post object. Returns the
626
	 * method if one exists, returns false if it doesn't.
627
	 *
628
	 * @param string $name
629
	 *
630
	 * @return false|string
631
	 */
632 69
	protected function has_map_method( $name ) {
633 69
		if ( method_exists( $this, $method = "map_{$name}" ) ) {
634 66
			return $method;
635
		}
636
637 69
		return false;
638
	}
639
640
	/**
641
	 * Checks whether the attribute has a compute method.
642
	 *
643
	 * This is used to determine if the attribute should be computed
644
	 * from other attributes.
645
	 *
646
	 * @param string $name
647
	 *
648
	 * @return false|string
649
	 */
650 39
	protected function has_compute_method( $name ) {
651 39
		if ( method_exists( $this, $method = "compute_{$name}" ) ) {
652 24
			return $method;
653
		}
654
655 36
		return false;
656
	}
657
658
	/**
659
	 * Clears all the current attributes from the model.
660
	 *
661
	 * This does not touch the model's original attributes, and will
662
	 * only clear fillable attributes, unless the model is unguarded.
663
	 *
664
	 * @return $this
665
	 */
666 69
	public function clear() {
667 69
		$keys = array_merge(
668 69
			$this->get_table_keys(),
669 69
			$this->get_wp_object_keys()
670 69
		);
671
672 69
		foreach ( $keys as $key ) {
673
			try {
674 69
				$this->set_attribute( $key, null );
675 69
			} catch ( GuardedPropertyException $e ) {
676
				// We won't clear out guarded attributes.
677
			}
678 69
		}
679
680 69
		return $this;
681
	}
682
683
	/**
684
	 * Unguards the model.
685
	 *
686
	 * Sets the model to be unguarded, allowing the filling of
687
	 * guarded attributes.
688
	 */
689 69
	public function unguard() {
690 69
		$this->is_guarded = false;
691 69
	}
692
693
	/**
694
	 * Reguards the model.
695
	 *
696
	 * Sets the model to be guarded, preventing filling of
697
	 * guarded attributes.
698
	 */
699 69
	public function reguard() {
700 69
		$this->is_guarded = true;
701 69
	}
702
703
	/**
704
	 * Retrieves all the compute methods on the model.
705
	 *
706
	 * @return array
707
	 */
708 9
	protected function get_compute_methods() {
709 9
		$methods = get_class_methods( get_called_class() );
710
		$methods = array_filter( $methods, function ( $method ) {
711 9
			return strrpos( $method, 'compute_', - strlen( $method ) ) !== false;
712 9
		} );
713 9
		$methods = array_map( function ( $method ) {
714 6
			return substr( $method, strlen( 'compute_' ) );
715 9
		}, $methods );
716
717 9
		return $methods;
718
	}
719
720
	/**
721
	 * Sets up the memo array for the creating model.
722
	 */
723 69
	private function maybe_boot() {
724 69
		if ( ! isset( self::$memo[ get_called_class() ] ) ) {
725 9
			self::$memo[ get_called_class() ] = array();
726 9
		}
727 69
	}
728
729
	/**
730
	 * Whether this Model uses an underlying WordPress object.
731
	 *
732
	 * @return bool
733
	 */
734 69
	protected function uses_wp_object() {
735 69
		return $this instanceof UsesWordPressPost ||
736 69
			$this instanceof UsesWordPressTerm;
737
	}
738
}
739