Completed
Push — master ( f8fd12...f064aa )
by James
10s
created

Model   D

Complexity

Total Complexity 87

Size/Duplication

Total Lines 718
Duplicated Lines 6.55 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 91.6%

Importance

Changes 0
Metric Value
wmc 87
lcom 1
cbo 3
dl 47
loc 718
ccs 218
cts 238
cp 0.916
rs 4.4444
c 0
b 0
f 0

34 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 2
A refresh() 0 5 1
A merge() 0 7 2
A get_table_attributes() 0 3 1
A get_original_table_attributes() 0 3 1
A get_changed_table_attributes() 0 13 3
A get_underlying_wp_object() 0 7 2
A get_original_underlying_wp_object() 0 3 1
A get_changed_wp_object_attributes() 0 13 3
A __set() 0 3 1
B set_attribute() 0 25 6
A get_attribute_keys() 0 12 2
B get_table_keys() 17 17 5
A get_wp_object_keys() 15 15 4
A get_computed_keys() 15 15 4
C serialize() 0 30 8
A sync_original() 0 15 4
A is_fillable() 0 19 4
A override_wp_object() 0 5 1
A override_table() 0 5 1
A create_wp_object() 0 15 3
A set_wp_object_constants() 0 11 3
A __get() 0 3 1
A get_attribute() 0 15 4
A get_original_attribute() 0 15 3
A get_primary_id() 0 12 3
A has_map_method() 0 7 2
A has_compute_method() 0 7 2
A clear() 0 16 3
A unguard() 0 3 1
A reguard() 0 3 1
A get_compute_methods() 0 11 1
A maybe_boot() 0 5 2
A uses_wp_object() 0 4 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Model often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Model, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Intraxia\Jaxion\Axolotl;
3
4
use Exception;
5
use Intraxia\Jaxion\Contract\Axolotl\Serializes;
6
use Intraxia\Jaxion\Contract\Axolotl\UsesWordPressPost;
7
use Intraxia\Jaxion\Contract\Axolotl\UsesWordPressTerm;
8
use LogicException;
9
use WP_Post;
10
use WP_Term;
11
12
/**
13
 * Class Model
14
 *
15
 * Shared model methods and properties, allowing models
16
 * to transparently map some attributes to an underlying WP_Post
17
 * object and others to postmeta or a custom table.
18
 *
19
 * @package    Intraxia\Jaxion
20
 * @subpackage Axolotl
21
 * @since      0.1.0
22
 */
