PageRenderTime 50ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/src/ORM/FieldType/DBComposite.php

https://gitlab.com/djpmedia/silverstripe-framework
PHP | 332 lines | 181 code | 37 blank | 114 comment | 22 complexity | ae47e7e09eb6b04ad2a23c387440cb58 MD5 | raw file
  1. <?php
  2. namespace SilverStripe\ORM\FieldType;
  3. use SilverStripe\Core\Injector\Injector;
  4. use SilverStripe\ORM\DataObject;
  5. use SilverStripe\ORM\DB;
  6. use SilverStripe\ORM\Queries\SQLSelect;
  7. /**
  8. * Apply this interface to any {@link DBField} that doesn't have a 1-1 mapping with a database field.
  9. * This includes multi-value fields and transformed fields
  10. *
  11. * @todo Unittests for loading and saving composite values (see GIS module for existing similiar unittests)
  12. *
  13. * Example with a combined street name and number:
  14. * <code>
  15. * class Street extends DBComposite {
  16. * private static $composite_db = return array(
  17. * "Number" => "Int",
  18. * "Name" => "Text"
  19. * );
  20. * }
  21. * </code>
  22. */
  23. abstract class DBComposite extends DBField
  24. {
  25. /**
  26. * Similiar to {@link DataObject::$db},
  27. * holds an array of composite field names.
  28. * Don't include the fields "main name",
  29. * it will be prefixed in {@link requireField()}.
  30. *
  31. * @config
  32. * @var array
  33. */
  34. private static $composite_db = array();
  35. /**
  36. * Either the parent dataobject link, or a record of saved values for each field
  37. *
  38. * @var array|DataObject
  39. */
  40. protected $record = array();
  41. public function __set($property, $value)
  42. {
  43. // Prevent failover / extensions from hijacking composite field setters
  44. // by intentionally avoiding hasMethod()
  45. if ($this->hasField($property) && !method_exists($this, "set$property")) {
  46. $this->setField($property, $value);
  47. return;
  48. }
  49. parent::__set($property, $value);
  50. }
  51. public function __get($property)
  52. {
  53. // Prevent failover / extensions from hijacking composite field getters
  54. // by intentionally avoiding hasMethod()
  55. if ($this->hasField($property) && !method_exists($this, "get$property")) {
  56. return $this->getField($property);
  57. }
  58. return parent::__get($property);
  59. }
  60. /**
  61. * Write all nested fields into a manipulation
  62. *
  63. * @param array $manipulation
  64. */
  65. public function writeToManipulation(&$manipulation)
  66. {
  67. foreach ($this->compositeDatabaseFields() as $field => $spec) {
  68. // Write sub-manipulation
  69. $fieldObject = $this->dbObject($field);
  70. $fieldObject->writeToManipulation($manipulation);
  71. }
  72. }
  73. /**
  74. * Add all columns which are defined through {@link requireField()}
  75. * and {@link $composite_db}, or any additional SQL that is required
  76. * to get to these columns. Will mostly just write to the {@link SQLSelect->select}
  77. * array.
  78. *
  79. * @param SQLSelect $query
  80. */
  81. public function addToQuery(&$query)
  82. {
  83. parent::addToQuery($query);
  84. foreach ($this->compositeDatabaseFields() as $field => $spec) {
  85. $table = $this->getTable();
  86. $key = $this->getName() . $field;
  87. if ($table) {
  88. $query->selectField("\"{$table}\".\"{$key}\"");
  89. } else {
  90. $query->selectField("\"{$key}\"");
  91. }
  92. }
  93. }
  94. /**
  95. * Return array in the format of {@link $composite_db}.
  96. * Used by {@link DataObject->hasOwnDatabaseField()}.
  97. *
  98. * @return array
  99. */
  100. public function compositeDatabaseFields()
  101. {
  102. return $this->config()->composite_db;
  103. }
  104. public function isChanged()
  105. {
  106. // When unbound, use the local changed flag
  107. if (! ($this->record instanceof DataObject)) {
  108. return $this->isChanged;
  109. }
  110. // Defer to parent record
  111. foreach ($this->compositeDatabaseFields() as $field => $spec) {
  112. $key = $this->getName() . $field;
  113. if ($this->record->isChanged($key)) {
  114. return true;
  115. }
  116. }
  117. return false;
  118. }
  119. /**
  120. * Composite field defaults to exists only if all fields have values
  121. *
  122. * @return boolean
  123. */
  124. public function exists()
  125. {
  126. // By default all fields
  127. foreach ($this->compositeDatabaseFields() as $field => $spec) {
  128. $fieldObject = $this->dbObject($field);
  129. if (!$fieldObject->exists()) {
  130. return false;
  131. }
  132. }
  133. return true;
  134. }
  135. public function requireField()
  136. {
  137. foreach ($this->compositeDatabaseFields() as $field => $spec) {
  138. $key = $this->getName() . $field;
  139. DB::require_field($this->tableName, $key, $spec);
  140. }
  141. }
  142. /**
  143. * Assign the given value.
  144. * If $record is assigned to a dataobject, this field becomes a loose wrapper over
  145. * the records on that object instead.
  146. *
  147. * {@see ViewableData::obj}
  148. *
  149. * @param mixed $value
  150. * @param mixed $record Parent object to this field, which could be a DataObject, record array, or other
  151. * @param bool $markChanged
  152. * @return $this
  153. */
  154. public function setValue($value, $record = null, $markChanged = true)
  155. {
  156. $this->isChanged = $markChanged;
  157. // When given a dataobject, bind this field to that
  158. if ($record instanceof DataObject) {
  159. $this->bindTo($record);
  160. $record = null;
  161. }
  162. foreach ($this->compositeDatabaseFields() as $field => $spec) {
  163. // Check value
  164. if ($value instanceof DBComposite) {
  165. // Check if saving from another composite field
  166. $this->setField($field, $value->getField($field));
  167. } elseif (isset($value[$field])) {
  168. // Check if saving from an array
  169. $this->setField($field, $value[$field]);
  170. }
  171. // Load from $record
  172. $key = $this->getName() . $field;
  173. if (is_array($record) && isset($record[$key])) {
  174. $this->setField($field, $record[$key]);
  175. }
  176. }
  177. return $this;
  178. }
  179. /**
  180. * Bind this field to the dataobject, and set the underlying table to that of the owner
  181. *
  182. * @param DataObject $dataObject
  183. */
  184. public function bindTo($dataObject)
  185. {
  186. $this->record = $dataObject;
  187. }
  188. public function saveInto($dataObject)
  189. {
  190. foreach ($this->compositeDatabaseFields() as $field => $spec) {
  191. // Save into record
  192. $key = $this->getName() . $field;
  193. $dataObject->setField($key, $this->getField($field));
  194. }
  195. }
  196. /**
  197. * get value of a single composite field
  198. *
  199. * @param string $field
  200. * @return mixed
  201. */
  202. public function getField($field)
  203. {
  204. // Skip invalid fields
  205. $fields = $this->compositeDatabaseFields();
  206. if (!isset($fields[$field])) {
  207. return null;
  208. }
  209. // Check bound object
  210. if ($this->record instanceof DataObject) {
  211. $key = $this->getName() . $field;
  212. return $this->record->getField($key);
  213. }
  214. // Check local record
  215. if (isset($this->record[$field])) {
  216. return $this->record[$field];
  217. }
  218. return null;
  219. }
  220. public function hasField($field)
  221. {
  222. $fields = $this->compositeDatabaseFields();
  223. return isset($fields[$field]);
  224. }
  225. /**
  226. * Set value of a single composite field
  227. *
  228. * @param string $field
  229. * @param mixed $value
  230. * @param bool $markChanged
  231. * @return $this
  232. */
  233. public function setField($field, $value, $markChanged = true)
  234. {
  235. $this->objCacheClear();
  236. // Non-db fields get assigned as normal properties
  237. if (!$this->hasField($field)) {
  238. parent::setField($field, $value);
  239. return $this;
  240. }
  241. // Set changed
  242. if ($markChanged) {
  243. $this->isChanged = true;
  244. }
  245. // Set bound object
  246. if ($this->record instanceof DataObject) {
  247. $key = $this->getName() . $field;
  248. $this->record->setField($key, $value);
  249. return $this;
  250. }
  251. // Set local record
  252. $this->record[$field] = $value;
  253. return $this;
  254. }
  255. /**
  256. * Get a db object for the named field
  257. *
  258. * @param string $field Field name
  259. * @return DBField|null
  260. */
  261. public function dbObject($field)
  262. {
  263. $fields = $this->compositeDatabaseFields();
  264. if (!isset($fields[$field])) {
  265. return null;
  266. }
  267. // Build nested field
  268. $key = $this->getName() . $field;
  269. $spec = $fields[$field];
  270. /** @var DBField $fieldObject */
  271. $fieldObject = Injector::inst()->create($spec, $key);
  272. $fieldObject->setValue($this->getField($field), null, false);
  273. return $fieldObject;
  274. }
  275. public function castingHelper($field)
  276. {
  277. $fields = $this->compositeDatabaseFields();
  278. if (isset($fields[$field])) {
  279. return $fields[$field];
  280. }
  281. return parent::castingHelper($field);
  282. }
  283. public function getIndexSpecs()
  284. {
  285. if ($type = $this->getIndexType()) {
  286. $columns = array_map(function ($name) {
  287. return $this->getName() . $name;
  288. }, array_keys((array) static::config()->get('composite_db')));
  289. return [
  290. 'type' => $type,
  291. 'columns' => $columns,
  292. ];
  293. }
  294. }
  295. }