diff --git a/Config.php b/Config.php new file mode 100644 index 0000000..acd76ea --- /dev/null +++ b/Config.php @@ -0,0 +1,67 @@ + + * @link http://netmonsters.ru + * @package Majestic + * @subpackage Model + * @since 2010-02-17 + * @version SVN: $Id$ + * @filesource $URL$ + */ + +class Config extends ArrayObject +{ + + private static $_instance = null; + + + public function __construct($config = array()) + { + parent::__construct($config, ArrayObject::ARRAY_AS_PROPS); + } + + /** + * @return Config + */ + static public function getInstance() + { + if (is_null(self::$_instance)) { + self::$_instance = new Config(); + } + return self::$_instance; + } + + static public function get($name) + { + $instance = self::getInstance(); + if (! $instance->offsetExists($name)) { + throw new Exception('Configuration variable "' . $name . '" undefined'); + } + return $instance->offsetGet($name); + } + + static public function set($name, $value) + { + if (is_array($value)) { + $value = new ConfigArray($value); + } + self::getInstance()->offsetSet($name, $value); + } + +} + +class ConfigArray extends ArrayObject +{ + public function __construct($array) + { + parent::__construct($array, ArrayObject::ARRAY_AS_PROPS); + } + + public function offsetGet($index) + { + if (! $this->offsetExists($index)) { + throw new Exception('Configuration variable "' . $index . '" undefined'); + } + return parent::offsetGet($index); + } +} \ No newline at end of file diff --git a/model/Db.php b/model/Db.php new file mode 100644 index 0000000..aa2445a --- /dev/null +++ b/model/Db.php @@ -0,0 +1,65 @@ + + * @link http://netmonsters.ru + * @package Majestic + * @subpackage db + * @since 2010-02-16 + * @version SVN: $Id$ + * @filesource $URL$ + */ + +class Db +{ + + const FETCH_ASSOC = 2; + const FETCH_NUM = 3; + const FETCH_BOTH = 4; + const FETCH_OBJ = 5; + + /** + * Databases connections + * + * @var array + */ + static protected $connections = array(); + + /** + * Connect to database + * + * @param string $name Database name. If not set 'default' will be used. + * @param array $config Configuration array. + * + * @return DbDriver + */ + static public function connect($name = null, $config = null) + { + if ($name === null) { + $name = 'default'; + } + + if (! isset(self::$connections[$name])) { + if (!$config) { + $config = Config::get('databases')->$name; + } + + if (! is_array($config)) { + throw new Exception('Connection parameters must be an array'); + } + + $driver = 'MySQLiDriver'; + if (isset($config['driver'])) { + $driver = $config['driver']; + unset($config['driver']); + } + + $connection = new $driver($config); + + if (! $connection instanceof DbDriver) { + throw new Exception('Database driver must extends DbDriver'); + } + self::$connections[$name] = $connection; + } + return self::$connections[$name]; + } +} \ No newline at end of file diff --git a/model/DbDriver.php b/model/DbDriver.php new file mode 100644 index 0000000..850537c --- /dev/null +++ b/model/DbDriver.php @@ -0,0 +1,229 @@ + + * @link http://netmonsters.ru + * @package Majestic + * @subpackage db + * @since 2010-02-16 + * @version SVN: $Id$ + * @filesource $URL$ + */ + +abstract class DbDriver +{ + + protected $identifier_quote = '`'; + + /** + * Database connection + * + * @var object + */ + protected $connection = null; + + /** + * Configuration data + * + * @var array + */ + protected $config = array(); + + + public function __construct($config) + { + $this->checkConfig($config); + $this->config = $config; + } + + protected function checkConfig($config) + { + $required = array('database', 'username', 'password', 'hostname'); + foreach ($required as $option) { + if (!isset($config[$option])) { + throw new Exception('Configuration must have a "' . $option . '".'); + } + } + } + + public function getConnection() + { + $this->connect(); + return $this->connection; + } + + public function beginTransaction() + { + $this->connect(); + $this->driverBeginTransaction(); + return $this; + } + + public function commit() + { + $this->connect(); + $this->driverCommitTransaction(); + return $this; + } + + public function rollback() + { + $this->connect(); + $this->driverRollbackTransaction(); + return $this; + } + + /** + * @param string $sql + * @param mixed $params + * @return DbStatement + */ + public function query($sql, $params = array()) + { + $this->connect(); + if (!is_array($params)) { + $params = array($params); + } + $stmt = $this->prepare($sql); + $stmt->execute($params); + return $stmt; + } + + /** + * @param string $table + * @param mixed $bind + * @return int Affected rows count + */ + public function insert($table, $bind) + { + $columns = array(); + foreach ($bind as $col => $val) { + $columns[] = $this->quoteIdentifier($col); + } + $values = array_values($bind); + + $sql = 'INSERT INTO ' . $this->quoteIdentifier($table) + . ' (' . implode(', ', $columns) . ') VALUES (' . $this->quote($values) . ')'; + return $this->query($sql)->affectedRows(); + } + + /** + * @param string $table + * @param array $bind + * @param mixed $where + * @return int + */ + public function update($table, $bind, $where = '') + { + $set = array(); + foreach ($bind as $col => $val) { + $set[] = $this->quoteIdentifier($col) . '=' . $this->quote($val); + } + $where = $this->whereExpr($where); + $sql = 'UPDATE ' . $this->quoteIdentifier($table) . ' SET ' . implode(', ', $set) + . (($where) ? (' WHERE ' . $where) : ''); + return $this->query($sql)->affectedRows(); + } + + /** + * @param string $table + * @param mixed $where + * @return int + */ + public function delete($table, $where = '') + { + $where = $this->whereExpr($where); + $sql = 'DELETE FROM ' . $this->quoteIdentifier($table) . (($where) ? (' WHERE ' . $where) : ''); + return $this->query($sql)->affectedRows(); + } + + /** + * @param mixed $value + * @return string + */ + public function quote($value) + { + if ($value instanceof DbExpr) { + return (string) $value; + } + if (is_array($value)) { + foreach ($value as &$val) { + $val = $this->quote($val); + } + return implode(', ', $value); + } + return $this->driverQuote($value); + } + + /** + * @param string $ident + * @return string + */ + public function quoteIdentifier($ident) + { + $ident = explode('.', $ident); + if (!is_array($ident)) { + $ident = array($ident); + } + foreach ($ident as &$segment) { + $segment = $this->identifier_quote . $segment . $this->identifier_quote; + } + return implode('.', $ident); + } + + public function quoteInto($text, $value) + { + $pos = strpos($text, '?'); + if ($pos === false) { + return $text; + } + return mb_substr($text, 0, $pos) . $this->quote($value) . mb_substr($text, $pos + 1); + } + + /** + * @param mixed $where + * @return string + */ + protected function whereExpr($where) + { + if (empty($where)) { + return $where; + } + + if (!is_array($where)) { + $where = array($where); + } + foreach ($where as $cond => &$term) { + if (is_int($cond)) { + if ($term instanceof DbExpr) { + $term = (string) $term; + } + } else { + $term = $this->quoteInto($cond, $term); + } + } + return implode(' AND ', $where); + } + + /* Abstract methods */ + + /** + * @return DbStatement + */ + abstract public function prepare($sql); + + abstract public function getInsertId($table = null, $key = null); + + abstract public function isConnected(); + + abstract public function disconnect(); + + abstract protected function connect(); + + abstract protected function driverQuote($value); + + abstract protected function driverBeginTransaction(); + + abstract protected function driverCommitTransaction(); + + abstract protected function driverRollbackTransaction(); +} \ No newline at end of file diff --git a/model/DbExpr.php b/model/DbExpr.php new file mode 100644 index 0000000..413d44d --- /dev/null +++ b/model/DbExpr.php @@ -0,0 +1,30 @@ + + * @link http://netmonsters.ru + * @package Majestic + * @subpackage db + * @since 2010-02-19 + * @version SVN: $Id$ + * @filesource $URL$ + */ + +class DbExpr +{ + + protected $expression; + + /** + * @param string $expression + * @return DbExpr + */ + public function __construct($expression) + { + $this->expression = (string) $expression; + } + + public function __toString() + { + return $this->expression; + } +} \ No newline at end of file diff --git a/model/DbStatement.php b/model/DbStatement.php new file mode 100644 index 0000000..72621e0 --- /dev/null +++ b/model/DbStatement.php @@ -0,0 +1,161 @@ + + * @link http://netmonsters.ru + * @package Majestic + * @subpackage db + * @since 2010-02-19 + * @version SVN: $Id$ + * @filesource $URL$ + */ + +abstract class DbStatement +{ + + /** + * @var DbDriver + */ + protected $driver; + + /** + * @var string + */ + protected $sql; + + protected $map = null; + + protected $params = array(); + + protected $result; + + public function __construct($driver, $sql) + { + $this->driver = $driver; + $this->sql = $sql; + } + + /** + * @param mixed $style + * @return array + */ + public function fetchAll($style = Db::FETCH_OBJ) + { + $data = array(); + while ($row = $this->fetch($style)) { + $data[] = $row; + } + return $data; + } + + /** + * @param string $field + */ + public function fetchField($field) + { + $row = $this->fetch(Db::FETCH_ASSOC); + if (isset($row[$field])) { + return $row[$field]; + } + return false; + } + + public function bindParam($param, &$value) + { + if ($this->map === null) { + $this->mapPlaceholders(); + } + if (count($this->map) > 0) { + if (!is_string($param) && !is_int($param)) { + throw new Exception('Placeholder must be an array or string'); + } + if (is_object($value) && ! ($value instanceof DbExpr)) { + throw new Exception('Objects excepts DbExpr not allowed.'); + } + if (isset($this->map[$param])) { + $this->params[$param] = &$value; + return true; + } + } + return false; + } + + /** + * @param array $params + * @return bool + */ + public function execute($params = null) + { + if (is_array($params)) { + foreach ($params as $param => &$value) { + $this->bindParam($param, $value); + } + } + return $this->driverExecute($this->assemble()); + } + + protected function mapPlaceholders() + { + $matches = array(); + if(preg_match_all('/(\?|:[A-z0-9_]+)/u', $this->sql, $matches, PREG_OFFSET_CAPTURE)) { + $noname = 0; + foreach ($matches[0] as $id=>$match) { + $match[2] = $matches[1][$id][0]; + $name = ($match[2][0] === ':') ? ltrim($match[2], ':') : $noname++; + $this->map[$name]['placeholder'] = $match[0]; + $this->map[$name]['offset'][] = $match[1]; + } + } + } + + protected function assemble() + { + if (empty($this->map)) { + return $this->sql; + } + + $query = $this->sql; + $placeholders = array(); + foreach($this->map as $name => $place) { + $value = $this->driver->quote($this->params[$name]); + foreach ($place['offset'] as $offset) { + $placeholders[$offset] = array('placeholder' => $place['placeholder'], 'value' => $value); + } + } + + ksort($placeholders); + + $increment = 0; + foreach($placeholders as $current_offset => $placeholder) { + $offset = $current_offset + $increment; + $length = mb_strlen($placeholder['placeholder']); + $query = mb_substr($query, 0, $offset) . $placeholder['value'] . mb_substr($query, $offset + $length); + $increment = (($increment - $length) + mb_strlen($placeholder['value'])); + } + return $query; + } + + public function __destruct() + { + $this->close(); + } + + /* Abstract methods */ + + abstract public function fetch($style = Db::FETCH_OBJ); + + abstract public function fetchObject($class = 'stdClass'); + + abstract public function close(); + + /** + * @return int + */ + abstract public function affectedRows(); + + abstract public function numRows(); + + /** + * @return bool + */ + abstract protected function driverExecute($sql); +} \ No newline at end of file diff --git a/model/Model.php b/model/Model.php new file mode 100644 index 0000000..ed3dd5b --- /dev/null +++ b/model/Model.php @@ -0,0 +1,135 @@ + + * @link http://netmonsters.ru + * @package Majestic + * @subpackage Model + * @since 2010-02-16 + * @version SVN: $Id$ + * @filesource $URL$ + */ + +abstract class Model +{ + + /** + * DbDriver instance + * + * @var DbDriver + */ + protected $db; + + protected $cache; + + protected $table; + + protected $connection = 'default'; + + protected $key = 'id'; + + public function __construct() + { + $this->db = Db::connect($this->connection); + } + + /** + * @return int + */ + public function getInsertId() + { + return $this->db->getInsertId($this->table(), $this->key); + } + + /** + * @param string $ident + * @return string Quoted identifier. + */ + public function identify($ident) + { + return $this->db->quoteIdentifier($ident); + } + + /** + * @param mixed $value + * @return string Quoted value. + */ + public function quote($value) + { + return $this->db->quote($value); + } + + /** + * @param int $id + * @return object + */ + public function get($id) + { + $sql = 'SELECT * FROM ' . $this->identify($this->table()) + . ' WHERE ' . $this->identify($this->key) . '=' . (int) $id; + return $this->db->query($sql)->fetch(); + } + + /** + * @param array $data + * @return int Affect row or id of inserted field. + */ + public function save($data) + { + $key = isset($data[$this->key]) ? $data[$this->key] : false; + $result = false; + if ($key) { + unset($data[$this->key]); + $result = $this->update($data, $this->identify($this->key) . '=' . (int) $id); + } else { + $result = $this->insert($data); + } + return $result; + } + + /** + * @param array $data + * @return int Id of inserted row + */ + public function insert($data) + { + if (! $this->db->insert($this->table(), $data)) { + return false; + } + return $this->getInsertId(); + } + + /** + * @param array $data + * @param mixed $where + * @return int Number of affected rows + */ + public function update($data, $where) + { + return $this->db->update($this->table(), $data, $where); + } + + /** + * @param mixed $where + * @return int Number of affected rows + */ + public function delete($where) + { + if (is_int($where)) { + $where = $this->identify($this->key) . '=' . (int) $where; + } + return $this->db->delete($this->table(), $where); + } + + /** + * @return string + */ + public function table() + { + if (! $this->table) { + $this->table = rtrim(strtolower(get_class($this)), 'model'); + } + return $this->table; + } +} \ No newline at end of file diff --git a/model/MySQLiDriver.php b/model/MySQLiDriver.php new file mode 100644 index 0000000..413b29e --- /dev/null +++ b/model/MySQLiDriver.php @@ -0,0 +1,104 @@ + + * @link http://netmonsters.ru + * @package Majestic + * @subpackage db + * @since 2010-02-17 + * @version SVN: $Id$ + * @filesource $URL$ + */ + +/** + * @property MySQLi $connection + */ +class MySQLiDriver extends DbDriver +{ + + /** + * @param mixed $sql + * @return DbStatement + */ + public function prepare($sql) + { + return new MySQLiStatement($this, $sql); + } + + public function getInsertId($table = null, $key = null) + { + return $this->connection->insert_id; + } + + public function isConnected() + { + return ($this->connection instanceof MySQLi); + } + + public function disconnect() + { + if ($this->isConnected()) { + $this->connection->close(); + } + $this->connection = null; + } + + protected function connect() + { + if ($this->connection) { + return; + } + + $port = isset($this->config['port']) ? (int) $this->config['port'] : null; + $this->connection = mysqli_init(); + $connected = @mysqli_real_connect($this->connection, + $this->config['hostname'], + $this->config['username'], + $this->config['password'], + $this->config['database'], + $port); + // Connection errors check + if (mysqli_connect_error()) { + throw new Exception(mysqli_connect_error(), mysqli_connect_errno()); + } + + $charset = (!empty($this->config['charset'])) ? $this->config['charset'] : 'utf8'; + $this->connection->set_charset($charset); + } + + protected function driverQuote($value) + { + if (is_int($value) || is_float($value)) { + return $value; + } + + if (is_bool($value)) { + var_dump($value); + return (int) $value; + } + + if ($value === null) { + return 'NULL'; + } + + $this->connect(); + return '\'' . $this->connection->real_escape_string($value) . '\''; + } + + protected function driverBeginTransaction() + { + $this->connect(); + $this->connection->autocommit(false); + } + + protected function driverCommitTransaction() + { + $this->connection->commit(); + $this->connection->autocommit(true); + } + + protected function driverRollbackTransaction() + { + $this->connection->rollback(); + $this->connection->autocommit(true); + } +} \ No newline at end of file diff --git a/model/MySQLiStatement.php b/model/MySQLiStatement.php new file mode 100644 index 0000000..2fc651d --- /dev/null +++ b/model/MySQLiStatement.php @@ -0,0 +1,89 @@ + + * @link http://netmonsters.ru + * @package Majestic + * @subpackage db + * @since 2010-02-19 + * @version SVN: $Id$ + * @filesource $URL$ + */ + +/** + * @property MySQLiDriver $driver + * @property MySQLi_Result $result + */ +class MySQLiStatement extends DbStatement +{ + + public function fetch($style = Db::FETCH_OBJ) + { + if (! $this->result) { + return false; + } + + $row = false; + switch ($style) { + case Db::FETCH_OBJ: + $row = $this->result->fetch_object(); + break; + case Db::FETCH_NUM: + $row = $this->result->fetch_array(MYSQLI_NUM); + break; + case Db::FETCH_ASSOC: + $row = $this->result->fetch_assoc(); + break; + case Db::FETCH_BOTH: + $row = $this->result->fetch_array(MYSQLI_BOTH); + break; + default: + throw new Exception('Invalid fetch mode "' . $style . '" specified'); + } + return $row; + } + + /** + * @param string $class + */ + public function fetchObject($class = 'stdClass') + { + return $this->result->fetch_object($class); + } + + public function close() + { + if ($this->result !== null) { + $this->result->close(); + $this->result = null; + } + } + + public function affectedRows() + { + return $this->driver->getConnection()->affected_rows; + } + + public function numRows() + { + if ($this->result) { + return $this->result->num_rows; + } + return false; + } + + protected function driverExecute($sql) + { + /** + * @var MySQLi + */ + $mysqli = $this->driver->getConnection(); + $result = $mysqli->query($sql); + if ($result === false) { + throw new Exception($mysqli->error, $mysqli->errno); + } + if ($result instanceof MySQLi_Result) { + $this->result = $result; + } + return true; + } +} \ No newline at end of file