Completed
Push — master ( 1eb4e7...fc353d )
by Sam
14:06
created

FixtureBlueprint::getClass()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
use SilverStripe\ORM\DataModel;
4
use SilverStripe\ORM\DB;
5
use SilverStripe\ORM\DataObject;
6
/**
7
 * A blueprint on how to create instances of a certain {@link DataObject} subclass.
8
 *
9
 * Relies on a {@link FixtureFactory} to manage database relationships between instances,
10
 * and manage the mappings between fixture identifiers and their database IDs.
11
 *
12
 * @package framework
13
 * @subpackage core
14
 */
15
class FixtureBlueprint {
16
17
	/**
18
	 * @var array Map of field names to values. Supersedes {@link DataObject::$defaults}.
19
	 */
20
	protected $defaults = array();
21
22
	/**
23
	 * @var String Arbitrary name by which this fixture type can be referenced.
24
	 */
25
	protected $name;
26
27
	/**
28
	 * @var String Subclass of {@link DataObject}
29
	 */
30
	protected $class;
31
32
	/**
33
	 * @var array
34
	 */
35
	protected $callbacks = array(
36
		'beforeCreate' => array(),
37
		'afterCreate' => array(),
38
	);
39
40
	/** @config */
41
	private static $dependencies = array(
42
		'factory' => '%$FixtureFactory'
43
	);
44
45
	/**
46
	 * @param String $name
47
	 * @param String $class Defaults to $name
48
	 * @param array $defaults
49
	 */
50
	public function __construct($name, $class = null, $defaults = array()) {
51
		if(!$class) $class = $name;
0 ignored issues
show
Bug Best Practice introduced by
The expression $class of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null 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...
52
53
		if(!is_subclass_of($class, 'SilverStripe\\ORM\\DataObject')) {
54
			throw new InvalidArgumentException(sprintf(
55
				'Class "%s" is not a valid subclass of DataObject',
56
				$class
57
			));
58
		}
59
60
		$this->name = $name;
61
		$this->class = $class;
62
		$this->defaults = $defaults;
63
	}
64
65
	/**
66
	 * @param string $identifier Unique identifier for this fixture type
67
	 * @param array $data Map of property names to their values.
68
	 * @param array $fixtures Map of fixture names to an associative array of their in-memory
69
	 *                        identifiers mapped to their database IDs. Used to look up
70
	 *                        existing fixtures which might be referenced in the $data attribute
71
	 *                        via the => notation.
72
	 * @return DataObject
73
	 * @throws Exception
74
	 */
75
	public function createObject($identifier, $data = null, $fixtures = null) {
76
		// We have to disable validation while we import the fixtures, as the order in
77
		// which they are imported doesnt guarantee valid relations until after the import is complete.
78
		// Also disable filesystem manipulations
79
		Config::nest();
80
		Config::inst()->update('SilverStripe\\ORM\\DataObject', 'validation_enabled', false);
81
		Config::inst()->update('File', 'update_filesystem', false);
82
83
		$this->invokeCallbacks('beforeCreate', array($identifier, &$data, &$fixtures));
84
85
		try {
86
			$class = $this->class;
87
			$obj = DataModel::inst()->$class->newObject();
88
89
			// If an ID is explicitly passed, then we'll sort out the initial write straight away
90
			// This is just in case field setters triggered by the population code in the next block
91
			// Call $this->write().  (For example, in FileTest)
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
92
			if(isset($data['ID'])) {
93
				$obj->ID = $data['ID'];
94
95
				// The database needs to allow inserting values into the foreign key column (ID in our case)
96
				$conn = DB::get_conn();
97
				$baseTable = DataObject::getSchema()->baseDataTable($class);
98
				if(method_exists($conn, 'allowPrimaryKeyEditing')) {
99
					$conn->allowPrimaryKeyEditing($baseTable, true);
100
				}
101
				$obj->write(false, true);
102
				if(method_exists($conn, 'allowPrimaryKeyEditing')) {
103
					$conn->allowPrimaryKeyEditing($baseTable, false);
104
				}
105
			}
106
107
			// Populate defaults
108
			if($this->defaults) foreach($this->defaults as $fieldName => $fieldVal) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->defaults 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...
109
				if(isset($data[$fieldName]) && $data[$fieldName] !== false) continue;
110
111
				if(is_callable($fieldVal)) {
112
					$obj->$fieldName = $fieldVal($obj, $data, $fixtures);
113
				} else {
114
					$obj->$fieldName = $fieldVal;
115
				}
116
			}
117
118
			// Populate overrides
119
			if($data) foreach($data as $fieldName => $fieldVal) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data 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...
120
				// Defer relationship processing
121
				if(
122
					$obj->manyManyComponent($fieldName)
123
					|| $obj->hasManyComponent($fieldName)
124
					|| $obj->hasOneComponent($fieldName)
125
				) {
126
					continue;
127
				}
128
129
				$this->setValue($obj, $fieldName, $fieldVal, $fixtures);
130
			}
131
132
			$obj->write();
133
134
			// Save to fixture before relationship processing in case of reflexive relationships
135
			if(!isset($fixtures[$class])) {
136
				$fixtures[$class] = array();
137
			}
138
			$fixtures[$class][$identifier] = $obj->ID;
139
140
			// Populate all relations
141
			if($data) foreach($data as $fieldName => $fieldVal) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data 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...
142
				$isManyMany = $obj->manyManyComponent($fieldName);
143
				$isHasMany = $obj->hasManyComponent($fieldName);
144
				if ($isManyMany && $isHasMany) {
145
					throw new InvalidArgumentException("$fieldName is both many_many and has_many");
146
				}
147
				if($isManyMany || $isHasMany) {
148
					$obj->write();
149
150
					// Many many components need a little extra work to extract extrafields
151
					if(is_array($fieldVal) && $isManyMany) {
152
						// handle lists of many_many relations. Each item can
153
						// specify the many_many_extraFields against each
154
						// related item.
155
						foreach($fieldVal as $relVal) {
156
							// Check for many_many_extrafields
157
							$extrafields = [];
158
							if (is_array($relVal)) {
159
								// Item is either first row, or key in yet another nested array
160
								$item = key($relVal);
161
								if (is_array($relVal[$item]) && count($relVal) === 1) {
162
									// Extra fields from nested array
163
									$extrafields = $relVal[$item];
164
								} else {
165
									// Extra fields from subsequent items
166
									array_shift($relVal);
167
									$extrafields = $relVal;
168
								}
169
							} else {
170
								$item = $relVal;
171
							}
172
							$id = $this->parseValue($item, $fixtures);
173
174
							$obj->getManyManyComponents($fieldName)->add(
175
								$id, $extrafields
176
							);
177
						}
178
					} else {
179
						$items = is_array($fieldVal)
180
							? $fieldVal
181
							: preg_split('/ *, */',trim($fieldVal));
182
183
						$parsedItems = [];
184
						foreach($items as $item) {
185
							// Check for correct format: =><relationname>.<identifier>.
186
							// Ignore if the item has already been replaced with a numeric DB identifier
187
							if(!is_numeric($item) && !preg_match('/^=>[^\.]+\.[^\.]+/', $item)) {
188
								throw new InvalidArgumentException(sprintf(
189
									'Invalid format for relation "%s" on class "%s" ("%s")',
190
									$fieldName,
191
									$class,
192
									$item
193
								));
194
							}
195
196
							$parsedItems[] = $this->parseValue($item, $fixtures);
197
						}
198
199
						if($isHasMany) {
200
							$obj->getComponents($fieldName)->setByIDList($parsedItems);
201
						} elseif($isManyMany) {
202
							$obj->getManyManyComponents($fieldName)->setByIDList($parsedItems);
203
						}
204
					}
205
				} else {
206
					$hasOneField = preg_replace('/ID$/', '', $fieldName);
207
					if($className = $obj->hasOneComponent($hasOneField)) {
208
						$obj->{$hasOneField.'ID'} = $this->parseValue($fieldVal, $fixtures, $fieldClass);
209
						// Inject class for polymorphic relation
210
						if($className === 'SilverStripe\\ORM\\DataObject') {
211
							$obj->{$hasOneField.'Class'} = $fieldClass;
212
						}
213
					}
214
				}
215
			}
