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, 'SilverStripe\\ORM\\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 * @throws Exception */ 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. // Also disable filesystem manipulations Config::nest(); Config::inst()->update('SilverStripe\\ORM\\DataObject', 'validation_enabled', false); Config::inst()->update('SilverStripe\\Assets\\File', 'update_filesystem', false); $this->invokeCallbacks('beforeCreate', array($identifier, &$data, &$fixtures)); try { $class = $this->class; $schema = DataObject::getSchema(); $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(); $baseTable = DataObject::getSchema()->baseDataTable($class); if(method_exists($conn, 'allowPrimaryKeyEditing')) { $conn->allowPrimaryKeyEditing($baseTable, true); } $obj->write(false, true); if(method_exists($conn, 'allowPrimaryKeyEditing')) { $conn->allowPrimaryKeyEditing($baseTable, 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) { if( $schema->manyManyComponent($class, $fieldName) || $schema->hasManyComponent($class, $fieldName) || $schema->hasOneComponent($class, $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 = $schema->manyManyComponent($class, $fieldName); $isHasMany = $schema->hasManyComponent($class, $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 = []; 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 = []; foreach($items as $item) { // Check for correct format: =>.. // 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 = $schema->hasOneComponent($class, $hasOneField)) { $obj->{$hasOneField.'ID'} = $this->parseValue($fieldVal, $fixtures, $fieldClass); // Inject class for polymorphic relation if($className === 'SilverStripe\\ORM\\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); } } catch(Exception $e) { Config::unnest(); throw $e; } Config::unnest(); $this->invokeCallbacks('afterCreate', array($obj, $identifier, &$data, &$fixtures)); return $obj; } /** * @param array $defaults * @return $this */ 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 * @return $this */ 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 * @return $this */ 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 $value * @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) { $class = get_class($obj); $table = DataObject::getSchema()->tableForField($class, $fieldName); $value = $this->parseValue($value, $fixtures); DB::manipulate(array( $table => array( "command" => "update", "id" => $obj->ID, "class" => $class, "fields" => array($fieldName => $value), ) )); $obj->$fieldName = $value; } }