Completed
Push — master ( 84afd6...650043 )
by Evstati
14s
created

Kohana_Jam_Model::loaded_insist()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 7
ccs 0
cts 4
cp 0
rs 9.4285
cc 2
eloc 4
nc 2
nop 0
crap 6
1
<?php defined('SYSPATH') OR die('No direct script access.');
2
/**
3
 * Jam Model
4
 *
5
 * Jam_Model is the class all models must extend. It handles
6
 * various CRUD operations and relationships to other models.
7
 *
8
 * @package    Jam
9
 * @category   Models
10
 * @author     Ivan Kerin
11
 * @copyright  (c) 2011-2012 Despark Ltd.
12
 * @license    http://www.opensource.org/licenses/isc-license.txt
13
 */
14
abstract class Kohana_Jam_Model extends Jam_Validated {
15
16
	/**
17
	 * @var  boolean  Whether or not the model is loaded
18
	 */
19
	protected $_loaded = FALSE;
20
21
	/**
22
	 * @var  boolean  Whether or not the model is saved
23
	 */
24
	protected $_saved = FALSE;
25
26
	/**
27
	 * @var  boolean  Whether or not the model is saving
28
	 */
29
	protected $_is_saving = FALSE;
30
31
	/**
32
	 * @var  Boolean  A flag that indicates a record has been deleted from the database
33
	 */
34
	 protected $_deleted = FALSE;
35
36
	/**
37
	 * Constructor.
38
	 *
39
	 * A key can be passed to automatically load a model by its
40
	 * unique key.
41
	 *
42
	 * @param  mixed|null  $key
0 ignored issues
show
Bug introduced by
There is no parameter named $key. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
43
	 */
44 437
	public function __construct($meta_name = NULL)
45
	{
46 437
		parent::__construct($meta_name);
47
48 437
		$this->meta()->events()->trigger('model.before_construct', $this);
49
50
		// Copy over the defaults into the original data.
51 437
		$this->_original = $this->meta()->defaults();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->meta()->defaults() of type * is incompatible with the declared type array of property $_original.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
52
53 437
		$this->meta()->events()->trigger('model.after_construct', $this);
54 437
	}
55
56
	/**
57
	 * Gets the value for a field.
58
	 *
59
	 * @param   string       $name The field's name
60
	 * @return  array|mixed
61
	 */
62 217
	public function get($name)
63
	{
64 217
		if ($association = $this->meta()->association($name))
65
		{
66 20
			$name = $association->name;
67
68 20
			return $association->get($this, Arr::get($this->_changed, $name), $this->changed($name));
0 ignored issues
show
Bug introduced by
The method get does only exist in Jam_Association, but not in Kohana_Jam_Meta.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
69
		}
70
71 216
		return parent::get($name);
72
	}
73
74
	public function __isset($name)
75
	{
76
		return ($this->meta()->association($name) OR parent::__isset($name));
77
	}
78
79
	/**
80
	 * Sets the value of a field.
81
	 *
82
	 * You can pass an array of key => value pairs
83
	 * to set multiple fields at the same time:
84
	 *
85
	 *    $model->set(array(
86
	 *        'field1' => 'value',
87
	 *        'field2' => 'value',
88
	 *         ....
89
	 *    ));
90
	 *
91
	 * @param   array|string  $values
92
	 * @param   mixed|null    $value
93
	 * @return  $this
94
	 */
95 94
	public function set($values, $value = NULL)
96
	{
97
		// Accept set('name', 'value');
98 94
		if ( ! is_array($values))
99
		{
100 79
			$values = array($values => $value);
101
		}
102
103 94
		foreach ($values as $key => & $value)
104
		{
105 94
			if ($association = $this->meta()->association($key))
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $association is correct as $this->meta()->association($key) (which targets Kohana_Jam_Meta::association()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
106
			{
107 20
				if ($association->readonly)
108
					throw new Kohana_Exception('Cannot change the value of :name, its readonly', array(':name' => $association->name));
109
110 20
				$this->_changed[$association->name] = $association->set($this, $value, TRUE);
111
112 20
				unset($this->_retrieved[$association->name]);
113 94
				unset($values[$key]);
114
			}
115
		}
116
117 94
		parent::set($values);
118
119 94
		if ($this->_saved AND $this->changed())
120
		{
121 26
			$this->_saved = FALSE;
122
		}
123
124 94
		return $this;
125
	}
126
127
	/**
128
	 * Clears the object and loads an array of values into the object.
129
	 *
130
	 * This should only be used for setting from database results
131
	 * since the model declares itself as saved and loaded after.
132
	 *
133
	 * @param   Jam_Model|array  $values
134
	 * @return  $this
135
	 */
136 197
	public function load_fields($values)
137
	{
138
		// Clear the object
139 197
		$this->clear();
140
141 197
		$this->meta()->events()->trigger('model.before_load', $this);
142
143 197
		$this->_loaded = TRUE;
144
145 197
		$columns = array();
146
147 197
		foreach ($this->meta()->fields() as $key => $field)
0 ignored issues
show
Bug introduced by
The expression $this->meta()->fields() of type array|object<Jam_Meta> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
148
		{
149 197
			$columns[$field->column] = $field;
150
		}
151
152 197
		foreach ($values as $key => $value)
0 ignored issues
show
Bug introduced by
The expression $values of type object<Jam_Model>|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
153
		{
154 194
			if ($field = $this->meta()->field($key))
155
			{
156 194
				$this->_original[$field->name] = $field->set($this, $value, FALSE);
0 ignored issues
show
Unused Code introduced by
The call to Kohana_Jam_Meta::set() has too many arguments starting with FALSE.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
157
			}
158 48
			elseif (isset($columns[$key]))
159
			{
160
				$field = $columns[$key];
161
				$this->_original[$field->name] = $field->set($this, $value, FALSE);
162
			}
163 48
			elseif ($association = $this->meta()->association($key))
164
			{
165 8
				$association_value = $association->load_fields($this, $value, FALSE);
166 8
				if (is_object($association_value))
167
				{
168 8
					$this->_retrieved[$association->name] = $association->load_fields($this, $value, FALSE);
169
				}
170
				else
171
				{
172 8
					$this->_changed[$association->name] = $association_value;
173
				}
174
			}
175
			else
176
			{
177 194
				$this->_unmapped[$key] = $value;
178
			}
179
		}
180
181
		// Model is now saved and loaded
182 197
		$this->_saved = TRUE;
183
184 197
		$this->meta()->events()->trigger('model.after_load', $this);
185
186 197
		return $this;
187
	}
188
189
	/**
190
	 * Validates the current model's data
191
	 *
192
	 * @throws  Jam_Exception_Validation
193
	 * @param   Validation|null   $extra_validation
0 ignored issues
show
Bug introduced by
There is no parameter named $extra_validation. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
194
	 * @return  bool
195
	 */
196 21
	public function check($force = FALSE)
197
	{
198 21
		$this->_move_retrieved_to_changed();
199
200 21
		return parent::check( ! $this->loaded() OR $force);
201
	}
202
203 752
	protected function _move_retrieved_to_changed()
204
	{
205 752
		foreach ($this->_retrieved as $column => $value)
206
		{
207 19
			if ($value instanceof Jam_Array_Association	AND $value->changed())
208
			{
209 19
				$this->_changed[$column] = $value;
210
			}
211
		}
212 752
	}
213
214
	public function update_fields($values, $value = NULL)
215
	{
216
		if ( ! $this->loaded())
217
			throw new Kohana_Exception('Model must be loaded to use update_fields method');
218
219
		if ( ! is_array($values))
220
		{
221
			$values = array($values => $value);
222
		}
223
224
		$this->set($values);
225
226
		Jam::update($this)
227
			->where_key($this->id())
228
			->set($values)
229
			->execute();
230
231
		return $this;
232
	}
233
234
	/**
235
	 * Creates or updates the current record.
236
	 *
237
	 * @param   bool|null        $validate
238
	 * @return  $this
239
	 */
240 20
	public function save($validate = NULL)
241
	{
242 20
		if ($this->_is_saving)
243
			throw new Kohana_Exception("Cannot save a model that is already in the process of saving");
244
245
246 20
		$key = $this->_original[$this->meta()->primary_key()];
247
248
		// Run validation
249 20
		if ($validate !== FALSE)
250
		{
251 20
			$this->check_insist();
252
		}
253
254 20
		$this->_is_saving = TRUE;
255
256
		// These will be processed later
257 20
		$values = $defaults = array();
258
259 20
		if ($this->meta()->events()->trigger('model.before_save', $this, array($this->_changed)) === FALSE)
260
		{
261
			return $this;
262
		}
263
264
		// Trigger callbacks and ensure we should proceed
265 20
		$event_type = $key ? 'update' : 'create';
266
267 20
		if ($this->meta()->events()->trigger('model.before_'.$event_type, $this, array($this->_changed)) === FALSE)
268
		{
269
			return $this;
270
		}
271
272 20
		$this->_move_retrieved_to_changed();
273
274
		// Iterate through all fields in original in case any unchanged fields
275
		// have convert() behavior like timestamp updating...
276
		//
277 20
		foreach (array_merge($this->_original, $this->_changed) as $column => $value)
278
		{
279 20
			if ($field = $this->meta()->field($column))
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $field is correct as $this->meta()->field($column) (which targets Kohana_Jam_Meta::field()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
280
			{
281
				// Only save in_db values
282 20
				if ($field->in_db)
283
				{
284
					// See if field wants to alter the value on save()
285 20
					$value = $field->convert($this, $value, $key);
286
287
					// Only set the value to be saved if it's changed from the original
288 20
					if ($value !== $this->_original[$column])
289
					{
290 17
						$values[$field->column] = $value;
291
					}
292
					// Or if we're INSERTing and we need to set the defaults for the first time
293 20
					elseif ( ! $key AND ( ! $this->changed($field->name) OR $field->default === $value) AND ! $field->primary)
294
					{
295 20
						$defaults[$field->column] = $field->default;
296
					}
297
				}
298
			}
299
		}
300
301
		// If we have a key, we're updating
302 20
		if ($key)
303
		{
304
			// Do we even have to update anything in the row?
305 4
			if ($values)
0 ignored issues
show
Bug Best Practice introduced by
The expression $values 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...
306
			{
307 3
				Jam::update($this)
308 3
					->where_key($key)
309 3
					->set($values)
310 4
					->execute();
311
			}
312
		}
313
		else
314
		{
315 18
			$insert_values = array_merge($defaults, $values);
316 18
			list($id) = Jam::insert($this)
317 18
				->columns(array_keys($insert_values))
318 18
				->values(array_values($insert_values))
319 18
				->execute();
320
321
			// Gotta make sure to set this
322 18
			$key = $values[$this->meta()->primary_key()] = $id;
0 ignored issues
show
Unused Code introduced by
$key is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
323
		}
324
325
		// Re-set any saved values; they may have changed
326 20
		$this->set($values);
327
328 20
		$this->_loaded = $this->_saved = TRUE;
329
330 20
		$this->meta()->events()->trigger('model.after_save', $this, array($this->_changed, $event_type));
331
332 20
		$this->meta()->events()->trigger('model.after_'.$event_type, $this, array($this->_changed));
333
334
		// Set the changed data back as original
335 20
		$this->_original = array_merge($this->_original, $this->_changed);
336
337 20
		$this->_changed = array();
338
339 20
		foreach ($this->_retrieved as $name => $retrieved)
340
		{
341 17
			if (($association = $this->meta()->association($name)))
342
			{
343 2
				if ($association instanceof Jam_Association_Collection)
344
				{
345 2
					$retrieved->clear_changed();
346
				}
347
			}
348 17
			elseif (($field = $this->meta()->field($name)) AND $field->in_db)
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $field is correct as $this->meta()->field($name) (which targets Kohana_Jam_Meta::field()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
349
			{
350 17
				unset($this->_retrieved[$name]);
351
			}
352
		}
353
354 20
		$this->_is_saving = FALSE;
355
356 20
		return $this;
357
	}
358
359
	/**
360
	 * Deletes a single record.
361
	 *
362
	 * @return  boolean
363
	 **/
364 10
	public function delete()
365
	{
366 10
		$result = FALSE;
367
368
		// Are we loaded? Then we're just deleting this record
369 10
		if ($this->_loaded)
370
		{
371 10
			$key = $this->_original[$this->meta()->primary_key()];
372
373 10
			if (($result = $this->meta()->events()->trigger('model.before_delete', $this)) !== FALSE)
374
			{
375 10
				$result = Jam::delete($this)->where_key($key)->execute();
376
			}
377
		}
378
379
		// Trigger the after-delete
380 10
		$this->meta()->events()->trigger('model.after_delete', $this, array($result));
381
382
		// Clear the object so it appears deleted anyway
383 10
		$this->clear();
384
385
		// Set the flag indicatig the model has been successfully deleted
386 10
		$this->_deleted = $result;
0 ignored issues
show
Documentation Bug introduced by
It seems like $result can also be of type object. However, the property $_deleted is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
387
388 10
		return $this->_deleted;
389
	}
390
391
	/**
392
	 * Removes any changes made to a model.
393
	 *
394
	 * This method only works on loaded models.
395
	 *
396
	 * @return  $this
397
	 */
398
	public function revert()
399
	{
400
		if ($this->_loaded)
401
		{
402
			$this->_loaded =
403
			$this->_saved  = TRUE;
404
405
			parent::revert();
406
		}
407
408
		return $this;
409
	}
410
411
	/**
412
	 * Sets a model to its original state, as if freshly instantiated
413
	 *
414
	 * @return  $this
415
	 */
416 197
	public function clear()
417
	{
418 197
		$this->_loaded =
419 197
		$this->_saved  = FALSE;
420
421 197
		parent::clear();
422
423 197
		return $this;
424
	}
425
426 1
	public function get_insist($attribute_name)
427
	{
428 1
		$attribute = $this->__get($attribute_name);
429
430 1
		if ($attribute === NULL)
431 1
			throw new Jam_Exception_Notfound('The association :name was empty on :model_name', NULL, array(
432 1
				':name' => $attribute_name,
433 1
				':model_name' => (string) $this,
434
			));
435
436 1
		return $attribute;
437
	}
438
439 1
	public function build($association_name, array $attributes = array())
440
	{
441 1
		$association = $this->meta()->association($association_name);
442
443 1
		if ( ! $association)
444
			throw new Kohana_Exception('There is no association named :association_name on model :model', array(':association_name' => $association_name, ':model' => $this->meta()->model()));
445
446 1
		if ($association instanceof Jam_Association_Collection)
447
			throw new Kohana_Exception(':association_name association must not be a collection on model :model', array(':association_name' => $association_name, ':model' => $this->meta()->model()));
448
449 1
		$item = $association->build($this, $attributes);
450
451 1
		$this->set($association_name, $item);
452
453 1
		return $item;
454
	}
455
456
	/**
457
	 * Returns whether or not the model is loaded
458
	 *
459
	 * @return  boolean
460
	 */
461 93
	public function loaded()
462
	{
463 93
		return $this->_loaded;
464
	}
465
466
	public function loaded_insist()
467
	{
468
		if ( ! $this->loaded())
469
			throw new Jam_Exception_NotLoaded("Model not loaded", $this);
470
471
		return $this;
472
	}
473
474
	/**
475
	 * Whether or not the model is saved
476
	 *
477
	 * @return  boolean
478
	 */
479 10
	public function saved()
480
	{
481 10
		return $this->_saved;
482
	}
483
484
	/**
485
	 * Whether or not the model is in the process of being saved
486
	 *
487
	 * @return  boolean
488
	 */
489 4
	public function is_saving()
490
	{
491 4
		return $this->_is_saving;
492
	}
493
494
	/**
495
	 * Whether or not the model is deleted
496
	 *
497
	 * @return  boolean
498
	 */
499
	public function deleted()
500
	{
501
		return $this->_deleted;
502
	}
503
504
	/**
505
	 * Build a new model object based on the current one, but without an ID, so it can be saved as a new object
506
	 * @return Jam_Model
507
	 */
508 1
	public function duplicate()
509
	{
510 1
		$fields = $this->as_array();
511
512 1
		unset($fields[$this->meta()->primary_key()]);
513
514 1
		return Jam::build($this->meta()->model(), $fields);
515
	}
516
517
	/**
518
	 * Returns a string representation of the model in the
519
	 * form of `Model_Name (id)` or `Model_Name (NULL)` if
520
	 * the model is not loaded.
521
	 *
522
	 * This is designed to be useful for debugging.
523
	 *
524
	 * @return  string
525
	 */
526 4
	public function __toString()
527
	{
528 4
		return (string) get_class($this).'('.($this->loaded() ? $this->id() : 'NULL').')';
529
	}
530
531 752
	public function serialize()
532
	{
533 752
		$this->_move_retrieved_to_changed();
534
535 752
		return serialize(array(
536 752
			'original' => $this->_original,
537 752
			'changed' => $this->_changed,
538 752
			'unmapped' => $this->_unmapped,
539 752
			'saved' => $this->_saved,
540 752
			'loaded' => $this->_loaded,
541 752
			'deleted' => $this->_deleted,
542
		));
543
	}
544
545 752
	public function unserialize($data)
546
	{
547 752
		$data = unserialize($data);
548
549 752
		$this->_meta = Jam::meta($this);
550 752
		$this->_original = Arr::merge($this->meta()->defaults(), $data['original']);
551 752
		$this->_changed = $data['changed'];
552 752
		$this->_unmapped = $data['unmapped'];
553 752
		$this->_saved = $data['saved'];
554 752
		$this->_loaded = $data['loaded'];
555 752
		$this->_deleted = $data['deleted'];
556
557 752
		foreach ($this->_changed as $name => $attribute)
558
		{
559 752
			$association = $this->meta()->association($name);
560 752
			if ($association AND $association instanceof Jam_Association_Collection)
561
			{
562 752
				$association->assign_internals($this, $attribute);
563
			}
564
		}
565 752
	}
566
}
567