소스 검색

Add own ORM without any dependencies, yay

ohartl 9 년 전
부모
커밋
a1e13a9919
2개의 변경된 파일948개의 추가작업 그리고 0개의 파일을 삭제
  1. 691 0
      include/php/models/AbstractModel.php
  2. 257 0
      include/php/models/ModelCollection.php

+ 691 - 0
include/php/models/AbstractModel.php

@@ -0,0 +1,691 @@
+<?php
+
+abstract class AbstractModel
+{
+
+	/**
+	 * Db table for find methods
+	 *
+	 * @var string
+	 */
+	public static $table;
+
+
+	/**
+	 * Db id attribute for find methods
+	 *
+	 * @var string
+	 */
+	public static $idAttribute;
+
+
+	/**
+	 * Mapping model attributes and database attributes for saving
+	 *
+	 * @var array
+	 */
+	protected $attributeDbAttributeMapping = array();
+
+
+	/**
+	 * Setup db attribute mapping
+	 *
+	 * @param array $childMapping
+	 *
+	 * @return array
+	 *
+	 * @throws Exception
+	 */
+	protected function setupDbMapping($childMapping = array())
+	{
+		return array_replace(
+			array(
+				'id' => static::$idAttribute,
+			),
+			$childMapping
+		);
+	}
+
+
+	/**
+	 * Format or do other things before saving
+	 *
+	 * @param array $data
+	 *
+	 * @return array
+	 */
+	protected function preSave($data)
+	{
+		return $data;
+	}
+
+
+	/**
+	 * Hold all data from a model
+	 *
+	 * @var mixed
+	 */
+	protected $data = array();
+
+
+	/**
+	 * Constructor.
+	 *
+	 * @param array $data
+	 */
+	protected function __construct($data)
+	{
+		$this->attributeDbAttributeMapping = $this->setupDbMapping();
+
+		if(isset($data[static::$idAttribute])){
+			$id = is_numeric($data[static::$idAttribute]) && strpos($data[static::$idAttribute], ',') === false
+				? intval($data[static::$idAttribute])
+				: $data[static::$idAttribute];
+
+			$this->setId($id);
+		}
+	}
+
+
+	/**
+	 * Create a model from data
+	 *
+	 * @param array $data
+	 *
+	 * @return static|null The Model
+	 */
+	public static function create($data)
+	{
+		if(count($data) > 0){
+			return new static($data);
+		}
+
+		return null;
+	}
+
+
+	/**
+	 * Create a model collection from data
+	 *
+	 * @param array $multiData
+	 *
+	 * @return ModelCollection|static[]
+	 */
+	public static function createMultiple($multiData = array())
+	{
+		$collection = new ModelCollection();
+
+		foreach($multiData as $data){
+			$model = static::create($data);
+
+			if(!is_null($model)){
+				if(is_null($model->getId())){
+					$collection->add($model);
+				}
+				else{
+					$collection->add($model, $model->getId());
+				}
+			}
+		}
+
+		return $collection;
+	}
+
+
+	/**
+	 * @see create
+	 *
+	 * @param array $data
+	 *
+	 * @return AbstractModel|null
+	 */
+	public static function createAndSave($data)
+	{
+		$model = static::create($data);
+
+		if(!is_null($model) && $model->save()){
+			return $model;
+		}
+
+		return null;
+	}
+
+
+	/**
+	 * @see createMultiple
+	 *
+	 * @param array $multiData
+	 *
+	 * @return ModelCollection|static[]
+	 */
+	public static function createMultipleAndSave($multiData = array())
+	{
+		$collection = new ModelCollection();
+
+		foreach($multiData as $data){
+			$model = static::createAndSave($data);
+
+			if(!is_null($model)){
+				$collection->add($model);
+			}
+		}
+
+		return $collection;
+	}
+
+
+	/**
+	 * Create a model from mysqli result
+	 *
+	 * @param mysqli_result $result
+	 *
+	 * @return static|null
+	 */
+	public static function createFromDbResult($result)
+	{
+		if($result->num_rows === 0){
+			return null;
+		}
+
+		return static::create($result->fetch_assoc());
+	}
+
+
+	/**
+	 * Create a model collection from mysqli result
+	 *
+	 * @param mysqli_result $result
+	 *
+	 * @return ModelCollection|static[]
+	 */
+	public static function createMultipleFromDbResult($result)
+	{
+		return static::createMultiple($result->fetch_all(MYSQLI_ASSOC));
+	}
+
+
+	/**
+	 * @param string $attribute
+	 * @param mixed $value
+	 */
+	public function setAttribute($attribute, $value)
+	{
+		$this->data[$attribute] = $value;
+	}
+
+
+	/**
+	 * @param string $attribute
+	 *
+	 * @return mixed|null
+	 */
+	public function getAttribute($attribute)
+	{
+		if(isset($this->data[$attribute])){
+			if(is_array($this->data[$attribute])){
+				return array_map('strip_tags', $this->data[$attribute]);
+			}
+			else{
+				return strip_tags($this->data[$attribute]);
+			}
+		}
+
+		return null;
+	}
+
+
+	/**
+	 * @return mixed
+	 */
+	public function getId()
+	{
+		return $this->getAttribute('id');
+	}
+
+
+	/**
+	 * @param mixed $value
+	 */
+	protected function setId($value)
+	{
+		$this->setAttribute('id', $value);
+	}
+
+
+	/**
+	 * @param array $attributes
+	 *
+	 * @return string
+	 */
+	protected static function sqlHelperAttributeList($attributes)
+	{
+		$sql = "%s";
+		$values = array();
+
+		$keywords = array('AS', 'ASC', 'DESC');
+
+		foreach($attributes as $val){
+			if(!is_array($val)){
+				// raw
+				$values[] = $val;
+				continue;
+			}
+
+			switch(count($val)){
+				case 1:
+					$values[] = "`{$val[0]}`";
+					break;
+				case 2:
+					if(in_array(strtoupper($val[1]), $keywords)){
+						$values[] = "`{$val[0]}` {$val[1]}";
+					}
+					else{
+						$values[] = "`{$val[0]}`.`{$val[1]}`";
+					}
+					break;
+				case 3:
+					if(in_array(strtoupper($val[1]), $keywords)){
+						$values[] = "`{$val[0]}` {$val[1]} `{$val[2]}`";
+					}
+					elseif(in_array(strtoupper($val[2]), $keywords)){
+						$values[] = "`{$val[0]}`.`{$val[1]}` {$val[2]}";
+					}
+					break;
+				case 4:
+					if(in_array(strtoupper($val[1]), $keywords)){
+						$values[] = "`{$val[0]}` {$val[1]} `{$val[2]}`.`{$val[3]}`";
+					}
+					elseif(in_array(strtoupper($val[2]), $keywords)){
+						$values[] = "`{$val[0]}`.`{$val[1]}` {$val[2]} `{$val[3]}`";
+					}
+					else{
+						$values[] = "`{$val[0]}`.`{$val[1]}` `{$val[2]}`.`{$val[3]}`";
+					}
+					break;
+				case 5:
+					if(in_array(strtoupper($val[2]), $keywords)){
+						$values[] = "`{$val[0]}`.`{$val[1]}` {$val[2]} `{$val[3]}`.`{$val[4]}`";
+					}
+					break;
+			}
+		}
+
+		return sprintf($sql, implode(', ', $values));
+	}
+
+
+	/**
+	 * @param mixed $value
+	 *
+	 * @return string
+	 */
+	protected static function sqlHelperValue($value)
+	{
+		global $db;
+		if(is_null($value) || (is_string($value) && strtoupper($value) === 'NULL')){
+			return "NULL";
+		}
+		elseif(is_array($value)){
+			return static::sqlHelperValueList($value);
+		}
+
+		return "'{$db->escape_string($value)}'";
+	}
+
+
+	/**
+	 * @param array $values
+	 *
+	 * @return string
+	 */
+	protected static function sqlHelperValueList($values)
+	{
+		$sql = "(%s)";
+		$sqlValues = array();
+
+		foreach($values as $val){
+			$sqlValues[] = static::sqlHelperValue($val);
+		}
+
+		return sprintf($sql, implode(', ', $sqlValues));
+	}
+
+
+	/**
+	 * @param array $conditions
+	 *     array('attr', '=', '3') => "`attr` = '3'"
+	 *     array(
+	 *         array('`attr` = '3') (raw SQL) => `attr` = '3'
+	 *         array('attr', 3) => `attr` = '3'
+	 *         array('attr', '=', '3') => `attr` = '3'
+	 *         array('attr', '<=', 3) => `attr` <= '3'
+	 *         array('attr', 'LIKE', '%asd') => `attr` LIKE '%asd'
+	 *         array('attr', 'IS', null) => `attr` IS NULL
+	 *         array('attr', 'IS NOT', null) => `attr` IS NOT NULL
+	 *     )
+	 * @param string $conditionConnector AND, OR
+	 *
+	 * @return string
+	 */
+	protected static function sqlHelperConditionList($conditions, $conditionConnector = 'AND')
+	{
+		$values = array();
+
+		// detect non nested array
+		if(count($conditions) > 0 && !is_array($conditions[0])){
+			$conditions = array($conditions);
+		}
+
+		$conditionConnector = strtoupper($conditionConnector);
+		if(in_array($conditionConnector, array('AND', 'OR'))){
+			$conditionConnector = " ".$conditionConnector;
+		}
+
+		$sql = "`%s` %s %s";
+
+		foreach($conditions as $val){
+			switch(count($val)){
+				case 1:
+					// raw
+					$values[] = $val;
+					break;
+				case 2:
+					$v = static::sqlHelperValue($val[1]);
+					$values[] = sprintf($sql, $val[0], "=", $v);
+					break;
+				case 3:
+					$v = static::sqlHelperValue($val[2]);
+					$values[] = sprintf($sql, $val[0], strtoupper($val[1]), $v);
+					break;
+			}
+		}
+
+		return implode($conditionConnector." ", $values);
+	}
+
+
+	/**
+	 * @param array $conditions
+	 * @param string $conditionConnector AND, OR
+	 *
+	 * @return string
+	 */
+	protected static function sqlHelperWhere($conditions, $conditionConnector = 'AND')
+	{
+		if(count($conditions) > 0){
+			$sql = " WHERE %s";
+
+			return sprintf($sql, static::sqlHelperConditionList($conditions, $conditionConnector));
+		}
+
+		return "";
+	}
+
+	/**
+	 * @param array|null $orderBy Examples below:
+	 *        null => ""
+	 *        array() => ""
+	 *        array('attr1' => 'asc', 'attr2' => 'desc') => " ORDER BY `attr1` ASC, `attr2` DESC "
+	 *        array('attr1') => " ORDER BY `attr1` ASC "
+	 *
+	 * @return string
+	 */
+	protected static function sqlHelperOrderBy($orderBy = null)
+	{
+		if(!is_null($orderBy) && count($orderBy) > 0){
+			$sql = " ORDER BY %s";
+
+			$values = array();
+			foreach($orderBy as $key => $val){
+				if(is_int($key)){
+					$values[] = array($val);
+				}
+				else{
+					$values[] = array($key, strtoupper($val));
+				}
+			}
+
+			return sprintf($sql, static::sqlHelperAttributeList($values));
+		}
+
+		return "";
+	}
+
+
+	/**
+	 * @param int|array $limit
+	 *        0 => ""
+	 *        3 => " LIMIT 3 "
+	 *        array(3, 4) => " LIMIT 3,4 "
+	 *
+	 * @return string
+	 */
+	protected static function sqlHelperLimit($limit = 0)
+	{
+		$sql = " LIMIT %s";
+
+		if(is_string($limit) || (is_int($limit) && $limit > 0)){
+			return sprintf($sql, $limit);
+		}
+		elseif(is_array($limit) && count($limit) == 2){
+			return sprintf($sql, $limit[0].",".$limit[1]);
+		}
+
+		return "";
+	}
+
+
+	/**
+	 * Find all models by raw sql
+	 *
+	 * @param $sql
+	 * @param null|string $useSpecificModel
+	 *
+	 * @return ModelCollection|static[]
+	 */
+	public static function findAllRaw($sql, $useSpecificModel = null)
+	{
+		global $db;
+
+		if(!$result = $db->query($sql)){
+			dbError($db->error, $sql);
+		}
+
+		if(is_null($useSpecificModel)){
+			return static::createMultipleFromDbResult($result);
+		}
+		elseif(class_exists($useSpecificModel)){
+			return call_user_func_array(array($useSpecificModel, 'createMultipleFromDbResult'), array($result));
+		}
+
+		return new ModelCollection();
+	}
+
+
+	/**
+	 * Find a model by raw sql
+	 *
+	 * @param $sql
+	 * @param null|string $useSpecificModel
+	 *
+	 * @return AbstractModel
+	 */
+	public static function findRaw($sql, $useSpecificModel = null)
+	{
+		global $db;
+
+		if(!$result = $db->query($sql)){
+			dbError($db->error, $sql);
+		}
+
+		if(is_null($useSpecificModel)){
+			return static::createFromDbResult($result);
+		}
+		elseif(class_exists($useSpecificModel)){
+			return call_user_func_array(array($useSpecificModel, 'createFromDbResult'), array($result));
+		}
+
+		return null;
+	}
+
+
+	/**
+	 * Find all models
+	 *
+	 * @param array|null $orderBy see sqlHelperOrderBy
+	 *
+	 * @return ModelCollection|static[]
+	 */
+	public static function findAll($orderBy = null)
+	{
+		$sql = "SELECT * FROM `".static::$table."`"
+			.static::sqlHelperOrderBy($orderBy);
+
+		return static::findAllRaw($sql);
+	}
+
+
+	/**
+	 * Find models by a condition
+	 *
+	 * @param array $conditions see sqlHelperConditionArray
+	 * @param string $conditionConnector see sqlHelperConditionArray
+	 * @param array|null $orderBy
+	 * @param int $limit see sqlHelperLimit
+	 *
+	 * @return ModelCollection|static[]|AbstractModel|null
+	 */
+	public static function findWhere($conditions = array(), $conditionConnector = 'AND', $orderBy = null, $limit = 0)
+	{
+		$sql = "SELECT * FROM `".static::$table."`"
+			.static::sqlHelperWhere($conditions, $conditionConnector)
+			.static::sqlHelperOrderBy($orderBy)
+			.static::sqlHelperLimit($limit);
+
+		if($limit === 1){
+			return static::findRaw($sql);
+		}
+
+		return static::findAllRaw($sql);
+	}
+
+
+	/**
+	 * Find first model matching a condition
+	 *
+	 * @param array $conditions see sqlHelperConditionArray
+	 * @param string $conditionConnector see sqlHelperConditionArray
+	 * @param array|null $orderBy
+	 *
+	 * @return AbstractModel|null
+	 */
+	public static function findWhereFirst($conditions = array(), $conditionConnector = 'AND', $orderBy = null)
+	{
+		return static::findWhere($conditions, $conditionConnector, $orderBy, 1);
+	}
+
+
+	/**
+	 * Find a model by id
+	 *
+	 * @param mixed $id
+	 *
+	 * @return AbstractModel|null
+	 */
+	public static function find($id)
+	{
+		return static::findWhereFirst(array(static::$idAttribute, $id));
+	}
+
+
+	/**
+	 * Save model data to database
+	 *
+	 * @return bool
+	 */
+	public function save()
+	{
+		global $db;
+
+		$data = $this->preSave($this->data);
+
+		if(is_null($this->getId())){
+			// insert
+
+			$attributes = array();
+			$values = array();
+
+			foreach($this->attributeDbAttributeMapping as $attribute => $sqlAttribute){
+				if($sqlAttribute === static::$idAttribute){
+					continue;
+				}
+
+				$attributes[] = array($sqlAttribute);
+				$values[] = $data[$attribute];
+			}
+
+			$sql = "INSERT INTO `".static::$table."`"
+				." (".static::sqlHelperAttributeList($attributes).")"
+				." VALUES ".static::sqlHelperValueList($values);
+		}
+		else{
+			// update
+
+			$values = array();
+			foreach($this->attributeDbAttributeMapping as $attribute => $sqlAttribute){
+				if($sqlAttribute === static::$idAttribute){
+					continue;
+				}
+
+				$values[] = array($sqlAttribute, '=', $data[$attribute]);
+			}
+
+			$sql = "UPDATE `".static::$table."`"
+				." SET ".static::sqlHelperConditionList($values, ',')
+				.static::sqlHelperWhere(array(static::$idAttribute, $this->getId()));
+		}
+
+
+		if($stmt = $db->prepare($sql)){
+			if($stmt->execute()){
+				if(is_null($this->getId())){
+					$this->setId(intval($db->insert_id));
+				}
+
+				return true;
+			}
+			else{
+				dbError($db->error, $sql);
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * Delete model from database
+	 *
+	 * @return bool
+	 */
+	public function delete()
+	{
+		global $db;
+
+		if(!is_null($this->getId())){
+			$sql = "DELETE FROM `".static::$table."`"
+				.static::sqlHelperWhere(array(static::$idAttribute, $this->getId()));
+
+			if($stmt = $db->prepare($sql)){
+				if($stmt->execute()){
+					return true;
+				}
+				else{
+					dbError($db->error, $sql);
+				}
+			}
+		}
+
+		return false;
+	}
+
+}

+ 257 - 0
include/php/models/ModelCollection.php

@@ -0,0 +1,257 @@
+<?php
+
+class ModelCollection implements Iterator, ArrayAccess, Countable
+{
+
+	/**
+	 * @var array|AbstractModel[]
+	 */
+	private $models = array();
+
+
+	/**
+	 * Constructor.
+	 *
+	 * @param array|AbstractModel[] $array
+	 */
+	public function __construct($array = array())
+	{
+		if($this->isNumericArray($array)){
+			foreach($array as $model){
+				$this->add($model);
+			}
+		}
+		else{
+			foreach($array as $key => $model){
+				$this->add($model, $key);
+			}
+		}
+	}
+
+
+	/**
+	 * @param array $array
+	 *
+	 * @return bool
+	 */
+	protected function isNumericArray($array)
+	{
+		return array_keys($array) === range(0, count($array) - 1)
+		&& count(array_filter($array, 'is_string')) === 0;
+	}
+
+
+	/**
+	 * Adds a model to the collection,
+	 * but will not replace if it exists with that key
+	 *
+	 * @param AbstractModel $model
+	 * @param mixed|null $key
+	 */
+	public function add($model, $key = null)
+	{
+		if(is_null($model) || !($model instanceof AbstractModel)){
+			return;
+		}
+
+		if(is_null($key)){
+			$this->models[] = $model;
+		}
+		elseif(!$this->has($key)){
+			$this->models[$key] = $model;
+		}
+	}
+
+
+	/**
+	 * Replace a model with given key
+	 *
+	 * @param AbstractModel $model
+	 * @param mixed $key
+	 */
+	public function replace($model, $key)
+	{
+		if(is_null($model) || !($model instanceof AbstractModel)){
+			return;
+		}
+
+		$model[$key] = $model;
+	}
+
+
+	/**
+	 * Delete a model by key
+	 *
+	 * @param mixed $key
+	 */
+	public function delete($key)
+	{
+		if($this->has($key)){
+			unset($this->models[$key]);
+		}
+	}
+
+
+	/**
+	 * Check if collection has a model by key
+	 *
+	 * @param mixed $key
+	 *
+	 * @return bool
+	 */
+	public function has($key)
+	{
+		return isset($this->models[$key]);
+	}
+
+
+	/**
+	 * Get a model from the collection by key
+	 *
+	 * @param mixed $key
+	 *
+	 * @return AbstractModel|null
+	 */
+	public function get($key)
+	{
+		if($this->has($key)){
+			return $this->models[$key];
+		}
+
+		return null;
+	}
+
+
+	/**
+	 * Search a model in collection with a condition
+	 *
+	 * @param callable $callable Gives back if the search matches
+	 *
+	 * @return AbstractModel|null
+	 */
+	public function search($callable)
+	{
+		if(is_callable($callable)){
+			foreach($this->models as $model){
+				if($callable($model)){
+					return $model;
+				}
+			}
+		}
+
+		return null;
+	}
+
+
+	/**
+	 * Search all models in collection with a condition
+	 *
+	 * @param callable $callable Gives back if the search matches
+	 *
+	 * @return static
+	 */
+	public function searchAll($callable)
+	{
+		$collection = new static;
+
+		if(is_callable($callable)){
+			foreach($this->models as $model){
+				if($callable($model)){
+					$collection->add($model);
+				}
+			}
+		}
+
+		return $collection;
+	}
+
+
+	/**
+	 * @inheritdoc
+	 */
+	public function current()
+	{
+		return current($this->models);
+	}
+
+
+	/**
+	 * @inheritdoc
+	 */
+	public function next()
+	{
+		return next($this->models);
+	}
+
+
+	/**
+	 * @inheritdoc
+	 */
+	public function key()
+	{
+		return key($this->models);
+	}
+
+
+	/**
+	 * @inheritdoc
+	 */
+	public function valid()
+	{
+		return $this->current() !== false;
+	}
+
+
+	/**
+	 * @inheritdoc
+	 */
+	public function rewind()
+	{
+		reset($this->models);
+	}
+
+
+	/**
+	 * @inheritdoc
+	 */
+	public function offsetExists($offset)
+	{
+		return $this->has($offset);
+	}
+
+
+	/**
+	 * @inheritdoc
+	 */
+	public function offsetGet($offset)
+	{
+		return $this->get($offset);
+	}
+
+
+	/**
+	 * @inheritdoc
+	 */
+	public function offsetSet($offset, $value)
+	{
+		$this->add($value, $offset);
+	}
+
+
+	/**
+	 * @inheritdoc
+	 */
+	public function offsetUnset($offset)
+	{
+		$this->delete($offset);
+	}
+
+
+	/**
+	 * @inheritdoc
+	 */
+	public function count()
+	{
+		return count($this->models);
+	}
+}