1: <?php
   2:    3:    4:    5: 
   6: 
   7: class TinyDBException extends Exception{}
   8: 
   9:   10:   11: 
  12: class TinyDB
  13: {
  14:     protected $current;
  15:     protected $connections = array();
  16:     protected $configs = array();
  17:     public function __construct($dsn = null, $username = null, $password = null, $options = array()){
  18:         $this->current = 'default';
  19:         $this->addConnection($this->current, $dsn, $username, $password, $options);
  20:     }
  21:     
  22:     public function getConfig($name = null){
  23:         if(null === $name){
  24:             $name = $this->current;
  25:         }
  26:         return $this->configs[$name];
  27:     }
  28:     
  29:     public function getPDO($name = null){
  30:         if(null === $name){
  31:             $name = $this->current;
  32:         }
  33:         if(!isset($this->connections[$name])){
  34:             $config = $this->configs[$name];
  35:             $this->connections[$name] = new PDO($config['dsn'], $config['username'], $config['password'], $config['options']);
  36:         }
  37:         
  38:         return $this->connections[$this->current];
  39:     }
  40:     
  41:       42:   43:   44:   45:   46:   47:   48:   49: 
  50:     public function addConnection($name, $dsn, $username = null, $password = null, $options = array()){
  51:         
  52:         static $defaultOptions = array(
  53:             PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
  54:             
  55:         );
  56:         if(isset($options['prefix'])){
  57:             $prefix = $options['prefix'];
  58:             unset($options['prefix']);
  59:         }
  60:         else{
  61:             $prefix = '';
  62:         }
  63:         $this->configs[$name] = array(
  64:             'dsn' => $dsn,
  65:             'username' => $username,
  66:             'password' => $password,
  67:             'prefix' => $prefix,
  68:             'options' =>  $options + $defaultOptions,
  69:         );
  70:         
  71:         return $this;
  72:     }
  73:     
  74:       75:   76:   77: 
  78:     public function switchConnection($name = 'default'){
  79:         $this->current = $name;
  80:         
  81:         return $this;
  82:     }
  83: 
  84:       85:   86:   87:   88:   89: 
  90:     public function factory($model){
  91:         return new TinyDBFactory($this, $model);
  92:     }
  93:     
  94:       95:   96:   97:   98: 
  99:     public function command(){
 100:         return new TinyDBCommand($this);
 101:     }
 102:     
 103:      104:  105: 
 106:     public function getAttribute($attribute){
 107:         return $this->getPDO()->getAttribute($attribute);
 108:     }
 109:     
 110:      111:  112: 
 113:     public function setAttribute($attribute, $value){
 114:         return $this->getPDO()->setAttribute($attribute, $value);
 115:     }
 116:     
 117:      118:  119: 
 120:     public function prepare(){
 121:         return call_user_func_array(array($this->getPDO(), 'prepare'), func_get_args());
 122:     }
 123:     
 124:      125:  126: 
 127:     public function query(){
 128:         return call_user_func_array(array($this->getPDO(), 'query'), func_get_args());
 129:     }
 130:     
 131:      132:  133: 
 134:     public function exec(){
 135:         return call_user_func_array(array($this->getPDO(), 'exec'), func_get_args());
 136:     }
 137:     
 138:      139:  140: 
 141:     public function beginTransaction(){
 142:         return $this->getPDO()->beginTransaction();
 143:     }
 144:     
 145:      146:  147: 
 148:     public function rollBack(){
 149:         return $this->getPDO()->rollBack();
 150:     }
 151:     
 152:      153:  154: 
 155:     public function commit(){
 156:         return $this->getPDO()->commit();
 157:     }
 158:     
 159:      160:  161: 
 162:     public function inTransaction(){
 163:         return $this->getPDO()->inTransaction();
 164:     }
 165:     
 166:      167:  168: 
 169:     public function lastInsertId(){
 170:         return call_user_func_array(array($this->getPDO(), 'lastInsertId'), func_get_args());
 171:     }
 172:     
 173:      174:  175: 
 176:     public function quote(){
 177:         return call_user_func_array(array($this->getPDO(), 'quote'), func_get_args());
 178:     }
 179:     
 180:     public function errorCode(){
 181:         return call_user_func_array(array($this->getPDO(), 'errorCode'), func_get_args());
 182:     }
 183:     
 184:     public function errorInfo(){
 185:         return call_user_func_array(array($this->getPDO(), 'errorInfo'), func_get_args());
 186:     }
 187:     
 188:      189:  190:  191:  192: 
 193:     public function quoteTable($name){
 194:         return $this->quoteIdentifier($name);
 195:     }
 196:     
 197:      198:  199:  200:  201: 
 202:     public function quoteColumn($name){
 203:         return $this->quoteIdentifier($name);
 204:     }
 205:     
 206:      207:  208:  209:  210: 
 211:     public function quoteIdentifier($name){
 212:         $quote = null;
 213:         switch($this->getAttribute(PDO::ATTR_DRIVER_NAME)){
 214:             case 'pgsql':
 215:             case 'sqlsrv':
 216:             case 'dblib':
 217:             case 'mssql':
 218:             case 'sybase':
 219:                 $quote = '"';
 220:             case 'mysql':
 221:             case 'sqlite':
 222:             case 'sqlite2':
 223:             default:
 224:                 $quote = '`';
 225:         }
 226:         
 227:         $parts = explode('.', $name);
 228:         foreach($parts as $k => $part){
 229:             if($part !== '*'){
 230:                 $parts[$k] = $quote.$part.$quote;
 231:             }
 232:         }
 233:         
 234:         return implode('.', $parts);
 235:     }
 236:     
 237:     public function buildLimitOffset($sql, $limit, $offset = 0){
 238:         switch($this->getAttribute(PDO::ATTR_DRIVER_NAME)){
 239:             case 'sqlsrv':
 240:             case 'dblib':
 241:             case 'mssql':
 242:             case 'sybase':
 243:                 throw new TinyDBException('Limit/offset not implemented yet');
 244:             case 'pgsql':
 245:             case 'mysql':
 246:             case 'sqlite':
 247:             case 'sqlite2':
 248:             default:
 249:                 if($limit > 0){
 250:                     $sql .= "\n LIMIT ".$limit;
 251:                 }
 252:                 if($offset > 0){
 253:                     $sql .= " OFFSET ".$offset;
 254:                 }
 255:                 
 256:                 return $sql;
 257:         }
 258:     }
 259: 
 260:     public function getPrefix(){
 261:         return $this->configs[$this->current]['prefix'];
 262:     }
 263:     
 264:     public function fixPrefix($sql){
 265:         
 266:     }
 267: }
 268: 
 269:  270:  271: 
 272: class TinyDBModel
 273: {
 274:     
 275:     
 276:     private $_table;
 277:     
 278:     private $_pk;
 279:     
 280:     private $_relations;
 281:     
 282:     
 283:     
 284:     protected $_db;
 285:     
 286:     protected $_data = array();
 287:     
 288:     protected $_safe;
 289:     
 290:     protected $_dirty = array();
 291:     
 292:     protected $_isNew;
 293:     
 294:      295:  296:  297:  298:  299: 
 300:     public static function config(){
 301:         return array(
 302:             
 303:             'pk' => 'id',
 304:             
 305:         );
 306:     }
 307:     
 308:      309:  310:  311:  312:  313: 
 314:     final public static function getConfig($key = null){
 315:         $config = static::config();
 316:         if(null === $key){
 317:             return $config;
 318:         }
 319:         elseif(isset($config[$key])){
 320:             return $config[$key];
 321:         }
 322:         else{
 323:             return null;
 324:         }
 325:     }
 326:     
 327:      328:  329: 
 330:     public function getTable(){
 331:         return $this->_table;
 332:     }
 333: 
 334:      335:  336: 
 337:     public function getPK(){
 338:         return $this->_pk;
 339:     }
 340:         
 341:      342:  343: 
 344:     public function getRelations(){
 345:         return $this->_relations;
 346:     }
 347:     
 348:      349:  350:  351:  352:  353:  354:  355: 
 356:     static public function entityNameToDBName($class){
 357:         
 358:         if(false !== $pos = strrpos($class, '\\')){
 359:             $class = substr($class, $pos + 1);
 360:         }
 361:         
 362:         return strtolower(preg_replace('/(?<=[a-z])([A-Z])/', '_$1', $class));
 363:     }
 364:     
 365:      366:  367:  368:  369:  370:  371: 
 372:     public function __construct($db, $data = array(), $isNew = true){
 373:         $this->_db = $db;
 374:         
 375:         if($isNew){
 376:             $this->_dirty = $data;
 377:         }
 378:         else{
 379:             $this->_data = $data;
 380:         }
 381:         $this->_isNew = $isNew;
 382:         
 383:         
 384:         $this->setAttributes(static::config());
 385:     }
 386:     
 387:      388:  389: 
 390:     public function setAttribute($attribute, $value){
 391:         if($attribute === 'table'){
 392:             $this->_table = $value;
 393:         }
 394:         elseif($attribute === 'pk'){
 395:             $this->_pk = $value;
 396:         }
 397:         elseif($attribute === 'relations'){
 398:             $this->_relations = $value;
 399:         }
 400:         
 401:         return $this;
 402:     }
 403:     
 404:      405:  406:  407: 
 408:     public function setAttributes($attributes){
 409:         foreach($attributes as $k => $v){
 410:             $this->setAttribute($k, $v);
 411:         }
 412:         
 413:         return $this;
 414:     }
 415:     
 416:     public function isNew(){
 417:         return $this->_isNew;
 418:     }
 419:     
 420:     public function isDirty(){
 421:         return !empty($this->_dirty);
 422:     }
 423:     
 424:      425:  426:  427:  428: 
 429:     public function getRaw($key = null){
 430:         if(null === $key){
 431:             return $this->_data;
 432:         }
 433:         else{
 434:             return isset($this->_data[$key])?$this->_data[$key]:null;
 435:         }
 436:     }
 437:     
 438:      439:  440:  441:  442: 
 443:     public function get($key = null){
 444:         if(null === $key){
 445:             return array_merge($this->_data, $this->_dirty);
 446:         }
 447:         else{
 448:             if(isset($this->_dirty[$key])){
 449:                 return $this->_dirty[$key];
 450:             }
 451:             elseif(isset($this->_data[$key])){
 452:                 return $this->_data[$key];
 453:             }
 454:             else{
 455:                 return null;
 456:             }
 457:         }
 458:     }
 459:     
 460:     public function __get($key){
 461:         $relations = $this->getRelations();
 462:         if(isset($relations[$key])){
 463:             return $this->getWithRelation($key);
 464:         }
 465:         return $this->get($key);
 466:     }
 467:     
 468:     public function getWithRelation($name){
 469:         $relations = $this->getRelations();
 470:         if(isset($relations[$name])){
 471:             $relation = $relations[$name];
 472:             if($relation['relation'] === 'OTO'){
 473:                 return $this->getOneToOne($relation['target'], isset($relation['key'])?$relation['key']:null, isset($relation['target_key'])?$relation['target_key']:null);
 474:             }
 475:             elseif($relation['relation'] == 'OTM'){
 476:                 return $this->getOneToMany($relation['target'], isset($relation['key'])?$relation['key']:null, isset($relation['target_key'])?$relation['target_key']:null);
 477:             }
 478:             elseif($relation['relation'] == 'MTO'){
 479:                 return $this->getManyToOne($relation['target'], isset($relation['key'])?$relation['key']:null, isset($relation['target_key'])?$relation['target_key']:null);
 480:             }
 481:             elseif($relation['relation'] == 'MTM'){
 482:                 return $this->getManyToMany($relation['target'], $relation['through'], isset($relation['key'])?$relation['key']:null, isset($relation['target_key'])?$relation['target_key']:null);
 483:             }
 484:             else{
 485:                 throw new TinyDBException('Invalid relation "'.$relation['relation'].'"');
 486:             }
 487:         }
 488:         else{
 489:             return false;
 490:         }
 491:     }
 492:     
 493:      494:  495:  496:  497:  498:  499:  500:  501: 
 502:     public function getOneToOne($target, $key = null, $target_key = null){
 503:         $factory = $this->_db->factory($target);
 504:         if(null === $key){
 505:             $key = $this->getPK();
 506:         }
 507:         if(null === $target_key){
 508:             $target_key = $factory->getPK();
 509:         }
 510:         
 511:         return $factory->findOneBy($target_key, $this->get($key));
 512:     }
 513:     
 514:      515:  516:  517:  518:  519:  520:  521: 
 522:     public function getOneToMany($target, $key = null, $target_key = null){
 523:         $factory = $this->_db->factory($target);
 524:         if(null === $key){
 525:             $key = $this->getPK();
 526:         }
 527:         if(null === $target_key){
 528:             $target_key = $key;
 529:         }
 530:         
 531:         return $factory->findManyBy($target_key, $this->get($key));
 532:     }
 533:     
 534:      535:  536:  537:  538:  539:  540:  541: 
 542:     public function getManyToOne($target, $key = null, $target_key = null){
 543:         $factory = $this->_db->factory($target);
 544:         if(null === $target_key){
 545:             $target_key = $factory->getPK();
 546:         }
 547:         if(null === $key){
 548:             $key = $target_key;
 549:         }
 550:         
 551:         return $factory->findOneBy($target_key, $this->get($key));
 552:     }
 553:     
 554:      555:  556:  557:  558:  559:  560:  561:  562: 
 563:     public function getManyToMany($target, $through, $key = null, $target_key = null){
 564:         $factory = $this->_db->factory($target);
 565: 
 566:         if(null === $key){
 567:             $key = $this->getPK();
 568:         }
 569:         if(null === $target_key){
 570:             $target_key = $factory->getPK();
 571:         }
 572:         
 573:         $through = $this->parseThrough($through);
 574:         if(!$through[1]){
 575:             $through[1] = $key;
 576:         }
 577:         if(!$through[2]){
 578:             $through[2] = $target_key;
 579:         }
 580:         
 581:         $rows = $this->_db->command()
 582:             ->select('t.*')
 583:             ->from($factory->getTable().' t')
 584:             ->leftJoin($through[0].' m', 'm.'.$through[2].'=t.'.$target_key)
 585:             ->where('m.'.$through[1].'=:value', array(
 586:                 ':value' => $this->get($key)
 587:             ))
 588:         ->queryAll();
 589:         
 590:         if(false === $rows){
 591:             return false;
 592:         }
 593:         
 594:         return $factory->mapModels($rows);
 595:     }
 596:     
 597:     protected function parseThrough($through){
 598:         $through = explode(',', $through);
 599:         $table = trim($through[0]);
 600:         $key = isset($through[1])?trim($through[1]):null;
 601:         $target_key = isset($through[2])?trim($through[2]):null;
 602:         
 603:         return array($table, $key, $target_key);
 604:     }
 605:     
 606:      607:  608:  609:  610: 
 611:     public function set($key, $value = null){
 612:         if(is_array($key)){
 613:             $this->_dirty = $key;
 614:         }
 615:         else{
 616:             $this->_dirty[$key] = $value;
 617:         }
 618:         
 619:         return $this;
 620:     }
 621:     
 622:     public function __set($key, $value){
 623:         $this->_dirty[$key] = $value;
 624:     }
 625:     
 626:     public function __isset($key){
 627:         return isset($this->_dirty[$key]) || isset($this->_data[$key]);
 628:     }
 629:     
 630:     public function __unset($key){
 631:         if(isset($this->_dirty[$key])){
 632:             unset($this->_dirty[$key]);
 633:         }
 634:     }
 635:     
 636:     protected function buildPKConditions(){
 637:         $pk = $this->getPK();
 638:         if(is_string($pk)){
 639:             $pks = array($pk);
 640:         }
 641:         else{
 642:             $pks = $pk;
 643:         }
 644:         $params = array();
 645:         foreach($pks as $k => $pk){
 646:             $pks[$k] = $pk.'=:pk'.$k;
 647:             $params[':pk'.$k] = $this->_data[$pk];
 648:         }
 649:         array_unshift($pks, 'AND');
 650:         
 651:         return array($pks, $params);
 652:     }
 653:     
 654:      655:  656:  657: 
 658:     public function save(){
 659:         if($this->beforeSave()){
 660:             if($this->isNew()){
 661:                 $data = $this->_dirty;
 662:                 
 663:                 
 664:                 if(false !== $rst = $this->_db->command()->insert($this->getTable(), $data))
 665:                 {
 666:                     if(is_string($this->getPK()) && $id = $this->_db->lastInsertId()){
 667:                         $data[$this->getPK()] = $id;
 668:                     }
 669:                     $this->_data = $data;
 670:                     $this->_dirty = array();
 671:                     $this->_isNew = false;
 672:                     $this->afterSave();
 673:                     return $rst;
 674:                 }
 675:             }
 676:             else{
 677:                 if($this->isDirty()){
 678:                     
 679:                     $pkConditions = $this->buildPKConditions();
 680:                     if(false !== $rst = $this->_db->command()->update($this->getTable(), $this->_dirty, $pkConditions[0], $pkConditions[1])){
 681:                         $this->_data = array_merge($this->_data, $this->_dirty);
 682:                         $this->_dirty = array();
 683:                         $this->afterSave();
 684:                         return $rst;
 685:                     }
 686:                 }
 687:             }
 688:         }
 689:         
 690:         return false;
 691:     }
 692:     
 693:      694:  695:  696: 
 697:     public function delete(){
 698:         if($this->beforeDelete()){
 699:             $pkConditions = $this->buildPKConditions();
 700:             if(false !== $rst = ($this->isNew() || $this->_db->command()->delete($this->getTable(), $pkConditions[0], $pkConditions[1]))){
 701:                 $this->_data = array();
 702:                 $this->_dirty = array();
 703:                 $this->afterDelete();
 704:                 
 705:                 return $rst;
 706:             }
 707:         }
 708:         
 709:         return false;
 710:     }
 711:     
 712:     protected function beforeSave(){
 713:         return true;
 714:     }
 715:     
 716:     protected function afterSave(){
 717:         return true;
 718:     }
 719:     
 720:     protected function beforeDelete(){
 721:         return true;
 722:     }
 723:     
 724:     protected function afterDelete(){
 725:         return true;
 726:     }
 727:     
 728: }
 729: 
 730:  731:  732: 
 733: class TinyDBFactory
 734: {
 735:     protected $db;
 736:     protected $modelClass;
 737:     protected $table;
 738:     protected $pk;
 739:     protected $with;
 740:     
 741:     public function __construct($db, $model, $pk = null){
 742:         $this->db = $db;
 743:         if($model[0] === '@'){
 744:             $this->modelClass = 'TinyDBModel';
 745:             $this->table = substr($model, 1);
 746:         }
 747:         else{
 748:             $this->modelClass = $model;
 749:             if(null !== $model::getConfig('table')){
 750:                 $this->table = $model::getConfig('table');
 751:             }
 752:             else{
 753:                 $this->table = $model::entityNameToDBName($model);
 754:             }
 755:         }
 756:         if(null !== $pk){
 757:             $this->pk = $pk;
 758:         }
 759:         else{
 760:             $class = $this->modelClass;
 761:             $this->pk = $class::getConfig('pk');
 762:         }
 763:     }
 764:     
 765:      766:  767: 
 768:     public function getPK(){
 769:         return $this->pk;
 770:     }
 771:     
 772:      773:  774: 
 775:     public function getTable(){
 776:         return $this->table;
 777:     }
 778:     
 779:      780:  781: 
 782:     public function with($with){
 783:         $this->with = $with;
 784:         
 785:         return $this;
 786:     }
 787:     
 788:     public function buildWith(){
 789:         $with = $this->with;
 790:         $columns = explode(',', $with);
 791:         foreach($columns as $column){
 792:             
 793:         }
 794:         $this->with = null;
 795:     }
 796:     
 797:      798:  799:  800:  801:  802: 
 803:     public function map($row){
 804:         $class = $this->modelClass;
 805:         $model = new $class($this->db, $row, false);
 806:         $model->setAttribute('table', $this->table)->setAttribute('pk', $this->pk);
 807:         return $model;
 808:     }
 809:     
 810:      811:  812:  813:  814:  815: 
 816:     public function mapModels($rows){
 817:         $rst = array();
 818:         foreach($rows as $row){
 819:             $rst[] = $this->map($row);
 820:         }
 821:         
 822:         return $rst;
 823:     }
 824:     
 825:      826:  827:  828:  829:  830: 
 831:     public function create($row = array()){
 832:         $class = $this->modelClass;
 833:         $model = new $class($this->db, $row);
 834:         $model->setAttribute('table', $this->table)->setAttribute('pk', $this->pk);
 835:         
 836:         return $model;
 837:     }
 838:     
 839:      840:  841:  842:  843:  844: 
 845:     protected function buildPKConditions($pk, $_data){
 846:         if(is_string($pk)){
 847:             $pks = array($pk);
 848:             if(!is_array($_data)){
 849:                 $_data = array($pk => $_data);
 850:             }
 851:         }
 852:         else{
 853:             $pks = $pk;
 854:         }
 855:         $params = array();
 856:         foreach($pks as $k => $pk){
 857:             $pks[$k] = $pk.'=:pk'.$k;
 858:             $params[':pk'.$k] = $_data[$pk];
 859:         }
 860:         array_unshift($pks, 'AND');
 861:         
 862:         return array($pks, $params);
 863:     }
 864:     
 865:      866:  867:  868:  869:  870:  871: 
 872:     public function count($conditions = '', $params = array()){
 873:         return $this->db->command()->select('COUNT(*)')->from($this->table)->where($conditions, $params)->queryScalar();
 874:     }
 875:     
 876:      877:  878:  879:  880:  881:  882: 
 883:     public function countBy($key, $value){
 884:         return $this->count($key.'=:key', array(':key' => $value));
 885:     }
 886:     
 887:      888:  889:  890: 
 891:     public function find($pk){
 892:         return $this->findByPK($pk);
 893:     }
 894:     
 895:      896:  897: 
 898:     public function findAll(){
 899:         $rows = $this->db->command()->select()->from($this->table)->queryAll();
 900:         if(false === $rows){
 901:             return $rows;
 902:         }
 903:         
 904:         return $this->mapModels($rows);
 905:     }
 906:     
 907:      908:  909:  910:  911:  912:  913: 
 914:     public function findOne($conditions, $params = array()){
 915:         $row = $this->db->command()->select()->from($this->table)->where($conditions, $params)->limit(1)->queryRow();
 916:         if(false === $row){
 917:             return false;
 918:         }
 919:         
 920:         return $this->map($row);
 921:     }
 922:         
 923:      924:  925:  926:  927:  928:  929: 
 930:     public function findOneBy($key, $value){
 931:         return $this->findOne($key.'=:key', array(':key' => $value));
 932:     }
 933:     
 934:      935:  936:  937:  938:  939: 
 940:     public function findByPK($pk){
 941:         $pkConditions = $this->buildPKConditions($this->pk, $pk);
 942:         return $this->findOne($pkConditions[0], $pkConditions[1]);
 943:     }
 944:     
 945:      946:  947:  948:  949:  950:  951:  952:  953:  954: 
 955:     public function findMany($conditions = '', $params = array(), $orderBy = null, $limit = null, $offset = null){
 956:         $cmd = $this->db->command()->select()->from($this->table)->where($conditions, $params);
 957:         if($orderBy){
 958:             $cmd->orderBy($orderBy);
 959:         }
 960:         $rows = $cmd->limit($limit, $offset)->queryAll();
 961:         if(false === $rows){
 962:             return false;
 963:         }
 964:         else{
 965:             return $this->mapModels($rows);
 966:         }
 967:     }
 968:     
 969:      970:  971:  972:  973:  974:  975:  976:  977:  978: 
 979:     public function findManyBy($key, $value,  $orderBy = null, $limit = null, $offset = null){
 980:         return $this->findMany($key.'=:key', array(':key' => $value), $orderBy, $limit, $offset);
 981:     }
 982:     
 983:      984:  985:  986:  987:  988:  989:  990: 
 991:     public function update($values, $conditions = '', $params = array()){
 992:         return $this->db->command()->update($this->table, $values, $conditions, $params);
 993:     }
 994:     
 995:      996:  997:  998:  999: 1000: 1001: 1002: 
1003:     public function updateBy($key, $value, $values){
1004:         return $this->update($values, $key.'=:key', array(':key' => $value));
1005:     }
1006:     
1007:     1008: 1009: 1010: 1011: 1012: 
1013:     public function updateByPK($pk, $values){
1014:         $pkConditions = $this->buildPKConditions($this->pk, $pk);
1015:         return $this->update($values, $pkConditions[0], $pkConditions[1]);
1016:     }
1017:     
1018:     1019: 1020: 1021: 1022: 1023: 
1024:     public function insert($values){
1025:         return $this->db->command()->insert($this->table, $values);
1026:     }
1027:     
1028:     1029: 1030: 1031: 1032: 1033: 1034: 
1035:     public function delete($conditions = '', $params = array()){
1036:         return $this->db->command()->delete($this->table, $conditions, $params);
1037:     }
1038:     
1039:     1040: 1041: 1042: 1043: 1044: 1045: 
1046:     public function deleteBy($key, $value){
1047:         return $this->delete($key.'=:key', array(':key' => $value));
1048:     }
1049:     
1050:     1051: 1052: 1053: 1054: 1055: 
1056:     public function deleteByPK($pk){
1057:         $pkConditions = $this->buildPKConditions($this->pk, $pk);
1058:         return $this->delete($pkConditions[0], $pkConditions[1]);
1059:     }
1060:     
1061:     1062: 1063: 1064: 1065: 1066: 1067: 
1068:     public function __call($name, $arguments){
1069:         
1070:         if(preg_match('#^find(One|Many)By(.+)$#', $name, $matches)){
1071:             $one = $matches[1] === 'One';
1072:             $findByKey = TinyDBModel::entityNameToDBName($matches[2]);
1073:             array_unshift($arguments, $findByKey);
1074:             if($one){
1075:                 return call_user_func_array(array($this, 'findOneBy'), $arguments);
1076:             }
1077:             else{
1078:                 return call_user_func_array(array($this, 'findManyBy'), $arguments);
1079:             }
1080:         }
1081:         elseif(preg_match('#^(update|delete|count)By(.+)$#', $name, $matches)){
1082:             $action = $matches[1];
1083:             $actionByKey = TinyDBModel::entityNameToDBName($matches[2]);
1084:             array_unshift($arguments, $actionByKey);
1085:             return call_user_func_array(array($this, $action.'By'), $arguments); 
1086:         }
1087:         else{
1088:             throw new TinyDBException(sprintf('Helper method "%s" does not exist', $name));
1089:         }
1090:     }
1091: }
1092: 
1093: 1094: 1095: 
1096: class TinyDBCommand
1097: {
1098:     protected $db;
1099:     protected $statement;
1100:     protected $params = array();
1101:     protected $query = array();
1102:     protected $sql;
1103:     
1104:     1105: 1106: 1107: 1108: 
1109:     public function __construct($db){
1110:         $this->db = $db;
1111:     }
1112:     
1113:     protected function matchAlias($entry){
1114:         if(preg_match('#^(.*?)(?i:\s+as\s+|\s+)(.*)$#', $entry, $matches)){
1115:             return array(
1116:                 $matches[1],
1117:                 $matches[2],
1118:             );
1119:         }
1120:         else{
1121:             return false;
1122:         }
1123:     }
1124:     
1125:     protected function splitParts($parts){
1126:         return preg_split('#\s*,\s*#', trim($parts), -1, PREG_SPLIT_NO_EMPTY);
1127:     }
1128:     
1129:     1130: 1131: 
1132:     public function reset(){
1133:         $this->statement = null;
1134:         $this->params = array();
1135:         $this->query = array();
1136:         $this->sql = null;
1137:         
1138:         return $this;
1139:     }
1140:     
1141:     1142: 1143: 
1144:     public function prepare(){
1145:         if(null === $this->statement){
1146:             $this->statement = $this->db->prepare($this->getSql());
1147:         }
1148:         
1149:         return $this;
1150:     }
1151:     
1152:     public function bindParam(){
1153:         $this->prepare();
1154:         call_user_func_array(array($this->statement, 'bindParam'), func_get_args);
1155:     }
1156:     
1157:     public function bindValue(){
1158:         $this->prepare();
1159:         call_user_func_array(array($this->statement, 'bindValue'), func_get_args());
1160:         
1161:         return $this;
1162:     }
1163:     
1164:     public function bindValues($values){
1165:         $this->prepare();
1166:         
1167:         foreach($values as $k=> $value){
1168:             $this->statement->bindValue($k, $value);
1169:         }
1170:         
1171:         return $this;
1172:     }
1173:     
1174:     public function mergeParams($params){
1175:         foreach($params as $k => $v){
1176:             $this->params[$k] = $v;
1177:         }
1178:     }
1179:     
1180:     1181: 1182: 1183: 1184: 1185: 1186: 
1187:     public function select($fields = '*'){
1188:         if(is_string($fields) && strpos($fields, '(') !== false){
1189:             $this->query['select'] = $fields;
1190:         }
1191:         else{
1192:             if(!is_array($fields)){
1193:                 $fields = $this->splitParts($fields);
1194:             }
1195:             foreach($fields as $k => $field){
1196:                 if(false === strpos($field, '(')){
1197:                     if($alias = $this->matchAlias($field)){
1198:                         $fields[$k] = $this->db->quoteColumn($alias[0]).' AS '.$this->db->quoteColumn($alias[1]);
1199:                     }
1200:                     else{
1201:                         $fields[$k] = $this->db->quoteColumn($field);
1202:                     }
1203:                 }
1204:             }
1205:             $this->query['select'] = implode(', ', $fields);
1206:         }
1207:         
1208:         return $this;
1209:     }
1210:     
1211:     1212: 1213: 
1214:     public function distinct(){
1215:         $this->query['distinct'] = true;
1216:         
1217:         return $this;
1218:     }
1219:     
1220:     1221: 1222: 1223: 1224: 1225: 1226: 
1227:     public function from($tables){
1228:         if(is_string($tables)){
1229:             $tables = $this->splitParts($tables);
1230:         }
1231:         foreach($tables as $k => $table){
1232:             if($alias = $this->matchAlias($table)){
1233:                 $tables[$k] = $this->db->quoteTable($alias[0]).' AS '.$this->db->quoteTable($alias[1]);
1234:             }
1235:             else{
1236:                 $tables[$k] = $this->db->quoteTable($table);
1237:             }
1238:         }
1239:         
1240:         $this->query['from'] = implode(', ', $tables);
1241:         
1242:         return $this;
1243:     }
1244:     
1245:     1246: 1247: 1248: 
1249:     public function buildConditions($conditions){
1250:         if(!is_array($conditions)){
1251:             if(null === $conditions){
1252:                 return '';
1253:             }
1254:             return $conditions;
1255:         }
1256:         elseif($conditions === array()){
1257:             return '';
1258:         }
1259:         
1260:         $n = count($conditions);
1261:         $operator = strtoupper($conditions[0]);
1262:         if($operator === 'AND' || $operator === 'OR'){
1263:             $result = array();
1264:             for($i = 1; $i < $n; $i++){
1265:                 $condition = $this->buildConditions($conditions[$i]);
1266:                 if('' !== $condition){
1267:                     $result[] = '('.$condition.')';
1268:                 }
1269:             }
1270:             if($result === array()){
1271:                 return '';
1272:             }
1273:             else{
1274:                 return implode(' '.$operator.' ', $result);
1275:             }
1276:         }
1277:         
1278:         if(!isset($conditions[1], $conditions[2])){
1279:             return '';
1280:         }
1281:         
1282:         $column = $this->db->quoteColumn($conditions[1]);
1283:         $values = $conditions[2];
1284:         if(!is_array($values)){
1285:             $values = array($values);
1286:         }
1287:         if($operator === 'IN' || $operator === 'NOT IN'){
1288:             if($values === array()){
1289:                 return $operator === 'IN'?'0':'';
1290:             }
1291:             foreach($values as $k => $value){
1292:                 $values[$k] = $this->db->quote($value);
1293:             }
1294:             
1295:             return $column.' '.$operator.' ('.implode(',', $values).')';
1296:         }
1297:         elseif($operator === 'LIKE' || $operator === 'NOT LIKE' || $operator === 'OR LIKE' || $operator === 'OR NOT LIKE'){
1298:             if($values === array()){
1299:                 return ($operator === 'LIKE' || $operator === 'OR LIKE')?'0':'';
1300:             }
1301:             if($operator === 'LIKE' || $operator === 'NOT LIKE'){
1302:                 $andor = 'AND';
1303:             }
1304:             else{
1305:                 $andor = 'OR';
1306:                 $operator = $operator === 'OR LIKE'?'LIKE':'NOT LIKE';
1307:             }
1308:             
1309:             $result = array();
1310:             foreach($values as $k => $value){
1311:                 $result[] = $column.' '.$operator.' '.$this->db->quote($value);
1312:             }
1313:             
1314:             return implode($andor, $result);
1315:         }
1316:         else{
1317:             throw new TinyDBException('Invalid operator "'.$operator.'"');
1318:         }
1319:     }
1320:     
1321:     1322: 1323: 1324: 1325: 
1326:     public function where($conditions, $params = array()){
1327:         $this->mergeParams($params);
1328:         $this->query['where'] = $this->buildConditions($conditions);
1329:         
1330:         return $this;
1331:     }
1332:     
1333:     1334: 1335: 1336: 1337: 1338: 1339: 
1340:     public function orderBy($fields){
1341:         if(!is_array($fields)){
1342:             $fields = $this->splitParts($fields);
1343:         }
1344:         
1345:         foreach($fields as $k => $field){
1346:             if(preg_match('#^(.*?)\s+(asc|desc)$#i', $field, $matches)){
1347:                 $fields[$k] = $this->db->quoteColumn($matches[1]).' '.$matches[2];
1348:             }
1349:             else{
1350:                 $fields[$k] = $this->db->quoteColumn($field);
1351:             }
1352:         }
1353:         
1354:         $this->query['order'] = implode(',', $fields);
1355:         
1356:         return $this;
1357:     }
1358:     
1359:     1360: 1361: 1362: 1363: 
1364:     public function limit($limit, $offset = null){
1365:         $this->query['limit'] = $limit;
1366:         if(null !== $offset){
1367:             $this->offset($offset);
1368:         }
1369:         
1370:         return $this;
1371:     }
1372:     
1373:     1374: 1375: 1376: 
1377:     public function offset($offset){
1378:         $this->query['offset'] = $offset;
1379:         
1380:         return $this;
1381:     }
1382:     
1383:     1384: 1385: 1386: 1387: 1388: 1389: 
1390:     protected function anyJoin($type, $table, $conditions = '', $params = array()){
1391:         $this->mergeParams($params);
1392:         if($alias = $this->matchAlias($table)){
1393:             $table = $this->db->quoteTable($alias[0]).' AS '.$this->db->quoteTable($alias[1]);
1394:         }
1395:         else{
1396:             $table = $this->db->quoteTable($table);
1397:         }
1398:         $conditions = $this->buildConditions($conditions);
1399:         if('' !== $conditions){
1400:             $conditions = ' ON '.$conditions;
1401:         }
1402:         
1403:         if(isset($this->query['join']) && is_string($this->query['join'])){
1404:             $this->query['join'] = array($this->query['join']);
1405:         }
1406:         
1407:         $this->query['join'][] = $type.' '.$table.' '.$conditions;
1408:         
1409:         return $this;
1410:     }
1411:     
1412:     1413: 1414: 1415: 1416: 1417: 1418: 
1419:     public function join($table, $conditions = '', $params = array()){
1420:         return $this->anyJoin('JOIN', $table, $conditions, $params);
1421:     }
1422:     
1423:     1424: 1425: 1426: 1427: 1428: 1429: 
1430:     public function leftJoin($table, $conditions = '', $params = array()){
1431:         return $this->anyJoin('LEFT JOIN', $table, $conditions, $params);
1432:     }
1433:     
1434:     1435: 1436: 1437: 1438: 1439: 1440: 
1441:     public function rightJoin($table, $condtions = '', $params = array()){
1442:         return $this->anyJoin('RIGHT JOIN', $table, $conditions, $params);
1443:     }
1444:     
1445:     1446: 1447: 1448: 1449: 
1450:     public function groupBy($fields){
1451:         if(!is_array($fields)){
1452:             $fields = $this->splitParts($fields);
1453:         }
1454:         
1455:         foreach($fields as $k => $field){
1456:             $fields[$k] = $this->db->quoteColumn($field);
1457:         }
1458:         
1459:         $this->query['group'] = implode(',', $fields);
1460:         
1461:         return $this;
1462:     }
1463:     
1464:     1465: 1466: 1467: 1468: 1469: 
1470:     public function having($conditions, $params = array()){
1471:         $this->mergeParams($params);
1472:         $this->query['having'] = $this->buildConditions($conditions);
1473:         
1474:         return $this;
1475:     }
1476:     
1477:     1478: 1479: 1480: 1481: 
1482:     public function union($sql){
1483:         if(isset($this->query['union']) && is_string($this->query['union'])){
1484:             $this->query['union'][] = $this->query['union'];
1485:         }
1486:         
1487:         $this->query['union'][] = $sql;
1488:         
1489:         return $this;
1490:     }
1491:     
1492:     1493: 1494: 1495: 1496: 1497: 
1498:     public function buildQuery($query = null){
1499:         if(null === $query){
1500:             $query = $this->query;
1501:         }
1502:         
1503:         $sql = "SELECT ";
1504:         if(isset($query['distinct']) && $query['distinct']){
1505:             $sql .= 'DISTINCT ';
1506:         }
1507:         $sql .= isset($query['select'])?$query['select']:'*';
1508:         
1509:         if(!isset($query['from'])){
1510:             return false;
1511:         }
1512:         
1513:         $sql .= "\nFROM ".$query['from'];
1514:         if(isset($query['join'])){
1515:             $sql .= "\n".(is_array($query['join'])?implode("\n", $query['join']):$query['join']);
1516:         }
1517:         if(isset($query['where']) && $query['where'] !== ''){
1518:             $sql .= "\nWHERE ".$query['where'];
1519:         }
1520:         if(isset($query['group'])){
1521:             $sql .= "\nGROUP BY ".$query['group'];
1522:             if(isset($query['having'])){
1523:                 $sql .= "\nHAVING ".$query['having'];
1524:             }
1525:         }
1526:         if(isset($query['order'])){
1527:             $sql .= "\n ORDER BY ".$query['order'];
1528:         }
1529:         $limit = isset($query['limit'])?$query['limit']:0;
1530:         $offset = isset($query['offset'])?$query['offset']:0;
1531:         $sql = $this->db->buildLimitOffset($sql, $limit, $offset);
1532:         
1533:         if(isset($query['union'])){
1534:             $sql .= "\n".(is_array($query['union'])?implode("\n", $query['union']):$query['union']);
1535:         }
1536:         
1537:         return $sql;
1538:     }
1539:     
1540:     1541: 1542: 1543: 1544: 
1545:     public function setSql($sql){
1546:         if('' !== $prefix = $this->db->getPrefix()){
1547:             $sql = preg_replace('#{{(.*?)}}#', $prefix.'\1', $sql);
1548:         }
1549:         $this->sql = $sql;
1550:         
1551:         return $this;
1552:     }
1553:     
1554:     1555: 1556: 1557: 1558: 
1559:     public function getSql(){
1560:         if(null === $this->sql){
1561:             if(!empty($this->query)){
1562:                 $this->setSql($this->buildQuery());
1563:             }
1564:             else{
1565:                 return false;
1566:             }
1567:         }
1568:         
1569:         return $this->sql;
1570:     }
1571:     
1572:     1573: 1574: 1575: 
1576:     protected function beginQuery($params = array()){
1577:         $params = array_merge($this->params, $params);
1578:         $this->prepare();
1579:         if(false === $this->statement->execute($params)){
1580:             $info = $this->statement->errorInfo();
1581:             throw new TinyDBException(sprintf('Statement error #%s: %s', $info[0], $info[2]));
1582:         }
1583:     }
1584:     
1585:     1586: 1587: 1588: 1589: 1590: 1591: 
1592:     public function query($params = array()){
1593:         $this->beginQuery($params);
1594:         return $this->statement;
1595:     }
1596:     
1597:     1598: 1599: 1600: 1601: 
1602:     public function queryAll($params = array()){
1603:         $this->beginQuery($params);
1604:         $rst = $this->statement->fetchAll(PDO::FETCH_ASSOC);
1605:         $this->statement->closeCursor();
1606:         return $rst;
1607:     }
1608:     
1609:     1610: 1611: 1612: 1613: 
1614:     public function queryRow($params = array()){
1615:         $this->beginQuery($params);
1616:         $rst = $this->statement->fetch(PDO::FETCH_ASSOC);
1617:         $this->statement->closeCursor();
1618:         return $rst;
1619:     }
1620:     
1621:     1622: 1623: 1624: 1625: 
1626:     public function queryColumn($params = array()){
1627:         $this->beginQuery($params);
1628:         $rst = $this->statement->fetchAll(PDO::FETCH_COLUMN);
1629:         $this->statement->closeCursor();
1630:         return $rst;
1631:     }
1632:     
1633:     1634: 1635: 1636: 1637: 
1638:     public function queryScalar($params = array()){
1639:         $this->beginQuery($params);
1640:         $rst = $this->statement->fetchColumn();
1641:         $this->statement->closeCursor();
1642:         
1643:         return $rst;
1644:     }
1645:     
1646:     1647: 1648: 1649: 1650: 
1651:     public function execute($params = array()){
1652:         $this->prepare();
1653:         if(false === $rst = $this->statement->execute($params)){
1654:             return false;
1655:         }
1656:         
1657:         return $this->statement->rowCount();
1658:     }
1659:     
1660:     1661: 1662: 1663: 1664: 1665: 1666: 
1667:     public function insert($table, $values){
1668:         $keys = array();
1669:         $params = array();
1670:         $placeholders = array();
1671:         foreach($values as $k => $v){
1672:             $keys[] = $this->db->quoteColumn($k);
1673:             $params[':'.$k] = $v;
1674:             $placeholders[] = ':'.$k;
1675:         }
1676:         $sql = "INSERT INTO ".$this->db->quoteTable($table).' ('.implode(', ',$keys).') VALUES ('.implode(',', $placeholders).')';
1677:         return $this->setSql($sql)->execute($params);
1678:     }
1679:     
1680:     1681: 1682: 1683: 1684: 1685: 1686: 1687: 1688: 
1689:     public function update($table, $values, $conditions = '', $params = array()){
1690:         $updates = array();
1691:         if (!is_array($values)) {
1692:             $values = array($values);
1693:         }
1694:         foreach($values as $k => $value){
1695:             if (is_integer($k)) {
1696:                 $updates[] = $value;
1697:             } else {
1698:                 $updates[] = $this->db->quoteColumn($k).' = :'.$k;
1699:                 $params[':'.$k] = $value;
1700:             }
1701:         }
1702:         
1703:         $sql = "UPDATE ".$this->db->quoteTable($table).' SET '.implode(', ', $updates);
1704:         $conditions = $this->buildConditions($conditions);
1705:         if('' !== $conditions){
1706:             $sql .= ' WHERE '.$conditions;
1707:         }
1708:         
1709:         return $this->setSql($sql)->execute($params);
1710:     }
1711:     
1712:     1713: 1714: 1715: 1716: 1717: 1718: 1719: 1720: 
1721:     public function delete($table, $conditions = '', $params = array()){
1722:         $sql = 'DELETE FROM '.$this->db->quoteTable($table);
1723:         $conditions = $this->buildConditions($conditions);
1724:         if('' !== $conditions){
1725:             $sql .= ' WHERE '.$conditions;
1726:         }
1727:         
1728:         return $this->setSql($sql)->execute($params);
1729:     }
1730: }
1731: