Browse Source

* Attempt to properly handle cleaning up connections/queries
* Connections are now persistent
* you can now get at affected rows on a query
* You can now use a memory efficient callback based each_{assoc,num} call instead of looping through result arrays
* unify how determining the name of the model is done. Cache the result
* remove __call and __callStatic magic methods
* cache model instances in memory and handle removal on unset
* Add static api for getting instances (from memory or from the db), checking if an item exists, checking if an item is cached, explicitly pulling a cached instance, looping through each instance, each cached instance, a str_replace_first helper and deleting

Nathaniel van Diepen 7 years ago
parent
commit
d8d28b1a80
2 changed files with 195 additions and 38 deletions
  1. 134 32
      orm.abstract.class.php
  2. 61 6
      sql.class.php

+ 134 - 32
orm.abstract.class.php

@@ -12,33 +12,35 @@
 		private $_changed = [];
 		private $_related = [];
 		private static $aliases = [];
+		private static $instances = [];
 		private static $sql;
+		protected static $name = null;
 		// Magic functions
-		public function __construct($idOrData){
-			if(empty(self::$aliases[$this->name])){
-				foreach(self::$belongs_to as $alias => $details){
+		private function __construct($idOrData){
+			if(!isset(self::$aliases[$this->name])){
+				foreach(static::$belongs_to as $alias => $details){
 					self::$aliases[$this->name]['belongs_to'][$alias] = array_merge(
 						[
 							'model'=>$alias,
-							'foreign_key'=>$alias.$this->foreign_key_suffix
+							'foreign_key'=>$alias.static::$foreign_key_suffix
 						],
 						$details
 					);
 				}
-				foreach(self::$has_one as $alias => $details){
+				foreach(static::$has_one as $alias => $details){
 					self::$aliases[$this->name]['has_one'][$alias] = array_merge(
 						[
 							'model'=>$alias,
-							'foreign_key'=>$alias.$this->foreign_key_suffix
+							'foreign_key'=>$alias.static::$foreign_key_suffix
 						],
 						$details
 					);
 				}
-				foreach(self::$has_many as $alias => $details){
+				foreach(static::$has_many as $alias => $details){
 					self::$aliases[$this->name]['has_many'][$alias] = array_merge(
 						[
 							'model'=>$alias,
-							'foreign_key'=>$alias.$this->foreign_key_suffix,
+							'foreign_key'=>$alias.static::$foreign_key_suffix,
 							'through'=> null
 						],
 						$details
@@ -46,29 +48,39 @@
 				}
 			}
 			// Clear relationship definitions to save memory
-			self::$belongs_to = self::$has_one = self::$has_many =null;
+			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])){
+					throw new Exception('Instance already cached');
+				}
 				foreach($idOrData as $key => $val){
 					$this->_data[$key] = $val;
 				}
 			}else{
-				$this->_data[self::$primary_key] = intval($idOrData);
+				if(self::cached($idOrData)){
+					throw new Exception('Instance already cached');
+				}
+				$this->_data[static::$primary_key] = (int)$idOrData;
 			}
+			self::$instances[] = $this;
 		}
-		public function __destruct(){
+		private function __destruct(){
 			$this->__sleep();
 			$this->_changed = [];
 			$this->_data = [];
 			$this->_related = [];
+			$key = array_search(self::$instances, $this);
+			if($key !== false){
+				array_splice(self::$instances, $key, 1);
+			}
 		}
 		public function __get(string $name){
 			switch($name){
 				case 'name':
-					$name=get_class($this);
-					return substr($name, strrpos($name, '\\')+1);
+					return static::name();
 				break;
 				case 'id':
-					return $this->get(self::$primary_key);
+					return $this->get(static::$primary_key);
 				break;
 				default:
 					throw new Exception('Unknown property '.$name);
@@ -83,15 +95,6 @@
 					throw new Exception('Unknown property '.$name);
 			}
 		}
-		public function __call(string $name, array $args){
-			switch($name){
-				default:
-					throw new Exception('Method '.$name.' not implemented');
-			}
-		}
-		public static function __callStatic(string $name, array $args){
-			throw new Exception('Static method '.$name.' not implemented');
-		}
 		public function __sleep(){
 			$this->save();
 		}
@@ -105,7 +108,7 @@
 			return $this->_data;
 		}
 		public function __clone(){
-			unset($this->$_data[self::$primary_key]);
+			unset($this->$_data[static::$primary_key]);
 		}
 		// JsonSerializable
 		public function jsonSerialize(){
@@ -125,6 +128,13 @@
 			return $this->get($key);
 		}
 		// Main API
+		public static function name(){
+			if(is_null(static::$name)){
+				$name = get_called_class();
+				static::$name = substr($name, strrpos($name, '\\') + 1);
+			}
+			return static::$name;
+		}
 		public static function bind(SQL $sql){
 			self::$sql = $sql;
 			// @todo handle updating live instances
@@ -132,6 +142,98 @@
 		public static function query(...$args){
 			return self::$sql->query(...$args);
 		}
+		public static function instance(int $id){
+			$instance = self::cached_instance($id);
+			if(!is_null($instance)){
+				return $instance;
+			}elseif(self::exists($id)){
+				return new static($id);
+			}
+			return null;
+		}
+		public static function exists(int $id){
+			return (int)self::query(
+				"select count(1) as count ".
+				"from ".static::name().' '.
+				"where ".static::$primary_key." = ?",
+				'i',
+				$id
+			)->assoc_result["count"] > 0;
+		}
+		public static function cached_instance(int $id){
+			$name = static::name();
+			if(isset(self::$instances[$name])){
+				$instances = array_filter(self::$instances[$name], function(&$instance){
+					return $instance->id === $id;
+				});
+				return isset($instances[0]) ? $instances[0] : null;
+			}
+			return null;
+		}
+		public static function cached(int $id){
+			return !is_null(self::cached_instance($id));
+		}
+		public static function delete(int $id){
+			$query = self::query(
+				"delete from ".static::name().' '.
+				"where ".static::$primary_key." = ?",
+				'i',
+				$id
+			);
+			return $query->execute() && $query->affected_rows > 0;
+		}
+		public static function each_cached(callable $fn){
+			$name = static::name();
+			if(self::$instances[$name]){
+				array_walk(self::$instances[$name], $fn);
+			}
+		}
+		public static function each_where(callable $fn, array $filter = null, int $start = null, int $amount = null){
+			$limit = ' ';
+			if(!is_null($start) && !is_null($amount)){
+				$limit .= "limit {$start}, {$amount}";
+			}
+			$where = ' ';
+			$types = null;
+			$bindings = null;
+			if(!is_null($filter)){
+				$types = '';
+				$bindings = array();
+				foreach($filter as $key => $val){
+					if(is_string($val)){
+						$types .= 's';
+					}elseif(is_double($val)){
+						$types .= 'd';
+					}elseif(is_int($val)){
+						$types .= 'i';
+					}else{
+						throw new Exception("Unknown data type");
+					}
+					$where .= 'and {$key} = ? ';
+					$bindings[] = $val;
+				}
+				$where = self::str_replace_first(' and ', ' ', $where);
+			}
+			self::query(
+				"select ".static::$primary_key.' id '.
+				"from ".static::name().
+				$where.
+				$limit,
+				$types,
+				$bindings
+			)->each_assoc(function($row) use($fn){
+				$fn(self::instance((int)$row['id']));
+			});
+		}
+		public static function each(callable $fn, int $start = null, int $amount = null){
+			self::each_where($fn, null, $start, $amount);
+		}
+		public static function str_replace_first($search, $replace, $source) {
+			$explode = explode($search, $source);
+			$shift = array_shift($explode);
+			$implode = implode($search, $explode);
+			return $shift.$replace.$implode;
+		}
 		// Instance Api
 		public function values($values){
 			foreach($values as $key => $val){
@@ -147,12 +249,12 @@
 					$data = self::query(
 						"select * " .
 						"from {$this->name} ".
-						"where ".self::$primary_key." = ?",
+						"where ".static::$primary_key." = ?",
 						'i',
 						$this->id
 					)->assoc_result;
 					if($data === false){
-						throw new Exception("{$this->name} with ".self::$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;
 				}
@@ -174,19 +276,19 @@
 				foreach($data as $key => $val){
 					if(is_string($val)){
 						$types .= 's';
-					}elseif(is_int($val)){
-						$types .= 'i';
 					}elseif(is_double($val)){
 						$types .= 'd';
+					}elseif(is_int($val)){
+						$types .= 'i';
 					}else{
 						throw new Exception('Unknown data type');
 					}
 					$set .= "{$key} = ? ";
 				}
-				if(!is_null($this->id) && !in_array(self::$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 ".self::primary_key." = ?",
+						"update {$this->name} {$set} where ".static::$primary_key." = ?",
 						$types.'i',
 						$data
 					)->execute();
@@ -194,7 +296,7 @@
 					self::query(
 						"insert {$this->name} {$set}"
 					)->execute();
-					$this->_data[self::$primary_key] = self::$sql->insert_id;
+					$this->_data[static::$primary_key] = self::$sql->insert_id;
 				}
 			}
 			// Always fetch again from the database in case saving
@@ -214,7 +316,7 @@
 			return $this->_data[$key];
 		}
 		public function set($key, $val){
-			if($key === self::$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;

+ 61 - 6
sql.class.php

@@ -16,8 +16,19 @@
 		* @required
 		*/
 		private $sql;
+		public $queries = [];
+		private static $connections = [];
 		public function __construct($server,$user,$pass,$db){
-			$this->sql = new mysqli($server,$user,$pass,$db) or die('Unable to connect to mysql');
+			$this->sql = new mysqli('p:'.$server,$user,$pass,$db) or die('Unable to connect to mysql');
+			self::$connections[] = $sql;
+		}
+		public function __destruct(){
+			$this->sql->rollback();
+			$this->sql->close();
+			foreach($this->queries as $query){
+				unset($query);
+			}
+			self::$connections = array_diff(self::$connections, [$this]);
 		}
 		public function __invoke(){
 			return $this->sql;
@@ -57,6 +68,11 @@
 			}
 			return $refs;
 		}
+		public static function shutdown(){
+			foreach(self::$connections as $sql){
+				unset($sql);
+			}
+		}
 	}
 	/**
 	* Query class. Returned by SQL::query()
@@ -67,7 +83,9 @@
 	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);
@@ -77,11 +95,24 @@
 				}
 			}
 		}
+		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;
@@ -89,6 +120,26 @@
 				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){
 				/**
@@ -100,14 +151,12 @@
 				* @public
 				*/
 				case 'results':
-					if($this->query){
+					if(!$this->_result && $this->query){
 						$this->execute();
-						$result = $this->query->get_result();
+						$this->_result = $this->query->get_result();
 						$this->query->close();
-						return $result;
-					}else{
-						return false;
 					}
+					return $this->_result;
 				break;
 				/**
 				* Returns an associative array of the query resulsts
@@ -180,7 +229,13 @@
 				case 'insert_id':
 					return $this->sql->insert_id;
 				break;
+				case 'affected_rows':
+					return $this->query->affected_rows;
+				break;
 			}
 		}
 	}
+	register_shutdown_function(function(){
+		SQL::shutdown();
+	});
 ?>