瀏覽代碼

* Clean up error handleing
* Make EArray countable and fix access to $data and fix an error with unset
* Make migration always update the version table to the latest
* Make migrations handle the change hook
* Clean up pdo __get
* Add ORM->pdo and ORM->table and ORM::pdo() and ORM::table()
* Get ORM->dirty() to check relationships
* Add PDO->stringFilter and PDO->stringSet
* Relationship now uses Table for handling data
* Fix a warning with securestring
* Lots of work on Table
* Add Transaction->stringFilter and ->stringSet()

Nathaniel van Diepen 7 年之前
父節點
當前提交
9cdfe7eb0d

+ 4 - 1
App/exception.class.php

@@ -3,7 +3,7 @@
 
 	class Exception extends \Exception {
 		private $included;
-		public function __construct($message = null, $code = 0, \Exception $previous = null, string $file = null, int $line = null, array $trace = null, array $included = []){
+		public function __construct($message = null, $code = 0, \Exception $previous = null, string $file = null, int $line = null, array $trace = null, array $included = null){
 			parent::__construct($message, $code, $previous);
 			if(!is_null($file)){
 				$this->file = $file;
@@ -14,6 +14,9 @@
 			if(!is_null($trace)){
 				$this->trace = $trace;
 			}
+			if(is_null($included)){
+				$included = get_included_files();
+			}
 			$this->included = $included;
 		}
 		public function getIncluded(){

+ 9 - 4
Data/earray.class.php

@@ -3,9 +3,9 @@
 	require_once(realpath(dirname(__DIR__).'/events.trait.php'));
 	use Juju\Events;
 
-	class EArray implements \ArrayAccess {
+	class EArray implements \ArrayAccess, \Countable {
 		use Events;
-		private $data = [];
+		protected $data = [];
 		public static function from(array $data){
 			return new static($data);
 		}
@@ -25,8 +25,13 @@
 			$this->data[$offset] = $value;
 		}
 		public function offsetUnset($offset){
-			$this->fire('unset', $offset, $this[$offset]);
-			unset($this->data[$offset]);
+			if($this->offsetExists($offset)){
+				$this->fire('unset', $offset, $this[$offset]);
+				unset($this->data[$offset]);
+			}
+		}
+		public function count(){
+			return count($this->data);
 		}
 		public function each(callable $fn){
 			foreach($this->data as $offset => $model){

+ 1 - 1
Data/securestring.class.php

@@ -21,7 +21,7 @@
 			}while($data != (string)$this);
 		}
 		public function __toString(){
-			return openssl_decrypt($this->data, $this->method, $this->password, \OPENSSL_RAW_DATA, $this->iv);
+			return ''.openssl_decrypt($this->data, $this->method, $this->password, \OPENSSL_RAW_DATA, $this->iv);
 		}
 		public function jsonSerialize(){
 			return "{$this}";

+ 57 - 50
ORM/relationship.class.php

@@ -9,18 +9,32 @@
 		private $alias;
 		private $removed = [];
 		private $added = [];
+		private $table;
+
 		public static function from(ORM $model, string $name, array $alias, array $data){
-			$earray = parent::from($data);
-			$earray->model = $model;
-			$earray->name = $name;
-			$earray->alias = $alias;
-			return $earray
-				->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]);
+			return new Relationship([
+				'data'=> $data,
+				'model'=>$model,
+				'name'=>$name,
+				'alias'=>$alias
+			]);
+		}
+		protected function __construct(array $data){
+			parent::__construct($data['data']);
+			$this->model = $data['model'];
+			$this->name = $data['name'];
+			$this->alias = $data['alias'];
+			if(isset($this->alias['through'])){
+				$this->table = $this->model->pdo->table($this->alias['through']);
+			}
+			$self = $this;
+			$this
+				->on('set', function(&$offset, &$value) use($self){
+					$self->added = array_merge($self->added, [$value]);
+					$self->removed = array_diff($self->removed, [$value]);
+				})->on('unset', function($offset, $value) use($self){
+					$self->removed = array_merge($self->removed, [$value]);
+					$self->added = array_diff($self->added, [$value]);
 				});
 		}
 		public function add(ORM $model){
@@ -59,54 +73,47 @@
 		public function save(){
 			if($this->dirty()){
 				$model = $this->model;
-				$alias = $this->$alias;
+				$alias = $this->alias;
 				$name = $this->name;
 				$class = "\\Models\\{$alias['model']}";
-				if(isset($model->has_many[$name])){
+				if(!isset($model->has_many[$name])){
 					throw new \Exception("Invalid relationship {$name}");
 				}
 				$rem_args = [];
 				if(isset($alias['through'])){
-					$rem = $class::prepare(
-						"delete from {$alias['through']} ".
-						"where {$alias['foreign_key']} = ".$class::quote($model->id).
-						" and ".$class::table_name().$class::foreign_key_suffix()." = ?"
-					);
-					$add = $class::prepare(
-						"insert into {$alias['through']} (".
-							"{$alias['foreign_key']}, ".
-							$class::table_name().$class::foreign_key_suffix().
-						") values (".
-							$class::quote($model->id).", ".
-							"?".
-						")"
-					);
-					$rem_args[] = $model->id;
+					$key = $class::table_name().$class::foreign_key_suffix();
+					$filter = [
+						$alias['foreign_key']=> $model->id
+					];
+					foreach($this->removed as $item){
+						$filter[$key] = $item->id;
+						$this->table->delete($filter);
+					}
+					foreach($this->added as $item){
+						$filter[$key] = $item->id;
+						$this->table->insert($filter);
+					}
 				}else{
-					$rem = $class::prepare(
-						"update {$alias['model']} ".
-						"set {$alias['foreign_key']} = null ".
-						"where ".$class::primary_key()." = ?"
-					);
-					$add = $class::prepare(
-						"update {$alias['model']} ".
-						"set {$alias['foreign_key']} = ".$class::quote($model->id).
-						" where ".$class::primary_key()." = ?"
-					);
-				}
-				$class = get_class($models);
-				foreach($this->removed as $item){
-					$rem_args[1] = $item->id;
-					$query = $rem->execute($rem_args);
-					while($query->fetch()){}
-					$query->closeCursor();
+					$table = $model->pdo->table($alias['model']);
+					$key = $class::primary_key();
+					$data = [
+						$alias['foreign_key']=> null
+					];
+					foreach($this->removed as $item){
+						$table->update($data, [
+							$key=> $item->id
+						]);
+					}
+					$data = [
+						$alias['foreign_key']=> $model->id
+					];
+					foreach($this->added as $item){
+						$table->update($data, [
+							$key=> $item->id
+						]);
+					}
 				}
 				$this->removed = [];
-				foreach($this->added as $item){
-					$query = $add->execute([$item->id]);
-					while($query->fetch()){}
-					$query->closeCursor();
-				}
 				$this->added = [];
 			}
 		}

+ 6 - 10
PDO/migration.abstract.class.php

@@ -6,7 +6,7 @@
 	abstract class Migration {
 		public abstract static function up(Transaction $pdo);
 		public abstract static function down(Transaction $pdo);
-		public abstract static function change(Transaction $pdo);
+		public abstract static function change(Transaction $pdo, string $direction);
 
 		const MIGRATE_UP = 'up';
 		const MIGRATE_DOWN = 'down';
@@ -47,21 +47,16 @@
 			}
 			$pdo = self::$pdo;
 			$table = $pdo->table(self::version_table());
-			if(!$table->exists){
-				$table->column('version', [
-						'type'=>'varchar(100)',
-						'null'=>false,
-						'key'=>'PRI'
-					])
-					->primaryKey('version')
-					->commit();
-			}
+			$table->column('version', 'varchar(100)')
+				->primaryKey('version')
+				->commit();
 			switch($direction){
 				case self::MIGRATE_UP:
 					foreach(self::migrations() as $migration){
 						if(!$migration::installed()){
 							$pdo->transaction(function($pdo) use($migration, $table){
 								$migration::up($pdo);
+								$migration::change($pdo, self::MIGRATE_UP);
 							});
 							$table->insert(['version'=>$migration::version()]);
 							if(!$migration::installed()){
@@ -77,6 +72,7 @@
 					foreach(array_reverse(self::migrations()) as $migration){
 						if($migration::installed()){
 							$pdo->transaction(function($pdo) use($migration, $table){
+								$migration::change($pdo, self::MIGRATE_DOWN);
 								$migration::down($pdo);
 							});
 							$table->delete(['version'=>$migration::version()]);

+ 192 - 69
PDO/table.class.php

@@ -8,6 +8,9 @@
 		private $name;
 		private $exists;
 		private $columns;
+		private $primaryKey;
+		private $index;
+		private $foreignKey;
 
 		public function __construct($pdo, string $name){
 			if($pdo instanceof PDO || $pdo instanceof Transaction){
@@ -39,57 +42,139 @@
 		}
 		public function describe(){
 			$this->columns = [];
+			$this->primaryKey = [];
+			$this->index = [];
+			$this->foreignKeys = [];
 			if($this->exists){
-				$query = $this->pdo->query("describe `{$this->name}`");
-				while($col = $query->fetch()){
-					$this->columns[$col['field']] = [
-						'type'=> $col['type'],
-						'null'=> $col['null'] !== 'NO',
-						'key'=> $col['key'],
-						'default'=> $col['default'],
-						'extra'=> $col['extra'],
-						'dirty'=> false
-					];
-				}
+				$pdo = $this->pdo;
+
+				$query = $pdo->query("show create table `{$this->name}`");
+				$sql = $query->fetch()['create table'];
 				$query->closeCursor();
-			}
-		}
-		public function stringFilter(array $filter = null){
-			if(!is_null($filter)){
-				$where = 'where ';
-				foreach($filter as $name => $value){
-					$where .= "`{$name}` = {$this->pdo->quote($value)}";
+				if(preg_match_all('/ PRIMARY KEY \(((?:`[^`]+`,?)+)\)/s', $sql, $primaryKey, PREG_SET_ORDER) > 0){
+					$this->primaryKey = array_map(
+						function($item){
+							return preg_replace('/`([^`]+)`/', '\\1', $item);
+						},
+						array_merge(
+							...array_map(
+								function($item){
+									return explode(',', $item[1]);
+								}, $primaryKey
+							)
+						)
+					);
+				}
+				unset($primaryKey);
+				if(preg_match_all('/ ((?:UNIQUE)? ?KEY) `([^`]+)` \(((?:`[^`]+`,?)+)\)/s', $sql, $indexes, PREG_SET_ORDER) > 0){
+					$this->indexes = array_reduce($indexes, function($indexes, $item){
+						$indexes[$item[2]] = [
+							'unique'=> $item[1] == 'UNIQUE KEY',
+							'columns'=> array_map(function($item){
+								return preg_replace('/`([^`]+)`/', '\\1', $item);
+							}, explode(',', $item[3])),
+							'dirty'=> false
+						];
+						return $indexes;
+					});
+				}
+				unset($indexes);
+				if(preg_match_all('/ CONSTRAINT `([^`]+)` FOREIGN KEY \(((?:`[^`]+`(?:, )?)+)\) REFERENCES `([^`]+)` \(((?:`[^`]+`(?:, )?)+)\)/s', $sql, $foreignKeys, PREG_SET_ORDER, 0) > 0){
+					$this->foreignKeys = array_reduce($foreignKeys, function($foreignKeys, $item) use($sql){
+						$columns = explode(',', $item[2]);
+						$matches = explode(',', $item[4]);
+						foreach($columns as $key => &$column){
+							$column = [
+								preg_replace('/`([^`]+)`/', '\\1', $column),
+								preg_replace('/`([^`]+)`/', '\\1', $matches[$key])
+							];
+						}
+						$foreignKeys[$item[1]] = [
+							'references'=> $item[3],
+							'columns'=> $columns
+						];
+						return $foreignKeys;
+					});
 				}
+				unset($foreignKeys);
+				if(preg_match_all('/ `([^`]+)` ([^\(]+\([^\)]+\))[^,\n]*,?/s', $sql, $columns, PREG_SET_ORDER) > 0){
+					foreach($columns as $column){
+						$default = null;
+						$line = $column[0];
+						$line .= substr($line, -1, 1) === ',' ? '' : ',';
+						if(preg_match('/ `[^`]+` [^\(]+\([^\)]+\)[^D$]+DEFAULT (?!NULL)(.+)(?: AUTO_INCREMENT|UNIQUE|KEY|PRIMARY|COMMENT|COLUMN_FORMAT|STORAGE|REFERENCES|,)/u', $line, $match) == 1){
+							$default = preg_replace("/'(.+)'/", "\\1", $match[1]);
+						}
+						$this->column(
+							$column[1], // name
+							$column[2], // type
+							$default, // default
+							preg_match('/`[^`]+` [^\(]+\([^\)]+\) NOT NULL/', $column[0]) === 1, // null
+							preg_match('/ `[^`]+` [^\(]+\([^\)]+\)[^A$]+AUTO_INCREMENT,?/u', $line) == 1 // increment
+						);
+						$this->columns[$column[1]]['dirty'] = false;
+					}
+				}
+				unset($columns);
 			}
-			return "$where";
 		}
-		public function stringSet(array $data){
-			$sets = '';
-			foreach($data as $name => $value){
-				$sets .= "`{$name}` = {$this->pdo->quote($value)},";
-			}
-			if(count($sets) > 0){
-				$sets = 'set '.substr($sets, 0, count($sets) - 2);
-			}
-			return $sets;
+		public function stringFilter(...$args){
+			return $this->pdo->stringFilter(...$args);
+		}
+		public function stringSet(...$args){
+			return $this->pdo->stringSet(...$args);
 		}
 		public function commit(){
 			$pdo = $this->pdo;
 			if(!$this->exists){
 				$columns = '';
 				foreach($this->columns as $name => $column){
-					$columns .= "{$name} {$column['type']},";
+					$default = '';
+					if(!is_null($column['default'])){
+						$default .= "DEFAULT {$pdo->quote($column['default'])}";
+					}
+					$null = $column['null'] ? 'NULL' : 'NOT NULL';
+					$ai = $column['increment'] ? 'AUTO_INCREMENT' : '';
+					$columns .= "{$name} {$column['type']} {$null} {$default} {$ai},";
 				}
 				if(count($columns) > 0){
-					$columns = substr($columns, 0, count($count) - 1);
+					$columns = rtrim($columns, ',');
 				}
 				$pk = $this->primaryKey();
-				if($pk){
-					$pk = ", primary key ({$pk})";
+				if(count($pk)){
+					$pk = ", primary key (".implode(',',$pk).")";
+				}
+				$index = '';
+				if(count($this->index) > 0){
+					foreach($this->index as $name => $idx){
+						if($idx['unique']){
+							$index .= ", constraint unique index {$name} (".implode(',', $idx['columns']).")";
+						}else{
+							$index .= ", index {$name} (".implode(',', $idx['columns']).")";
+						}
+					}
 				}
-				if($pdo->exec("create table `{$this->name}` ({$columns} {$pk})") === false){
+				$fk = '';
+				if(count($this->foreignKeys) > 0){
+					foreach($this->foreignKeys as $name => $k){
+						$cols0 = '';
+						$cols1 = '';
+						foreach($k['columns'] as $row){
+							$cols0 .= "{$row[0]},";
+							$cols1 .= "{$row[1]},";
+						}
+						$cols0 = rtrim($cols0, ',');
+						$cols1 = rtrim($cols1, ',');
+						$fk .= ", constraint `{$name}` foreign key ({$cols0}) references `{$k['references']}` ({$cols1})";
+					}
+				}
+				$count = $pdo->exec("create table `{$this->name}` ({$columns}{$pk}{$index}{$fk})");
+				if($count === false){
 					throw $pdo->getError();
 				}
+				if(!$this->exists()){
+					throw new \Exception("Unable to create table {$this->name}");
+				}
 				$this->exists();
 			}else{
 				// @todo alter table to add and remove columns
@@ -108,66 +193,104 @@
 				$this->pdo->exec("drop table `{$this->name}`");
 			}
 		}
-		public function column(string $name, array $column){
-			if(!isset($this->columns[$name])){
+		public function column(string $name, string $type = null, string $default = null, bool $null = false, bool $increment = false){
+			if(!is_null($type)){
 				$this->columns[$name] = [
-					'type'=> null,
-					'null'=> false,
-					'key'=> null,
-					'default'=> null,
-					'extra'=> null
+					'type'=> $type,
+					'default'=> $default,
+					'null'=> $null,
+					'increment'=> $increment,
+					'dirty'=> true
 				];
+				$column['dirty'] = true;
+				return $this;
+			}else{
+				return isset($this->columns[$name]) ? $this->columns[$name] : null;
 			}
-			foreach($this->columns[$name] as $key => &$val){
-				if(isset($column[$key])){
-					$val = $column[$key];
+		}
+		public function index(string $name, array $columns = null, bool $unique = false){
+			if(!is_null($columns)){
+				foreach($columns as $column){
+					if(!isset($this->columns[$column])){
+						throw new \Exception("Can't add index. Column {$this->name}.{$column} doesn't exist");
+					}
 				}
+				$this->index[$name] = [
+					'columns'=> $columns,
+					'unique'=> $unique,
+					'dirty'=>true
+				];
+				return $this;
+			}else{
+				return isset($this->index[$name]) ? $this->index[$name] : null;
 			}
-			$column['dirty'] = true;
-			return $this;
 		}
-		public function primaryKey(...$columns){
-			if(count($columns) > 0){
-				foreach($columns as $name){
-					if(!isset($this->columns[$name])){
-						throw new \Exception("Can't add Primary key. Column {$this->name}.{$name} doesn't exist");
-					}
-					$this->columns[$name]['key'] = 'PRI';
+		public function addToIndex(string $name, string $column){
+			if(!isset($this->index[$name])){
+				throw new \Exception("Can't add column to index. Index {$this->name}.{$name} doesn't exist");
+			}
+			if(!isset($this->columns[$column])){
+				throw new \Exception("Can't add column to index. Column {$this->name}.{$column} doesn't exist");
+			}
+			$this->index[$name]['columns'] = array_merge($this->index[$name]['columns'], [$column]);
+		}
+		public function foreignKey(string $name, string $references = null, array $columns = []){
+			if(!is_null($references)){
+				$table = $this->pdo->table($references);
+				if(!$table->exists){
+					throw new \Exception("Can't create foreign key {$name}. Table {$references} does not exist.");
 				}
-				foreach($this->columns as $name => &$col){
-					if(!in_array($name, $columns) && $col['key'] == 'PRI'){
-						$col['key'] = '';
+				foreach($columns as $column){
+					if(is_null($this->column($column[0]))){
+						throw new \Exception("Can't create foreign key {$name}. Column {$this->name}.{$column[0]} does not exist");
+					}
+					if(is_null($table->column($column[1]))){
+						throw new \Exception("Can't create foreign key {$name}. Column {$references}.{$column[1]} does not exist");
 					}
 				}
+				$this->foreignKeys[$name] = [
+					'references'=> $references,
+					'columns'=> $columns
+				];
 				return $this;
 			}else{
-				$columns = $this->columns;
-				return substr(array_reduce(array_keys($columns), function($ret, $name) use($columns){
-					$col = $columns[$name];
-					if($col['key'] == 'PRI'){
-						$ret .= ",{$name}";
+				return isset($this->foreignKeys[$name]) ? $this->foreignKeys[$name] : null;
+			}
+		}
+		public function primaryKey(...$columns){
+			if(count($columns) > 0){
+				foreach($columns as $column){
+					if(!isset($this->columns[$column])){
+						throw new \Exception("Can't add Primary key. Column {$this->name}.{$column} doesn't exist");
 					}
-					return $ret;
-				}), 1);
+				}
+				$this->primaryKey = $columns;
+				return $this;
+			}else{
+				return $this->primaryKey;
 			}
 		}
 		public function insert(array $data){
-			return $this->pdo->exec("insert into `{$this->name}` {$this->stringSet($data)}");
+			return $this->exists ? $this->pdo->exec("insert into `{$this->name}` {$this->stringSet($data)}") : 0;
 		}
 		public function update(array $data, array $filter = null){
-			return $this->pdo->exec("update `{$this->name} {$this->stringSet($data)} {$this->stringFilter($filter)}`");
+			return $this->exists ? $this->pdo->exec("update `{$this->name} {$this->stringSet($data)} {$this->stringFilter($filter)}`") : 0;
 		}
 		public function delete(array $filter = null){
-			return $this->pdo->exec("delete from `{$this->name}` {$this->stringFilter($filter)}");
+			return $this->exists ? $this->pdo->exec("delete from `{$this->name}` {$this->stringFilter($filter)}") : 0;
 		}
 		public function fetch(array $filter = null){
-			$query = $this->pdo->query("select * from `{$this->name}` {$this->stringFilter($filter)}");
-			$results = $query->fetchAll();
-			$query->closeCursor();
+			if($this->exists){
+				$query = $this->pdo->query("select * from `{$this->name}` {$this->stringFilter($filter)}");
+				$results = $query->fetchAll();
+				$query->closeCursor();
+			}else{
+				$results = [];
+			}
 			return $results;
 		}
 		public function count(array $filter = null){
-			return $this->pdo->exec("select 1 from `{$this->name}` {$this->stringFilter($filter)}");
+			return $this->exists ? $this->pdo->exec("select 1 from `{$this->name}` {$this->stringFilter($filter)}") : 0;
 		}
 	}
 ?>

+ 6 - 0
PDO/transaction.class.php

@@ -78,5 +78,11 @@
 		public function getError(){
 			return $this->pdo->getError();
 		}
+		public function stringFilter(...$args){
+			return $this->pdo->stringFilter(...$args);
+		}
+		public function stringSet(...$args){
+			return $this->pdo->stringSet(...$args);
+		}
 	}
 ?>

+ 10 - 6
app.class.php

@@ -186,12 +186,16 @@
 			}
 		}
 	}
-	error_reporting(E_ALL);
-	ini_set('display_errors', 'On');
+	set_error_handler(function($errno, $errstr, $errfile, $errline){
+		App::shutdown_error(new App\Exception($errstr, $errno, null, $errfile, $errline, debug_backtrace()));
+	}, E_ALL);
 	register_shutdown_function(function(){
-		App::shutdown();
+		error_reporting(E_ALL);
+		ini_set('display_errors', 'On');
+		try{
+			App::shutdown();
+		}catch(Exception $error){
+			App::shutdown_error(new App\Exception($error->getMessage(), $error->getCode(), $error, $error->getFile(), $error->getLine(), $error->getTrace()));
+		}
 	});
-	set_error_handler(function($errno, $errstr, $errfile, $errline){
-		App::shutdown_error(new App\Exception($errstr, $errno, null, $errfile, $errline, debug_backtrace(), get_included_files()));
-	},E_ALL);
 ?>

+ 25 - 9
orm.abstract.class.php

@@ -19,8 +19,8 @@
 		private $_related = [];
 		private static $aliases = [];
 		private static $instances = [];
-		private static $pdo;
-		private static $table;
+		protected static $pdo;
+		protected static $table;
 		// Magic functions
 		private function __construct($idOrData){
 			if(!isset(self::$aliases[$this->name])){
@@ -82,7 +82,7 @@
 			}
 			self::$instances[] = $this;
 		}
-		private function __destruct(){
+		public function __destruct(){
 			$this->__sleep();
 			$this->_changed = [];
 			$this->_data = [];
@@ -94,21 +94,20 @@
 		}
 		public function __get(string $name){
 			switch($name){
+				case 'pdo':
+					return static::pdo();
+				case 'table':
+					return static::table();
 				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);
 			}
@@ -162,6 +161,9 @@
 			}
 			return static::$table_name;
 		}
+		public static function table(){
+			return static::$table;
+		}
 		public static function primary_key(){
 			return static::$primary_key;
 		}
@@ -183,6 +185,9 @@
 				throw new \Exception("Invalid argument. Must pass a DSN string or a PDO instance");
 			}
 		}
+		public static function pdo(){
+			return static::$pdo;
+		}
 		public static function query(...$args){
 			return self::$pdo->query(...$args);
 		}
@@ -356,7 +361,18 @@
 		}
 		public function dirty(string $key = null){
 			if(is_null($key)){
-				return count($this->_changed) > 0;
+				$dirty = count($this->_changed) > 0;
+				if(!$dirty){
+					foreach($this->_related as $rel){
+						if($rel instanceof Relationship){
+							$dirty = $rel->dirty();
+						}
+						if($dirty){
+							break;
+						}
+					}
+				}
+				return $dirty;
 			}else{
 				return in_array($key, $this->_changed);
 			}

+ 21 - 0
pdo.class.php

@@ -130,5 +130,26 @@
 			$error = $this->pdo->errorInfo();
 			return new \Exception($error[2], $error[1]);
 		}
+		public function stringFilter(array $filter = null){
+			$where = '';
+			if(!is_null($filter)){
+				$where = 'where ';
+				foreach($filter as $name => $value){
+					$where .= "`{$name}` = {$this->quote($value)} and";
+				}
+				$where = rtrim($where, ' and');
+			}
+			return $where;
+		}
+		public function stringSet(array $data){
+			$sets = '';
+			foreach($data as $name => $value){
+				$sets .= "`{$name}` = {$this->quote($value)},";
+			}
+			if(count($sets) > 0){
+				$sets = 'set '.rtrim($sets, ',');
+			}
+			return $sets;
+		}
 	}
 ?>