Completed
Pull Request — master (#85)
by
unknown
11:52 queued 09:51
created

Kohana_Jam_Model::__isset()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
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 440
	public function __construct($meta_name = NULL)
45
	{
46 440
		parent::__construct($meta_name);
47
48 440
		$this->meta()->events()->trigger('model.before_construct', $this);
49
50
		// Copy over the defaults into the original data.
51 440
		$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 440
		$this->meta()->events()->trigger('model.after_construct', $this);
54 440
	}
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 755
	protected function _move_retrieved_to_changed()
204
	{
205 755
		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 755
	}
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 20
	public function save($validate = NULL)
243
	{
244 20
		if ($this->_is_saving)
245
			throw new Kohana_Exception("Cannot save a model that is already in the process of saving");
246
247
248 20
		$key = $this->_original[$this->meta()->primary_key()];
249
250
		// Run validation
251 20
		if ($validate !== FALSE && !$this->check())
252
		{
253
			throw new Jam_Exception_Validation('There was an error validating the :model: :errors', $this);
254
		}
255
256 20
		$this->_is_saving = TRUE;
257
258
		// These will be processed later
259 20
		$values = $defaults = array();
260
261 20
		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 20
		$event_type = $key ? 'update' : 'create';
268
269 20
		if ($this->meta()->events()->trigger('model.before_'.$event_type, $this, array($this->_changed)) === FALSE)
270
		{
271
			return $this;
272
		}
273
274 20
		$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 20
		foreach (array_merge($this->_original, $this->_changed) as $column => $value)
280
		{
281 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...
282
			{
283
				// Only save in_db values
284 20
				if ($field->in_db)
285
				{
286
					// See if field wants to alter the value on save()
287 20
					$value = $field->convert($this, $value, $key);
288
289
					// Only set the value to be saved if it's changed from the original
290 20
					if ($value !== $this->_original[$column])
291
					{
292 17
						$values[$field->column] = $value;
293
					}
294
					// Or if we're INSERTing and we need to set the defaults for the first time
295 20
					elseif ( ! $key AND ( ! $this->changed($field->name) OR $field->default === $value) AND ! $field->primary)
296
					{
297 20
						$defaults[$field->column] = $field->default;
298
					}
299
				}
300
			}
301
		}
302
303
		// If we have a key, we're updating
304 20
		if ($key)
305
		{
306
			// Do we even have to update anything in the row?
307 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...
308
			{
309 3
				Jam::update($this)
310 3
					->where_key($key)
311 3
					->set($values)
312 4
					->execute();
313
			}
314
		}
315
		else
316
		{
317 18
			$insert_values = array_merge($defaults, $values);
318 18
			list($id) = Jam::insert($this)
319 18
				->columns(array_keys($insert_values))
320 18
				->values(array_values($insert_values))
321 18
				->execute();
322
323
			// Gotta make sure to set this
324 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...
325
		}
326
327
		// Re-set any saved values; they may have changed
328 20
		$this->set($values);
329
330 20
		$this->_loaded = $this->_saved = TRUE;
331
332 20
		$this->meta()->events()->trigger('model.after_save', $this, array($this->_changed, $event_type));
333
334 20
		$this->meta()->events()->trigger('model.after_'.$event_type, $this, array($this->_changed));
335
336
		// Set the changed data back as original
337 20
		$this->_original = array_merge($this->_original, $this->_changed);
338
339 20
		$this->_changed = array();
340
341 20
		foreach ($this->_retrieved as $name => $retrieved)
342
		{
343 17
			if (($association = $this->meta()->association($name)))
344
			{
345 2
				if ($association instanceof Jam_Association_Collection)
346
				{
347 2
					$retrieved->clear_changed();
348
				}
349
			}
350 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...
351
			{
352 17
				unset($this->_retrieved[$name]);
353
			}
354
		}
355
356 20
		$this->_is_saving = FALSE;
357
358 20
		return $this;
359
	}
360
361
	/**
362
	 * Deletes a single record.
363
	 *
364
	 * @return  boolean
365
	 **/
366 10
	public function delete()
367
	{
368 10
		$result = FALSE;
369
370
		// Are we loaded? Then we're just deleting this record
371 10
		if ($this->_loaded)
372
		{
373 10
			$key = $this->_original[$this->meta()->primary_key()];
374
375 10
			if (($result = $this->meta()->events()->trigger('model.before_delete', $this)) !== FALSE)
376
			{
377 10
				$result = Jam::delete($this)->where_key($key)->execute();
378
			}
379
		}
380
381
		// Trigger the after-delete
382 10
		$this->meta()->events()->trigger('model.after_delete', $this, array($result));
383
384
		// Clear the object so it appears deleted anyway
385 10
		$this->clear();
386
387
		// Set the flag indicatig the model has been successfully deleted
388 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...
389
390 10
		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 197
	public function clear()
419
	{
420 197
		$this->_loaded =
421 197
		$this->_saved  = FALSE;
422
423 197
		parent::clear();
424
425 197
		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 93
	public function loaded()
464
	{
465 93
		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 4
	public function is_saving()
492
	{
493 4
		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 755
	public function serialize()
534
	{
535 755
		$this->_move_retrieved_to_changed();
536
537 755
		return serialize(array(
538 755
			'original' => $this->_original,
539 755
			'changed' => $this->_changed,
540 755
			'unmapped' => $this->_unmapped,
541 755
			'saved' => $this->_saved,
542 755
			'loaded' => $this->_loaded,
543 755
			'deleted' => $this->_deleted,
544
		));
545
	}
546
547 755
	public function unserialize($data)
548
	{
549 755
		$data = unserialize($data);
550
551 755
		$this->_meta = Jam::meta($this);
552 755
		$this->_original = Arr::merge($this->meta()->defaults(), $data['original']);
553 755
		$this->_changed = $data['changed'];
554 755
		$this->_unmapped = $data['unmapped'];
555 755
		$this->_saved = $data['saved'];
556 755
		$this->_loaded = $data['loaded'];
557 755
		$this->_deleted = $data['deleted'];
558
559 755
		foreach ($this->_changed as $name => $attribute)
560
		{
561 755
			$association = $this->meta()->association($name);
562 755
			if ($association AND $association instanceof Jam_Association_Collection)
563
			{
564 755
				$association->assign_internals($this, $attribute);
565
			}
566
		}
567 755
	}
568
}
569