1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-09 00:06:55 +02:00

Add support for $page->meta() method for maintaining persistent meta data for pages independent of fields and the normal load/save process.

This commit is contained in:
Ryan Cramer
2019-06-07 12:17:27 -04:00
parent 50a6f3585e
commit 204335e2d3
4 changed files with 431 additions and 3 deletions

View File

@@ -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
@@ -603,6 +606,14 @@ class Page extends WireData implements \Countable, WireMatchable {
'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);
@@ -4364,5 +4380,55 @@ class Page extends WireData implements \Countable, WireMatchable {
}
*/
/**
* Get or set pages 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
}
}

View File

@@ -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);

358
wire/core/WireDataDB.php Normal file
View File

@@ -0,0 +1,358 @@
<?php namespace ProcessWire;
/**
* WireData with database storage
*
* A WireData object that maintains its data in a database table rather than just in memory.
* An example of usage is the `$page->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());
}
}

View File

@@ -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 = '<pre>' . $this->wire('sanitizer')->entities(file_get_contents($logfile)) . '</pre>';
$logContent = $this->wire('sanitizer')->unentities(file_get_contents($logfile));
$f->value = '<pre>' . $this->wire('sanitizer')->entities($logContent) . '</pre>';
$inputfields->add($f);
}