23
abstract class Model implements Serializes {
24
	/**
25
	 * Table attribute key.
26
	 */
27
	const TABLE_KEY = '@@table';
28
29
	/**
30
	 * Object attribute key.
31
	 */
32
	const OBJECT_KEY = '@@object';
33
34
	/**
35
	 * Memoized values for class methods.
36
	 *
37
	 * @var array
38
	 */
39
	private static $memo = array();
40
41
	/**
42
	 * Model attributes.
43
	 *
44
	 * @var array
45
	 */
46
	private $attributes = array(
47
		self::TABLE_KEY  => array(),
48
		self::OBJECT_KEY => null,
49
	);
50
51
	/**
52
	 * Model's original attributes.
53
	 *
54
	 * @var array
55
	 */
56
	private $original = array(
57
		self::TABLE_KEY  => array(),
58
		self::OBJECT_KEY => null,
59
	);
60
61
	/**
62
	 * Properties which are allowed to be set on the model.
63
	 *
64
	 * If this array is empty, any attributes can be set on the model.
65
	 *
66
	 * @var string[]
67
	 */
68
	protected $fillable = array();
69
70
	/**
71
	 * Properties which cannot be automatically filled on the model.
72
	 *
73
	 * If the model is unguarded, these properties can be filled.
74
	 *
75
	 * @var array
76
	 */
77
	protected $guarded = array();
78
79
	/**
80
	 * Properties which should not be serialized.
81
	 *
82
	 * @var array
83
	 */
84
	protected $hidden = array();
85
86
	/**
87
	 * Properties which should be serialized.
88
	 *
89
	 * @var array
90
	 */
91
	protected $visible = array();
92
93
	/**
94
	 * Whether the model's properties are guarded.
95
	 *
96
	 * When false, allows guarded properties to be filled.
97
	 *
98
	 * @var bool
99
	 */
100
	protected $is_guarded = true;
101
102
	/**
103
	 * Constructs a new model with provided attributes.
104
	 *
105
	 * If self::OBJECT_KEY is passed as one of the attributes, the underlying post
106
	 * will be overwritten.
107
	 *
108
	 * @param array <string, mixed> $attributes
109
	 */
110 69
	public function __construct( array $attributes = array() ) {
111 69
		$this->maybe_boot();
112 69
		$this->sync_original();
113
114 69
		if ( $this->uses_wp_object() ) {
115 66
			$this->create_wp_object();
116 66
		}
117
118 69
		$this->unguard();
119 69
		$this->refresh( $attributes );
120 69
		$this->reguard();
121 69
	}
122
123
	/**
124
	 * Refreshes the model's current attributes with the provided array.
125
	 *
126
	 * The model's attributes will match what was provided in the array,
127
	 * and any attributes not passed
128
	 *
129
	 * @param array $attributes
130
	 *
131
	 * @return $this
132
	 */
133 69
	public function refresh( array $attributes ) {
134 69
		$this->clear();
135
136 69
		return $this->merge( $attributes );
137
	}
138
139
	/**
140
	 * Merges the provided attributes with the provided array.
141
	 *
142
	 * @param array $attributes
143
	 *
144
	 * @return $this
145
	 */
146 69
	public function merge( array $attributes ) {
147 69
		foreach ( $attributes as $name => $value ) {
148 39
			$this->set_attribute( $name, $value );
149 69
		}
150
151 69
		return $this;
152
	}
153
154
	/**
155
	 * Get the model's table attributes.
156
	 *
157
	 * Returns the array of for the model that will either need to be
158
	 * saved in postmeta or a separate table.
159
	 *
160
	 * @return array
161
	 */
162 12
	public function get_table_attributes() {
163 12
		return $this->attributes[ self::TABLE_KEY ];
164
	}
165
166
	/**
167
	 * Get the model's original attributes.
168
	 *
169
	 * @return array
170
	 */
171 6
	public function get_original_table_attributes() {
172 6
		return $this->original[ self::TABLE_KEY ];
173
	}
174
175
	/**
176
	 * Retrieve an array of the attributes on the model
177
	 * that have changed compared to the model's
178
	 * original data.
179
	 *
180
	 * @return array
181
	 */
182 3
	public function get_changed_table_attributes() {
183 3
		$changed = array();
184
185 3
		foreach ( $this->get_table_attributes() as $key => $value ) {
186
			if ( $value !==
187 3
			     $this->get_original_attribute( $key )
188 3
			) {
189 3
				$changed[ $key ] = $value;
190 3
			}
191 3
		}
192
193 3
		return $changed;
194
	}
195
196
	/**
197
	 * Get the model's underlying post.
198
	 *
199
	 * Returns the underlying WP_Post object for the model, representing
200
	 * the data that will be save in the wp_posts table.
201
	 *
202
	 * @return false|WP_Post|WP_Term
203
	 */
204 18
	public function get_underlying_wp_object() {
205 18
		if ( isset( $this->attributes[ self::OBJECT_KEY ] ) ) {
206 15
			return $this->attributes[ self::OBJECT_KEY ];
207
		}
208
209 3
		return false;
210
	}
211
212
	/**
213
	 * Get the model's original underlying post.
214
	 *
215
	 * @return WP_Post
216
	 */
217 6
	public function get_original_underlying_wp_object() {
218 6
		return $this->original[ self::OBJECT_KEY ];
219
	}
220
221
	/**
222
	 * Get the model attributes on the WordPress object
223
	 * that have changed compared to the model's
224
	 * original attributes.
225
	 *
226
	 * @return array
227
	 */
228 3
	public function get_changed_wp_object_attributes() {
229 3
		$changed = array();
230
231 3
		foreach ( $this->get_wp_object_keys() as $key ) {
232 3
			if ( $this->get_attribute( $key ) !==
233 3
			     $this->get_original_attribute( $key )
234 3
			) {
235 3
				$changed[ $key ] = $this->get_attribute( $key );
236 3
			}
237 3
		}
238
239 3
		return $changed;
240
	}
241
242
	/**
243
	 * Magic __set method.
244
	 *
245
	 * Passes the name and value to set_attribute, which is where the magic happens.
246
	 *
247
	 * @param string $name
248
	 * @param mixed  $value
249
	 */
250 6
	public function __set( $name, $value ) {
251 6
		$this->set_attribute( $name, $value );
252 6
	}
253
254
	/**
255
	 * Sets the model attributes.
256
	 *
257
	 * Checks whether the model attribute can be set, check if it
258
	 * maps to the WP_Post property, otherwise, assigns it to the
259
	 * table attribute array.
260
	 *
261
	 * @param string $name
262
	 * @param mixed  $value
263
	 *
264
	 * @return $this
265
	 *
266
	 * @throws Exception
267
	 * @throws GuardedPropertyException
268
	 */
269 69
	public function set_attribute( $name, $value ) {
270 69
		if ( self::OBJECT_KEY === $name ) {
271 27
			if ( ! $value ) {
272
				throw new Exception;
273
			}
274
275 27
			return $this->override_wp_object( $value );
276
		}
277
278 69
		if ( self::TABLE_KEY === $name ) {
279 6
			return $this->override_table( $value );
280
		}
281
282 69
		if ( ! $this->is_fillable( $name ) ) {
283 9
			throw new GuardedPropertyException;
284
		}
285
286 69
		if ( $method = $this->has_map_method( $name ) ) {
287 66
			$this->attributes[ self::OBJECT_KEY ]->{$this->{$method}()} = $value;
288 66
		} else {
289 69
			$this->attributes[ self::TABLE_KEY ][ $name ] = $value;
290
		}
291
292 69
		return $this;
293
	}
294
295
	/**
296
	 * Retrieves all the attribute keys for the model.
297
	 *
298
	 * @return array
299
	 */
300 18
	public function get_attribute_keys() {
301 18
		if ( isset( self::$memo[ get_called_class() ][ __METHOD__ ] ) ) {
302 18
			return self::$memo[ get_called_class() ][ __METHOD__ ];
303
		}
304
305 9
		return self::$memo[ get_called_class() ][ __METHOD__ ]
306 9
			= array_merge(
307 9
				$this->fillable,
308 9
				$this->guarded,
309 9
				$this->get_compute_methods()
310 9
			);
311
	}
312
313
	/**
314
	 * Retrieves the attribute keys that aren't mapped to a post.
315
	 *
316
	 * @return array
317
	 */
318 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...
319 69
		if ( isset( self::$memo[ get_called_class() ][ __METHOD__ ] ) ) {
320 66
			return self::$memo[ get_called_class() ][ __METHOD__ ];
321
		}
322
323 9
		$keys = array();
324
325 9
		foreach ( $this->get_attribute_keys() as $key ) {
326 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...
327 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...
328 9
			) {
329 9
				$keys[] = $key;
330 9
			}
331 9
		}
