silverstripe-framework/src/ORM/FieldType/DBComposite.php

343 lines
9.5 KiB
PHP

<?php
namespace SilverStripe\ORM\FieldType;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\Queries\SQLSelect;
/**
* Apply this interface to any {@link DBField} that doesn't have a 1-1 mapping with a database field.
* This includes multi-value fields and transformed fields
*
* @todo Unittests for loading and saving composite values (see GIS module for existing similiar unittests)
*
* Example with a combined street name and number:
* <code>
* class Street extends DBComposite {
* private static $composite_db = return array(
* "Number" => "Int",
* "Name" => "Text"
* );
* }
* </code>
*/
abstract class DBComposite extends DBField
{
/**
* Similiar to {@link DataObject::$db},
* holds an array of composite field names.
* Don't include the fields "main name",
* it will be prefixed in {@link requireField()}.
*
* @config
* @var array
*/
private static $composite_db = array();
/**
* Marker as to whether this record has changed
* Only used when deference to the parent object isn't possible
*/
protected $isChanged = false;
/**
* Either the parent dataobject link, or a record of saved values for each field
*
* @var array|DataObject
*/
protected $record = array();
public function __set($property, $value)
{
// Prevent failover / extensions from hijacking composite field setters
// by intentionally avoiding hasMethod()
if ($this->hasField($property) && !method_exists($this, "set$property")) {
$this->setField($property, $value);
return;
}
parent::__set($property, $value);
}
public function __get($property)
{
// Prevent failover / extensions from hijacking composite field getters
// by intentionally avoiding hasMethod()
if ($this->hasField($property) && !method_exists($this, "get$property")) {
return $this->getField($property);
}
return parent::__get($property);
}
/**
* Write all nested fields into a manipulation
*
* @param array $manipulation
*/
public function writeToManipulation(&$manipulation)
{
foreach ($this->compositeDatabaseFields() as $field => $spec) {
// Write sub-manipulation
$fieldObject = $this->dbObject($field);
$fieldObject->writeToManipulation($manipulation);
}
}
/**
* Add all columns which are defined through {@link requireField()}
* and {@link $composite_db}, or any additional SQL that is required
* to get to these columns. Will mostly just write to the {@link SQLSelect->select}
* array.
*
* @param SQLSelect $query
*/
public function addToQuery(&$query)
{
parent::addToQuery($query);
foreach ($this->compositeDatabaseFields() as $field => $spec) {
$table = $this->getTable();
$key = $this->getName() . $field;
if ($table) {
$query->selectField("\"{$table}\".\"{$key}\"");
} else {
$query->selectField("\"{$key}\"");
}
}
}
/**
* Return array in the format of {@link $composite_db}.
* Used by {@link DataObject->hasOwnDatabaseField()}.
*
* @return array
*/
public function compositeDatabaseFields()
{
return $this->config()->composite_db;
}
/**
* Returns true if this composite field has changed.
* For fields bound to a DataObject, this will be cleared when the DataObject is written.
*/
public function isChanged()
{
// When unbound, use the local changed flag
if (! ($this->record instanceof DataObject)) {
return $this->isChanged;
}
// Defer to parent record
foreach ($this->compositeDatabaseFields() as $field => $spec) {
$key = $this->getName() . $field;
if ($this->record->isChanged($key)) {
return true;
}
}
return false;
}
/**
* Composite field defaults to exists only if all fields have values
*
* @return boolean
*/
public function exists()
{
// By default all fields
foreach ($this->compositeDatabaseFields() as $field => $spec) {
$fieldObject = $this->dbObject($field);
if (!$fieldObject->exists()) {
return false;
}
}
return true;
}
public function requireField()
{
foreach ($this->compositeDatabaseFields() as $field => $spec) {
$key = $this->getName() . $field;
DB::require_field($this->tableName, $key, $spec);
}
}
/**
* Assign the given value.
* If $record is assigned to a dataobject, this field becomes a loose wrapper over
* the records on that object instead.
*
* {@see ViewableData::obj}
*
* @param mixed $value
* @param mixed $record Parent object to this field, which could be a DataObject, record array, or other
* @param bool $markChanged
* @return $this
*/
public function setValue($value, $record = null, $markChanged = true)
{
$this->isChanged = $markChanged;
// When given a dataobject, bind this field to that
if ($record instanceof DataObject) {
$this->bindTo($record);
$record = null;
}
foreach ($this->compositeDatabaseFields() as $field => $spec) {
// Check value
if ($value instanceof DBComposite) {
// Check if saving from another composite field
$this->setField($field, $value->getField($field));
} elseif (isset($value[$field])) {
// Check if saving from an array
$this->setField($field, $value[$field]);
}
// Load from $record
$key = $this->getName() . $field;
if (is_array($record) && isset($record[$key])) {
$this->setField($field, $record[$key]);
}
}
return $this;
}
/**
* Bind this field to the dataobject, and set the underlying table to that of the owner
*
* @param DataObject $dataObject
*/
public function bindTo($dataObject)
{
$this->record = $dataObject;
}
public function saveInto($dataObject)
{
foreach ($this->compositeDatabaseFields() as $field => $spec) {
// Save into record
$key = $this->getName() . $field;
$dataObject->setField($key, $this->getField($field));
}
}
/**
* get value of a single composite field
*
* @param string $field
* @return mixed
*/
public function getField($field)
{
// Skip invalid fields
$fields = $this->compositeDatabaseFields();
if (!isset($fields[$field])) {
return null;
}
// Check bound object
if ($this->record instanceof DataObject) {
$key = $this->getName() . $field;
return $this->record->getField($key);
}
// Check local record
if (isset($this->record[$field])) {
return $this->record[$field];
}
return null;
}
public function hasField($field)
{
$fields = $this->compositeDatabaseFields();
return isset($fields[$field]);
}
/**
* Set value of a single composite field
*
* @param string $field
* @param mixed $value
* @param bool $markChanged
* @return $this
*/
public function setField($field, $value, $markChanged = true)
{
$this->objCacheClear();
// Non-db fields get assigned as normal properties
if (!$this->hasField($field)) {
parent::setField($field, $value);
return $this;
}
// Set changed
if ($markChanged) {
$this->isChanged = true;
}
// Set bound object
if ($this->record instanceof DataObject) {
$key = $this->getName() . $field;
$this->record->setField($key, $value);
return $this;
}
// Set local record
$this->record[$field] = $value;
return $this;
}
/**
* Get a db object for the named field
*
* @param string $field Field name
* @return DBField|null
*/
public function dbObject($field)
{
$fields = $this->compositeDatabaseFields();
if (!isset($fields[$field])) {
return null;
}
// Build nested field
$key = $this->getName() . $field;
$spec = $fields[$field];
/** @var DBField $fieldObject */
$fieldObject = Injector::inst()->create($spec, $key);
$fieldObject->setValue($this->getField($field), null, false);
return $fieldObject;
}
public function castingHelper($field)
{
$fields = $this->compositeDatabaseFields();
if (isset($fields[$field])) {
return $fields[$field];
}
return parent::castingHelper($field);
}
public function getIndexSpecs()
{
if ($type = $this->getIndexType()) {
$columns = array_map(function ($name) {
return $this->getName() . $name;
}, array_keys((array) static::config()->get('composite_db')));
return [
'type' => $type,
'columns' => $columns,
];
}
}
}