216
			$obj->write();
217
218
			// If LastEdited was set in the fixture, set it here
219
			if($data && array_key_exists('LastEdited', $data)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data 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...
220
				$this->overrideField($obj, 'LastEdited', $data['LastEdited'], $fixtures);
221
			}
222
		} catch(Exception $e) {
223
			Config::unnest();
224
			throw $e;
225
		}
226
227
		Config::unnest();
228
		$this->invokeCallbacks('afterCreate', array($obj, $identifier, &$data, &$fixtures));
229
230
		return $obj;
231
	}
232
233
	/**
234
	 * @param array $defaults
235
	 * @return $this
236
	 */
237
	public function setDefaults($defaults) {
238
		$this->defaults = $defaults;
239
		return $this;
240
	}
241
242
	/**
243
	 * @return array
244
	 */
245
	public function getDefaults() {
246
		return $this->defaults;
247
	}
248
249
	/**
250
	 * @return string
251
	 */
252
	public function getClass() {
253
		return $this->class;
254
	}
255
256
	/**
257
	 * See class documentation.
258
	 *
259
	 * @param string $type
260
	 * @param callable $callback
261
	 * @return $this
262
	 */
263
	public function addCallback($type, $callback) {
264
		if(!array_key_exists($type, $this->callbacks)) {
265
			throw new InvalidArgumentException(sprintf('Invalid type "%s"', $type));
266
		}
267
268
		$this->callbacks[$type][] = $callback;
269
		return $this;
270
	}
271
272
	/**
273
	 * @param string $type
274
	 * @param callable $callback
275
	 * @return $this
276
	 */
277
	public function removeCallback($type, $callback) {
278
		$pos = array_search($callback, $this->callbacks[$type]);
279
		if($pos !== false) {
280
			unset($this->callbacks[$type][$pos]);
281
		}
282
283
		return $this;
284
	}
285
286
	protected function invokeCallbacks($type, $args = array()) {
287
		foreach($this->callbacks[$type] as $callback) {
288
			call_user_func_array($callback, $args);
289
		}
290
	}
291
292
	/**
293
	 * Parse a value from a fixture file.  If it starts with =>
294
	 * it will get an ID from the fixture dictionary
295
	 *
296
	 * @param string $value
297
	 * @param array $fixtures See {@link createObject()}
298
	 * @param string $class If the value parsed is a class relation, this parameter
299
	 * will be given the value of that class's name
300
	 * @return string Fixture database ID, or the original value
301
	 */
302
	protected function parseValue($value, $fixtures = null, &$class = null) {
0 ignored issues
show
Unused Code introduced by
The parameter $class is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
303
		if(substr($value,0,2) == '=>') {
304
			// Parse a dictionary reference - used to set foreign keys
305
			list($class, $identifier) = explode('.', substr($value,2), 2);
306
307
			if($fixtures && !isset($fixtures[$class][$identifier])) {
308
				throw new InvalidArgumentException(sprintf(
309
					'No fixture definitions found for "%s"',
310
					$value
311
				));
312
			}
313
314
			return $fixtures[$class][$identifier];
315
		} else {
316
			// Regular field value setting
317
			return $value;
318
		}
319
	}
320
321
	protected function setValue($obj, $name, $value, $fixtures = null) {
322
		$obj->$name = $this->parseValue($value, $fixtures);
323
	}
324
325
	protected function overrideField($obj, $fieldName, $value, $fixtures = null) {
326
		$class = get_class($obj);
327
		$table = DataObject::getSchema()->tableForField($class, $fieldName);
328
		$value = $this->parseValue($value, $fixtures);
329
330
		DB::manipulate(array(
331
			$table => array(
332
				"command" => "update",
333
				"id" => $obj->ID,
334
				"class" => $class,
335
				"fields" => array($fieldName => $value),
336
			)
337
		));
338
		$obj->$fieldName = $value;
339
	}
340
341
}
342