Przeglądaj źródła

* Add EArray
* Add Relationship
* Make events chainable
* Make Query error handling a little better
* Move query class to it's own file and move it to the SQL namespace
* Make relationships work on ORM
* Make ORM model information available through static functions and through dynamic properties

Nathaniel van Diepen 7 lat temu
rodzic
commit
cf75feb858
6 zmienionych plików z 376 dodań i 200 usunięć
  1. 34 0
      earray.class.php
  2. 2 0
      events.trait.php
  3. 75 38
      orm.abstract.class.php
  4. 168 0
      query.class.php
  5. 95 0
      relationship.class.php
  6. 2 162
      sql.class.php

+ 34 - 0
earray.class.php

@@ -0,0 +1,34 @@
+<?php
+	class EArray implements ArrayAccess {
+		use Events;
+		private $data = [];
+		public static function from(array $data){
+			return new static($data);
+		}
+		protected function __construct(array $data){
+			$this->data = array_values($data);
+		}
+		public function offsetExists($offset){
+			$this->fire('exists', $offset);
+			return isset($this->data[$offset]);
+		}
+		public function offsetGet($offset){
+			$this->fire('get', $offset);
+			return $this->data[$offset];
+		}
+		public function offsetSet($offset, $value){
+			$this->fire('set', $offset, $value);
+			$this->data[$offset] = $value;
+		}
+		public function offsetUnset($offset){
+			$this->fire('unset', $offset, $this[$offset]);
+			unset($this->data[$offset]);
+		}
+		public function each(callable $fn){
+			foreach($this->data as $offset => $model){
+				$fn($model, $offset);
+			}
+			return $this;
+		}
+	}
+?>

+ 2 - 0
events.trait.php

@@ -17,6 +17,7 @@
 					}
 				}
 			}
+			return $this;
 		}
 		public function fire(string $name, ...$args){
 			if(isset($this->events[$name])){
@@ -24,6 +25,7 @@
 					$fn(...$args);
 				}
 			}
+			return $this;
 		}
 	}
 ?>

+ 75 - 38
orm.abstract.class.php

@@ -1,7 +1,9 @@
 <?php
 	require_once('sql.class.php');