332
333 9
		return self::$memo[ get_called_class() ][ __METHOD__ ] = $keys;
334
	}
335
336
	/**
337
	 * Retrieves the attribute keys that are mapped to a post.
338
	 *
339
	 * @return array
340
	 */
341 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...
342 69
		if ( isset( self::$memo[ get_called_class() ][ __METHOD__ ] ) ) {
343 66
			return self::$memo[ get_called_class() ][ __METHOD__ ];
344
		}
345
346 9
		$keys = array();
347
348 9
		foreach ( $this->get_attribute_keys() as $key ) {
349 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...
350 6
				$keys[] = $key;
351 6
			}
352 9
		}
353
354 9
		return self::$memo[ get_called_class() ][ __METHOD__ ] = $keys;
355
	}
356
357
	/**
358
	 * Returns the model's keys that are computed at call time.
359
	 *
360
	 * @return array
361
	 */
362 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...
363 3
		if ( isset( self::$memo[ get_called_class() ][ __METHOD__ ] ) ) {
364 3
			return self::$memo[ get_called_class() ][ __METHOD__ ];
365
		}
366
367 3
		$keys = array();
368
369 3
		foreach ( $this->get_attribute_keys() as $key ) {
370 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...
371 3
				$keys[] = $key;
372 3
			}
