Kohana_Jam_Model::save()   F
last analyzed

Complexity

Conditions 22
Paths 185

Size

Total Lines 118

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 44
CRAP Score 22.2796

Importance

Changes 0
Metric Value
dl 0
loc 118
ccs 44
cts 48
cp 0.9167
rs 2.7666
c 0
b 0
f 0
cc 22
nc 185
nop 1
crap 22.2796

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 441
	public function __construct($meta_name = NULL)
45
	{
46 441
		parent::__construct($meta_name);
47
48 441
		$this->meta()->events()->trigger('model.before_construct', $this);
49
50
		// Copy over the defaults into the original data.
51 441
		$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 441
		$this->meta()->events()->trigger('model.after_construct', $this);
54 441
	}
55
56
	/**
57
	 * Gets the value for a field.
58
	 *
59
	 * @param   string       $name The field's name
60
	 * @return  array|mixed
61
	 */
62 216
	public function get($name)
63
	{
64 216
		if ($association = $this->meta()->association($name))
65
		{
66 19
			$name = $association->name;
67
68 19
			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 215
		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 93
	public function set($values, $value = NULL)
96
	{
97
		// Accept set('name', 'value');
98 93
		if ( ! is_array($values))
99
		{
100 78
			$values = array($values => $value);
101
		}
102
103 93
		foreach ($values as $key => & $value)
104
		{
105 93
			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 19
				if ($association->readonly)
108
					throw new Kohana_Exception('Cannot change the value of :name, its readonly', array(':name' => $association->name));
109
110 19
				$this->_changed[$association->name] = $association->set($this, $value, TRUE);
111
112 19
				unset($this->_retrieved[$association->name]);
113 93
				unset($values[$key]);
114
			}
115
		}
116
117 93
		parent::set($values);
118
119 93
		if ($this->_saved AND $this->changed())
120
		{
121 26
			$this->_saved = FALSE;
122
		}
123
124 93
		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 196
	public function load_fields($values)
137
	{
138
		// Clear the object
139 196
		$this->clear();
140
141 196
		$this->meta()->events()->trigger('model.before_load', $this);
142
143 196
		$this->_loaded = TRUE;
144
145 196
		$columns = array();
146
147 196
		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 196
			$columns[$field->column] = $field;
150
		}
151
152 196
		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 193
			if ($field = $this->meta()->field($key))
155
			{
156 193
				$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 47
			elseif (isset($columns[$key]))
159
			{
160
				$field = $columns[$key];
161
				$this->_original[$field->name] = $field->set($this, $value, FALSE);
162
			}
163 47
			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 193
				$this->_unmapped[$key] = $value;
178
			}
179
		}
180
181
		// Model is now saved and loaded
182 196
		$this->_saved = TRUE;
183
184 196
		$this->meta()->events()->trigger('model.after_load', $this);
185
186 196
		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 19
	public function check($force = FALSE)
197
	{
198 19
		$this->_move_retrieved_to_changed();
199
200 19
		return parent::check( ! $this->loaded() OR $force);
201
	}
202
203 754
	protected function _move_retrieved_to_changed()
204
	{
205 754
		foreach ($this->_retrieved as $column => $value)
206
		{
207 17
			if ($value instanceof Jam_Array_Association	AND $value->changed())
208
			{
209 17
				$this->_changed[$column] = $value;
210
			}
211
		}
212 754
	}
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
	 * @throws Kohana_Exception
240
	 * @throws Jam_Exception_Validation
241
	 */
242 18
	public function save($validate = NULL)
243
	{
244 18
		if ($this->_is_saving)
245
			throw new Kohana_Exception("Cannot save a model that is already in the process of saving");
246
247
248 18
		$key = $this->_original[$this->meta()->primary_key()];
249
250
		// Run validation
251 18
		if ($validate !== FALSE && !$this->check())
252
		{
253
			throw new Jam_Exception_Validation('There was an error validating the :model: :errors', $this);
254
		}
255
256 18
		$this->_is_saving = TRUE;
257
258
		// These will be processed later
259 18
		$values = $defaults = array();
260
261 18
		if ($this->meta()->events()->trigger('model.before_save', $this, array($this->_changed)) === FALSE)
262
		{
263
			return $this;
264
		}
265
266
		// Trigger callbacks and ensure we should proceed
267 18
		$event_type = $key ? 'update' : 'create';
268
269 18
		if ($this->meta()->events()->trigger('model.before_'.$event_type, $this, array($this->_changed)) === FALSE)
270
		{
271
			return $this;
272
		}
273
274 18
		$this->_move_retrieved_to_changed();
275
276
		// Iterate through all fields in original in case any unchanged fields
277
		// have convert() behavior like timestamp updating...
278
		//
279 18
		foreach (array_merge($this->_original, $this->_changed) as $column => $value)
280
		{
281 18
			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...
282
			{
283
				// Only save in_db values
284 18
				if ($field->in_db)
285
				{
286
					// See if field wants to alter the value on save()
287 18
					$value = $field->convert($this, $value, $key);
288
289
					// Only set the value to be saved if it's changed from the original
290 18
					if ($value !== $this->_original[$column])
291
					{
292 15
						$values[$field->column] = $value;
293
					}
294
					// Or if we're INSERTing and we need to set the defaults for the first time
295 18
					elseif ( ! $key AND ( ! $this->changed($field->name) OR $field->default === $value) AND ! $field->primary)
296
					{
297 18
						$defaults[$field->column] = $field->default;
298
					}
299
				}
300
			}
301
		}
302
303
		// If we have a key, we're updating
304 18
		if ($key)
305
		{
306
			// Do we even have to update anything in the row?
307 3
			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...
308
			{
309 2
				Jam::update($this)
310 2
					->where_key($key)
311 2
					->set($values)
312 3
					->execute();
313
			}
314
		}
315
		else
316
		{
317 16
			$insert_values = array_merge($defaults, $values);
318 16
			list($id) = Jam::insert($this)
319 16
				->columns(array_keys($insert_values))
320 16
				->values(array_values($insert_values))
321 16
				->execute();
322
323
			// Gotta make sure to set this
324 16
			$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...
325
		}
326
327
		// Re-set any saved values; they may have changed
328 18
		$this->set($values);
329
330 18
		$this->_loaded = $this->_saved = TRUE;
331
332 18
		$this->meta()->events()->trigger('model.after_save', $this, array($this->_changed, $event_type));
333
334 18
		$this->meta()->events()->trigger('model.after_'.$event_type, $this, array($this->_changed));
335
336
		// Set the changed data back as original
337 18
		$this->_original = array_merge($this->_original, $this->_changed);
338
339 18
		$this->_changed = array();
340
341 18
		foreach ($this->_retrieved as $name => $retrieved)
342
		{
343 15
			if (($association = $this->meta()->association($name)))
344
			{
345 1
				if ($association instanceof Jam_Association_Collection)
346
				{
347 1
					$retrieved->clear_changed();
348
				}
349
			}
350 15
			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...
351
			{
352 15
				unset($this->_retrieved[$name]);
353
			}
354
		}
355
356 18
		$this->_is_saving = FALSE;
357
358 18
		return $this;
359
	}
360
361
	/**
362
	 * Deletes a single record.
363
	 *
364
	 * @return  boolean
365
	 **/
366 9
	public function delete()
367
	{
368 9
		$result = FALSE;
369
370
		// Are we loaded? Then we're just deleting this record
371 9
		if ($this->_loaded)
372
		{
373 9
			$key = $this->_original[$this->meta()->primary_key()];
374
375 9
			if (($result = $this->meta()->events()->trigger('model.before_delete', $this)) !== FALSE)
376
			{
377 9
				$result = Jam::delete($this)->where_key($key)->execute();
378
			}
379
		}
380
381
		// Trigger the after-delete
382 9
		$this->meta()->events()->trigger('model.after_delete', $this, array($result));
383
384
		// Clear the object so it appears deleted anyway
385 9
		$this->clear();
386
387
		// Set the flag indicatig the model has been successfully deleted
388 9
		$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...
389
390 9
		return $this->_deleted;
391
	}
392
393
	/**
394
	 * Removes any changes made to a model.
395
	 *
396
	 * This method only works on loaded models.
397
	 *
398
	 * @return  $this
399
	 */
400
	public function revert()
401
	{
402
		if ($this->_loaded)
403
		{
404
			$this->_loaded =
405
			$this->_saved  = TRUE;
406
407
			parent::revert();
408
		}
409
410
		return $this;
411
	}
412
413
	/**
414
	 * Sets a model to its original state, as if freshly instantiated
415
	 *
416
	 * @return  $this
417
	 */
418 196
	public function clear()
419
	{
420 196
		$this->_loaded =
421 196
		$this->_saved  = FALSE;
422
423 196
		parent::clear();
424
425 196
		return $this;
426
	}
427
428 1
	public function get_insist($attribute_name)
429
	{
430 1
		$attribute = $this->__get($attribute_name);
431
432 1
		if ($attribute === NULL)
433 1
			throw new Jam_Exception_Notfound('The association :name was empty on :model_name', NULL, array(
434 1
				':name' => $attribute_name,
435 1
				':model_name' => (string) $this,
436
			));
437
438 1
		return $attribute;
439
	}
440
441 1
	public function build($association_name, array $attributes = array())
442
	{
443 1
		$association = $this->meta()->association($association_name);
444
445 1
		if ( ! $association)
446
			throw new Kohana_Exception('There is no association named :association_name on model :model', array(':association_name' => $association_name, ':model' => $this->meta()->model()));
447
448 1
		if ($association instanceof Jam_Association_Collection)
449
			throw new Kohana_Exception(':association_name association must not be a collection on model :model', array(':association_name' => $association_name, ':model' => $this->meta()->model()));
450
451 1
		$item = $association->build($this, $attributes);
452
453 1
		$this->set($association_name, $item);
454
455 1
		return $item;
456
	}
457
458
	/**
459
	 * Returns whether or not the model is loaded
460
	 *
461
	 * @return  boolean
462
	 */
463 91
	public function loaded()
464
	{
465 91
		return $this->_loaded;
466
	}
467
468
	public function loaded_insist()
469
	{
470
		if ( ! $this->loaded())
471
			throw new Jam_Exception_NotLoaded("Model not loaded", $this);
472
473
		return $this;
474
	}
475
476
	/**
477
	 * Whether or not the model is saved
478
	 *
479
	 * @return  boolean
480
	 */
481 10
	public function saved()
482
	{
483 10
		return $this->_saved;
484
	}
485
486
	/**
487
	 * Whether or not the model is in the process of being saved
488
	 *
489
	 * @return  boolean
490
	 */
491 3
	public function is_saving()
492
	{
493 3
		return $this->_is_saving;
494
	}
495
496
	/**
497
	 * Whether or not the model is deleted
498
	 *
499
	 * @return  boolean
500
	 */
501
	public function deleted()
502
	{
503
		return $this->_deleted;
504
	}
505
506
	/**
507
	 * Build a new model object based on the current one, but without an ID, so it can be saved as a new object
508
	 * @return Jam_Model
509
	 */
510 1
	public function duplicate()
511
	{
512 1
		$fields = $this->as_array();
513
514 1
		unset($fields[$this->meta()->primary_key()]);
515
516 1
		return Jam::build($this->meta()->model(), $fields);
517
	}
518
519
	/**
520
	 * Returns a string representation of the model in the
521
	 * form of `Model_Name (id)` or `Model_Name (NULL)` if
522
	 * the model is not loaded.
523
	 *
524
	 * This is designed to be useful for debugging.
525
	 *
526
	 * @return  string
527
	 */
528 4
	public function __toString()
529
	{
530 4
		return (string) get_class($this).'('.($this->loaded() ? $this->id() : 'NULL').')';
531
	}
532
533 754
	public function serialize()
534
	{
535 754
		$this->_move_retrieved_to_changed();
536
537 754
		return serialize(array(
538 754
			'original' => $this->_original,
539 754
			'changed' => $this->_changed,
540 754
			'unmapped' => $this->_unmapped,
541 754
			'saved' => $this->_saved,
542 754
			'loaded' => $this->_loaded,
543 754
			'deleted' => $this->_deleted,
544
		));
545
	}
546
547 754
	public function unserialize($data)
548
	{
549 754
		$data = unserialize($data);
550
551 754
		$this->_meta = Jam::meta($this);
552 754
		$this->_original = Arr::merge($this->meta()->defaults(), $data['original']);
553 754
		$this->_changed = $data['changed'];
554 754
		$this->_unmapped = $data['unmapped'];
555 754
		$this->_saved = $data['saved'];
556 754
		$this->_loaded = $data['loaded'];
557 754
		$this->_deleted = $data['deleted'];
558
559 754
		foreach ($this->_changed as $name => $attribute)
560
		{
561 754
			$association = $this->meta()->association($name);
562 754
			if ($association AND $association instanceof Jam_Association_Collection)
563
			{
564 754
				$association->assign_internals($this, $attribute);
565
			}
566
		}
567 754
	}
568
}
569