silverstripe-framework/src/Dev/FixtureFactory.php

281 lines
8.6 KiB
PHP

<?php
namespace SilverStripe\Dev;
use SilverStripe\ORM\Queries\SQLInsert;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Queries\SQLDelete;
use SilverStripe\Core\Injector\Injector;
use InvalidArgumentException;
/**
* Manages a set of database fixtures for {@link DataObject} records
* as well as raw database table rows.
*
* Delegates creation of objects to {@link FixtureBlueprint},
* which can implement class- and use-case specific fixture setup.
*
* Supports referencing model relations through a specialized syntax:
* <code>
* $factory = new FixtureFactory();
* $relatedObj = $factory->createObject(
* 'MyRelatedClass',
* 'relation1'
* );
* $obj = $factory->createObject(
* 'MyClass',
* 'object1'
* array('MyRelationName' => '=>MyRelatedClass.relation1')
* );
* </code>
* Relation loading is order dependent.
*/
class FixtureFactory
{
/**
* @var array Array of fixture items, keyed by class and unique identifier,
* with values being the generated database ID. Does not store object instances.
*/
protected $fixtures = [];
/**
* @var FixtureBlueprint[] Callbacks
*/
protected $blueprints = [];
/**
* @param string $name Unique name for this blueprint
* @param array|FixtureBlueprint $defaults Array of default values, or a blueprint instance
* @return $this
*/
public function define($name, $defaults = [])
{
if ($defaults instanceof FixtureBlueprint) {
$this->blueprints[$name] = $defaults;
} else {
$class = $name;
$this->blueprints[$name] = Injector::inst()->create(
'SilverStripe\\Dev\\FixtureBlueprint',
$name,
$class,
$defaults
);
}
return $this;
}
/**
* Writes the fixture into the database using DataObjects
*
* @param string $name Name of the {@link FixtureBlueprint} to use,
* usually a DataObject subclass.
* @param string $identifier Unique identifier for this fixture type
* @param array $data Map of properties. Overrides default data.
* @return DataObject
*/
public function createObject($name, $identifier, $data = null)
{
if (!isset($this->blueprints[$name])) {
$this->blueprints[$name] = new FixtureBlueprint($name);
}
$blueprint = $this->blueprints[$name];
$obj = $blueprint->createObject($identifier, $data, $this->fixtures);
$class = $blueprint->getClass();
if (!isset($this->fixtures[$class])) {
$this->fixtures[$class] = [];
}
$this->fixtures[$class][$identifier] = $obj->ID;
return $obj;
}
/**
* Writes the fixture into the database directly using a database manipulation.
* Does not use blueprints. Only supports tables with a primary key.
*
* @param string $table Existing database table name
* @param string $identifier Unique identifier for this fixture type
* @param array $data Map of properties
* @return int Database identifier
*/
public function createRaw($table, $identifier, $data)
{
$fields = [];
foreach ($data as $fieldName => $fieldVal) {
$fields["\"{$fieldName}\""] = $this->parseValue($fieldVal);
}
$insert = new SQLInsert("\"{$table}\"", $fields);
$insert->execute();
$id = DB::get_generated_id($table);
$this->fixtures[$table][$identifier] = $id;
return $id;
}
/**
* Get the ID of an object from the fixture.
*
* @param string $class The data class, as specified in your fixture file. Parent classes won't work
* @param string $identifier The identifier string, as provided in your fixture file
* @return int
*/
public function getId($class, $identifier)
{
if (isset($this->fixtures[$class][$identifier])) {
return $this->fixtures[$class][$identifier];
} else {
return false;
}
}
/**
* Return all of the IDs in the fixture of a particular class name.
*
* @param string $class The data class or table name
* @return array|false A map of fixture-identifier => object-id
*/
public function getIds($class)
{
if (isset($this->fixtures[$class])) {
return $this->fixtures[$class];
} else {
return false;
}
}
/**
* @param string $class
* @param string $identifier
* @param int $databaseId
* @return $this
*/
public function setId($class, $identifier, $databaseId)
{
$this->fixtures[$class][$identifier] = $databaseId;
return $this;
}
/**
* Get an object from the fixture.
*
* @param string $class The data class or table name, as specified in your fixture file. Parent classes won't work
* @param string $identifier The identifier string, as provided in your fixture file
* @return DataObject
*/
public function get($class, $identifier)
{
$id = $this->getId($class, $identifier);
if (!$id) {
return null;
}
// If the class doesn't exist, look for a table instead
if (!class_exists($class ?? '')) {
$tableNames = DataObject::getSchema()->getTableNames();
$potential = array_search($class, $tableNames ?? []);
if (!$potential) {
throw new \LogicException("'$class' is neither a class nor a table name");
}
$class = $potential;
}
return DataObject::get_by_id($class, $id);
}
/**
* @return array Map of class names, containing a map of in-memory identifiers
* mapped to database identifiers.
*/
public function getFixtures()
{
return $this->fixtures;
}
/**
* Remove all fixtures previously defined through {@link createObject()}
* or {@link createRaw()}, both from the internal fixture mapping and the database.
* If the $class argument is set, limit clearing to items of this class.
*
* @param string $limitToClass
* @param bool $metadata Clear internal mapping as well as data.
* Set to false by default since sometimes data is rolled back by translations.
*/
public function clear($limitToClass = null, $metadata = false)
{
$classes = ($limitToClass) ? [$limitToClass] : array_keys($this->fixtures ?? []);
foreach ($classes as $class) {
$ids = $this->fixtures[$class];
foreach ($ids as $id => $dbId) {
if (class_exists($class ?? '')) {
$instance = DataObject::get($class)->byId($dbId);
if ($instance) {
$instance->delete();
}
} else {
$table = $class;
$delete = new SQLDelete("\"$table\"", [
"\"$table\".\"ID\"" => $dbId
]);
$delete->execute();
}
if ($metadata) {
unset($this->fixtures[$class][$id]);
}
}
}
}
/**
* @return array Of {@link FixtureBlueprint} instances
*/
public function getBlueprints()
{
return $this->blueprints;
}
/**
* @param string $name
* @return FixtureBlueprint|false
*/
public function getBlueprint($name)
{
return (isset($this->blueprints[$name])) ? $this->blueprints[$name] : false;
}
/**
* Parse a value from a fixture file. If it starts with =>
* it will get an ID from the fixture dictionary
*
* @param string $value
* @return string Fixture database ID, or the original value
*/
protected function parseValue($value)
{
if (substr($value ?? '', 0, 2) == '=>') {
// Parse a dictionary reference - used to set foreign keys
if (strpos($value ?? '', '.') !== false) {
list($class, $identifier) = explode('.', substr($value ?? '', 2), 2);
} else {
throw new \LogicException("Bad fixture lookup identifier: " . $value);
}
if ($this->fixtures && !isset($this->fixtures[$class][$identifier])) {
throw new InvalidArgumentException(sprintf(
'No fixture definitions found for "%s"',
$value
));
}
return $this->fixtures[$class][$identifier];
} else {
// Regular field value setting
return $value;
}
}
}