373 3
		}
374
375 3
		return self::$memo[ get_called_class() ][ __METHOD__ ] = $keys;
376
	}
377
378
	/**
379
	 * Serializes the model's public data into an array.
380
	 *
381
	 * @return array
382
	 */
383 12
	public function serialize() {
384 12
		$attributes = array();
385
386 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...
387
			// If visible attributes are set, we'll only reveal those.
388 6
			foreach ( $this->visible as $key ) {
389 6
				$attributes[ $key ] = $this->get_attribute( $key );
390 6
			}
391 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...
392
			// If hidden attributes are set, we'll grab everything and hide those.
393 3
			foreach ( $this->get_attribute_keys() as $key ) {
394 3
				if ( ! in_array( $key, $this->hidden ) ) {
395 3
					$attributes[ $key ] = $this->get_attribute( $key );
396 3
				}
397 3
			}
398 3
		} else {
399
			// If nothing is hidden/visible, we'll grab and reveal everything.
400 3
			foreach ( $this->get_attribute_keys() as $key ) {
401 3
				$attributes[ $key ] = $this->get_attribute( $key );
402 3
			}
403
		}
404
405
		return array_map( function ( $attribute ) {
406 12
			if ( $attribute instanceof Serializes ) {
407 3
				return $attribute->serialize();
408
			}
409
410 12
			return $attribute;
411 12
		}, $attributes );
412
	}
413
414
	/**
415
	 * Syncs the current attributes to the model's original.
416
	 *
417
	 * @return $this
418
	 */
419 69
	public function sync_original() {
420 69
		$this->original = $this->attributes;
421
422 69
		if ( $this->attributes[ self::OBJECT_KEY ] ) {
423 9
			$this->original[ self::OBJECT_KEY ] = clone $this->attributes[ self::OBJECT_KEY ];
424 9
		}
425
426 69
		foreach ( $this->original[ self::TABLE_KEY ] as $key => $item ) {
427 9
			if ( is_object( $item ) ) {
428 9
				$this->original[ $key ] = clone $item;
429 9
			}
430 69
		}
431
432 69
		return $this;
433
	}
434
435
	/**
436
	 * Checks if a given attribute is mass-fillable.
437
	 *
438
	 * Returns true if the attribute can be filled, false if it can't.
439
	 *
440
	 * @param string $name
441
	 *
442
	 * @return bool
443
	 */
444 69
	private function is_fillable( $name ) {
445
		// If this model isn't guarded, everything is fillable.
446 69
		if ( ! $this->is_guarded ) {
447 69
			return true;
448
		}
449
450
		// If it's in the fillable array, then it's fillable.
451 18
		if ( in_array( $name, $this->fillable ) ) {
452 12
			return true;
453
		}
454
455
		// If it's explicitly guarded, then it's not fillable.
456 9
		if ( in_array( $name, $this->guarded ) ) {
457 9
			return false;
458
		}
459
460
		// If fillable hasn't been defined, then everything else fillable.
461
		return ! $this->fillable;
462
	}
463
464
	/**
465
	 * Overrides the current WordPress object with a provided one.
466
	 *
467
	 * Resets the post's default values and stores it in the attributes.
468
	 *
469
	 * @param WP_Post|WP_Term $value
470
	 *
471
	 * @return $this
472
	 */
473 27
	private function override_wp_object( $value ) {
474 27
		$this->attributes[ self::OBJECT_KEY ] = $this->set_wp_object_constants( $value );
475
476 27
		return $this;
477
	}
