From 204335e2d336b0931d96843f031daf68962cbc1c Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 7 Jun 2019 12:17:27 -0400 Subject: [PATCH] Add support for $page->meta() method for maintaining persistent meta data for pages independent of fields and the normal load/save process. --- wire/core/Page.php | 70 +++- wire/core/PagesEditor.php | 3 + wire/core/WireDataDB.php | 358 ++++++++++++++++++ .../System/SystemUpdater/SystemUpdater.module | 3 +- 4 files changed, 431 insertions(+), 3 deletions(-) create mode 100644 wire/core/WireDataDB.php diff --git a/wire/core/Page.php b/wire/core/Page.php index ecfd6d90..71b28f6c 100644 --- a/wire/core/Page.php +++ b/wire/core/Page.php @@ -8,7 +8,7 @@ * 1. Providing get/set access to the Page's properties * 2. Accessing the related hierarchy of pages (i.e. parents, children, sibling pages) * - * ProcessWire 3.x, Copyright 2018 by Ryan Cramer + * ProcessWire 3.x, Copyright 2019 by Ryan Cramer * https://processwire.com * * #pw-summary Class used by all Page objects in ProcessWire. @@ -88,6 +88,9 @@ * @property int $hasLinks Number of visible pages (to current user) linking to this page in Textarea/HTML fields. #pw-group-traversal * @property int $instanceID #pw-internal * @property bool $quietMode #pw-internal + * @property WireData|null $_meta #pw-internal + * @property WireData $meta #pw-internal + * * * @property Page|null $_cloning Internal runtime use, contains Page being cloned (source), when this Page is the new copy (target). #pw-internal * @property bool|null $_hasAutogenName Internal runtime use, set by Pages class when page as auto-generated name. #pw-internal @@ -601,7 +604,15 @@ class Page extends WireData implements \Countable, WireMatchable { 'created' => 0, 'modified' => 0, 'published' => 0, - ); + ); + + /** + * Page meta data + * + * @var null|WireDataDB + * + */ + protected $_meta = null; /** * Properties that can be accessed, mapped to method of access (excluding custom fields of course) @@ -777,6 +788,7 @@ class Page extends WireData implements \Countable, WireMatchable { $this->filesManager = clone $this->filesManager; $this->filesManager->setPage($this); } + $this->_meta = null; foreach($this->template->fieldgroup as $field) { $name = $field->name; if(!$field->type->isAutoload() && !isset($this->data[$name])) continue; // important for draft loading @@ -1176,9 +1188,13 @@ class Page extends WireData implements \Countable, WireMatchable { case 'loaderCache': $value = $this->loaderCache; break; + case '_meta': + $value = $this->_meta; // null or WireDataDB + break; default: if($key && isset($this->settings[(string)$key])) return $this->settings[$key]; + if($key === 'meta' && !$this->wire('fields')->get('meta')) return $this->meta(); // always WireDataDB // populate a formatted string with {tag} vars if(strpos($key, '{') !== false && strpos($key, '}')) return $this->getMarkup($key); @@ -4363,6 +4379,56 @@ class Page extends WireData implements \Countable, WireMatchable { return $this; } */ + + /** + * Get or set page’s persistent meta data + * + * This meta data is managed in the DB. Setting a value immediately saves it in the DB, while + * getting a value immediately loads it from the DB. As a result, this data is independent of the + * usual Page load and save operations. This is primarily for internal core use, but may be + * useful for other specific non-core purposes as well. + * + * Note that this meta data is completely free-form and has no connection to ProcessWire fields. + * Values for meta data must be basic PHP types, whether arrays, strings, numbers, etc. Please do + * not use objects for meta values at this time. + * + * ~~~~~ + * // set and save a meta value + * $page->meta()->set('colors', [ 'red, 'green', 'blue' ]); + * + * // get a meta value + * $colors = $page->meta()->get('colors'); + * + * // alternate shorter syntax for either of the above + * $page->meta('colors', [ 'red', 'green', 'blue' ]); // set + * $colors = $page->meta('colors'); // get + * + * // delete a meta value + * $page->meta()->remove('colors'); + * + * // get the WireDataDB instance that stores the meta values, + * // it has all the same methods as WireData objects... + * $meta = $page->meta(); + * + * // ...such as, get all values in an array: + * $values = $meta->getArray(); + * ~~~~~ + * + * #pw-internal + * + * @param string|bool $key Omit to get the WireData instance or specify property name to get or set. + * @param null|mixed $value Value to set for given $key or omit if getting a value. + * @return WireDataDB|string|array|int|float + * @since 3.0.133 + * + */ + public function meta($key = '', $value = null) { + /** @var Pages $pages */ + if($this->_meta === null) $this->_meta = $this->wire(new WireDataDB($this->id, 'pages_meta')); + if(empty($key)) return $this->_meta; // return instance + if($value === null) return $this->_meta->get($key); // get value + return $this->_meta->set($key, $value); // set value + } } diff --git a/wire/core/PagesEditor.php b/wire/core/PagesEditor.php index 6da2698e..c908c9f2 100644 --- a/wire/core/PagesEditor.php +++ b/wire/core/PagesEditor.php @@ -1062,6 +1062,8 @@ class PagesEditor extends Wire { } catch(\Exception $e) { } + $page->meta()->removeAll(); + /** @var PagesAccess $access */ $access = $this->wire(new PagesAccess()); $access->deletePage($page); @@ -1220,6 +1222,7 @@ class PagesEditor extends Wire { $copy->setQuietly('_cloning', null); $copy->of($of); $page->of($of); + $page->meta()->copyTo($copy->id); $copy->resetTrackChanges(); $this->pages->cloned($page, $copy); $this->pages->debugLog('clone', "page=$page, parent=$parent", $copy); diff --git a/wire/core/WireDataDB.php b/wire/core/WireDataDB.php new file mode 100644 index 00000000..839da508 --- /dev/null +++ b/wire/core/WireDataDB.php @@ -0,0 +1,358 @@ +meta()` method. + * + * ProcessWire 3.x, Copyright 2019 + * https://processwire.com + * + */ +class WireDataDB extends WireData implements \Countable { + + /** + * True when all data from the table has been loaded (a call to getArray will trigger this) + * + * @var bool + * + */ + protected $fullyLoaded = false; + + /** + * ID of the source object for this WireData + * + * @var int + * + */ + protected $sourceID = 0; + + /** + * Name of the table that data will be stored in + * + * @var string + * + */ + protected $table = ''; + + /** + * Construct + * + * @param int $sourceID ID of the source item this WireData is maintaining/persisting data for. + * @param string $tableName Name of the table to store data in. If it does not exist, it will be created. + * + */ + public function __construct($sourceID, $tableName) { + $this->table($tableName); + $this->sourceID($sourceID); + parent::__construct(); + } + + /** + * Get the value for a specific property/name/key + * + * @param string $key + * @return array|mixed|null + * @throws WireException + * + */ + public function get($key) { + $value = parent::get($key); + if($value !== null) return $value; + $value = $this->load($key); + parent::set($key, $value); + return $value; + } + + /** + * Get all values in an associative array + * + * @return array|mixed|null + * @throws WireException + * + */ + public function getArray() { + return $this->load(true); + } + + /** + * Set and save a value for a specific property/name/key + * + * @param string $key + * @param mixed $value + * @return self + * @throws WireException + * + */ + public function set($key, $value) { + if(parent::get($key) === $value) return $this; // no change + if($value === null) return $this->remove($key); // remove + $this->save($key, $value); // set + parent::set($key, $value); + return $this; + } + + /** + * Remove value for a specific property/name/key + * + * @param string $key + * @return self + * @throws WireException + * + */ + public function remove($key) { + $this->delete("$key"); + parent::remove($key); + return $this; + } + + /** + * Remove all values for sourceID from the DB + * + * @return $this + * + */ + public function removeAll() { + $this->delete(true); + $this->reset(); + return $this; + } + + /** + * Reset all loaded data so that it will re-load from DB on next access + * + * @return $this + * + */ + public function reset() { + $this->data = array(); + $this->fullyLoaded = false; + return $this; + } + + /** + * Delete meta value or all meta values (if you specify true) + * + * @param string|bool $name Meta property name to delete or specify boolean true for all + * @return int Number of rows deleted + * @throws WireException + * + */ + protected function delete($name) { + if(empty($name)) return 0; + $table = $this->table(); + $sql = "DELETE FROM `$table` WHERE source_id=:source_id "; + if($name !== true) $sql .= "AND name=:name"; + $query = $this->wire('database')->prepare($sql); + $query->bindValue(':source_id', $this->sourceID(), \PDO::PARAM_INT); + if($name !== true) $query->bindValue(':name', $name); + try { + $query->execute(); + $result = $query->rowCount(); + $query->closeCursor(); + } catch(\Exception $e) { + $result = 0; + } + return $result; + } + + /** + * Load a value or all values + * + * @param string|bool $name Property name to load or boolean true to load all + * @return array|mixed|null + * @throws WireException + * + */ + protected function load($name) { + if(empty($name)) return null; + if($this->fullyLoaded) return $name === true ? parent::getArray() : parent::get($name); + $table = $this->table(); + $sql = "SELECT name, data FROM `$table` WHERE source_id=:source_id "; + if($name !== true) $sql .= "AND name=:name "; + $query = $this->wire('database')->prepare($sql); + $query->bindValue(':source_id', $this->sourceID(), \PDO::PARAM_INT); + if($name !== true) $query->bindValue(':name', $name); + try { + $query->execute(); + } catch(\Exception $e) { + return $name === true ? array() : null; + } + if($query->rowCount()) { + $meta = array(); + while($row = $query->fetch(\PDO::FETCH_NUM)) { + list($key, $data) = $row; + $meta[$key] = json_decode($data, true); + parent::set($key, $meta[$key]); + if($name !== true) break; + } + if($name !== true) $meta = empty($meta) ? null : $meta[$name]; + } else { + $meta = null; + } + if($name === true) $this->fullyLoaded = true; + $query->closeCursor(); + return $meta; + } + + /** + * Save a value + * + * @param string $name + * @param mixed $value + * @param bool $recursive + * @return bool + * @throws WireException + * + */ + protected function save($name, $value, $recursive = false) { + if(is_object($value)) return false; // we do not currently save objects + $data = json_encode($value); + $table = $this->table(); + $sourceID = $this->sourceID(); + if(!$sourceID) return false; + $sql = + "INSERT INTO `$table` (source_id, name, data) VALUES(:source_id, :name, :data) " . + "ON DUPLICATE KEY UPDATE source_id=VALUES(source_id), name=VALUES(name), data=VALUES(data)"; + $query = $this->wire('database')->prepare($sql); + $query->bindValue(':source_id', $this->sourceID(), \PDO::PARAM_INT); + $query->bindValue(':name', $name); + $query->bindValue(':data', $data); + try { + $query->execute(); + $result = $query->rowCount(); + } catch(\Exception $e) { + if($recursive) throw $e; + // table might not yet exist, try to create and save() again + $result = $this->install() ? $this->save($name, $value, true) : false; + } + return $result ? true : false; + } + + /** + * Get or set the the source ID for this instance + * + * @param int|null $id + * @return int + * @throws WireException + * + */ + public function sourceID($id = null) { + if(!is_int($id)) return $this->sourceID; + if($id < 1) throw new WireException($this->className() . ' sourceID must be greater than 0'); + $this->sourceID = $id; + return $this->sourceID; + } + + /** + * Count the number of rows this WireDataDB maintains in the database for source ID. + * + * This implements the \Countable interface. + * + * @return int + * + */ + public function count() { + $table = $this->table(); + $sql = "SELECT COUNT(*) FROM `$table` WHERE source_id=:source_id"; + $query = $this->wire('database')->prepare($sql); + $query->bindValue(':source_id', $this->sourceID(), \PDO::PARAM_INT); + try { + $query->execute(); + $count = (int) $query->fetchColumn(); + } catch(\Exception $e) { + $count = 0; + } + return $count; + } + + /** + * Copy all data to a new source ID + * + * Useful to call on the source object after a clone has been created from it. + * + * @param int $newSourceID + * @throws WireException + * @return int Number of rows copied + * + */ + public function copyTo($newSourceID) { + if(!$this->count()) return 0; + $sourceID = $this->sourceID; + if($newSourceID == $sourceID) return 0; + $data = $this->getArray(); + $this->sourceID($newSourceID); // temporarily set new + foreach($data as $key => $value) { + $this->save($key, $value); + } + $this->sourceID($sourceID); // set back + return count($data); + } + + /** + * Get the current table name + * + * @param string $tableName + * @return string + * + */ + public function table($tableName = '') { + if($tableName !== '') $this->table = strtolower($this->wire('database')->escapeTable($tableName)); + return $this->table; + } + + /** + * Get DB schema in an array + * + * @return array + * + */ + protected function schema() { + return array( + "source_id INT UNSIGNED NOT NULL", + "name VARCHAR(128) NOT NULL", + "data MEDIUMTEXT NOT NULL", + "PRIMARY KEY (source_id, name)", + "INDEX name (name)", + "FULLTEXT KEY data (data)" + ); + } + + /** + * Install the table + * + * @return bool + * @throws WireException + * + */ + public function install() { + $engine = $this->wire('config')->dbEngine; + $charset = $this->wire('config')->dbCharset; + $table = $this->table(); + if($this->wire('database')->tableExists($table)) return false; + $schema = implode(', ', $this->schema()); + $sql = "CREATE TABLE `$table` ($schema) ENGINE=$engine DEFAULT CHARSET=$charset"; + $this->wire('database')->exec($sql); + $this->message("Added '$table' table to database"); + return true; + } + + /** + * Uninstall the table + * + * @return bool + * @throws WireException + * + */ + public function uninstall() { + $table = $this->table(); + $this->wire('database')->exec("DROP TABLE `$table`"); + return true; + } + + public function getIterator() { + return new \ArrayObject($this->getArray()); + } + +} \ No newline at end of file diff --git a/wire/modules/System/SystemUpdater/SystemUpdater.module b/wire/modules/System/SystemUpdater/SystemUpdater.module index 89626cc8..e20a57b3 100644 --- a/wire/modules/System/SystemUpdater/SystemUpdater.module +++ b/wire/modules/System/SystemUpdater/SystemUpdater.module @@ -266,7 +266,8 @@ class SystemUpdater extends WireData implements Module, ConfigurableModule { $f = $this->wire('modules')->get('InputfieldMarkup'); $f->attr('name', '_log'); $f->label = $this->_('System Update Log'); - $f->value = '
' . $this->wire('sanitizer')->entities(file_get_contents($logfile)) . '
'; + $logContent = $this->wire('sanitizer')->unentities(file_get_contents($logfile)); + $f->value = '
' . $this->wire('sanitizer')->entities($logContent) . '
'; $inputfields->add($f); }