Completed
Push — hash-nonce ( 07e2e8 )
by Sam
08:52
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
 * A blueprint on how to create instances of a certain {@link DataObject} subclass.
4
 *
5
 * Relies on a {@link FixtureFactory} to manage database relationships between instances,
6
 * and manage the mappings between fixture identifiers and their database IDs.
7
 *
8
 * @package framework
9
 * @subpackage core
10
 */
11
class FixtureBlueprint {
12
13
	/**
14
	 * @var array Map of field names to values. Supersedes {@link DataObject::$defaults}.
15
	 */
16
	protected $defaults = array();
17
18
	/**
19
	 * @var String Arbitrary name by which this fixture type can be referenced.
20
	 */
21
	protected $name;
22
23
	/**
24
	 * @var String Subclass of {@link DataObject}
25
	 */
26
	protected $class;
27
28
	/**
29
	 * @var array
30
	 */
31
	protected $callbacks = array(
32
		'beforeCreate' => array(),
33
		'afterCreate' => array(),
34
	);
35
36
	/** @config */
37
	private static $dependencies = array(
38
		'factory' => '%$FixtureFactory'
39
	);
40
41
	/**
42
	 * @param String $name
43
	 * @param String $class Defaults to $name
44
	 * @param array $defaults
45
	 */
46
	public function __construct($name, $class = null, $defaults = array()) {
47
		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...
48
49
		if(!is_subclass_of($class, 'DataObject')) {
50
			throw new InvalidArgumentException(sprintf(
51
				'Class "%s" is not a valid subclass of DataObject',
52
				$class
53
			));
54
		}
55
56
		$this->name = $name;
57
		$this->class = $class;
58
		$this->defaults = $defaults;
59
	}
60
61
	/**
62
	 * @param String $identifier Unique identifier for this fixture type
63
	 * @param Array $data Map of property names to their values.
64
	 * @param Array $fixtures Map of fixture names to an associative array of their in-memory
65
	 *                        identifiers mapped to their database IDs. Used to look up
66
	 *                        existing fixtures which might be referenced in the $data attribute
67
	 *                        via the => notation.
68
	 * @return DataObject
69
	 */
70
	public function createObject($identifier, $data = null, $fixtures = null) {
71
		// We have to disable validation while we import the fixtures, as the order in
72
		// which they are imported doesnt guarantee valid relations until after the import is complete.
73
		$validationenabled = Config::inst()->get('DataObject', 'validation_enabled');
74
		Config::inst()->update('DataObject', 'validation_enabled', false);
75
76
		$this->invokeCallbacks('beforeCreate', array($identifier, &$data, &$fixtures));
77
78
		try {
79
			$class = $this->class;
80
			$obj = DataModel::inst()->$class->newObject();
81
82
			// If an ID is explicitly passed, then we'll sort out the initial write straight away
83
			// This is just in case field setters triggered by the population code in the next block
84
			// Call $this->write().  (For example, in FileTest)
85
			if(isset($data['ID'])) {
86
				$obj->ID = $data['ID'];
87
88
				// The database needs to allow inserting values into the foreign key column (ID in our case)
89
				$conn = DB::get_conn();
90
				if(method_exists($conn, 'allowPrimaryKeyEditing')) {
91
					$conn->allowPrimaryKeyEditing(ClassInfo::baseDataClass($class), true);
92
				}
93
				$obj->write(false, true);
94
				if(method_exists($conn, 'allowPrimaryKeyEditing')) {
95
					$conn->allowPrimaryKeyEditing(ClassInfo::baseDataClass($class), false);
96
				}
97
			}
98
99
			// Populate defaults
100
			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...
101
				if(isset($data[$fieldName]) && $data[$fieldName] !== false) continue;
102
103
				if(is_callable($fieldVal)) {
104
					$obj->$fieldName = $fieldVal($obj, $data, $fixtures);
105
				} else {
106
					$obj->$fieldName = $fieldVal;
107
				}
108
			}
109
110
			// Populate overrides
111
			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...
112
				// Defer relationship processing
113
				if(
114
					$obj->manyManyComponent($fieldName)
115
					|| $obj->hasManyComponent($fieldName)
116
					|| $obj->hasOneComponent($fieldName)
117
				) {
118
					continue;
119
				}
120
121
				$this->setValue($obj, $fieldName, $fieldVal, $fixtures);
122
			}
123
124
			$obj->write();
125
126
			// Save to fixture before relationship processing in case of reflexive relationships
127
			if(!isset($fixtures[$class])) {
128
				$fixtures[$class] = array();
129
			}
130
			$fixtures[$class][$identifier] = $obj->ID;
131
132
			// Populate all relations
133
			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...
134
				if($obj->manyManyComponent($fieldName) || $obj->hasManyComponent($fieldName)) {
135
					$obj->write();
136
137
					$parsedItems = array();
138
139
					if(is_array($fieldVal)) {
140
						// handle lists of many_many relations. Each item can
141
						// specify the many_many_extraFields against each
142
						// related item.
143
						foreach($fieldVal as $relVal) {
144
							$item = key($relVal);
145
							$id = $this->parseValue($item, $fixtures);
146
							$parsedItems[] = $id;
147
148
							array_shift($relVal);
149
150
							$obj->getManyManyComponents($fieldName)->add(
151
								$id, $relVal
152
							);
153
						}
154
					} else {
155
						$items = preg_split('/ *, */',trim($fieldVal));
156
157
						foreach($items as $item) {
158
							// Check for correct format: =><relationname>.<identifier>.
159
							// Ignore if the item has already been replaced with a numeric DB identifier
160
							if(!is_numeric($item) && !preg_match('/^=>[^\.]+\.[^\.]+/', $item)) {
161
								throw new InvalidArgumentException(sprintf(
162
									'Invalid format for relation "%s" on class "%s" ("%s")',
163
									$fieldName,
164
									$class,
165
									$item
166
								));
167
							}
168
169
							$parsedItems[] = $this->parseValue($item, $fixtures);
170
						}
171
172
						if($obj->hasManyComponent($fieldName)) {
173
							$obj->getComponents($fieldName)->setByIDList($parsedItems);
174
						} elseif($obj->manyManyComponent($fieldName)) {
175
							$obj->getManyManyComponents($fieldName)->setByIDList($parsedItems);
176
						}
177
					}
178
				} else {
179
					$hasOneField = preg_replace('/ID$/', '', $fieldName);
180
					if($className = $obj->hasOneComponent($hasOneField)) {
181
						$obj->{$hasOneField.'ID'} = $this->parseValue($fieldVal, $fixtures, $fieldClass);
182
						// Inject class for polymorphic relation
183
						if($className === 'DataObject') {
184
							$obj->{$hasOneField.'Class'} = $fieldClass;
185
						}
186
					}
187
				}
188
			}