478
479
	/**
480
	 * Overrides the current table attributes array with a provided one.
481
	 *
482
	 * @param array $value
483
	 *
484
	 * @return $this
485
	 */
486 6
	private function override_table( array $value ) {
487 6
		$this->attributes[ self::TABLE_KEY ] = $value;
488
489 6
		return $this;
490
	}
491
492
	/**
493
	 * Create and set with a new blank post.
494
	 *
495
	 * Creates a new WP_Post object, assigns it the default attributes,
496
	 * and stores it in the attributes.
497
	 *
498
	 * @throws LogicException
499
	 */
500 66
	private function create_wp_object() {
501 66
		switch ( true ) {
502 66
			case $this instanceof UsesWordPressPost:
503 66
				$object = new WP_Post( (object) array() );
504 66
				break;
505
			case $this instanceof UsesWordPressTerm:
506
				$object = new WP_Term( (object) array() );
507
				break;
508
			default:
509
				throw new LogicException;
510
				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...
511
		}
512
513 66
		$this->attributes[ self::OBJECT_KEY ] = $this->set_wp_object_constants( $object );
514 66
	}
515
516
	/**
517
	 * Enforces values on the post that can't change.
518
	 *
519
	 * Primarily, this is used to make sure the post_type always maps
520
	 * to the model's "$type" property, but this can all be overridden
521
	 * by the developer to enforce other values in the model.
522
	 *
523
	 * @param object $object
524
	 *
525
	 * @return object
526
	 */
527 66
	protected function set_wp_object_constants( $object ) {
528 66
		if ( $this instanceof UsesWordPressPost ) {
529 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...
530 66
		}
531
532 66
		if ( $this instanceof UsesWordPressTerm ) {
533
			$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...
534
		}
535
536 66
		return $object;
537
	}
538
539
	/**
540
	 * Magic __get method.
541
	 *
542
	 * Passes the name and value to get_attribute, which is where the magic happens.
543
	 *
544
	 * @param string $name
545
	 *
546
	 * @return mixed
547
	 */
548 18
	public function __get( $name ) {
549 18
		return $this->get_attribute( $name );
550
	}
551
552
	/**
553
	 * Retrieves the model attribute.
554
	 *
555
	 * @param string $name
556
	 *
557
	 * @return mixed
558
	 *
559
	 * @throws PropertyDoesNotExistException If property isn't found.
560
	 */
561 45
	public function get_attribute( $name ) {
562 45
		if ( $method = $this->has_map_method( $name ) ) {
563 33
			$value = $this->attributes[ self::OBJECT_KEY ]->{$this->{$method}()};
564 45
		} elseif ( $method = $this->has_compute_method( $name ) ) {
565 15
			$value = $this->{$method}();
566 15
		} else {
567 24
			if ( ! isset( $this->attributes[ self::TABLE_KEY ][ $name ] ) ) {
568 3
				throw new PropertyDoesNotExistException( $name );
569
			}
570
571 21
			$value = $this->attributes[ self::TABLE_KEY ][ $name ];
572
		}
573
574 42
		return $value;
575
	}
576
577
	/**
578
	 * Retrieve the model's original attribute value.
579
	 *
580
	 * @param string $name
581
	 *
582
	 * @return mixed
583
	 *
584
	 * @throws PropertyDoesNotExistException If property isn't found.
585
	 */
586 6
	public function get_original_attribute( $name ) {
587 6
		$original_attributes = $this->original;
588
589 6
		if ( ! is_object( $original_attributes[ static::OBJECT_KEY ] ) ) {
590
			unset( $original_attributes[ static::OBJECT_KEY ] );
591
		}
592
593 6
		$original = new static( $original_attributes );
594
595
		try {
596 6
			return $original->get_attribute( $name );
597
		} catch ( Exception $exception ) {
598
			return null;
599
		}
600
	}
601
602
	/**
603
	 * Fetches the Model's primary ID, depending on the model
604
	 * implementation.
605
	 *
606
	 * @return int
607
	 *
608
	 * @throws LogicException
609
	 */
