/wire/core/Fields.php

https://bitbucket.org/webbear/processwire-base-installation · PHP · 499 lines · 262 code · 79 blank · 158 comment · 58 complexity · b6f6fdb5a3f12cb26ef1dd38d7103fc3 MD5 · raw file

  1. <?php
  2. /**
  3. * ProcessWire Fields
  4. *
  5. * Manages collection of ALL Field instances, not specific to any particular Fieldgroup
  6. *
  7. * ProcessWire 2.x
  8. * Copyright (C) 2010 by Ryan Cramer
  9. * Licensed under GNU/GPL v2, see LICENSE.TXT
  10. *
  11. * http://www.processwire.com
  12. * http://www.ryancramer.com
  13. *
  14. */
  15. /**
  16. * WireArray of Field instances, as used by Fields class
  17. *
  18. */
  19. class FieldsArray extends WireArray {
  20. /**
  21. * Per WireArray interface, only Field instances may be added
  22. *
  23. */
  24. public function isValidItem($item) {
  25. return $item instanceof Field;
  26. }
  27. /**
  28. * Per WireArray interface, Field keys have to be integers
  29. *
  30. */
  31. public function isValidKey($key) {
  32. return is_int($key) || ctype_digit($key);
  33. }
  34. /**
  35. * Per WireArray interface, Field instances are keyed by their ID
  36. *
  37. */
  38. public function getItemKey($item) {
  39. return $item->id;
  40. }
  41. /**
  42. * Per WireArray interface, return a blank Field
  43. *
  44. */
  45. public function makeBlankItem() {
  46. return new Field();
  47. }
  48. }
  49. /**
  50. * Manages the collection of all Field instances, not specific to any one Fieldgroup
  51. *
  52. */
  53. class Fields extends WireSaveableItems {
  54. /**
  55. * Instance of FieldsArray
  56. *
  57. */
  58. protected $fieldsArray = null;
  59. /**
  60. * Field names that are native/permanent to the system and thus treated differently in several instances.
  61. *
  62. * For example, a Field can't be given one of these names.
  63. *
  64. * @TODO This really doesn't belong here. This check can be performed from a Page without needing to maintain this silly list.
  65. *
  66. */
  67. static protected $nativeNames = array(
  68. 'id',
  69. 'parent_id',
  70. 'parent', // alias
  71. 'parents',
  72. 'templates_id',
  73. 'template', // alias
  74. 'name',
  75. 'status',
  76. 'created',
  77. 'createdUser',
  78. 'createdUserID',
  79. 'createdUsersID',
  80. 'created_users_id',
  81. 'include',
  82. 'modified',
  83. 'modifiedUser',
  84. 'modifiedUserID',
  85. 'modifiedUsersID',
  86. 'modified_users_id',
  87. 'num_children',
  88. 'numChildren',
  89. 'sort',
  90. 'sortfield',
  91. 'flags',
  92. 'find',
  93. 'get',
  94. 'child',
  95. 'children',
  96. 'siblings',
  97. //'roles',
  98. 'url',
  99. 'path',
  100. 'templatePrevious',
  101. 'rootParent',
  102. 'fieldgroup',
  103. 'fields',
  104. 'description',
  105. 'data',
  106. 'isNew',
  107. );
  108. public function __construct() {
  109. $this->fieldsArray = new FieldsArray();
  110. }
  111. /**
  112. * Construct and load the Fields
  113. *
  114. */
  115. public function init() {
  116. $this->load($this->fieldsArray);
  117. }
  118. /**
  119. * Per WireSaveableItems interface, return a blank instance of a Field
  120. *
  121. */
  122. public function makeBlankItem() {
  123. return new Field();
  124. }
  125. /**
  126. * Per WireSaveableItems interface, return all available Field instances
  127. *
  128. */
  129. public function getAll() {
  130. return $this->fieldsArray;
  131. }
  132. /**
  133. * Per WireSaveableItems interface, return the table name used to save Fields
  134. *
  135. */
  136. public function getTable() {
  137. return "fields";
  138. }
  139. /**
  140. * Return the name that fields should be initially sorted by
  141. *
  142. */
  143. public function getSort() {
  144. return $this->getTable() . ".name";
  145. }
  146. /**
  147. * Save a Field to the database
  148. *
  149. * @param Field|Saveable $item The field to save
  150. * @return bool True on success, false on failure
  151. * @throws WireException
  152. *
  153. */
  154. public function ___save(Saveable $item) {
  155. if($item->flags & Field::flagFieldgroupContext) throw new WireException("Field $item is not saveable because it is in a specific context");
  156. $database = $this->wire('database');
  157. $isNew = $item->id < 1;
  158. $prevTable = $database->escapeTable($item->prevTable);
  159. $table = $database->escapeTable($item->getTable());
  160. if(!$isNew && $prevTable && $prevTable != $table) {
  161. // note that we rename the table twice in order to force MySQL to perform the rename
  162. // even if only the case has changed.
  163. $database->exec("RENAME TABLE `$prevTable` TO `tmp_$table`"); // QA
  164. $database->exec("RENAME TABLE `tmp_$table` TO `$table`"); // QA
  165. $item->prevTable = '';
  166. }
  167. if($item->prevFieldtype && $item->prevFieldtype->name != $item->type->name) {
  168. if(!$this->changeFieldtype($item)) {
  169. $item->type = $item->prevFieldtype;
  170. $this->error("Error changing fieldtype for '$item', reverted back to '{$item->type}'");
  171. } else {
  172. $item->prevFieldtype = null;
  173. }
  174. }
  175. if(!$item->type) throw new WireException("Can't save a Field that doesn't have it's 'type' property set to a Fieldtype");
  176. if(!parent::___save($item)) return false;
  177. if($isNew) $item->type->createField($item);
  178. if($item->flags & Field::flagGlobal) {
  179. // make sure that all template fieldgroups contain this field and add to any that don't.
  180. foreach(wire('templates') as $template) {
  181. if($template->noGlobal) continue;
  182. $fieldgroup = $template->fieldgroup;
  183. if(!$fieldgroup->hasField($item)) {
  184. $fieldgroup->add($item);
  185. $fieldgroup->save();
  186. if(wire('config')->debug) $this->message("Added field '{$item->name}' to template/fieldgroup '{$fieldgroup->name}'");
  187. }
  188. }
  189. }
  190. return true;
  191. }
  192. /**
  193. * Check that the given Field's table exists and create it if it doesn't
  194. *
  195. * @param Field $field
  196. *
  197. */
  198. protected function checkFieldTable(Field $field) {
  199. // if(!$this->wire('config')->debug) return;
  200. $database = $this->wire('database');
  201. $table = $database->escapeTable($field->getTable());
  202. if(empty($table)) return;
  203. $exists = $database->query("SHOW TABLES LIKE '$table'")->rowCount() > 0;
  204. if($exists) return;
  205. try {
  206. if($field->type && count($field->type->getDatabaseSchema($field))) {
  207. if($field->type->createField($field)) $this->message("Created table '$table'");
  208. }
  209. } catch(Exception $e) {
  210. $this->error($e->getMessage());
  211. }
  212. }
  213. /**
  214. * Check that all fields in the system have their tables installed
  215. *
  216. * This enables you to re-create field tables when migrating over entries from the Fields table manually (via SQL dumps or the like)
  217. *
  218. * @param Field $field
  219. *
  220. */
  221. public function checkFieldTables() {
  222. foreach($this as $field) $this->checkFieldTable($field);
  223. }
  224. /**
  225. * Delete a Field from the database
  226. *
  227. * @param Field|Saveable $item Item to save
  228. * @return bool True on success, false on failure
  229. * @throws WireException
  230. *
  231. */
  232. public function ___delete(Saveable $item) {
  233. if(!$this->fieldsArray->isValidItem($item)) throw new WireException("Fields::delete(item) only accepts items of type Field");
  234. // if the field doesn't have an ID, so it's not one that came from the DB
  235. if(!$item->id) throw new WireException("Unable to delete from '" . $item->getTable() . "' for field that doesn't exist in fields table");
  236. // if it's in use by any fieldgroups, then we don't allow it to be deleted
  237. if($item->numFieldgroups()) throw new WireException("Unable to delete field '{$item->name}' because it is in use by " . $item->numFieldgroups() . " fieldgroups");
  238. // if it's a system field, it may not be deleted
  239. if($item->flags & Field::flagSystem) throw new WireException("Unable to delete field '{$item->name}' because it is a system field.");
  240. // delete entries in fieldgroups_fields table. Not really necessary since the above exception prevents this, but here in case that changes.
  241. $this->fuel('fieldgroups')->deleteField($item);
  242. // drop the field's table
  243. $item->type->deleteField($item);
  244. return parent::___delete($item);
  245. }
  246. /**
  247. * Create and return a cloned copy of the given Field
  248. *
  249. * @param Field|Saveable $item Item to clone
  250. * @return bool|Saveable $item Returns the new clone on success, or false on failure
  251. * @throws WireException
  252. *
  253. */
  254. public function ___clone(Saveable $item) {
  255. $item = $item->type->cloneField($item);
  256. // don't clone system flags
  257. if($item->flags & Field::flagSystem || $item->flags & Field::flagPermanent) {
  258. $item->flags = $item->flags | Field::flagSystemOverride;
  259. if($item->flags & Field::flagSystem) $item->flags = $item->flags & ~Field::flagSystem;
  260. if($item->flags & Field::flagPermanent) $item->flags = $item->flags & ~Field::flagPermanent;
  261. $item->flags = $item->flags & ~Field::flagSystemOverride;
  262. }
  263. // don't clone the 'global' flag
  264. if($item->flags & Field::flagGlobal) $item->flags = $item->flags & ~Field::flagGlobal;
  265. return parent::___clone($item);
  266. }
  267. /**
  268. * Save the context of the given field for the given fieldgroup
  269. *
  270. * @param Field $field Field to save context for
  271. * @param Fieldgroup $fieldgroup Context for when field is in this fieldgroup
  272. * @return bool True on success
  273. * @throws WireException
  274. *
  275. */
  276. public function ___saveFieldgroupContext(Field $field, Fieldgroup $fieldgroup) {
  277. // get field without contxt
  278. $fieldOriginal = wire('fields')->get($field->name);
  279. $data = array();
  280. // make sure given field and fieldgroup are valid
  281. if(!($field->flags & Field::flagFieldgroupContext)) throw new WireException("Field must be in fieldgroup context before its context can be saved");
  282. if(!$fieldgroup->has($fieldOriginal)) throw new WireException("Fieldgroup $fieldgroup does not contain field $field");
  283. $newValues = $field->getArray();
  284. $oldValues = $fieldOriginal->getArray();
  285. // 0 is the same as 100 for columnWidth, so we specifically set it just to prevent this from being saved when it doesn't need to be
  286. if(!isset($oldValues['columnWidth'])) $oldValues['columnWidth'] = 100;
  287. // add the label and description built-in fields
  288. foreach(array('label', 'description') as $key) {
  289. $newValues[$key] = $field->$key;
  290. $oldValues[$key] = $fieldOriginal->$key;
  291. }
  292. // cycle through and determine which values should be saved
  293. foreach($newValues as $key => $value) {
  294. $oldValue = empty($oldValues[$key]) ? '' : $oldValues[$key];
  295. // if both old and new are empty, then don't store a blank value in the context
  296. if(empty($oldValue) && empty($value)) continue;
  297. // if old and new value are the same, then don't duplicate the value in the context
  298. if($value == $oldValue) continue;
  299. // $value differs from $oldValue and should be saved
  300. $data[$key] = $value;
  301. }
  302. // keep all in the same order so that it's easier to compare (by eye) in the DB
  303. ksort($data);
  304. // if there is something in data, then JSON encode it. If it's empty then make it null.
  305. $data = count($data) ? wireEncodeJSON($data, true) : null;
  306. if(is_null($data)) {
  307. $data = 'NULL';
  308. } else {
  309. $data = "'" . $this->wire('database')->escapeStr($data) . "'";
  310. }
  311. $field_id = (int) $field->id;
  312. $fieldgroup_id = (int) $fieldgroup->id;
  313. $database = $this->wire('database');
  314. $query = $database->prepare("UPDATE fieldgroups_fields SET data=$data WHERE fields_id=:field_id AND fieldgroups_id=:fieldgroup_id"); // QA
  315. $query->bindValue(':field_id', $field_id, PDO::PARAM_INT);
  316. $query->bindValue(':fieldgroup_id', $fieldgroup_id, PDO::PARAM_INT);
  317. $result = $query->execute();
  318. return $result;
  319. }
  320. /**
  321. * Change a field's type
  322. *
  323. * @param Field $field1 Field with the new type
  324. * @throws WireException
  325. * @return bool
  326. *
  327. */
  328. protected function ___changeFieldtype(Field $field1) {
  329. if(!$field1->prevFieldtype) throw new WireException("changeFieldType requires that the given field has had a type change");
  330. if( ($field1->type instanceof FieldtypeMulti && !$field1->prevFieldtype instanceof FieldtypeMulti) ||
  331. ($field1->prevFieldtype instanceof FieldtypeMulti && !$field1->type instanceof FieldtypeMulti)) {
  332. throw new WireException("Cannot convert between single and multiple value field types");
  333. }
  334. $fromType = $field1->prevFieldtype;
  335. $toType = $field1->type;
  336. $this->changeTypeReady($field1, $fromType, $toType);
  337. $field2 = clone $field1;
  338. $flags = $field2->flags;
  339. if($flags & Field::flagSystem) {
  340. $field2->flags = $flags | Field::flagSystemOverride;
  341. $field2->flags = 0;
  342. }
  343. $field2->name = $field2->name . "_PWTMP";
  344. $field2->type->createField($field2);
  345. $field1->type = $field1->prevFieldtype;
  346. $schema1 = array();
  347. $schema2 = array();
  348. $database = $this->wire('database');
  349. $table1 = $database->escapeTable($field1->table);
  350. $table2 = $database->escapeTable($field2->table);
  351. $query = $database->prepare("DESCRIBE `$table1`"); // QA
  352. $query->execute();
  353. while($row = $query->fetch(PDO::FETCH_ASSOC)) $schema1[] = $row['Field'];
  354. $query = $database->prepare("DESCRIBE `$table2`"); // QA
  355. $query->execute();
  356. while($row = $query->fetch(PDO::FETCH_ASSOC)) $schema2[] = $row['Field'];
  357. foreach($schema1 as $key => $value) {
  358. if(!in_array($value, $schema2)) {
  359. if($this->config->debug) $this->message("changeFieldType loses table field '$value'");
  360. unset($schema1[$key]);
  361. }
  362. }
  363. $sql = "INSERT INTO `$table2` (`" . implode('`,`', $schema1) . "`) " .
  364. "SELECT `" . implode('`,`', $schema1) . "` FROM `$table1` ";
  365. $error = '';
  366. try {
  367. $result = $database->exec($sql);
  368. if($result === false || $query->errorCode() > 0) {
  369. $errorInfo = $query->errorInfo();
  370. $error = !empty($errorInfo[2]) ? $errorInfo[2] : 'Unknown Error';
  371. }
  372. } catch(Exception $e) {
  373. $result = false;
  374. $error = $e->getMessage();
  375. }
  376. if($error) {
  377. $this->error("Field type change failed. Database reports: $error");
  378. $database->exec("DROP TABLE `$table2`"); // QA
  379. return false;
  380. }
  381. $database->exec("DROP TABLE `$table1`"); // QA
  382. $database->exec("RENAME TABLE `$table2` TO `$table1`"); // QA
  383. $field1->type = $field2->type;
  384. // clear out the custom data, which contains settings specific to the Inputfield and Fieldtype
  385. foreach($field1->getArray() as $key => $value) {
  386. // skip fields that may be shared among any fieldtype
  387. if(in_array($key, array('description', 'required', 'collapsed', 'notes'))) continue;
  388. // skip over language labels/descriptions
  389. if(preg_match('/^(description|label|notes)\d+/', $key)) continue;
  390. // remove the custom field
  391. $field1->remove($key);
  392. }
  393. $this->changedType($field1, $fromType, $toType);
  394. return true;
  395. }
  396. /**
  397. * Is the given field name native/permanent to the database?
  398. *
  399. * @param string $name
  400. * @return bool
  401. *
  402. */
  403. public static function isNativeName($name) {
  404. return in_array($name, self::$nativeNames);
  405. }
  406. /**
  407. * Overridden from WireSaveableItems to retain keys with 0 values and remove defaults we don't need saved
  408. *
  409. */
  410. protected function encodeData(array $value) {
  411. if(isset($value['collapsed']) && $value['collapsed'] === 0) unset($value['collapsed']);
  412. if(isset($value['columnWidth']) && (empty($value['columnWidth']) || $value['columnWidth'] == 100)) unset($value['columnWidth']);
  413. return wireEncodeJSON($value, 0);
  414. }
  415. public function ___changedType(Saveable $item, Fieldtype $fromType, Fieldtype $toType) { }
  416. public function ___changeTypeReady(Saveable $item, Fieldtype $fromType, Fieldtype $toType) { }
  417. }