+	require_once('relationship.class.php');
 	abstract class ORM implements ArrayAccess, JsonSerializable {
 		// Model definition
+		protected  static $table_name = null;
 		protected static $primary_key = 'id';
 		protected static $foreign_key_suffix = '_id';
 		protected static $has_one = [];
@@ -14,7 +16,6 @@
 		private static $aliases = [];
 		private static $instances = [];
 		private static $sql;
-		protected static $name = null;
 		// Magic functions
 		private function __construct($idOrData){
 			if(!isset(self::$aliases[$this->name])){
@@ -22,7 +23,7 @@
 					self::$aliases[$this->name]['belongs_to'][$alias] = array_merge(
 						[
 							'model'=>$alias,
-							'foreign_key'=>$alias.static::$foreign_key_suffix
+							'foreign_key'=>$this->name.static::foreign_key_suffix()
 						],
 						$details
 					);
@@ -31,7 +32,7 @@
 					self::$aliases[$this->name]['has_one'][$alias] = array_merge(
 						[
 							'model'=>$alias,
-							'foreign_key'=>$alias.static::$foreign_key_suffix
+							'foreign_key'=>$alias.static::foreign_key_suffix()
 						],
 						$details
 					);
@@ -40,7 +41,7 @@
 					self::$aliases[$this->name]['has_many'][$alias] = array_merge(
 						[
 							'model'=>$alias,
-							'foreign_key'=>$alias.static::$foreign_key_suffix,
+							'foreign_key'=>$this->name.static::foreign_key_suffix(),
 							'through'=> null
 						],
 						$details
@@ -50,7 +51,7 @@
 			// Clear relationship definitions to save memory
 			static::$belongs_to = static::$has_one = static::$has_many =null;
 			if(is_array($idOrData)){
-				if(isset($idOrData[static::$primary_key]) && self::cached($idOrData[static::$primary_key])){
+				if(isset($idOrData[static::primary_key()]) && self::cached($idOrData[static::primary_key()])){
 					throw new Exception('Instance already cached');
 				}
 				foreach($idOrData as $key => $val){
@@ -60,7 +61,7 @@
 				if(self::cached($idOrData)){
 					throw new Exception('Instance already cached');
 				}
-				$this->_data[static::$primary_key] = (int)$idOrData;
+				$this->_data[static::primary_key()] = (int)$idOrData;
 			}
 			self::$instances[] = $this;
 		}
@@ -77,10 +78,19 @@
 		public function __get(string $name){
 			switch($name){
 				case 'name':
-					return static::name();
+					return static::table_name();
+				break;
+				case 'primary_key':
+					return static::primary_key();
+				break;
+				case 'foreign_key_suffix':
+					return static::foreign_key_suffix();
+				break;
+				case 'has_one':case 'has_many':case 'belongs_to':
+					return self::$aliases[$this->name][$name];
 				break;
 				case 'id':
-					return $this->get(static::$primary_key);
+					return $this->get(static::primary_key());
 				break;
 				default:
 					throw new Exception('Unknown property '.$name);
@@ -108,7 +118,7 @@
 			return $this->_data;
 		}
 		public function __clone(){
-			unset($this->$_data[static::$primary_key]);
+			unset($this->$_data[static::primary_key()]);
 		}
 		// JsonSerializable
 		public function jsonSerialize(){
@@ -128,12 +138,18 @@
 			return $this->get($key);
 		}
 		// Main API
-		public static function name(){
-			if(is_null(static::$name)){
+		public static function table_name(){
+			if(is_null(static::$table_name)){
 				$name = get_called_class();
-				static::$name = substr($name, strrpos($name, '\\') + 1);
+				return substr($name, strrpos($name, '\\') + 1);
 			}
-			return static::$name;
+			return static::$table_name;
+		}
+		public static function primary_key(){
+			return static::$primary_key;
+		}
+		public static function foreign_key_suffix(){
+			return static::$foreign_key_suffix;
 		}
 		public static function bind(SQL $sql){
 			self::$sql = $sql;
@@ -154,14 +170,14 @@
 		public static function exists(int $id){
 			return (int)self::query(
 				"select count(1) as count ".
-				"from ".static::name().' '.
-				"where ".static::$primary_key." = ?",
+				"from ".static::table_name().' '.
+				"where ".static::primary_key()." = ?",
 				'i',
 				$id
 			)->assoc_result["count"] > 0;
 		}
 		public static function cached_instance(int $id){
-			$name = static::name();
+			$name = static::table_name();
 			if(isset(self::$instances[$name])){
 				$instances = array_filter(self::$instances[$name], function(&$instance){
 					return $instance->id === $id;
@@ -175,15 +191,15 @@
 		}
 		public static function delete(int $id){
 			$query = self::query(
-				"delete from ".static::name().' '.
-				"where ".static::$primary_key." = ?",
+				"delete from ".static::table_name().' '.
+				"where ".static::primary_key()." = ?",
 				'i',
 				$id
 			);
 			return $query->execute() && $query->affected_rows > 0;
 		}
 		public static function each_cached(callable $fn){
-			$name = static::name();
+			$name = static::table_name();
 			if(self::$instances[$name]){
 				array_walk(self::$instances[$name], $fn);
 			}
@@ -215,8 +231,8 @@
 				$where = self::str_replace_first(' and ', ' ', $where);
 			}
 			self::query(
-				"select ".static::$primary_key.' id '.
-				"from ".static::name().
+				"select ".static::primary_key().' id '.
+				"from ".static::table_name().
 				$where.
 				$limit,
 				$types,
@@ -256,12 +272,12 @@
 					$data = self::query(
 						"select * " .
 						"from {$this->name} ".
-						"where ".static::$primary_key." = ?",
+						"where ".static::primary_key()." = ?",
 						'i',
 						$this->id
 					)->assoc_result;
 					if($data === false){
-						throw new Exception("{$this->name} with ".static::$primary_key." of {$this->id} does not exist");
+						throw new Exception("{$this->name} with ".static::primary_key()." of {$this->id} does not exist");
 					}
 					$this->_data = $data;
 				}
@@ -292,10 +308,10 @@
 					}
 					$set .= "{$key} = ? ";
 				}
-				if(!is_null($this->id) && !in_array(static::$primary_key, $this->_changed)){
+				if(!is_null($this->id) && !in_array(static::primary_key(), $this->_changed)){
 					$data = array_merge(array_values($data), [$this->id]);
 					self::query(
-						"update {$this->name} {$set} where ".static::$primary_key." = ?",
+						"update {$this->name} {$set} where ".static::primary_key()." = ?",
 						$types.'i',
 						$data
 					)->execute();
@@ -303,7 +319,10 @@
 					self::query(
 						"insert {$this->name} {$set}"
 					)->execute();
-					$this->_data[static::$primary_key] = self::$sql->insert_id;
+					$this->_data[static::primary_key()] = self::$sql->insert_id;
+				}
+				foreach($this->_related as $related){
+					$related->save();
 				}
 			}
 			// Always fetch again from the database in case saving
@@ -323,7 +342,7 @@
 			return $this->_data[$key];
 		}
 		public function set($key, $val){
-			if($key === static::$primary_key && !is_null($this->id)){
+			if($key === static::primary_key() && !is_null($this->id)){
 				throw new Exception('You are not allowed to change the primary key');
 			}
 			$this->_data[$key] = $val;
@@ -345,17 +364,35 @@
 			}
 		}
 		public function related(string $name){
-			if(isset($this->_related[$name])){
-				return $this->_related[$name];
-			}else{
-				$class = "Model\\{$name}";
-				if(self::$aliases['belongs_to'][$name]){
-					$this->_related[$name] = $class::fetch([static::$foreign_key => $this->id]);
-				}elseif(self::$aliases['has_one'][$name]){
-					$this->_related[$name] = $class::fetch([$this->name.static::$foreign_key_suffix => $this->id]);
-				}elseif(self::$aliases['has_many'][$name]){
-					//$this->_related[$name] = $class::fetch([$this->name.static::$foreign_key_suffix => $this->id]);
-					throw new Exception("has_many relationships are not implemented yet");
+			if(!isset($this->_related[$name])){
+				$aliases = self::$aliases[$this->name];
+				if($aliases['belongs_to'][$name]){
+					$alias = $aliases['has_many'][$name];
+					$class = "Models\\{$alias['model']}";
+					$this->_related[$name] = $class::fetch([$alias['foreign_key'] => $this->id])[0];
+				}elseif($aliases['has_one'][$name]){
+					$alias = $aliases['has_many'][$name];
+					$class = "Models\\{$alias['model']}";
+					$this->_related[$name] = $class::instance($this[$alias['foreign_key']]);
+				}elseif($aliases['has_many'][$name]){
+					$alias = $aliases['has_many'][$name];
+					$class = "Models\\{$alias['model']}";
+					$sql = "select ";
+					if($alias['through']){
+						$sql .= $class::table_name().$class::foreign_key_suffix()." id from {$alias['through']} ";
+					}else{
+						$sql .= $class::primary_key()." id from {$alias['model']} ";
+					}
+					$sql .= "where ".$alias['foreign_key']." = ?";
+					$related = [];
+					self::query(
+						$sql,
+						'i',
+						$this->id
+					)->each_assoc(function($row) use(&$related, $class){
+						$related[] = new $class($row['id']);
+					});
+					$this->_related[$name] = Relationship::from($this, $name, $alias, $related);
 				}
 			}
 			if(isset($this->_related[$name])){

+ 168 - 0
query.class.php

@@ -0,0 +1,168 @@
+<?php
+	namespace SQL {
+		/**
+		* Query class. Returned by \SQL::query()
+		*
+		* @class Query
+		* @constructor
+		*/
+		class Query {
+			private $query;
+			private $sql;
+			private $_result = false;
+			public function __construct($sql, $source, $types=null, ...$args){
+				$sql->queries[] = $this;
+				$args = array_merge([$types], $args);
+				$this->sql = $sql();
+				$this->query = $this->sql->stmt_init();
+				if(!$this->query->prepare($source)){
+					throw new Exception($this->query->error);
+				}
+				if(!is_null($types)){
+					if(!$this->query->bind_param(...\SQL::make_referenced($args))){
+						throw new Exception("Unable to bind parameter {$this->query->error}");
+					}
+				}
+			}
+			public function __destruct(){
+				if($this->_result){
+					$this->_result = false;
+					$this->query->free_result();
+				}
+				if($this->query){
+					$this->query->close();
+				}
+				$sql->queries = array_diff($sql->queries, [$this]);
+			}
+			public function __invoke(){
+				return $this->query;
+			}
+			public function execute(){
+				if($this->query){
+					$this->query->free_result();
+					$this->_result = false;
+					$this->query->reset();
+					$r = $this->query->execute();
+					$this->sql->commit();
+					return $r;
+				}else{
+					return false;
+				}
+			}
+			public function each_assoc(callable $fn){
+				if($this->query){
+					$r = $this->results;
+					while($row = $r->fetch_assoc()){
+						$fn($row);
+					}
+				}else{
+					return false;
+				}
+			}
+			public function each_num(callable $fn){
+				if($this->query){
+					$r = $this->results;
+					while($row = $r->fetch_num()){
+						$fn($row);
+					}
+				}else{
+					return false;
+				}
+			}
+			public function __get($name){
+				switch($name){
+					/**
+					* Returns the mysqli::results object for the
+					* query
+					* 
+					* @property results
+					* @type {mysqli::results}
+					* @public
+					*/
+					case 'results':
+						if(!$this->_result && $this->query){
+							$this->execute();
+							$this->_result = $this->query->get_result();
+							$this->query->close();
+						}
+						return $this->_result;
+					break;
+					/**
+					* Returns an associative array of the query resulsts
+					* 
+					* @property assoc_results
+					* @type {Array}
+					* @public
+					*/
+					/**
+					* Returns an associative array of the query resulsts
+					* 
+					* @property resulsts_assoc
+					* @type {Array}
+					* @public
+					*/
+					case 'assoc_results':case 'results_assoc':
+						if($this->query){
+							$a = [];
+							$r = $this->results;
+							while($row = $r->fetch_assoc()){
+								array_push($a,$row);
+							}
+							return $a;
+						}else{
+							return false;
+						}
+					break;
+					/**
+					* Returns a numbered array of the query results
+					* 
+					* @property num_results
+					* @type {Array}
+					* @public
+					*/
+					/**
+					* Returns a numbered array of the query results
+					* 
+					* @property resulsts_num
+					* @type {Array}
+					* @public
+					*/
+					case 'num_results':case 'results_num':
+						if($this->query){
+							$a = [];
+							$r = $this->results;
+							while($row = $r->fetch_num()){
+								array_push($a,$row);
+							}
+							return $a;
+						}else{
+							return false;
+						}
+					break;
+					case 'assoc_result':case 'result_assoc':
+						if($this->query){
+							$r = $this->results;
+							return $r?$r->fetch_assoc():false;
+						}else{
+							return false;
+						}
+					break;
+					case 'num_result':case 'result_num':
+						if($this->query){
+							$r = $this->results;
+							return $r?$r->fetch_num():false;
+						}else{
+							return false;
+						}
+					break;
+					case 'insert_id':
+						return $this->sql->insert_id;
+					break;
+					case 'affected_rows':
+						return $this->query->affected_rows;
+					break;
+				}
+			}
+		}
+	}
+?>

+ 95 - 0
relationship.class.php

@@ -0,0 +1,95 @@
+<?php
+	require_once('earray.class.php');
+	class Relationship extends EArray {
+		private $model;
+		private $name;
+		private $alias;
+		private $removed = [];
+		private $added = [];
+		public static function from(ORM $model, string $name, array $alias, array $data){
+			$earray = parent::from($data)
+				->on('set', function(&$offset, &$value) use($earray){
+					$earray->added = array_merge($earray->added, [$value]);
+					$earray->removed = array_diff($earray->removed, [$value]);
+				})->on('unset', function($offset, $value) use($earray){
+					$earray->removed = array_merge($earray->removed, [$value]);
+					$earray->added = array_diff($earray->added, [$value]);
+				});
+			$earray->model = $model;
+			$earray->name = $name;
+			$earray->alias = $alias;
+			return $earray;
+		}
+		public function add(ORM $model){
+			$this[] = $model;
+			return $this;
+		}
+		public function remove(ORM $model){
+			$index = $this->index($model);
+			if($index !== false){
+				unset($this->data[$index]);
+			}
+			return $this;
+		}
+		public function has(ORM $model){
+			return $this->index($model) !== false;
+		}
+		public function index(ORM $model){
+			return array_search($model, $this->data);
+		}
+		public function dirty(){
+			return count($this->removed) + count($this->added) > 0;
+		}
+		public function reset(){
+			foreach($this->added as $model){
+				$index = array_search($model, $this->data);
+				if($index !== false){
+					unset($this->data[$index]);
+				}
+			}
+			$this->added = [];
+			foreach($this->removed as $model){
+				$this->data = array_merge($this->data, [$model]);
+			}
+			$this->removed = [];
+		}
+		public function save(){
+			if($this->dirty()){
+				$model = $this->model;
+				$alias = $this->$alias;
+				$name = $this->name;
+				$class = "Models\\{$alias['model']}";
+				if(isset($model->has_many[$name])){
+					throw new Exception("Invalid relationship {$name}");
+				}
+				if(isset($alias['through'])){
+					$rem_sql = "delete from {$alias['through']} where {$alias['foreign_key']} = ? and ".$class::table_name().$class::foreign_key_suffix()." = ?";
+					$rem_args = [$model->id];
+					$add_sql = "insert into {$alias['through']} ({$alias['foreign_key']}, ".$class::table_name().$class::foreign_key_suffix().") values (?, ?)";
+				}else{
+					$rem_sql = "update {$alias['model']} set {$alias['foreign_key']} = null where ".$class::primary_key()." = ?";
+					$rem_args = [];
+					$add_sql .= "update {$alias['model']} set {$alias['foreign_key']} = ? where ".$class::primary_key()." = ?";
+				}
+				$class = get_class($models);
+				foreach($this->removed as $item){
+					$class::query(
+						$rem_sql,
+						'ii',
+						...array_merge($rem_args, [$item->id])
+					)->execute();
+				}
+				$this->removed = [];
+				foreach($this->added as $item){
+					$class::query(
+						$add_sql,
+						'ii',
+						$model->id,
+						$item->id
+					)->execute();
+				}
+				$this->added = [];
+			}
+		}
+	}
+?>

+ 2 - 162
sql.class.php

@@ -1,4 +1,5 @@
 <?php
+	require_once('query.class.php');
 	/**
 	* SQL class. Used for handling SQL connections
 	*
@@ -53,7 +54,7 @@
 		* @return {Query} Returns the query object
 		*/
 		public function query(...$args){
-			return new Query(...array_merge([$this], $args));
+			return new SQL\Query(...array_merge([$this], $args));
 		}
 		public function escape($s){
 			return $this->sql->escape_string($s);
@@ -74,167 +75,6 @@
 			}
 		}
 	}
-	/**
-	* Query class. Returned by SQL::query()
-	*
-	* @class Query
-	* @constructor
-	*/
-	class Query {
-		private $query;
-		private $sql;
-		private $_result = false;
-		public function __construct($sql, $source, $types=null, ...$args){
-			$sql->queries[] = $this;
-			$args = array_merge([$types], $args);
-			$this->sql = $sql();
-			$this->query = $this->sql->prepare($source);
-			if(!is_null($types)){
-				if(!$this->query->bind_param(...SQL::make_referenced($args))){
-					throw new Exception("Unable to bind parameter {$this->query->error}");
-				}
-			}
-		}
-		public function __destruct(){
-			if($this->_result){
-				$this->_result = false;
-				$this->query->free_result();
-			}
-			if($this->query){
-				$this->query->close();
-			}
-			$sql->queries = array_diff($sql->queries, [$this]);
-		}
-		public function __invoke(){
-			return $this->query;
-		}
-		public function execute(){
-			if($this->query){
-				$this->query->free_result();
-				$this->_result = false;
-				$this->query->reset();
-				$r = $this->query->execute();
-				$this->sql->commit();
-				return $r;
-			}else{
-				return false;
-			}
-		}
-		public function each_assoc(callable $fn){
-			if($this->query){
-				$r = $this->results;
-				while($row = $r->fetch_assoc()){
-					$fn($row);
-				}
-			}else{
-				return false;
-			}
-		}
-		public function each_num(callable $fn){
-			if($this->query){
-				$r = $this->results;
-				while($row = $r->fetch_num()){
-					$fn($row);
-				}
-			}else{
-				return false;
-			}
-		}
-		public function __get($name){
-			switch($name){
-				/**
-				* Returns the mysqli::results object for the
-				* query
-				* 
-				* @property results
-				* @type {mysqli::results}
-				* @public
-				*/
-				case 'results':
-					if(!$this->_result && $this->query){
-						$this->execute();
-						$this->_result = $this->query->get_result();
-						$this->query->close();
-					}
-					return $this->_result;
-				break;
-				/**
-				* Returns an associative array of the query resulsts
-				* 
-				* @property assoc_results
-				* @type {Array}
-				* @public
-				*/
-				/**
-				* Returns an associative array of the query resulsts
-				* 
-				* @property resulsts_assoc
-				* @type {Array}
-				* @public
-				*/
-				case 'assoc_results':case 'results_assoc':
-					if($this->query){
-						$a = [];
-						$r = $this->results;
-						while($row = $r->fetch_assoc()){
-							array_push($a,$row);
-						}
-						return $a;
-					}else{
-						return false;
-					}
-				break;
-				/**
-				* Returns a numbered array of the query results
-				* 
-				* @property num_results
-				* @type {Array}
-				* @public
-				*/
-				/**
-				* Returns a numbered array of the query results
-				* 
-				* @property resulsts_num
-				* @type {Array}
-				* @public
-				*/
-				case 'num_results':case 'results_num':
-					if($this->query){
-						$a = [];
-						$r = $this->results;
-						while($row = $r->fetch_num()){
-							array_push($a,$row);
-						}
-						return $a;
-					}else{
-						return false;
-					}
-				break;
-				case 'assoc_result':case 'result_assoc':
-					if($this->query){
-						$r = $this->results;
-						return $r?$r->fetch_assoc():false;
-					}else{
-						return false;
-					}
-				break;
-				case 'num_result':case 'result_num':
-					if($this->query){
-						$r = $this->results;
-						return $r?$r->fetch_num():false;
-					}else{
-						return false;
-					}
-				break;
-				case 'insert_id':
-					return $this->sql->insert_id;
-				break;
-				case 'affected_rows':
-					return $this->query->affected_rows;
-				break;
-			}
-		}
-	}
 	register_shutdown_function(function(){
 		SQL::shutdown();
 	});