610
	public function get_primary_id() {
611
		if ( $this instanceof UsesWordPressPost ) {
612
			return $this->get_underlying_wp_object()->ID;
613
		}
614
615
		if ( $this instanceof UsesWordPressTerm ) {
616
			return $this->get_underlying_wp_object()->term_id;
617
		}
618
619
		// Model w/o wp_object not yet supported.
620
		throw new LogicException;
621
	}
622
623
	/**
624
	 * Checks whether the attribute has a map method.
625
	 *
626
	 * This is used to determine whether the attribute maps to a
627
	 * property on the underlying WP_Post object. Returns the
628
	 * method if one exists, returns false if it doesn't.
629
	 *
630
	 * @param string $name
631
	 *
632
	 * @return false|string
633
	 */
634 69
	protected function has_map_method( $name ) {
635 69
		if ( method_exists( $this, $method = "map_{$name}" ) ) {
636 66
			return $method;
637
		}
638
639 69
		return false;
640
	}
641
642
	/**
643
	 * Checks whether the attribute has a compute method.
644
	 *
645
	 * This is used to determine if the attribute should be computed
646
	 * from other attributes.
647
	 *
648
	 * @param string $name
649
	 *
650
	 * @return false|string
651
	 */
652 39
	protected function has_compute_method( $name ) {
653 39
		if ( method_exists( $this, $method = "compute_{$name}" ) ) {
654 24
			return $method;
655
		}
656
657 36
		return false;
658
	}
659
660
	/**
661
	 * Clears all the current attributes from the model.
662
	 *
663
	 * This does not touch the model's original attributes, and will
664
	 * only clear fillable attributes, unless the model is unguarded.
665
	 *
666
	 * @return $this
667
	 */
668 69
	public function clear() {
669 69
		$keys = array_merge(
670 69
			$this->get_table_keys(),
671 69
			$this->get_wp_object_keys()
672 69
		);
673
674 69
		foreach ( $keys as $key ) {
675
			try {
676 69
				$this->set_attribute( $key, null );
677 69
			} catch ( GuardedPropertyException $e ) {
678
				// We won't clear out guarded attributes.
679
			}
680 69
		}
681
682 69
		return $this;
683
	}
684
685
	/**
686
	 * Unguards the model.
687
	 *
688
	 * Sets the model to be unguarded, allowing the filling of
689
	 * guarded attributes.
690
	 */
691 69
	public function unguard() {
692 69
		$this->is_guarded = false;
693 69
	}
694
695
	/**
696
	 * Reguards the model.
697
	 *
698
	 * Sets the model to be guarded, preventing filling of
699
	 * guarded attributes.
700
	 */
701 69
	public function reguard() {
702 69
		$this->is_guarded = true;
703 69
	}
704
705
	/**
706
	 * Retrieves all the compute methods on the model.
707
	 *
708
	 * @return array
709
	 */
710 9
	protected function get_compute_methods() {
711 9
		$methods = get_class_methods( get_called_class() );
712
		$methods = array_filter( $methods, function ( $method ) {
713 9
			return strrpos( $method, 'compute_', - strlen( $method ) ) !== false;
714 9
		} );
715 9
		$methods = array_map( function ( $method ) {
716 6
			return substr( $method, strlen( 'compute_' ) );
717 9
		}, $methods );
718
719 9
		return $methods;
720
	}
721
722
	/**
723
	 * Sets up the memo array for the creating model.
724
	 */
725 69
	private function maybe_boot() {
726 69
		if ( ! isset( self::$memo[ get_called_class() ] ) ) {
727 9
			self::$memo[ get_called_class() ] = array();
728 9
		}
729 69
	}
730
731
	/**
732
	 * Whether this Model uses an underlying WordPress object.
733
	 *
734
	 * @return bool
735
	 */
736 69
	protected function uses_wp_object() {
737 69
		return $this instanceof UsesWordPressPost ||
738 69
			$this instanceof UsesWordPressTerm;
739
	}
740
}
741