mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
d8e9af8af8
Database abstraction broken up into controller, connector, query builder, and schema manager, each independently configurable via YAML / Injector Creation of new DBQueryGenerator for database specific generation of SQL Support for parameterised queries, move of code base to use these over escaped conditions Refactor of SQLQuery into separate query classes for each of INSERT UPDATE DELETE and SELECT Support for PDO Installation process upgraded to use new ORM SS_DatabaseException created to handle database errors, maintaining details of raw sql and parameter details for user code designed interested in that data. Renamed DB static methods to conform correctly to naming conventions (e.g. DB::getConn -> DB::get_conn) 3.2 upgrade docs Performance Optimisation and simplification of code to use more concise API API Ability for database adapters to register extensions to ConfigureFromEnv.php
296 lines
8.4 KiB
PHP
296 lines
8.4 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->many_many($fieldName) || $obj->has_many($fieldName) || $obj->has_one($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) {
|
|
if($obj->many_many($fieldName) || $obj->has_many($fieldName)) {
|
|
$obj->write();
|
|
|
|
$parsedItems = array();
|
|
|
|
if(is_array($fieldVal)) {
|
|
// handle lists of many_many relations. Each item can
|
|
// specify the many_many_extraFields against each
|
|
// related item.
|
|
foreach($fieldVal as $relVal) {
|
|
$item = key($relVal);
|
|
$id = $this->parseValue($item, $fixtures);
|
|
$parsedItems[] = $id;
|
|
|
|
array_shift($relVal);
|
|
|
|
$obj->getManyManyComponents($fieldName)->add(
|
|
$id, $relVal
|
|
);
|
|
}
|
|
} else {
|
|
$items = preg_split('/ *, */',trim($fieldVal));
|
|
|
|
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($obj->has_many($fieldName)) {
|
|
$obj->getComponents($fieldName)->setByIDList($parsedItems);
|
|
} elseif($obj->many_many($fieldName)) {
|
|
$obj->getManyManyComponents($fieldName)->setByIDList($parsedItems);
|
|
}
|
|
}
|
|
} else {
|
|
$hasOneField = preg_replace('/ID$/', '', $fieldName);
|
|
if($className = $obj->has_one($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)) {
|
|
$edited = $this->parseValue($data['LastEdited'], $fixtures);
|
|
$update = new SQLUpdate(
|
|
$class,
|
|
array('"LastEdited"' => $edited),
|
|
array('"ID"' => $obj->id)
|
|
);
|
|
$update->execute();
|
|
}
|
|
} 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);
|
|
}
|
|
|
|
} |