name])){ $aliases = [ 'belongs_to' => [], 'has_one' => [], 'has_many' => [] ]; if(is_array(static::$belongs_to)){ foreach(static::$belongs_to as $alias => $details){ $aliases['belongs_to'][$alias] = array_merge( [ 'model'=>$alias, 'foreign_key'=>$this->name.static::foreign_key_suffix() ], $details ); } } if(is_array(static::$has_one)){ foreach(static::$has_one as $alias => $details){ $aliases['has_one'][$alias] = array_merge( [ 'model'=>$alias, 'foreign_key'=>$alias.static::foreign_key_suffix() ], $details ); } } if(is_array(static::$has_many)){ foreach(static::$has_many as $alias => $details){ $aliases['has_many'][$alias] = array_merge( [ 'model'=>$alias, 'foreign_key'=>$this->name.static::foreign_key_suffix(), 'through'=> null ], $details ); } } self::$aliases[$this->name] = $aliases; // 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()])){ throw new \Exception('Instance already cached'); } foreach($idOrData as $key => $val){ $this->_data[$key] = $val; } }else{ if(self::cached($idOrData)){ throw new \Exception('Instance already cached'); } $this->_data[static::primary_key()] = (int)$idOrData; } self::$instances[] = $this; } private function __destruct(){ $this->__sleep(); $this->_changed = []; $this->_data = []; $this->_related = []; $key = array_search($this, self::$instances); if($key !== false){ array_splice(self::$instances, $key, 1); } } public function __get(string $name){ switch($name){ case '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()); break; default: throw new \Exception('Unknown property '.$name); } } public function __set(string $name, $val){ switch($name){ case 'id':case 'name': throw new \Exception('Property '.$name.' is read only'); break; default: throw new \Exception('Unknown property '.$name); } } public function __sleep(){ $this->save(); } public function __wakeup(){ $this->reload(true); } public function __toString(){ return $this->name.'('.$this->id.')'; } public function __invoke(){ return $this->_data; } public function __clone(){ unset($this->$_data[static::primary_key()]); } // JsonSerializable public function jsonSerialize(){ return $this->_data; } // ArrayAccess public function offsetSet($key, $val){ $this->set($key, $val); } public function offsetExists($key){ return $this->has($key); } public function offsetUnset($key){ return $this->unset($key); } public function offsetGet($key){ return $this->get($key); } // Main API public static function table_name(){ if(is_null(static::$table_name)){ $name = get_called_class(); return substr($name, strrpos($name, '\\') + 1); } 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){ if(is_string($sql)){ $sql = SQL::FromDSN($sql); } if($sql instanceof SecureString){ $sql = SQL::FromDSN((string)$sql); } if($sql instanceof SQL){ self::$sql = $sql; // @todo handle updating live instances }else{ throw new \Exception("Invalid argument. Must pass a DSN string or an SQL instance"); } } 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::table_name().' '. "where ".static::primary_key()." = ?", 'i', $id )->assoc_result["count"] > 0; } public static function cached_instance(int $id){ $name = static::table_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::table_name().' '. "where ".static::primary_key()." = ?", 'i', $id ); return $query->execute() && $query->affected_rows > 0; } public static function each_cached(callable $fn){ $name = static::table_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::table_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 fetch(array $filter = null, int $start = null, int $amount = null){ $results = []; self::each_where(function($item) use($results){ $results[] = $item; }, $filter, $start, $amount); return $results; } 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; } public static function models(){ array_filter(get_declared_classes(), function($class){ return substr($class, 0, 5) == "Model\\"; }); } // Instance Api public function values($values){ foreach($values as $key => $val){ $this->set($key, $val); } return $this; } public function load(bool $force = false){ if(!$force && $this->dirty()){ throw new \Exception('Cannot load, there are pending changes'); }else{ if(!is_null($this->id)){ $data = self::query( "select * " . "from {$this->name} ". "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"); } $this->_data = $data; } } return $this; } public function save(){ if($this->dirty()){ $data = []; $set = "set "; $types = ''; foreach($this->_changed as $key){ if(isset($this->_data[$key]) || is_null($this->_data[$key])){ $data[$key] = $this->_data[$key]; }else{ $set .= "{$key} = null"; } } foreach($data 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'); } $set .= "{$key} = ? "; } 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()." = ?", $types.'i', $data )->execute(); }else{ self::query( "insert {$this->name} {$set}" )->execute(); $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 // forces something to change at the database level return $this->reload(true); } public function reload(bool $force = false){ if($force){ $this->_changed = []; } return $this->load($force); } public function clear(){ return $this->reload(true); } public function get($key){ return $this->_data[$key]; } public function set($key, $val){ 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; $this->_changed = array_merge($this->_changed, [$key]); return $this; } public function unset($key){ unset($this->_data[$key]); return $this; } public function has($key){ return isset($this->_data[$key]); } public function dirty(string $key = null){ if(is_null($key)){ return count($this->_changed) > 0; }else{ return in_array($key, $this->_changed); } } public function related(string $name){ if(!isset($this->_related[$name])){ $aliases = self::$aliases[$this->name]; if(isset($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(isset($aliases['has_one'][$name])){ $alias = $aliases['has_many'][$name]; $class = "\\Models\\{$alias['model']}"; $this->_related[$name] = $class::instance($this[$alias['foreign_key']]); }elseif(isset($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])){ return $this->_related[$name]; }else{ throw new \Exception("Relationship {$name} does not exist"); } } } ?>