189
			$obj->write();
190
191
			// If LastEdited was set in the fixture, set it here
192
			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...
193
				$this->overrideField($obj, 'LastEdited', $data['LastEdited'], $fixtures);
194
			}
195
196
			// Ensure Folder objects exist physically, as otherwise future File fixtures can't detect them
197
			if($obj instanceof Folder) {
198
				Filesystem::makeFolder($obj->getFullPath());
199
			}
200
		} catch(Exception $e) {
201
			Config::inst()->update('DataObject', 'validation_enabled', $validationenabled);
202
			throw $e;
203
		}
204
205
		Config::inst()->update('DataObject', 'validation_enabled', $validationenabled);
206
207
		$this->invokeCallbacks('afterCreate', array($obj, $identifier, &$data, &$fixtures));
208
209
		return $obj;
210
	}
211
212
	/**
213
	 * @param Array $defaults
214
	 */
215
	public function setDefaults($defaults) {
216
		$this->defaults = $defaults;
217
		return $this;
218
	}
219
220
	/**
221
	 * @return Array
222
	 */
223
	public function getDefaults() {
224
		return $this->defaults;
225
	}
226
227
	/**
228
	 * @return String
229
	 */
230
	public function getClass() {
231
		return $this->class;
232
	}
233
234
	/**
235
	 * See class documentation.
236
	 *
237
	 * @param String $type
238
	 * @param callable $callback
239
	 */
240
	public function addCallback($type, $callback) {
241
		if(!array_key_exists($type, $this->callbacks)) {
242
			throw new InvalidArgumentException(sprintf('Invalid type "%s"', $type));
243
		}
244
245
		$this->callbacks[$type][] = $callback;
246
		return $this;
247
	}
248
249
	/**
250
	 * @param String $type
251
	 * @param callable $callback
252
	 */
253
	public function removeCallback($type, $callback) {
254
		$pos = array_search($callback, $this->callbacks[$type]);
255
		if($pos !== false) unset($this->callbacks[$type][$pos]);
256
257
		return $this;
258
	}
259
260
	protected function invokeCallbacks($type, $args = array()) {
261
		foreach($this->callbacks[$type] as $callback) {
262
			call_user_func_array($callback, $args);
263
		}
264
	}
265
266
	/**
267
	 * Parse a value from a fixture file.  If it starts with =>
268
	 * it will get an ID from the fixture dictionary
269
	 *
270
	 * @param string $fieldVal
0 ignored issues
show
Bug introduced by
There is no parameter named $fieldVal. 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...
271
	 * @param array $fixtures See {@link createObject()}
272
	 * @param string $class If the value parsed is a class relation, this parameter
273
	 * will be given the value of that class's name
274
	 * @return string Fixture database ID, or the original value
275
	 */
276
	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...
277
		if(substr($value,0,2) == '=>') {
278
			// Parse a dictionary reference - used to set foreign keys
279
			list($class, $identifier) = explode('.', substr($value,2), 2);
280
281
			if($fixtures && !isset($fixtures[$class][$identifier])) {
282
				throw new InvalidArgumentException(sprintf(
283
					'No fixture definitions found for "%s"',
284
					$value
285
				));
286
			}
287
288
			return $fixtures[$class][$identifier];
289
		} else {
290
			// Regular field value setting
291
			return $value;
292
		}
293
	}
294
295
	protected function setValue($obj, $name, $value, $fixtures = null) {
296
		$obj->$name = $this->parseValue($value, $fixtures);
297
	}
298
299
	protected function overrideField($obj, $fieldName, $value, $fixtures = null) {
300
		$table = ClassInfo::table_for_object_field(get_class($obj), $fieldName);
301
		$value = $this->parseValue($value, $fixtures);
302
303
		DB::manipulate(array(
304
			$table => array(
305
				"command" => "update", "id" => $obj->ID,
306
				"fields" => array($fieldName => $value)
307
			)
308
		));
309
		$obj->$fieldName = $value;
310
	}
311
312
}
313