From 26d00fe51bd4399ec2f9dff4a797daf2fc23961a Mon Sep 17 00:00:00 2001 From: Florent Bruneau Date: Thu, 14 Oct 2010 17:40:36 +0200 Subject: [PATCH] Add a new class that aims at providing a simple gateway between stored data and php code when data are fetched 'as is'. UseCase: classes like Phones or Addresses. How to use: /* Fetching the content of a entry */ $entry = new PlDBTableEntry('accounts', true /* autofetch */); $entry->uid = 26071; print $entry->hruid; /* Iterating on entries and saving changes*/ $selector = new PlDBTableEntry('profile_phones'); $selector->pid = 26071; $selector->link_type = 'pro'; foreach ($selector as $entry) { $entry->comment = 'Professional phone number'; $entry->save(); } Note: ATM, SQL joins are not supported, but this is planned for the future with an API that might looks like: $entry = new PlDBTableEntry(array($mainTable, PlSqlJoin::left(), PlSqlJoin::inner())); Signed-off-by: Florent Bruneau --- classes/pldbtableentry.php | 400 +++++++++++++++++++++++++++++++++++++++++++++ classes/plflagset.php | 7 + include/misc.inc.php | 47 ++++++ include/platal.inc.php | 4 +- 4 files changed, 457 insertions(+), 1 deletion(-) create mode 100644 classes/pldbtableentry.php diff --git a/classes/pldbtableentry.php b/classes/pldbtableentry.php new file mode 100644 index 0000000..2e1bc11 --- /dev/null +++ b/classes/pldbtableentry.php @@ -0,0 +1,400 @@ +table->table . '.' . $field->name . '\'): ' + . $reason); + } +} + +class PlDBNoSuchFieldException extends PlException +{ + public function __construct($field, PlDBTable $table) + { + parent::__construct('Erreur lors de l\'accès à la base de données', + 'No such field ' . $field . ' in table ' . $table->table); + } +} + +class PlDBIncompleteEntryDescription extends PlException +{ + public function __construct($field, PlDBTable $table) + { + parent::__construct('Erreur lors de l\'accès à la base de données', + 'The field ' . $field . ' is required to describe an entry in table ' + . $table->table); + } +} + +class PlDBTableField +{ + public $table; + + public $name; + public $inPrimaryKey; + public $inUniqueKey; + public $inKey; + + public $type; + public $typeLength; + public $typeParameters; + + public $allowNull; + public $defaultValue; + public $autoIncrement; + + public function __construct(array $column) + { + $this->name = $column['Field']; + $this->typeParameters = explode(' ', str_replace(array('(', ')', ',', '\''), ' ', + $column['Type'])); + $this->type = array_shift($this->typeParameters); + if ($this->type == 'enum' || $this->type == 'set') { + $this->typeParameters = new PlFlagSet(implode(',', $this->typeParameters)); + } else if (ctype_digit($this->typeParameters[0])) { + $this->typeLength = intval($this->typeParameters[0]); + array_shift($this->typeParameters); + } + $this->allowNull = ($column['Null'] === 'YES'); + $this->autoIncrement = (strpos($column['Extra'], 'auto_increment') !== false); + $this->inPrimaryKey = ($column['Key'] === 'PRI'); + $this->inUniqueKey = $this->inPrimaryKey || ($column['Key'] === 'UNI'); + $this->inKey = $this->inUniqueKey || ($column['Key'] === 'MUL'); + + try { + $this->defaultValue = $this->format($column['Default']); + } catch (PlDBBadValueException $e) { + $this->defaultValue = null; + } + } + + public function format($value, $badNullFallbackToDefault = false) + { + if (is_null($value)) { + if ($this->allowNull || $this->autoIncrement) { + return $value; + } + if ($badNullFallbackToDefault) { + return $this->defaultValue; + } + throw new PlDBBadValueException($value, $this, 'null not allowed'); + } else if ($this->type == 'enum') { + if (!$this->typeParameters->hasFlag($value)) { + throw new PlDBBadValueException($value, $this, 'invalid value for enum ' . $this->typeParameters->flags()); + } + return $value; + } else if ($this->type == 'set') { + $value = new PlFlagSet($value); + foreach ($value as $flag) { + if (!$this->typeParameters->hasFlag($flag)) { + throw new PlDBBadValueException($value, $this, 'invalid flag for set ' . $this->typeParameters->flags()); + } + } + return $value; + } else if (ends_with($this->type, 'int')) { + if (!is_int($value) && !ctype_digit($value)) { + throw new PlDBBadValueException($value, $this, 'value is not an integer'); + } + $value = intval($value); + if (count($this->typeParameters) > 0 && $this->typeParameters[0] == 'unsigned') { + if ($value < 0) { + throw new PlDBBadValueException($value, $this, 'value is negative in an unsigned field'); + } + } + /* TODO: Check bounds */ + return $value; + } else if ($this->type == 'varchar') { + if (strlen($value) > $this->typeLength) { + throw new PlDBBadValueException($value, $this, 'value is expected to be at most ' . $this->typeLength . ' characters long, ' . strlen($value) . ' given'); + } + return $value; + } else if ($this->type == 'char') { + if (strlen($value) != $this->typeLength) { + throw new PlDBBadValueException($value, $this, 'value is expected to be ' . $this->typeLength . ' characters long, ' . strlen($value) . ' given'); + } + return $value; + } + /* TODO: Support data and times */ + return $value; + } +} + + +/** This class aims at providing a simple interface to interact with a single + * table of a database. It is implemented as a wrapper around XDB. + */ +class PlDBTable +{ + public $table; + + private $schema; + private $keyFields; + private $mutableFields; + + public function __construct($table) + { + $this->table = $table; + $this->schema(); + } + + private function parseSchema(PlIterator $schema) + { + $this->schema = array(); + $this->keyFields = array(); + $this->mutableFields = array(); + while ($column = $schema->next()) { + $field = new PlDBTableField($column); + $this->schema[$field->name] = $field; + if ($field->inPrimaryKey) { + $this->keyFields[] = $field->name; + } else { + $this->mutableFields[] = $field->name; + } + } + } + + + private function schema() + { + if (!$this->schema) { + $schema = XDB::iterator('DESCRIBE ' . $this->table); + $this->parseSchema($schema); + } + return $this->schema; + } + + private function field($field) + { + $schema = $this->schema(); + if (!isset($schema[$field])) { + throw new PlDBNoSuchFieldException($field, $this); + } + return $schema[$field]; + } + + public function formatField($field, $value) + { + return $this->field($field)->format($value); + } + + public function defaultValue($field) + { + return $this->field($field)->defaultValue; + } + + public function primaryKey(PlDBTableEntry $entry) + { + $key = array(); + foreach ($this->keyFields as $field) { + if (!isset($entry->$field)) { + throw new PlDBIncompleteEntryDescription($field, $this); + } else { + $key[] = XDB::escape($this->$field); + } + } + return implode('-', $key); + } + + private function buildKeyCondition(PlDBTableEntry $entry, $allowIncomplete) + { + $condition = array(); + foreach ($this->keyFields as $field) { + if (!isset($entry->$field)) { + if (!$allowIncomplete) { + throw new PlDBIncompleteEntryDescription($field, $this); + } + } else { + $condition[] = XDB::format($field . ' = {?}', $entry->$field); + } + } + return implode(' AND ', $condition); + } + + public function fetchEntry(PlDBTableEntry $entry) + { + $result = XDB::rawFetchOneAssoc('SELECT * + FROM ' . $this->table . ' + WHERE ' . $this->buildKeyCondition($entry, false)); + if (!$result) { + return false; + } + return $entry->fillFromDBData($result); + } + + public function iterateOnEntry(PlDBTableEntry $entry) + { + $it = XDB::rawIterator('SELECT * + FROM ' . $this->table . ' + WHERE ' . $this->buildKeyCondition($entry, true)); + return PlIteratorUtils::map($it, array($entry, 'cloneAndFillFromDBData')); + } + + public function updateEntry(PlDBTableEntry $entry) + { + $values = array(); + foreach ($this->mutableFields as $field) { + if ($entry->hasChanged($field)) { + $values[] = XDB::format($field . ' = {?}', $entry->$field); + } + } + if (count($values) > 0) { + XDB::rawExecute('UPDATE ' . $this->table . ' + SET ' . implode(', ', $values) . ' + WHERE ' . $this->buildKeyCondition($entry, false)); + } + } + + public static function get($name) + { + var_dump('blah'); + return new PlDBTable($name); + } +} + +class PlDBTableEntry extends PlAbstractIterable +{ + private $table; + private $changed; + private $fetched = false; + private $autoFetch; + + private $data = array(); + + public function __construct($table, $autoFetch = false) + { + if ($table instanceof PlDBTable) { + $this->table = $table; + } else { + $this->table = PlCache::getGlobal('pldbtable_' . $table, array('PlDBTable', 'get'), array($table)); + } + $this->autoFetch = $autoFetch; + $this->changed = new PlFlagSet(); + } + + /** This hook is called when the entry is going to be updated in the db. + * + * A typical usecase is a class that stores low-level representation of + * an object in db and perform a conversion between this low-level representation + * and a higher-level representation. + * + * @return true in case of success + */ + protected function preSave() + { + return true; + } + + /** This hook is called when the entry has just been fetched from the db. + * + * This is the counterpart of @ref preSave and a typical use-case is the conversion + * from a high-level representation of the objet to a representation suitable for + * storage in the database. + * + * @return true in case of success. + */ + protected function postFetch() + { + return true; + } + + public function __get($field) + { + if (isset($this->data[$field])) { + return $this->data[$field]; + } else if (!$this->fetched && $this->autoFetch) { + $this->fetch(); + if (isset($this->data[$field])) { + return $this->data[$field]; + } + } + return $this->table->defaultValue($field); + } + + public function __set($field, $value) + { + $this->data[$field] = $this->table->formatField($field, $value); + $this->changed->addFlag($field); + } + + public function __isset($field) + { + return isset($this->data[$field]); + } + + public function primaryKey() + { + $this->table->primaryKey($this); + } + + public function hasChanged($field) + { + return $this->changed->hasFlag($field); + } + + public function fillFromArray(array $data) + { + foreach ($data as $field => $value) { + $this->$field = $value; + } + } + + public function fillFromDBData(array $data) + { + $this->fillFromArray($data); + $this->changed->clear(); + return $this->postFetch(); + } + + public function cloneAndFillFromDBData(array $data) + { + $clone = clone $this; + $clone->fillFromDBData($data); + return $clone; + } + + public function fetch() + { + return $this->table->fetchEntry($this); + } + + public function iterate() + { + return $this->table->iterateOnEntry($this); + } + + public function save() + { + if (!$this->preSave()) { + return false; + } + $this->table->updateEntry($this); + $this->changed->clear(); + return true; + } +} + +// vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8: +?> diff --git a/classes/plflagset.php b/classes/plflagset.php index b5f30d9..6e5a039 100644 --- a/classes/plflagset.php +++ b/classes/plflagset.php @@ -104,6 +104,13 @@ class PlFlagSet extends PlAbstractIterable implements XDBFormat } } + /** Remove all the flags. + */ + public function clear() + { + $this->values = array(); + } + /** return the PlFlagSet */ diff --git a/include/misc.inc.php b/include/misc.inc.php index 3615bcb..bfa5163 100644 --- a/include/misc.inc.php +++ b/include/misc.inc.php @@ -323,5 +323,52 @@ function format_datetime($date, $format) // } } +/** Get the first n characters of the string + */ +function left($string, $count) +{ + return substr($string, 0, $count); +} + +/** Get the last n characters of the string + */ +function right($string, $count) +{ + return substr($string, -$count); +} + +/** Check if a string is a prefix for another one. + */ +function starts_with($string, $prefix, $caseSensitive = true) +{ + $prefixLen = strlen($prefix); + if (strlen($string) < $prefixLen) { + return false; + } + $part = left($string, $prefixLen); + if ($caseSensitive) { + return strcmp($prefix, $part) === 0; + } else { + return strcasecmp($prefix, $part) === 0; + } +} + +/** Check if a string is a suffix for another one. + */ +function ends_with($string, $suffix, $caseSensitive = true) +{ + $suffixLen = strlen($suffix); + if (strlen($string) < $suffixLen) { + return false; + } + $part = right($string, $suffixLen); + if ($caseSensitive) { + return strcmp($suffix, $part) === 0; + } else { + return strcasecmp($suffix, $part) === 0; + } +} + + // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8: ?> diff --git a/include/platal.inc.php b/include/platal.inc.php index 7b491c8..069a661 100644 --- a/include/platal.inc.php +++ b/include/platal.inc.php @@ -39,8 +39,10 @@ define('NO_HTTPS', 2); function pl_autoload($cls, array $pathes = array()) { $cls = strtolower($cls); - if (substr($cls, 0, 3) == 'xdb') { + if (starts_with($cls, 'xdb')) { $cls = 'xdb'; + } else if (starts_with($cls, 'pldbtable')) { + $cls = 'pldbtableentry'; } $basepath = dirname(dirname(dirname(__FILE__))); -- 2.1.4