artabro/wire/core/WireDataDB.php
2024-08-27 11:35:37 +02:00

366 lines
8.7 KiB
PHP

<?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 2023
* 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
*
*/
#[\ReturnTypeWillChange]
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 === '') return $this->table;
if(!ctype_alnum(str_replace('_', '', $tableName))) {
$tableName = preg_replace('/[^_a-zA-Z0-9]/', '_', $tableName);
}
$this->table = strtolower($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() {
$config = $this->wire()->config;
$database = $this->wire()->database;
$engine = $config->dbEngine;
$charset = $config->dbCharset;
$table = $this->table();
if($database->tableExists($table)) return false;
$schema = implode(', ', $this->schema());
$sql = "CREATE TABLE `$table` ($schema) ENGINE=$engine DEFAULT CHARSET=$charset";
$this->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;
}
#[\ReturnTypeWillChange]
public function getIterator() {
return new \ArrayObject($this->getArray());
}
}