mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-08-28 21:46:10 +02:00
BUG Fix issue with parsing of extrafields in fixtures BUG Fix issue in duplicate relation name, and ensure FixtureBlueprint fails on these
332 lines
9.7 KiB
PHP
332 lines
9.7 KiB
PHP
<?php
|
|
/**
|
|
* A blueprint on how to create instances of a certain {@link DataObject} subclass.
|
|
*
|
|
* Relies on a {@link FixtureFactory} to manage database relationships between instances,
|
|
* and manage the mappings between fixture identifiers and their database IDs.
|
|
*
|
|
* @package framework
|
|
* @subpackage core
|
|
*/
|
|
class FixtureBlueprint {
|
|
|
|
/**
|
|
* @var array Map of field names to values. Supersedes {@link DataObject::$defaults}.
|
|
*/
|
|
protected $defaults = array();
|
|
|
|
/**
|
|
* @var String Arbitrary name by which this fixture type can be referenced.
|
|
*/
|
|
protected $name;
|
|
|
|
/**
|
|
* @var String Subclass of {@link DataObject}
|
|
*/
|
|
protected $class;
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected $callbacks = array(
|
|
'beforeCreate' => array(),
|
|
'afterCreate' => array(),
|
|
);
|
|
|
|
/** @config */
|
|
private static $dependencies = array(
|
|
'factory' => '%$FixtureFactory'
|
|
);
|
|
|
|
/**
|
|
* @param String $name
|
|
* @param String $class Defaults to $name
|
|
* @param array $defaults
|
|
*/
|
|
public function __construct($name, $class = null, $defaults = array()) {
|
|
if(!$class) $class = $name;
|
|
|
|
if(!is_subclass_of($class, 'DataObject')) {
|
|
throw new InvalidArgumentException(sprintf(
|
|
'Class "%s" is not a valid subclass of DataObject',
|
|
$class
|
|
));
|
|
}
|
|
|
|
$this->name = $name;
|
|
$this->class = $class;
|
|
$this->defaults = $defaults;
|
|
}
|
|
|
|
/**
|
|
* @param String $identifier Unique identifier for this fixture type
|
|
* @param Array $data Map of property names to their values.
|
|
* @param Array $fixtures Map of fixture names to an associative array of their in-memory
|
|
* identifiers mapped to their database IDs. Used to look up
|
|
* existing fixtures which might be referenced in the $data attribute
|
|
* via the => notation.
|
|
* @return DataObject
|
|
*/
|
|
public function createObject($identifier, $data = null, $fixtures = null) {
|
|
// We have to disable validation while we import the fixtures, as the order in
|
|
// which they are imported doesnt guarantee valid relations until after the import is complete.
|
|
$validationenabled = Config::inst()->get('DataObject', 'validation_enabled');
|
|
Config::inst()->update('DataObject', 'validation_enabled', false);
|
|
|
|
$this->invokeCallbacks('beforeCreate', array($identifier, &$data, &$fixtures));
|
|
|
|
try {
|
|
$class = $this->class;
|
|
$obj = DataModel::inst()->$class->newObject();
|
|
|
|
// If an ID is explicitly passed, then we'll sort out the initial write straight away
|
|
// This is just in case field setters triggered by the population code in the next block
|
|
// Call $this->write(). (For example, in FileTest)
|
|
if(isset($data['ID'])) {
|
|
$obj->ID = $data['ID'];
|
|
|
|
// The database needs to allow inserting values into the foreign key column (ID in our case)
|
|
$conn = DB::get_conn();
|
|
if(method_exists($conn, 'allowPrimaryKeyEditing')) {
|
|
$conn->allowPrimaryKeyEditing(ClassInfo::baseDataClass($class), true);
|
|
}
|
|
$obj->write(false, true);
|
|
if(method_exists($conn, 'allowPrimaryKeyEditing')) {
|
|
$conn->allowPrimaryKeyEditing(ClassInfo::baseDataClass($class), false);
|
|
}
|
|
}
|
|
|
|
// Populate defaults
|
|
if($this->defaults) foreach($this->defaults as $fieldName => $fieldVal) {
|
|
if(isset($data[$fieldName]) && $data[$fieldName] !== false) continue;
|
|
|
|
if(is_callable($fieldVal)) {
|
|
$obj->$fieldName = $fieldVal($obj, $data, $fixtures);
|
|
} else {
|
|
$obj->$fieldName = $fieldVal;
|
|
}
|
|
}
|
|
|
|
// Populate overrides
|
|
if($data) foreach($data as $fieldName => $fieldVal) {
|
|
// Defer relationship processing
|
|
if(
|
|
$obj->manyManyComponent($fieldName)
|
|
|| $obj->hasManyComponent($fieldName)
|
|
|| $obj->hasOneComponent($fieldName)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
$this->setValue($obj, $fieldName, $fieldVal, $fixtures);
|
|
}
|
|
|
|
$obj->write();
|
|
|
|
// Save to fixture before relationship processing in case of reflexive relationships
|
|
if(!isset($fixtures[$class])) {
|
|
$fixtures[$class] = array();
|
|
}
|
|
$fixtures[$class][$identifier] = $obj->ID;
|
|
|
|
// Populate all relations
|
|
if($data) foreach($data as $fieldName => $fieldVal) {
|
|
$isManyMany = $obj->manyManyComponent($fieldName);
|
|
$isHasMany = $obj->hasManyComponent($fieldName);
|
|
if ($isManyMany && $isHasMany) {
|
|
throw new InvalidArgumentException("$fieldName is both many_many and has_many");
|
|
}
|
|
if($isManyMany || $isHasMany) {
|
|
$obj->write();
|
|
|
|
// Many many components need a little extra work to extract extrafields
|
|
if(is_array($fieldVal) && $isManyMany) {
|
|
// handle lists of many_many relations. Each item can
|
|
// specify the many_many_extraFields against each
|
|
// related item.
|
|
foreach($fieldVal as $relVal) {
|
|
// Check for many_many_extrafields
|
|
$extrafields = array();
|
|
if (is_array($relVal)) {
|
|
// Item is either first row, or key in yet another nested array
|
|
$item = key($relVal);
|
|
if (is_array($relVal[$item]) && count($relVal) === 1) {
|
|
// Extra fields from nested array
|
|
$extrafields = $relVal[$item];
|
|
} else {
|
|
// Extra fields from subsequent items
|
|
array_shift($relVal);
|
|
$extrafields = $relVal;
|
|
}
|
|
} else {
|
|
$item = $relVal;
|
|
}
|
|
$id = $this->parseValue($item, $fixtures);
|
|
|
|
$obj->getManyManyComponents($fieldName)->add(
|
|
$id, $extrafields
|
|
);
|
|
}
|
|
} else {
|
|
$items = is_array($fieldVal)
|
|
? $fieldVal
|
|
: preg_split('/ *, */',trim($fieldVal));
|
|
|
|
$parsedItems = array();
|
|
foreach($items as $item) {
|
|
// Check for correct format: =><relationname>.<identifier>.
|
|
// Ignore if the item has already been replaced with a numeric DB identifier
|
|
if(!is_numeric($item) && !preg_match('/^=>[^\.]+\.[^\.]+/', $item)) {
|
|
throw new InvalidArgumentException(sprintf(
|
|
'Invalid format for relation "%s" on class "%s" ("%s")',
|
|
$fieldName,
|
|
$class,
|
|
$item
|
|
));
|
|
}
|
|
|
|
$parsedItems[] = $this->parseValue($item, $fixtures);
|
|
}
|
|
|
|
if($isHasMany) {
|
|
$obj->getComponents($fieldName)->setByIDList($parsedItems);
|
|
} elseif($isManyMany) {
|
|
$obj->getManyManyComponents($fieldName)->setByIDList($parsedItems);
|
|
}
|
|
}
|
|
} else {
|
|
$hasOneField = preg_replace('/ID$/', '', $fieldName);
|
|
if($className = $obj->hasOneComponent($hasOneField)) {
|
|
$obj->{$hasOneField.'ID'} = $this->parseValue($fieldVal, $fixtures, $fieldClass);
|
|
// Inject class for polymorphic relation
|
|
if($className === 'DataObject') {
|
|
$obj->{$hasOneField.'Class'} = $fieldClass;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$obj->write();
|
|
|
|
// If LastEdited was set in the fixture, set it here
|
|
if($data && array_key_exists('LastEdited', $data)) {
|
|
$this->overrideField($obj, 'LastEdited', $data['LastEdited'], $fixtures);
|
|
}
|
|
|
|
// Ensure Folder objects exist physically, as otherwise future File fixtures can't detect them
|
|
if($obj instanceof Folder) {
|
|
Filesystem::makeFolder($obj->getFullPath());
|
|
}
|
|
} catch(Exception $e) {
|
|
Config::inst()->update('DataObject', 'validation_enabled', $validationenabled);
|
|
throw $e;
|
|
}
|
|
|
|
Config::inst()->update('DataObject', 'validation_enabled', $validationenabled);
|
|
|
|
$this->invokeCallbacks('afterCreate', array($obj, $identifier, &$data, &$fixtures));
|
|
|
|
return $obj;
|
|
}
|
|
|
|
/**
|
|
* @param Array $defaults
|
|
*/
|
|
public function setDefaults($defaults) {
|
|
$this->defaults = $defaults;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return Array
|
|
*/
|
|
public function getDefaults() {
|
|
return $this->defaults;
|
|
}
|
|
|
|
/**
|
|
* @return String
|
|
*/
|
|
public function getClass() {
|
|
return $this->class;
|
|
}
|
|
|
|
/**
|
|
* See class documentation.
|
|
*
|
|
* @param String $type
|
|
* @param callable $callback
|
|
*/
|
|
public function addCallback($type, $callback) {
|
|
if(!array_key_exists($type, $this->callbacks)) {
|
|
throw new InvalidArgumentException(sprintf('Invalid type "%s"', $type));
|
|
}
|
|
|
|
$this->callbacks[$type][] = $callback;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @param String $type
|
|
* @param callable $callback
|
|
*/
|
|
public function removeCallback($type, $callback) {
|
|
$pos = array_search($callback, $this->callbacks[$type]);
|
|
if($pos !== false) unset($this->callbacks[$type][$pos]);
|
|
|
|
return $this;
|
|
}
|
|
|
|
protected function invokeCallbacks($type, $args = array()) {
|
|
foreach($this->callbacks[$type] as $callback) {
|
|
call_user_func_array($callback, $args);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse a value from a fixture file. If it starts with =>
|
|
* it will get an ID from the fixture dictionary
|
|
*
|
|
* @param string $fieldVal
|
|
* @param array $fixtures See {@link createObject()}
|
|
* @param string $class If the value parsed is a class relation, this parameter
|
|
* will be given the value of that class's name
|
|
* @return string Fixture database ID, or the original value
|
|
*/
|
|
protected function parseValue($value, $fixtures = null, &$class = null) {
|
|
if(substr($value,0,2) == '=>') {
|
|
// Parse a dictionary reference - used to set foreign keys
|
|
list($class, $identifier) = explode('.', substr($value,2), 2);
|
|
|
|
if($fixtures && !isset($fixtures[$class][$identifier])) {
|
|
throw new InvalidArgumentException(sprintf(
|
|
'No fixture definitions found for "%s"',
|
|
$value
|
|
));
|
|
}
|
|
|
|
return $fixtures[$class][$identifier];
|
|
} else {
|
|
// Regular field value setting
|
|
return $value;
|
|
}
|
|
}
|
|
|
|
protected function setValue($obj, $name, $value, $fixtures = null) {
|
|
$obj->$name = $this->parseValue($value, $fixtures);
|
|
}
|
|
|
|
protected function overrideField($obj, $fieldName, $value, $fixtures = null) {
|
|
$table = ClassInfo::table_for_object_field(get_class($obj), $fieldName);
|
|
$value = $this->parseValue($value, $fixtures);
|
|
|
|
DB::manipulate(array(
|
|
$table => array(
|
|
"command" => "update", "id" => $obj->ID,
|
|
"fields" => array($fieldName => $value)
|
|
)
|
|
));
|
|
$obj->$fieldName = $value;
|
|
}
|
|
|
|
}
|